竞态条件场景、测试思路讲解

竞态条件场景、测试思路讲解

竞态条件是高并发场景下数据正确性的隐形杀手。本文从经典场景测试思路测试手段具体代码与操作步骤四个维度,系统梳理竞态条件测试的设计与落地方法。


一、竞态条件是什么?

竞态条件:多个操作并发执行时,最终结果依赖执行顺序,而顺序不可控,导致数据错乱。

典型问题:读-改-写 非原子。例如:

  • 用户 A 读到库存 10,准备扣 1 写成 9
  • 用户 B 也读到 10,准备扣 1 写成 9
  • 两人都以为自己是「最后一个」,结果库存被错误写成 9,实际应扣 2 次变成 8

二、经典场景

2.1 库存扣减(超卖)

场景 并发操作 风险
秒杀/抢购 N 人同时下单同一商品 库存为 1 时可能成交多单,出现负库存
多门店调拨 多仓同时调出同一商品 调拨量超过实际库存
盘点调整 多人同时调整同一商品库存 最终库存与预期不符

2.2 限量资源领取

场景 并发操作 风险
优惠券 多人同时领限量券 领取数超过券总量
福袋/盲盒 多人同时抢限量活动 中奖数超过配置数量
会员积分 并发消费/充值 积分余额错误

2.3 同一资源并发更新

场景 并发操作 风险
商品价格 多系统同时推送同一商品价格 后写入覆盖先写入,或版本错乱
商品分类 同时创建新分类与编辑已有分类 分类数据丢失或错乱
订单状态 支付、取消、发货同时发生 状态不一致

2.4 批量操作与事务

场景 并发操作 风险
批量价格同步 多请求同时更新同一批商品 部分成功部分失败,数据不一致
主从同步 主库写入后从库未及时同步 读从库得到旧数据

三、测试思路与流程图

3.1 竞态条件测试整体流程



识别共享资源
分析读-改-写路径
设计并发场景
选择测试手段
执行并发请求
数据校验
结果正确?
通过
定位问题
开发修复

3.2 场景设计决策流程

库存/限量
价格/配置
状态机
确定被测资源
资源类型?
同资源多请求
同记录多版本更新
多状态并发流转
设计: 库存=1, N人同时扣1
设计: 同商品多version并发更新
设计: 支付/取消/发货同时触发
预期: 仅1单成功
预期: version最大者生效
预期: 状态符合业务规则

3.3 数据校验流程





并发测试结束
记录成功请求数
查询数据库实际值
库存类?
库存 = 初始 - 成功订单数
版本类?
version = 最大提交值
业务规则校验
断言通过?
输出报告

3.4 测试设计检查清单

  1. 识别共享资源 2. 确定并发数 3. 准备边界数据 4. 设计验证SQL 5. 执行并校验

3.5 竞态发生时间线示意

数据库 用户3 用户2 用户1 数据库 用户3 用户2 用户1 库存=1,三人同时下单 三人都读到1,均认为可下单 若无锁:后写覆盖前写,可能3单都成功,库存=-2 正确做法:乐观锁/悲观锁,仅1单成功 读库存(1) 读库存(1) 读库存(1) 写库存(0) 写库存(0) 写库存(0)


四、测试手段

4.1 手段对比

手段 适用场景 优点 缺点
Python ThreadPoolExecutor 接口并发、自定义逻辑 灵活、易集成断言 需自己管理线程
k6 接口压测、竞态专项 并发控制好、有报告 需学 k6 语法
Locust 混合场景、自定义 Python 编写、易扩展 报告不如 k6 直观
JMeter 传统压测 图形化、插件多 脚本维护成本高

4.2 核心要点

  1. 同一资源:所有并发请求必须操作同一库存、同一商品、同一券等。
  2. 尽量同时:减少请求间隔,提高竞态触发概率。
  3. 边界数据:库存 1、券余量 1 等,更容易暴露问题。
  4. 结果校验:不只看接口返回,必须查库或业务数据校验。

五、实战一:Python 并发库存扣减测试

5.1 场景

库存为 1 的商品,10 个用户同时下单各买 1 件,预期仅 1 单成功。

5.2 代码

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
竞态条件测试:库存扣减(超卖场景)
场景:库存=1,10人同时下单,预期仅1单成功
"""
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

# 配置
API_URL = "http://your-api.com/order/create"
PRODUCT_ID = "P001"
INITIAL_STOCK = 1
CONCURRENT_USERS = 10

# 用于收集结果
results = []
lock = threading.Lock()

def create_order(user_id):
    """模拟用户下单"""
    try:
        resp = requests.post(API_URL, json={
            "product_id": PRODUCT_ID,
            "quantity": 1,
            "user_id": f"user_{user_id}",
        }, timeout=10)
        data = resp.json()
        with lock:
            results.append({
                "user_id": user_id,
                "success": data.get("code") == 200,
                "order_id": data.get("data", {}).get("order_id"),
                "message": data.get("message", ""),
            })
        return results[-1]
    except Exception as e:
        with lock:
            results.append({"user_id": user_id, "success": False, "error": str(e)})
        return results[-1]

def main():
    print("=" * 60)
    print("竞态条件测试:库存扣减(超卖)")
    print("=" * 60)
    print(f"商品: {PRODUCT_ID}, 初始库存: {INITIAL_STOCK}")
    print(f"并发用户数: {CONCURRENT_USERS}")
    print("预期: 仅1单成功")
    print("=" * 60)

    # 先重置库存为1(需根据实际接口调整)
    # reset_stock(PRODUCT_ID, INITIAL_STOCK)

    with ThreadPoolExecutor(max_workers=CONCURRENT_USERS) as executor:
        futures = [executor.submit(create_order, i) for i in range(CONCURRENT_USERS)]
        for f in as_completed(futures):
            f.result()

    success_count = sum(1 for r in results if r.get("success"))
    print(f"\n结果: 成功 {success_count} 单, 失败 {CONCURRENT_USERS - success_count} 单")

    print("\n验证步骤:")
    print("1. 查询数据库: SELECT stock FROM product WHERE id = ?")
    print("2. 预期: stock = 0 (若仅1单成功)")
    print("3. 若 stock < 0 或 成功数 > 1 -> 存在超卖")

    if success_count > 1:
        print("\n[结论] 存在超卖风险!")
    elif success_count == 1:
        print("\n[结论] 通过: 仅1单成功")
    else:
        print("\n[结论] 需检查: 可能全部失败或接口异常")

if __name__ == "__main__":
    main()

5.3 操作步骤

  1. 准备测试环境,确保有库存重置接口或可直接改库。
  2. 将商品库存设为 1。
  3. 修改 API_URLPRODUCT_ID 为实际值。
  4. 执行:python race_stock_test.py
  5. 统计成功订单数,并查库校验库存是否为 0。

六、实战二:k6 并发价格更新(乐观锁验证)

6.1 场景

多个请求同时更新同一商品价格,带 version 乐观锁。预期:version 最大者生效,且 price = version。

6.2 代码

javascript 复制代码
/**
 * 竞态条件测试:同一商品多版本并发更新
 * 验证乐观锁:version 最大者生效,price 与 version 一致
 */
import http from 'k6/http';
import { check } from 'k6';

const VUS = 200;
const BASE_URL = 'https://api.example.com';
const PRODUCT_CODE = '25040500127163';
const STORE_CODE = 'MYV001';
let TOKEN = '';

export const options = {
  vus: VUS,
  iterations: VUS,
};

export function setup() {
  // 登录获取 token(示例,需替换实际登录逻辑)
  const loginResp = http.post(`${BASE_URL}/login`, JSON.stringify({
    username: 'test',
    passwd: 'xxx',
  }), { headers: { 'Content-Type': 'application/json' } });
  const body = JSON.parse(loginResp.body);
  TOKEN = body.data?.token || '';
  return { token: TOKEN };
}

export default function (data) {
  const currentVersion = 100 + __VU;
  const price = currentVersion;

  const payload = JSON.stringify({
    prices: [{
      product_code: PRODUCT_CODE,
      store_code: STORE_CODE,
      price: price.toFixed(10),
      version: currentVersion,
    }],
  });

  const res = http.post(`${BASE_URL}/sync/batchPrice`, payload, {
    headers: {
      'Content-Type': 'application/json',
      'Account-Token': data.token,
    },
    timeout: '30s',
  });

  check(res, {
    'HTTP 200': (r) => r.status === 200,
    '业务成功': (r) => {
      try {
        const b = JSON.parse(r.body);
        return b.code === 200;
      } catch (e) { return false; }
    },
  });
}

export function teardown(data) {
  console.log('');
  console.log('验证方法:');
  console.log('1. 查库: SELECT product_code, price, version FROM product_price WHERE ...');
  console.log('2. 预期: version = max(所有请求的version), price = version');
  console.log('3. 若 price != version -> 存在并发覆盖问题');
}

6.3 操作步骤

  1. 安装 k6:brew install k6(macOS)
  2. 替换 BASE_URL、登录逻辑、商品编码、门店编码。
  3. 执行:k6 run race_price_test.js
  4. 测试结束后,查库校验 priceversion 是否一致。

七、实战三:优惠券领取(限量竞态)

7.1 场景

限量 5 张券,20 人同时领取,预期最多 5 人成功。

7.2 思路(无代码)

若暂时无法写脚本,可手动模拟:

  1. 准备:创建限量 5 张的优惠券活动。
  2. 并发:用 Postman Runner 或 Apifox 批量执行 20 个领取请求(并发数 20)。
  3. 校验
    • 成功领取数 <= 5
    • 数据库 coupon.remain_count >= 0
    • 用户领取记录数 = 成功数

7.4 无代码时的完整测试思路

若暂时无法编写脚本,可按以下步骤执行:

  1. 明确共享资源:如优惠券剩余量、库存、价格 version。
  2. 准备边界数据:库存=1、券=1、或同商品多 version。
  3. 选择并发工具
    • Postman Runner:设置 Iterations=20,Delay=0
    • Apifox:批量执行,并发数调至最大
    • 浏览器多开 + 手动同时点击(适合简单场景)
  4. 同时触发:尽量让请求在同一时刻发出。
  5. 结果校验
    • 统计接口返回的成功数
    • 查库核对:库存/券余量/version 是否符合预期
  6. 多轮执行:竞态非必现,可执行 5~10 轮提高发现概率。

7.5 校验 SQL 示例

sql 复制代码
-- 券剩余量
SELECT remain_count FROM coupon_activity WHERE id = ?;

-- 领取记录数
SELECT COUNT(*) FROM coupon_receive WHERE activity_id = ?;

八、实战四:Python 通用并发模板

适用于任意接口的竞态测试,只需替换请求逻辑。

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
竞态条件测试 - 通用并发模板
替换 request_fn 即可适配不同接口
"""
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

def request_fn(request_id):
    """
    替换为实际请求逻辑
    返回: {"success": bool, "data": any}
    """
    import requests
    resp = requests.post("http://api.example.com/your-api", json={"id": request_id})
    data = resp.json()
    return {"success": data.get("code") == 200, "data": data}

def run_race_test(concurrent_count=50, request_fn=request_fn):
    results = []
    lock = threading.Lock()

    with ThreadPoolExecutor(max_workers=concurrent_count) as executor:
        futures = [executor.submit(request_fn, i) for i in range(concurrent_count)]
        for f in as_completed(futures):
            r = f.result()
            with lock:
                results.append(r)

    success = sum(1 for r in results if r.get("success"))
    print(f"并发数: {concurrent_count}, 成功: {success}, 失败: {concurrent_count - success}")
    return results

if __name__ == "__main__":
    run_race_test(concurrent_count=20)

九、验证方法汇总

场景 验证方式 预期
库存扣减 SELECT stock FROM product WHERE id=? stock = 初始 - 成功订单数
优惠券 SELECT remain_count FROM coupon_activity remain_count >= 0
价格更新 SELECT price, version FROM product_price price = version = max(提交的version)
订单状态 SELECT status FROM order WHERE id=? 符合状态机规则

十、常见问题与对策

问题 可能原因 对策
难以复现 并发不够或间隔大 提高并发数、减少 sleep
全部失败 限流、鉴权、超时 检查限流配置、token、超时时间
结果不稳定 随机性、环境差异 多次执行、固定测试数据
不知道如何校验 不了解表结构 与开发确认读写表、字段含义

十一、总结

  1. 识别共享资源:库存、券、价格、状态等可能被并发修改的数据。
  2. 设计边界场景:库存 1、券 1、同商品多 version 等。
  3. 选对工具:Python 灵活,k6 适合接口压测。
  4. 必须做数据校验:不只依赖接口返回,要查库或业务数据。
  5. 流程图辅助设计:用流程图梳理「识别资源 -> 设计场景 -> 执行 -> 校验」全流程。

掌握以上思路与代码,可以系统化开展竞态条件测试,有效发现高并发下的数据一致性问题。

相关推荐
财迅通Ai21 小时前
6000万吨产能承压 卫星化学迎来战略窗口期
大数据·人工智能·物联网·卫星化学
武子康1 天前
大数据-263 实时数仓-Canal 增量订阅与消费原理:MySQL Binlog 数据同步实践
大数据·hadoop·后端
LJ97951111 天前
媒体发布新武器:Infoseek融媒体平台使用指南
大数据·人工智能
科技小花1 天前
AI重塑数据治理:2026年核心方案评估与场景适配
大数据·人工智能·云原生·ai原生
方向研究1 天前
存储芯片生产
大数据
代码青铜1 天前
如何用 Zion 实现 AI 图片分析与电商文案自动生成流程
大数据·人工智能
gaoshengdainzi1 天前
GB/T23448-2019卫生洁具软管专用检测设备全套解决方案
大数据·卫生洁具软管检测设备·软管试验机
茶靡花开04151 天前
什么是DMS经销商管理系统?经销商管理系统哪个好?
大数据·人工智能
Gofarlic_OMS1 天前
HyperWorks用户仿真行为分析与许可证资源分点配置
java·大数据·运维·服务器·人工智能
fire-flyer1 天前
ClickHouse系列(二):MergeTree 家族详解
大数据·数据库·clickhouse