TT Lab
开始
学习 学习路径 课程

幂等性 — 点两次也只扣一次款

两位顾客同时预订最后一件商品

在 TT Lab 中继续学习

目标

把库存扣减、预留 id 和取消以原子方式绑在一起,防止超卖。

为什么重要

库存只剩一件,两个请求同时查询,结果都预留成功了。之后同一条取消消息到达了两次,库存比原来还多了。预留的去重与取消的去重是两种不同的状态转换,而且剩余数量的检查也必须与扣减处于同一个事务中。

步骤

  1. 在 /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
  1. 在 /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)。

  2. 在 /root/work/idem-stock-reservation-lab/service.py 中,add_stock(path,sku,amount) 检查是否为(不含 bool 的)大于等于 0 的 int,并且只对新商品执行 INSERT。重复的商品抛出 IntegrityError。

  3. 在 /root/work/idem-stock-reservation-lab/service.py 中,available(path,sku) 返回已保存的数量,商品不存在时抛出 KeyError。

  4. 在 /root/work/idem-stock-reservation-lab/service.py 中,reserve(path,rid,sku,qty,fault=lambda:None) 先校验数量。已有的预留 id 如果商品和数量都相同就返回 False,不同则抛出 ValueError。新的预留要确认商品存在和库存,依次执行扣减 → fault → 插入预留,然后返回 True。库存不足或商品不存在时抛出 ValueError。

  5. 在 /root/work/idem-stock-reservation-lab/service.py 中,cancel(path,rid) 在预留不存在或已被取消时返回 False。如果是 active 的预留,则在同一个事务中执行 cancelled=1 和归还库存,并返回 True。

  6. 在 /root/work/idem-stock-reservation-lab/service.py 中,reservation(path,rid) 返回 (sku,qty,cancelled) 元组或 None。

  7. 在 /root/work/idem-stock-reservation-lab/service.py 中,compete(path,sku) 用两个不同的 id(first 和 second)、数量均为 1,在两个线程中执行 reserve。返回按输入顺序排列的结果列表,其中只把 ValueError 转换为 False。

参考

把数量固定为整数契约

在 /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 确认。