两位顾客同时预订最后一件商品
目标
把库存扣减、预留 id 和取消以原子方式绑在一起,防止超卖。
为什么重要
库存只剩一件,两个请求同时查询,结果都预留成功了。之后同一条取消消息到达了两次,库存比原来还多了。预留的去重与取消的去重是两种不同的状态转换,而且剩余数量的检查也必须与扣减处于同一个事务中。
步骤
- 在
/root/work/idem-stock-reservation-lab/service.py中,quantity(value) 只返回(不含 bool 的)正 int,其余情况抛出 ValueError。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-stock-reservation-lab
test -e /root/work/idem-stock-reservation-lab/service.py || cp /opt/fixtures/ten_labs/idem-stock-reservation-lab/service.py /root/work/idem-stock-reservation-lab/service.py
cd /root/work/idem-stock-reservation-lab
-
在
/root/work/idem-stock-reservation-lab/service.py中,init_db(path) 以幂等方式创建 stock(sku TEXT PRIMARY KEY,available INTEGER NOT NULL) 和 reservations(id TEXT PRIMARY KEY,sku TEXT NOT NULL,qty INTEGER NOT NULL,cancelled INTEGER NOT NULL DEFAULT 0)。 -
在
/root/work/idem-stock-reservation-lab/service.py中,add_stock(path,sku,amount) 检查是否为(不含 bool 的)大于等于 0 的 int,并且只对新商品执行 INSERT。重复的商品抛出 IntegrityError。 -
在
/root/work/idem-stock-reservation-lab/service.py中,available(path,sku) 返回已保存的数量,商品不存在时抛出 KeyError。 -
在
/root/work/idem-stock-reservation-lab/service.py中,reserve(path,rid,sku,qty,fault=lambda:None) 先校验数量。已有的预留 id 如果商品和数量都相同就返回 False,不同则抛出 ValueError。新的预留要确认商品存在和库存,依次执行扣减 → fault → 插入预留,然后返回 True。库存不足或商品不存在时抛出 ValueError。 -
在
/root/work/idem-stock-reservation-lab/service.py中,cancel(path,rid) 在预留不存在或已被取消时返回 False。如果是 active 的预留,则在同一个事务中执行 cancelled=1 和归还库存,并返回 True。 -
在
/root/work/idem-stock-reservation-lab/service.py中,reservation(path,rid) 返回 (sku,qty,cancelled) 元组或 None。 -
在
/root/work/idem-stock-reservation-lab/service.py中,compete(path,sku) 用两个不同的 id(first 和 second)、数量均为 1,在两个线程中执行 reserve。返回按输入顺序排列的结果列表,其中只把 ValueError 转换为 False。
参考
- 无需联网和安装软件包,在现有的 lab-dev 环境中进行。
- 每个步骤都在 45 秒的评分预算内运行。不要加入真实的 sleep 或网络调用。
- 评分会重新导入提交的模块,并用独立的输入和临时 DB 检查。不要直接返回预期值的常量,而要实现契约。
- FastAPI 官方文档 · pytest 官方文档 · Python sqlite3
- 局限:这并不是把支付授权和库存预留绑进同一个分布式事务的实验。预留过期以及支付失败的补偿,属于另外的流程。这里通过一个小型 SQLite DB,学习的是这样的原理:不要轻信查询时看到的库存数字,而要在写入的事务内部再确认一遍。
把数量固定为整数契约
在 /root/work/idem-stock-reservation-lab/service.py 中,quantity(value) 只返回(不含 bool 的)正 int,其余情况抛出 ValueError。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-stock-reservation-lab
test -e /root/work/idem-stock-reservation-lab/service.py || cp /opt/fixtures/ten_labs/idem-stock-reservation-lab/service.py /root/work/idem-stock-reservation-lab/service.py
cd /root/work/idem-stock-reservation-lab
防止负数的预留反而增加库存。
保存后用 bash /opt/lab/checks/idem-stock-reservation-lab/01-contract.sh 确认。
把库存和预留分开保存
在 /root/work/idem-stock-reservation-lab/service.py 中,init_db(path) 以幂等方式创建 stock(sku TEXT PRIMARY KEY,available INTEGER NOT NULL) 和 reservations(id TEXT PRIMARY KEY,sku TEXT NOT NULL,qty INTEGER NOT NULL,cancelled INTEGER NOT NULL DEFAULT 0)。
已取消的预留也要保留,才能区分重复的取消与重新预留。
保存后用 bash /opt/lab/checks/idem-stock-reservation-lab/02-contract.sh 确认。
区分商品初始化与库存变更
在 /root/work/idem-stock-reservation-lab/service.py 中,add_stock(path,sku,amount) 检查是否为(不含 bool 的)大于等于 0 的 int,并且只对新商品执行 INSERT。重复的商品抛出 IntegrityError。
避免同一条初始化命令覆盖正在使用的库存。
保存后用 bash /opt/lab/checks/idem-stock-reservation-lab/03-contract.sh 确认。
查询剩余数量
在 /root/work/idem-stock-reservation-lab/service.py 中,available(path,sku) 返回已保存的数量,商品不存在时抛出 KeyError。
把不存在的商品与缺货区分开,不要掩盖错误的商品 id。
保存后用 bash /opt/lab/checks/idem-stock-reservation-lab/04-contract.sh 确认。
把预留与库存扣减绑在一起
在 /root/work/idem-stock-reservation-lab/service.py 中,reserve(path,rid,sku,qty,fault=lambda:None) 先校验数量。已有的预留 id 如果商品和数量都相同就返回 False,不同则抛出 ValueError。新的预留要确认商品存在和库存,依次执行扣减 → fault → 插入预留,然后返回 True。库存不足或商品不存在时抛出 ValueError。
把检查和扣减放在同一个事务中,出故障时全部回滚。
保存后用 bash /opt/lab/checks/idem-stock-reservation-lab/05-contract.sh 确认。
取消也只生效一次
在 /root/work/idem-stock-reservation-lab/service.py 中,cancel(path,rid) 在预留不存在或已被取消时返回 False。如果是 active 的预留,则在同一个事务中执行 cancelled=1 和归还库存,并返回 True。
不能因为取消消息被重新发送,库存就不断增加。
保存后用 bash /opt/lab/checks/idem-stock-reservation-lab/06-contract.sh 确认。
确认预留状态
在 /root/work/idem-stock-reservation-lab/service.py 中,reservation(path,rid) 返回 (sku,qty,cancelled) 元组或 None。
不只看返回值,还要读取取消状态是否真的留在了 DB 里。
保存后用 bash /opt/lab/checks/idem-stock-reservation-lab/07-contract.sh 确认。
重现最后一件库存的竞争
在 /root/work/idem-stock-reservation-lab/service.py 中,compete(path,sku) 用两个不同的 id(first 和 second)、数量均为 1,在两个线程中执行 reserve。返回按输入顺序排列的结果列表,其中只把 ValueError 转换为 False。
要比较库存为 1 和库存为 2 这两种情形,才能筛掉那种无条件只让一个成功的实现。
保存后用 bash /opt/lab/checks/idem-stock-reservation-lab/08-contract.sh 确认。