FunTester 线程安全类在性能测试中应用

FunTester · 2020年04月01日 · 1042 次阅读

最近在做一个支付成功之后回调接口的压测,场景是用户购买 VIP,详情如下:

测试场景

用户支付成功之后,端上会请求后端来进行 VIP 开通和续费操作。

接口处理逻辑

首先验证接口参数签名是否正确,然后加锁去判断订单信息和状态,处理用户增添 VIP 时间事务,成功之后释放锁。锁是针对用户和订单的分布式锁,使用方案是用的redis

接口文档

接口基本信息

  1. 接口名称购买会员或续费会员
  2. 请求 Url /api/member/createOrRenewMember
  3. 将请求参数转为 JSON 字符串走验签处理,请求方式见 加签示例

请求参数

{
    "sign": "CGvmkLeGtDv/Om8RbY52pMKNMi/xdLYAgRhIxC3uPqJsEdOvHj+S6zFobN0fxA/vksmCKJHhN6hFQrFIa3C6oK6EH7/BnJEsUMmIRsXMra32lzE/Tq9nqjYIdy996qc6eJWsxshqquj9Tb78pN152ndCNgvujMYsUHT4v7QxUW4=",
    "orderNo": "E2341234",
    "systemId": 94848494
}

请求参数说明

字段说明 字段名称 字段类型 备注
订单号 orderNo string 订单编号
用户账号 systemId number 必传
签名 sign String 签名字符串,请用我方提供工具类生成

返回参数

{
    "code": 0,
    "message": "success",
    "data": {
        "memberId": 123123,
        "systemId": 86123123
    }
}

code 等于 0 的时候成功

测试方案

类似方案参考如何对消息队列做性能测试

难点

  • 因为锁的关系,一个用户只能同时有一个订单在处理,压测参数必需是每次必不相同。
  • 用户必需是存在的用户,对压测用户量提出了要求。

解决方案

  • 将用户 id 和订单号进行参数化,使用AtomicInteger这个线程安全的类和一个提前加载好的参数数组来保证每一次参数都是唯一且相互不同。(不适用随机的方法,因为有概率重复和消耗更多性能)
  • 储备更多用户,由于获取用户是按照数组索引增大顺序获取,并不需要每一个请求都绑定一个用户。经过尝试 2000 个用户循环去取就能满足需求。

关于 Java 线程安全的问题参考:操作的原子性与线程安全快看,i++ 真的不安全原子操作组合与线程安全

测试脚本

保留一下调试的方法和功能,性能测试框架第三版里面有引用类的代码。

package com.okayqa.others.payyst

import com.fun.base.constaint.ThreadLimitTimesCount
import com.fun.frame.excute.Concurrent
import com.fun.frame.httpclient.FanLibrary
import com.fun.utils.ArgsUtil
import com.fun.utils.RString
import com.okayqa.common.RSAUtilLJT
import com.okayqa.common.Users
import com.alibaba.fastjson.JSONObject
import org.apache.http.client.methods.HttpPost
import org.slf4j.Logger

import java.util.concurrent.atomic.AtomicInteger

class T extends FanLibrary {

    static Logger logger = getLogger(T.class)


    static AtomicInteger i = new AtomicInteger(111000);

    public static void main(String[] args) {
        def argsUtil = new ArgsUtil(args)
        def thread = argsUtil.getIntOrdefault(0, 1)
        def times = argsUtil.getIntOrdefault(1, 100)
        def reqs = []

        thread.times {
//            def mark = new HeaderMark("requestid")
            reqs << new Thr(times)
        }

        new Concurrent(reqs, "会员支付和续费接口").start()
        testOver()
    }

    static class Thr extends ThreadLimitTimesCount {

        static Logger logger = getLogger(Thr.class)

        public Thr(int times) {
            super(null, times, null);
        }

        @Override
        protected void doing() throws Exception {
            String url = com.okayqa.studentapd.base.OkayBase.HOST+"/api/member/createOrRenewMember"
            Map<String, String> p = new HashMap<>();
            p.put("days", "1");
            p.put("memberId", "208");
            p.put("orderNo", "F" + RString.getString(4) + i.getAndAdd(1));
            p.put("orderPaySystemId", "85123213");
            p.put("orderPayTime", "2020-02-09 10:00:00");
            p.put("payMoney", "30");
            p.put("recordSources", "3");
            p.put("renewal", "false");
            def user = Users.getStuUser(i.getAndAdd(1) % 2000)
//            output(user)
            p.put("systemId", user);
            String sign = RSAUtilLJT.sign(p, RSAUtilLJT.getPrivateKey(RSAUtilLJT.RSA_PRIVATE_KEY));
            p.put("sign", sign);
            HttpPost post = getHttpPost(url, JSONObject.fromObject(p).toString());
            def s = "F" + getNanoMark()
            post.addHeader(getHeader("requestid", s));

            def simlple = FanLibrary.excuteSimlple(post)
            if (!simlple.contains("success")) {
                logger.warn(s + OR + user + simlple.toString())
                fail()
            }
        }

    }
}

这里有一个坑,AtomicInteger类虽然是一个线程安全的类,但是并不是所有的方法都是安全的,比如get(),所以我两次都使用了getAndAdd()方法,虽然增加了用户量循环一次的速度,但准确性还是最重要的,经过试验验证 2000 个用户足够用。


  • 郑重声明:文章首发于公众号 “FunTester”,禁止第三方(腾讯云除外)转载、发表。

技术类文章精选

非技术文章精选

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册