针对现如今高并发场景的业务系统,“并发问题” 终归是必不可少的一类(占比接近 10%),每次出现问题和事故后,需要耗费大量人力成本排查分析并修复。那如果能在事前尽可能避免岂不是很香?
采用 CI 持续集成机制,依靠行云流水线,底层利用junit5单元测试框架并发parallel引擎,嵌入同步数据库的自定义unit test 脚本,将每个并发 case 维护成单元测试,数据自我闭环,可重复执行!
将核心的并发场景进行及时的运行验证,最早洞察,最早验证,最小成本,最大保障!
前提:配置 junit-platform.properties
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=20
👉 核心代码块
public class ManualCheckAppConcurrentTest extends ConcurrentTest {
@Resource
ManualCheckAppService manualCheckAppService;
//记录执行成功的线程数
static int successThreadCount = 0;
///////////////////////////////////////////////////////////////////////////
// 单接口并发
///////////////////////////////////////////////////////////////////////////
@DisplayName("(单接口并发)并发测试【手动确认复核】")
@Description("(10个线程)场景:复核1件,一共5件,应该有5个线程成功,5个线程失败:没有查询到容器明细记录" +
"使用友好式分布式锁防止并发,并发后等待重试,保证顺序执行无异常!")
@Execution(CONCURRENT)
@RepeatedTest(value = 10, name = "{displayName}:{totalRepetitions}-{currentRepetition}")
public void testConfirmChecked(TestInfo testInfo) {
manualCheckAppService.confirmChecked(mockConfirmCheckedDto());
successThreadCount++;
}
/**
* 断言最终结果:数据无问题,线程执行无问题
*/
@AfterAll
public static void assertResult() {
//线程执行成功数期望:一共5件,每个线程复核1件,共有5个线程成功
Assertions.assertEquals(5, successThreadCount);
//数据成功期望:没有待复核的容器明细了,因为都复核成功了,一共5件
ConfirmCheckedDto confirmCheckedDto = mockConfirmCheckedDto();
List<ContainerDetailPo> containerDetailPos = SpringUtil.getBean(ContainerDetailDao.class).selectUncheckDetailsBySoAndSku(
confirmCheckedDto.getTaskNo(), confirmCheckedDto.getShipmentOrderNo(), confirmCheckedDto.getSku(), confirmCheckedDto.getWarehouseNo());
Assertions.assertTrue(CollectionUtils.isEmpty(containerDetailPos));
}
@Test
@Sql({"/concurrent/manualCheck.sql"})
@Override
void prepareData()
👉 核心代码块
public class CheckAppConcurrentTest extends ConcurrentTest {
@Resource
ManualCheckAppService manualCheckAppService;
@Resource
AutoCheckAppService autoCheckAppService;
///////////////////////////////////////////////////////////////////////////
// 多场景并发
///////////////////////////////////////////////////////////////////////////
@DisplayName("(多场景并发)并发测试【自动确认复核】")
@Description("与手动复核发生并发场景,期望可能存在业务异常(自定义锁冲突发生的消息)")
@Execution(CONCURRENT)
@Test
public void testAutoCheckBySo() {
autoCheckAppService.autoCheckBySo(Lists.newArrayList("SO-6_6_601-1492066800186167296"), mockAutoCheckBySoDto());
}
@DisplayName("(多场景并发)并发测试【手动确认复核】")
@Description("与自动复核发生并发场景,期望可能存在业务异常(自定义锁冲突发生的消息)")
@Execution(CONCURRENT)
@Test
public void testConfirmChecked() {
manualCheckAppService.confirmChecked(mockConfirmCheckedDto());
}
/**
* 断言最终结果:数据无问题
*/
@AfterAll
public static void assertResult() {
//数据成功期望:没有待复核的容器明细了,无论是手动复核还是自动复核,都会全部复核完
ConfirmCheckedDto confirmCheckedDto = mockConfirmCheckedDto();
List<ContainerDetailPo> containerDetailPos = SpringUtil.getBean(ContainerDetailDao.class).selectUncheckDetailsBySoAndSku(
confirmCheckedDto.getTaskNo(), confirmCheckedDto.getShipmentOrderNo(), confirmCheckedDto.getSku(), confirmCheckedDto.getWarehouseNo());
Assertions.assertTrue(CollectionUtils.isEmpty(containerDetailPos));
}
@Test
@Sql({"/concurrent/manualCheck.sql"})
@Override
void prepareData() {}
ConcurrentTest 建议抽出并发测试基类(主要目的:准备数据、设置路由、数据清除、独立执行)
@Tag("parallel") 分组: 并发测试用例,有助于单独执行套件!
👉 核心代码块
@SpringBootTest(classes = WebApplication.class)
@Tag("parallel")
public abstract class ConcurrentTest {
/**
* 并发测试场景的前提数据准备
* { @Sql 数据脚本配置 }
*/
@Transactional
@Order(0)
@Rollback(false)
abstract void prepareData();
/**
* 设置当前线程数据源
*/
@BeforeTransaction
public void setThreadDataSource() {
DataSourceContextHolder.clearDataSourceKey();
//多数据源,分库分表
DataSourceContextHolder.setDataSource("ds0");
}
/**
* 清除数据
*/
@Rollback(false)
@AfterAll
public static void clearData(){
new DatabaseSyncTest().execute("wms_check","wms_check_test");
}
如何准备数据?
=> 新建一个专门单元测试/并发测试的空数据库
准备测试场景的前置数据 SQL 脚本
👉 源脚本
DELETE FROM ck_task;
INSERT INTO ck_task (id, task_no, sku_qty, total_qty, platform_no, status, warehouse_no, create_user,
update_user, create_time, update_time, ts, deleted, suggest_platform, uuid,
parent_task_no, pick_differ_allow, operation_type, picking_flag, task_type,
ext_info,
subtask_qty, tenant_code, current_stream_no, confluence, batch_no, requirements)
VALUES (1492071049884340224, 'T6X6X60122021100000329', 1.0000, 5.0000, '', 0, '6_6_601', 'xiaoyan', 'xiaoyan',
'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,
null, 'TC30020150', 0, 1, 'cj006001', '{"allowBatchCheck": true}');
CI 自动同步数据库表结构: 测试环境数据库->单测数据库
利好:(研发无需被动维护 schema,自动与真实数据库结构同步)
只需要将下面单测 copy 到代码中,将 fromDb 和 toDb 参数修改成自己数据库即可!
👉 源代码
@DisplayName("单元测试MYSQL-DB结构同步")
@SneakyThrows
@ParameterizedTest
@CsvSource("wms_check,wms_check_test")
public void execute(String fromDb, String toDb) {
ResultSet resultSet = null;
Class.forName("com.mysql.jdbc.Driver");
try (
Connection connection = DriverManager.getConnection("***","user", "***");
Statement statement = connection.createStatement()
) {
String initDb = "DROP DATABASE IF EXISTS " + toDb + ";CREATE DATABASE " + toDb + ";";
log.info(initDb);
statement.executeUpdate(initDb);
resultSet = statement.executeQuery("SHOW TABLES FROM " + fromDb + ";");
List<String> tableNames = Lists.newArrayList();
while (resultSet.next()) {
tableNames.add(resultSet.getString("Tables_in_" + fromDb));
}
for (String tableName : tableNames) {
String syncSql = "DROP TABLE IF EXISTS " + toDb + "." + tableName + ";" +
"CREATE TABLE " + toDb + "." + tableName + " LIKE " + fromDb + "." + tableName + ";";
log.info(syncSql);
statement.executeUpdate(syncSql);
}
} finally {
if(resultSet != null){
resultSet.close();
}
}
}
建议在提测流水线增加,不要再日常 dev 流水线(集成测试相对耗时)
只执行并发单测用例-Dtest.mode 基于 junit5 @Tag
https://junit.org/junit5/docs/current/user-guide/#writing-tests-tagging-and-filtering
mvn test -Dtest.mode=parallel
—— 只运行并发测试用例
单接口并发单测
多场景并发单测
2022-02 月实践后
因为「并发测试」前置到「研发单元测试」环节,所以「测试阶段」时长缩短(2.5 天 -> 1 天)
2022-Q1
2022-Q2
2022-Q3
2022-Q4
「测试周期」阶段停留时长和占比,呈下降趋势!
2022-02 月实践后
因为「测试周期」缩短,研发单元测试成本几乎不变,所以「需求交付全周期」随之缩短(55 天 -> 35 天)!
「case by case」,通过单元测试「断言机制」,最细粒度全方位验证!
在【开发阶段】识别到接口存在并发问题,及时编写单元测试进行验证,针对分布式锁和乐观锁等常用防并发手段,对应不同的 assert 方式:
减少花大量时间专项测试 N 个接口并发测试成本,「最早发现,最早处理,最小成本」!
根据下图可见,从编码阶段、单元测试阶段、接口测试阶段、集成测试阶段、预发布阶段等软件生命周期中,越早发现问题,付出成本越小。
2022-02 月实践后
因为减少人力成本,所以会直接提升需求的吞吐量(200 个 -> 225 个)!
「并发测试前置」到研发单元测试环节,可减少缺陷数,降低问题发生概率!
👉 今年线上问题 - 并发问题 类别为 0
👉过程质量中并发问题趋势逐步降低
作者:京东物流 周奕儒
来源:京东云开发者社区 自猿其说 Tech