来张图:
图上:预期中的多线程
图下:实际上遇到的多线程
做为测试人员,遇到潜在的并发问题时,需要通过技术或工具手段去模拟并发场景,避免产生上图中的第二种情况。
电商APP常见的秒抢活动,1000张券,1秒瞬间被抢完,根据这个例子,讲下一般的设计方式和如何测试。秒抢的关键词:固定数量、同一时间请求流量大、一人一张,内部实现方式常见为:
- 入口限流,因为只有1000张券,接收到人数到达1000后,后面的人直接拦截
- 异步处理,A系统负责验证记录用户身份,人员满额后,停止接待。发消息给券系统,进行发券
- 高并发,支持多线程抢券,保证count=1000的线程安全
秒抢的处理方式,类似于mq的使用场景,限流、削峰、解耦。
并发案例的测试,相对来说不复杂,在Jmeter里,通过设置线程数、csv存取用户数据、添加并发集合点,执行完毕后,验证结果(券被消耗的数量、用户实际领到的数量,过程持续的时间)。
用Java在本地模拟高并发的调用,这里贴下code:
public class ThreadCollect implements Runnable {
private HttpClient httpClient;
private CyclicBarrier cyclicBarrier;
public ThreadCollect(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier; /** 循环屏障,集合点,设置用户数 */
}
public void addThread(HttpClient httpClient){
this.httpClient = httpClient;
}
@Override
public void run() {
try {
Message.outPut(String.format("线程:%s 正在集结", Thread.currentThread().getName()));
cyclicBarrier.await(); /** 线程数全部到达后,才会执行后面的代码,否则继续在此排队 */
httpClient.send(); /** 所有的API请求,同时运行*/
Message.outPut(String.format("线程:%s started", Thread.currentThread().getName()));
Message.outPut(httpClient.getResponse());
} catch (Exception var) {
System.out.println(var.getMessage());
}
}
}
循环添加用户的请求,在达到并发数之前,做等待,本地httpClient请求里包装了每个用户请求的具体参数。
public class ThreadRunner {
public void execute(List<HttpClient> httpClients, int count) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(count);
ExecutorService executorService = Executors.newFixedThreadPool(count);
// api请求去屏障处集结
for (int i = 0; i < count; i++) {
ThreadCollect collect = new ThreadCollect(cyclicBarrier);
collect.addThread(httpClients.get(i));
executorService.execute(collect);
}
executorService.shutdown(); // 关闭线程池
}
}
并发测试的入参为,httpClients{api具体参数的list对象},count{并发数量}
mock代码:
public static void main(String[] args) {
List<HttpClient> httpClients = new ArrayList<>();
ThreadRunner runner = new ThreadRunner();
for (int i = 0; i < 10; i++) {
HttpClient httpClient = new HttpClient()
.addEndpoint("https://********/bg/v1/information/user")
.addMethod(HttpMethod.POST)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "bearer 87ea05cdf70146b8ab8c361c39832452d99b72891c194a9086e030bf3678c2c1")
.addBodyParam("email", StringUtil.getEmail(9, 18))
.addBodyParam("callingPrefix", "+86")
.addBodyParam("phoneNumber", StringUtil.getPhone())
.bodyToJson()
.build();
httpClients.add(httpClient);
}
runner.execute(httpClients, 3);
}
输出:
线程:pool-1-thread-3 正在集结
线程:pool-1-thread-1 正在集结
线程:pool-1-thread-5 正在集结
线程:pool-1-thread-9 正在集结
线程:pool-1-thread-6 正在集结
线程:pool-1-thread-4 正在集结
线程:pool-1-thread-8 正在集结
线程:pool-1-thread-10 正在集结
线程:pool-1-thread-2 正在集结
线程:pool-1-thread-7 正在集结
线程:pool-1-thread-8 started
- - - - - - - - - -http request details- - - - - - - - - -
| Endpoint: https://**********/bg/v1/information/user
| Method: POST
| Headers:
| - { Authorization : bearer 87ea05cdf70146b8ab8c361c39832452d99b72891c194a9086e030bf3678c2c1 }
| - { Content-Type : application/json }
| Body: (Json || Form || Form-Data)
{"phoneNumber":"15604213233","callingPrefix":"+86","email":"vau007aqo57fpb@hotmail.com"}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - http response details - - - - - - - - -
| Response Code: 201
| Response Time: 2.54s
| Cookie: {Set-Cookie=}
| Response:
| - {"clientId":"8522009271012242069","code":"adfa009be9bb46029642179c62ef9a31"}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
线程:pool-1-thread-4 started
............................................
.............................