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

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

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


一、竞态条件是什么?

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

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

  • 用户 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. 流程图辅助设计:用流程图梳理「识别资源 -> 设计场景 -> 执行 -> 校验」全流程。

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

相关推荐
QYR_114 小时前
香叶醇行业深度解析:香精香料领域核心原料的发展潜力与挑战
大数据·人工智能·物联网
港股研究社6 小时前
腾讯音乐的多元增长新路径:音乐IP经济
大数据·人工智能·tcp/ip
GIOTTO情6 小时前
技术解析:Infoseek基于AI重构媒介投放全链路,适配2026年奥斯卡高端投放场景
大数据·人工智能
Data-Miner6 小时前
46页精品PPT | 数据治理大数据平台资源规划与建设解决方案
大数据
信道者6 小时前
乌克兰开放战场数据宝库:AI无人机迎来“实战级”进化
大数据·人工智能·无人机
margu_1687 小时前
【Elasticsearch】es7.2单节点集群内索引重组迁移
大数据·elasticsearch
武子康7 小时前
大数据-251 离线数仓 - Airflow 安装部署避坑指南:1.10.11 与 2.x 命令差异、MySQL 配置与错误排查
大数据·后端·apache hive
Elastic 中国社区官方博客7 小时前
用于 Elasticsearch 的 Gemini CLI 扩展,包含工具和技能
大数据·开发语言·人工智能·elasticsearch·搜索引擎·全文检索