# 盘点 **Repository Path**: enioy/take-stock ## Basic Information - **Project Name**: 盘点 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-04 - **Last Updated**: 2026-07-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 成品盘点并发/阻塞复现实验 这个目录是独立实验工程,只创建和操作 `sim_` 前缀表,不修改现有业务代码。 现在推荐优先使用 `src/field_inventory_replay.py`。它会自动完成: 1. 重建 `sim_` 表。 2. 插入固定扫码包池。 3. 造 4w 主表、16w 明细。 4. 启动多名盘点人线程,按“扫码 -> 查包信息 -> 查 7 天是否盘过 -> 整批提交”的现场流程回放。 5. 自动采集接口耗时、阻塞采样、重复包号。 6. 输出 Markdown 和 JSON 报告。 实验目标是复现 2026-06-30 大盘点中“扫码检查 + 整批提交”的真实窗口: 1. PDA 扫码后先查包信息。 2. 每个包号调用 `GET /FinishedInventory/checkWeatherInventor`,按真实接口查询 7 天内最近一次盘点记录。 3. 多个 PDA/重复提交进入 `POST /FinishedInventory/finishedInventory`。 4. 后端批量查询最近 7 天旧记录、删除旧明细、插入新明细。 5. 如果没有最终包号级 cleanup,就可能留下多张新单中的同包号明细。 ## 文件说明 - `sql/00_create_schema.sql`:创建 `sim_finished_inventor`、`sim_finished_inventor_detail`、`sim_finished_inventor_detail_log` 和 delete/update 日志触发器。 - `sql/01_seed_data.sql`:初始化旧盘点单、8 个重点包号和 5000 条无关历史数据。 - `sql/02_create_indexes.sql`:创建推荐索引,用于和无索引场景对比。 - `sql/03_drop_indexes.sql`:删除模拟索引,观察慢查询/阻塞更明显的情况。 - `sql/04_observe_blocking.sql`:查看当前阻塞、锁、活跃事务和最近 deadlock graph。 - `sql/05_verify_duplicates.sql`:统计重复包号、明细和触发器日志。 - `src/finished_inventory_sim.py`:并发提交复现程序。 ## 初始化 只在测试库执行,脚本会删除同名 `sim_` 表。 ```sql -- 1. 建表 :r sql/00_create_schema.sql -- 2. 造数 :r sql/01_seed_data.sql -- 3. 可选:创建索引 :r sql/02_create_indexes.sql ``` Navicat 不支持 `:r` 时,就按顺序打开三个 SQL 文件分别执行。 SSMS 使用 `:r` 前需要打开 `查询 -> SQLCMD 模式`;没开的话也可以像 Navicat 一样逐个文件执行。 ## Python 环境 ```bash cd /Users/sxw/code/hy/inventory-lock-sim python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` 设置连接串示例: ```bash export SQLSERVER_CONN='DRIVER={ODBC Driver 17 for SQL Server};SERVER=192.168.0.105;DATABASE=sim;UID=sa;PWD=123456;TrustServerCertificate=yes;Encrypt=no' ``` 如果本机装的是 ODBC Driver 17,把 `DRIVER` 改成 `{ODBC Driver 17 for SQL Server}`。 ## 推荐:全自动现场回放 默认每次运行都会自动还原数据库,只操作 `sim_` 表: ```bash cd /Users/sxw/code/hy/inventory-lock-sim source .venv/bin/activate python src/field_inventory_replay.py ``` 如果没有用环境变量,也可以直接传连接串: ```bash python src/field_inventory_replay.py \ --conn 'DRIVER={ODBC Driver 17 for SQL Server};SERVER=192.168.0.105;DATABASE=HYTestBak;UID=sa;PWD=123456;TrustServerCertificate=yes;Encrypt=no' ``` 默认配置: - 主表:`40000` 行 - 明细:`160000` 行 - 固定扫码包池:24 个包号,包含 RCVB、RCV、U9SN - 盘点人:按 6 月 30 日排查时看到的几个人配置 - 每个包模拟调用: - `GET /FinishedInventory/getInfoBySerialNo` - `GET /FinishedInventory/checkWeatherInventor` - 每批模拟调用: - `POST /FinishedInventory/finishedInventory` - 默认不加索引、不手工造锁、不做最终 cleanup 当前还原范围: - RCV/RCVB 的 `getInfoBySerialNo` 按真实后端 SQL 形状还原:`SourceMaterialLot left join BaseMaterials where SerialNo = ?`,对应模拟表 `sim_source_material_lot`、`sim_base_materials`。 - `checkWeatherInventor` 按真实前端接口 SQL 形状还原:`finished_inventor_detail left join finished_inventor ... where serialNo = ? and createTime >= ? order by createTime desc`,不主动加 `NOLOCK`。 - `finishedInventory` 提交里的 7 天覆盖查询按真实批量 SQL 形状还原:`WITH (NOLOCK)` + `ROW_NUMBER()` 取每个包号最新一条旧明细,再删除旧明细、插入新明细。 - U9SN 普通 MES 包的 `getInfoBySerialNo` 当前仍用 `sim_label_pool` 简化返回包信息,没有完整还原 `ProdLots / ProdLotTrace / ProductionOrder / T_HY_MesToU9_CompRpt` 链路。 报告输出到: ```text /Users/sxw/code/hy/inventory-lock-sim/reports/ ``` 每次会生成: - `field-replay-YYYYMMDD-HHMMSS.md` - `field-replay-YYYYMMDD-HHMMSS.json` 报告里重点看: - `重复多出来明细数` - `接口耗时` - `提交记录` - `重复包号 Top 100` - `阻塞采样` ### 常用测试模式 正常现场回放,不同步提交: ```bash python src/field_inventory_replay.py ``` 模拟第一波多人几乎同时点“确定”: ```bash python src/field_inventory_replay.py --sync-submit ``` 验证最终 cleanup 是否能收敛同包号重复: ```bash python src/field_inventory_replay.py --cleanup ``` 不重建数据库,只在当前 `sim_` 数据上继续跑: ```bash python src/field_inventory_replay.py --no-reset ``` 缩小数据量快速试跑: ```bash python src/field_inventory_replay.py --header-count 1000 --detail-count 4000 ``` 调慢扫码/提交节奏,更贴近现场: ```bash python src/field_inventory_replay.py \ --scan-delay-min-ms 1000 \ --scan-delay-max-ms 5000 \ --submit-delay-min-ms 10000 \ --submit-delay-max-ms 60000 ``` ## 阻塞压测看板 如果目标是复现“现场大量阻塞”,使用: ```bash python src/field_inventory_replay.py --stress-blocking ``` 这个模式会启动实时终端看板: - 顶部:重复盘点统计、主表/明细数量、阻塞采样数、deadlock 数。 - 左侧:每一行一个线程,显示当前步骤、正在处理的包号、轮次、包数、完成次数、耗时。 - 右侧:SQL Server DMV 采集到的阻塞/等待结果。 - 底部:每个线程的步骤日志。 默认压力: - PDA 线程:20 个 - 每个 PDA:10 轮 - 固定小包池:8 个包,所有 PDA 从同一个小池里取,制造高重叠 - 扫码检查线程:4 个,只模拟 `GET /FinishedInventory/checkWeatherInventor` - 检查查询按真实接口写法执行,不主动加锁、不主动等待,只观察自然并发下 SQL Server 是否出现阻塞 更激烈一点: ```bash python src/field_inventory_replay.py \ --stress-blocking \ --stress-workers 40 \ --stress-rounds 20 \ --stress-pool-size 8 \ --query-workers 8 \ --monitor-interval-ms 300 ``` 先小数据试看板: ```bash python src/field_inventory_replay.py \ --stress-blocking \ --header-count 2000 \ --detail-count 8000 \ --stress-workers 8 \ --stress-rounds 3 \ --query-workers 2 ``` 看板结束后同样会生成报告到 `reports/`。 ## 复现当前漏洞 下面这个 `finished_inventory_sim.py` 是旧的诊断型脚本,用于人为制造锁等待;如果要贴近现场,优先用上面的 `field_inventory_replay.py`。 先重新造数,保证环境干净: ```sql -- 执行 sql/01_seed_data.sql ``` 然后运行: ```bash python src/finished_inventory_sim.py \ --mode legacy \ --workers 7 \ --lock-holder-seconds 12 \ --delay-ms 300 ``` 同时在 SSMS 另一个窗口反复执行: ```sql -- sql/04_observe_blocking.sql ``` 运行完成后执行: ```sql -- sql/05_verify_duplicates.sql ``` 预期现象: - 输出里通常只有一个 worker 删除了旧明细,其它 worker `deleted=0`。 - `sim_finished_inventor_detail` 里同一批 `RCV...` 包号会出现在多张新单上。 - `sim_finished_inventor_detail_log` 会看到旧明细 delete 日志,但不会自动删除这些新插入的重复明细。 这和 2026-06-30 的证据很接近:旧记录被删除过,但多个新单据明细仍然都留了下来。 ## 验证最终 cleanup 方案 重新造数后运行: ```bash python src/finished_inventory_sim.py \ --mode final-cleanup \ --workers 7 \ --lock-holder-seconds 12 \ --delay-ms 300 ``` 再执行 `sql/05_verify_duplicates.sql`。 预期现象: - 即使多个请求都插入过明细,最终同包号只保留 `createTime` 最新、`detailId` 最大的那条。 - 触发器日志里能看到 cleanup 删除旧的新明细。 ## 复现 SQL Server 死锁 这个模式专门制造两个事务反向锁定两个包号,用来观察 SQL Server 1205 deadlock victim: ```bash python src/finished_inventory_sim.py --mode deadlock-pair --lock-holder-seconds 0 ``` 然后查看 `sql/04_observe_blocking.sql` 中的 system_health deadlock graph。 ## 索引对比 无索引场景: ```sql -- sql/03_drop_indexes.sql -- sql/01_seed_data.sql ``` 有索引场景: ```sql -- sql/01_seed_data.sql -- sql/02_create_indexes.sql ``` 重点看: - 查询 7 天旧记录是否走 `sim_finished_inventor_detail(serialNo)` seek。 - 删除旧明细是否能通过主键/明细 id 快速定位。 - `sim_finished_inventor(createTime DESC, id)` 是否能降低按时间过滤/排序成本。 索引只能减少慢查询和锁持有时间,不能解决覆盖算法漏洞。覆盖算法仍然需要提交后按包号最终收敛,或者加包号级幂等/锁。