執行緒安全類在性能測試中應用

  • 2020 年 2 月 17 日
  • 筆記

最近在做一個支付成功之後回調介面的壓測,場景是用戶購買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);        publicstaticvoid 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()      }        staticclass Thr extends ThreadLimitTimesCount {static Logger logger = getLogger(Thr.class)            public Thr(int times) {              super(null, times, null);          }            @Overrideprotectedvoid 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」,禁止第三方(騰訊雲除外)轉載、發表。

Tags: