博客 / 詳情

返回

CI+JUnit5併發單測機制創新實踐 | 京東物流技術團隊

一. 現狀·問題

針對現如今高併發場景的業務系統,“併發問題” 終歸是必不可少的一類(佔比接近10%),每次出現問題和事故後,需要耗費大量人力成本排查分析並修復。那如果能在事前儘可能避免豈不是很香?

二. 分析原因

  • 當前併發測試多數依賴測試人員進行腳本測試,同時還依賴了研發和產品識別出併發操作的場景用例。
  • 對於併發測試,大概兩條路子:
  1. 所有修改同樣數據的命令式接口都測一遍?【耗費巨大測試成本】
  2. 保證黃金流程的接口,研發從頭扒代碼。【可能會遺漏,耗費一定研發成本】

🤔自我反思

  • 作為研發,是不是在剛開發接口時候,識別到併發場景隨着單元測試階段同時進行併發測試,這樣的成本是最小的,收益是最高效的!

三. 採取措施

併發測試前置

採用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

單接口併發-@RepeatedTest

  • ManualCheckAppConcurrentTest 出庫複核併發測試「單接口併發」-> 手動複核 10個線程

👉 核心代碼塊

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()

多場景併發-@Execution(CONCURRENT)

  • CheckAppConcurrentTest 出庫複核併發測試「多場景併發」-> 手動複核|自動複核

👉 核心代碼塊

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() {}


併發單測基類-@Transactional

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

如何準備數據?

=\> 新建一個專門單元測試/併發測試的空數據庫

準備測試場景的前置數據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}');     

數據回滾-@ParameterizedTest

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();
            }
        }
    }

配置CI-@行雲流水線

建議在提測流水線增加,不要再日常dev流水線(集成測試相對耗時)

只執行併發單測用例-Dtest.mode 基於junit5 @Tag

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

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

user avatar wukongnotnull 頭像 u_16099302 頭像 u_16099220 頭像 u_11841527 頭像
4 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.