一. 现状·问题

针对现如今高并发场景的业务系统,“并发问题” 终归是必不可少的一类(占比接近10%),每次出现问题和事故后,需要耗费大量人力成本排查分析并修复。那如果能在事前尽可能避免岂不是很香?

二. 分析原因

  • 当前并发测试多数依赖测试人员进行脚本测试,同时还依赖了研发和产品识别出并发操作的场景用例。
  • 对于并发测试,大概两条路子:
  1. 所有修改同样数据的命令式接口都测一遍?【耗费巨大测试成本】
  2. 保证黄金流程的接口,研发从头扒代码。【可能会遗漏,耗费一定研发成本】

自我反思

  • 作为研发,是不是在刚开发接口时候,识别到并发场景随着单元测试阶段同时进行并发测试,这样的成本是最小的,收益是最高效的!

三. 采取措施

并发测试前置

采用CI持续集成机制,依靠行云流水线,底层利用junit5单元测试框架并发parallel引擎,嵌入同步数据库的自定义unit test脚本,将每个并发case维护成单元测试,数据自我闭环,可重复执行

将核心的并发场景进行及时的运行验证,最早洞察,最早验证,最小成本,最大保障!

四. 实践步骤

前提:配置junit-platform.properties

  1. # src/test/resources/junit-platform.properties
  2. junit.jupiter.execution.parallel.enabled=true
  3. junit.jupiter.execution.parallel.config.strategy=fixed
  4. junit.jupiter.execution.parallel.config.fixed.parallelism=20

单接口并发-@RepeatedTest

  • ManualCheckAppConcurrentTest 出库复核并发测试「单接口并发」-> 手动复核 10个线程

核心代码块

  1. public class ManualCheckAppConcurrentTest extends ConcurrentTest {
  2. @Resource
  3. ManualCheckAppService manualCheckAppService;
  4. //记录执行成功的线程数
  5. static int successThreadCount = 0;
  6. ///////////////////////////////////////////////////////////////////////////
  7. // 单接口并发
  8. ///////////////////////////////////////////////////////////////////////////
  9. @DisplayName("(单接口并发)并发测试【手动确认复核】")
  10. @Description("(10个线程)场景:复核1件,一共5件,应该有5个线程成功,5个线程失败:没有查询到容器明细记录" +
  11. "使用友好式分布式锁防止并发,并发后等待重试,保证顺序执行无异常!")
  12. @Execution(CONCURRENT)
  13. @RepeatedTest(value = 10, name = "{displayName}:{totalRepetitions}-{currentRepetition}")
  14. public void testConfirmChecked(TestInfo testInfo) {
  15. manualCheckAppService.confirmChecked(mockConfirmCheckedDto());
  16. successThreadCount++;
  17. }
  18. /**
  19. * 断言最终结果:数据无问题,线程执行无问题
  20. */
  21. @AfterAll
  22. public static void assertResult() {
  23. //线程执行成功数期望:一共5件,每个线程复核1件,共有5个线程成功
  24. Assertions.assertEquals(5, successThreadCount);
  25. //数据成功期望:没有待复核的容器明细了,因为都复核成功了,一共5件
  26. ConfirmCheckedDto confirmCheckedDto = mockConfirmCheckedDto();
  27. List<ContainerDetailPo> containerDetailPos = SpringUtil.getBean(ContainerDetailDao.class).selectUncheckDetailsBySoAndSku(
  28. confirmCheckedDto.getTaskNo(), confirmCheckedDto.getShipmentOrderNo(), confirmCheckedDto.getSku(), confirmCheckedDto.getWarehouseNo());
  29. Assertions.assertTrue(CollectionUtils.isEmpty(containerDetailPos));
  30. }
  31. @Test
  32. @Sql({"/concurrent/manualCheck.sql"})
  33. @Override
  34. void prepareData()

多场景并发-@Execution(CONCURRENT)

  • CheckAppConcurrentTest 出库复核并发测试「多场景并发」-> 手动复核|自动复核

核心代码块

  1. public class CheckAppConcurrentTest extends ConcurrentTest {
  2. @Resource
  3. ManualCheckAppService manualCheckAppService;
  4. @Resource
  5. AutoCheckAppService autoCheckAppService;
  6. ///////////////////////////////////////////////////////////////////////////
  7. // 多场景并发
  8. ///////////////////////////////////////////////////////////////////////////
  9. @DisplayName("(多场景并发)并发测试【自动确认复核】")
  10. @Description("与手动复核发生并发场景,期望可能存在业务异常(自定义锁冲突发生的消息)")
  11. @Execution(CONCURRENT)
  12. @Test
  13. public void testAutoCheckBySo() {
  14. autoCheckAppService.autoCheckBySo(Lists.newArrayList("SO-6_6_601-1492066800186167296"), mockAutoCheckBySoDto());
  15. }
  16. @DisplayName("(多场景并发)并发测试【手动确认复核】")
  17. @Description("与自动复核发生并发场景,期望可能存在业务异常(自定义锁冲突发生的消息)")
  18. @Execution(CONCURRENT)
  19. @Test
  20. public void testConfirmChecked() {
  21. manualCheckAppService.confirmChecked(mockConfirmCheckedDto());
  22. }
  23. /**
  24. * 断言最终结果:数据无问题
  25. */
  26. @AfterAll
  27. public static void assertResult() {
  28. //数据成功期望:没有待复核的容器明细了,无论是手动复核还是自动复核,都会全部复核完
  29. ConfirmCheckedDto confirmCheckedDto = mockConfirmCheckedDto();
  30. List<ContainerDetailPo> containerDetailPos = SpringUtil.getBean(ContainerDetailDao.class).selectUncheckDetailsBySoAndSku(
  31. confirmCheckedDto.getTaskNo(), confirmCheckedDto.getShipmentOrderNo(), confirmCheckedDto.getSku(), confirmCheckedDto.getWarehouseNo());
  32. Assertions.assertTrue(CollectionUtils.isEmpty(containerDetailPos));
  33. }
  34. @Test
  35. @Sql({"/concurrent/manualCheck.sql"})
  36. @Override
  37. void prepareData() {}

并发单测基类-@Transactional

ConcurrentTest 建议抽出并发测试基类(主要目的:准备数据、设置路由、数据清除、独立执行)

@Tag("parallel")分组: 并发测试用例,有助于单独执行套件! ​

核心代码块


  1. @SpringBootTest(classes = WebApplication.class)
  2. @Tag("parallel")
  3. public abstract class ConcurrentTest {
  4. /**
  5. * 并发测试场景的前提数据准备
  6. * { @Sql 数据脚本配置 }
  7. */
  8. @Transactional
  9. @Order(0)
  10. @Rollback(false)
  11. abstract void prepareData();
  12. /**
  13. * 设置当前线程数据源
  14. */
  15. @BeforeTransaction
  16. public void setThreadDataSource() {
  17. DataSourceContextHolder.clearDataSourceKey();
  18. //多数据源,分库分表
  19. DataSourceContextHolder.setDataSource("ds0");
  20. }
  21. /**
  22. * 清除数据
  23. */
  24. @Rollback(false)
  25. @AfterAll
  26. public static void clearData(){
  27. new DatabaseSyncTest().execute("wms_check","wms_check_test");
  28. }

数据准备-@Sql

如何准备数据?

=> 新建一个专门单元测试/并发测试的空数据库

准备测试场景的前置数据SQL脚本

源脚本

  1. DELETE FROM ck_task;
  2. INSERT INTO ck_task (id, task_no, sku_qty, total_qty, platform_no, status, warehouse_no, create_user,
  3. update_user, create_time, update_time, ts, deleted, suggest_platform, uuid,
  4. parent_task_no, pick_differ_allow, operation_type, picking_flag, task_type,
  5. ext_info,
  6. subtask_qty, tenant_code, current_stream_no, confluence, batch_no, requirements)
  7. VALUES (1492071049884340224, 'T6X6X60122021100000329', 1.0000, 5.0000, '', 0, '6_6_601', 'xiaoyan', 'xiaoyan',
  8. '2022-02-11 17:45:26', '2022-02-11 17:45:26', '2022-02-11 17:45:26', 0, '', 'zyr1228003', '', 0, 0, 0, 0, null,
  9. null, 'TC30020150', 0, 1, 'cj006001', '{"allowBatchCheck": true}');

数据回滚-@ParameterizedTest

CI自动同步数据库表结构: 测试环境数据库->单测数据库

利好:(研发无需被动维护schema,自动与真实数据库结构同步)

只需要将下面单测copy到代码中,将fromDb和toDb参数修改成自己数据库即可!

源代码

  1. @DisplayName("单元测试MYSQL-DB结构同步")
  2. @SneakyThrows
  3. @ParameterizedTest
  4. @CsvSource("wms_check,wms_check_test")
  5. public void execute(String fromDb, String toDb) {
  6. ResultSet resultSet = null;
  7. Class.forName("com.mysql.jdbc.Driver");
  8. try (
  9. Connection connection = DriverManager.getConnection("***","user", "***");
  10. Statement statement = connection.createStatement()
  11. ) {
  12. String initDb = "DROP DATABASE IF EXISTS " + toDb + ";CREATE DATABASE " + toDb + ";";
  13. log.info(initDb);
  14. statement.executeUpdate(initDb);
  15. resultSet = statement.executeQuery("SHOW TABLES FROM " + fromDb + ";");
  16. List<String> tableNames = Lists.newArrayList();
  17. while (resultSet.next()) {
  18. tableNames.add(resultSet.getString("Tables_in_" + fromDb));
  19. }
  20. for (String tableName : tableNames) {
  21. String syncSql = "DROP TABLE IF EXISTS " + toDb + "." + tableName + ";" +
  22. "CREATE TABLE " + toDb + "." + tableName + " LIKE " + fromDb + "." + tableName + ";";
  23. log.info(syncSql);
  24. statement.executeUpdate(syncSql);
  25. }
  26. } finally {
  27. if(resultSet != null){
  28. resultSet.close();
  29. }
  30. }
  31. }

配置CI-@行云流水线

建议在提测流水线增加,不要再日常dev流水线(集成测试相对耗时)

只执行并发单测用例-Dtest.mode 基于junit5 @Tag

https://junit.org/junit5/docs/current/user-guide/#writing-tests-tagging-and-filtering

  1. mvn test -Dtest.mode=parallel

配置IDEA-本地测试

—— 只运行并发测试用例

执行结果

单接口并发单测

多场景并发单测

五. 效能提升

5.1需求交付效率提升

5.1.1降低测试周期阶段时长

2022-02月实践后

因为「并发测试」前置到「研发单元测试」环节,所以「测试阶段」时长缩短 (2.5 天 -> 1 天)

2022-Q1

2022-Q2

2022-Q3

2022-Q4

「测试周期」阶段停留时长和占比,呈下降趋势!

5.1.2缩短需求交付全周期

2022-02月实践后

因为「测试周期」缩短,研发单元测试成本几乎不变,所以「需求交付全周期」随之缩短(55 天 -> 35 天)!

5.2人效提升

5.2.1提升验证全面性

「case by case」 ,通过单元测试「断言机制」,最细粒度全方位验证!

在【开发阶段】识别到接口存在并发问题,及时编写单元测试进行验证,针对分布式锁和乐观锁等常用防并发手段,对应不同的assert方式:

  • 数据库乐观锁:通过判断最终数据保证执行无问题
  • 分布式友好锁:不会报错,会等待,最终所有请求处理成功
  • 分布式冲突锁:直接报错,断言异常信息
  • ......

5.2.2降低测试人力成本

减少花大量时间专项测试N个接口并发测试成本,「最早发现,最早处理,最小成本」!

根据下图可见,从编码阶段、单元测试阶段、接口测试阶段、集成测试阶段、预发布阶段等软件生命周期中,越早发现问题,付出成本越小。

5.2.3提升需求吞吐量

2022-02月实践后

因为减少人力成本,所以会直接提升需求的吞吐量(200个 -> 225个)!

5.3过程质量提升

5.3.1降低问题的发生概率

「并发测试前置」 到研发单元测试环节,可减少缺陷数,降低问题发生概率!

5.3.2减少线上问题数

今年线上问题-并发问题 类别为 0

5.3.2减少Bug数

过程质量中并发问题趋势逐步降低

作者:京东物流 周奕儒

来源:京东云开发者社区 自猿其说Tech

CI+JUnit5并发单测机制创新实践的更多相关文章

  1. 阿里聚安全受邀参加SFDC安全大会,分享互联网业务面临问题和安全创新实践

    现今,技术引领的商业变革已无缝渗透入我们的日常生活,「技术改变生活」的开发者们被推向了创新浪潮的顶端.国内知名的开发者技术社区 SegmentFault 至今已有四年多了,自技术问答开始,他们已经发展 ...

  2. 腾讯云“智能+互联网TechDay”:揭秘智慧出行核心技术与创新实践

    现如今,地面交通出行与大家的生活息息相关.在当前城市道路日益复杂和拥挤的情况下,如何保证交通出行的安全和便捷相信是每个人以及众多专家.科研工作者重点关注的问题. “智慧交通”系统是解决交通发展瓶颈的有 ...

  3. Atitit  文件上传  架构设计 实现机制 解决方案  实践java php c#.net js javascript  c++ python

    Atitit  文件上传  架构设计 实现机制 解决方案  实践java php c#.net js javascript  c++ python 1. 上传的几点要求2 1.1. 本地预览2 1.2 ...

  4. FMZ发明者量化平台回测机制说明

    原文连接:https://www.fmz.com/digest-topic/4009 大部分策略在实盘之前都需要回测进行验证,FMZ支持部分品种数字货币现货.期货和永续合约,以及商品期货所有品种.但发 ...

  5. Sql Server Tempdb原理-日志机制解析实践

    笔者曾经在面试DBA时的一句”tempdb为什么比其他数据库快?”使得95%以上的应试者都一脸茫然.Tempdb作为Sqlserver的重要特征,一直以来大家对它可能即熟悉又陌生.熟悉是我们时时刻刻都 ...

  6. 国产PLM软件在创新实践中强势崛起

    近日,"璞华PLM"先后获得微度医疗.埃特斯等多个客户的订单,即使在疫情环境下也展现出了强劲的高速增长.在产品生命周期管理(PLM,Product Lifecycle Manage ...

  7. CI Weekly #9 | 揭秘阿里 Docker 化实践之路

    2017年悄然而至,对 flow.ci 你有什么新的期待呢?新的一年,flow.ci会越来越强大好用,希望继续得到你的支持与反馈.最近,我们做了如下的「功能优化」与「问题修复」,看看有没有你想要的: ...

  8. CI框架源码阅读笔记9 CI的自动加载机制autoload

    本篇并不是对某一组件的详细源码分析,而只是简单的跟踪了下CI的autoload的基本流程.因此,可以看做是Loader组件的分析前篇. CI框架中,允许你配置autoload数组,这样,在你的应用程序 ...

  9. springmvc学习笔记--Interceptor机制和实践

    前言: Spring的AOP理念, 以及j2ee中责任链(过滤器链)的设计模式, 确实深入人心, 处处可以看到它的身影. 这次借项目空闲, 来总结一下SpringMVC的Interceptor机制, ...

  10. CI Weekly #17 | flow.ci 支持 Java 构建以及 Docker/DevOps 实践分享

    这周一,我们迫不及待写下了最新的 changelog -- 项目语言新增「Java」.创建 Java 项目工作流和其它语言项目配置很相似,flow.ci 提供了默认的 Java 项目构建流程模版,快去 ...

随机推荐

  1. 2020-09-19:TCP状态有哪些?

    福哥答案2020-09-19:#福大大架构师每日一题# [答案来自此链接](https://www.zhihu.com/question/421833613) 11种状态1.CLOSED状态:初始状态 ...

  2. golang调用sdl2,播放yuv视频

    golang调用sdl2,播放yuv视频 win10 x64下测试成功,其他操作系统下不保证成功. 采用的是syscall方式,不是cgo方式. 见地址 代码如下: package main impo ...

  3. vue对vue-giant-tree进行节点操作

    vue 项目中使用到了vue-giant-tree这个使用ztree封装的树形插件,在对其节点进行操作时遇到了无法向传统的jquery那样获取到ztreeObj:而导致了无法控制节点dom:浪费了许多 ...

  4. Redash 可视化BI系统部署安装及简单使用

    这篇文章主要为介绍一下Redash的使用和安装 概览 Redash 主要使用的语言为 Python 和 TypeScript 这个安装主要是基于Docker 来安装的,官网教程基本没有不是基于Dock ...

  5. GaussDB(DWS)迁移实践丨row_number输出结果不一致

    摘要:迁移前后结果集row_number字段值前后不一致,前在DWS上运行不一致. 本文分享自华为云社区<GaussDB(DWS)迁移 - oracle兼容 --row_number输出结果不一 ...

  6. 入门 Python GUI 开发的第一个坑

    由于微信不允许外部链接,你需要点击文章尾部左下角的 "阅读原文",才能访问文中链接. 使用 Anaconda 3(conda 4.5.11)的 tkinter python 包(c ...

  7. C++面试八股文:如何在堆上和栈上分配一块内存?

    某日二师兄参加XXX科技公司的C++工程师开发岗位6面: 面试官: 如何在堆上申请一块内存? 二师兄:常用的方法有malloc,new等. 面试官:两者有什么区别? 二师兄:malloc是向操作系统申 ...

  8. S32DS---make: *** No rule to make target 'clean'. Stop和make: *** No rule to make target 'all'. Stop的一个解决方法

    问题: 最近在用S32DS调试代码的时候,遇到一个稀奇古怪的问题: and 折腾了半天,发现从这个页面导入工程编译就不会出现这个问题???? file-->import projects fro ...

  9. Go 语言 context 都能做什么?

    原文链接: Go 语言 context 都能做什么? 很多 Go 项目的源码,在读的过程中会发现一个很常见的参数 ctx,而且基本都是作为函数的第一个参数. 为什么要这么写呢?这个参数到底有什么用呢? ...

  10. ChatGPT「代码解释器」正式开放,图片转视频仅需30秒!十大令人震惊的魔法揭秘

    经过超过三个月的等待,ChatGPT「代码解释器」终于全面开放.这是一大波神奇魔法的高潮. OpenAI的科学家Karpathy对这个强大的代码解释器测试版赞不绝口.他把它比作你的个人数据分析师,可以 ...