LabHub
学习 学习路径 课程

并发 — 两个人碰同一行时

一个人绝对复现不出来的 bug

在 LabHub 中继续学习

一句话总结

同步性错误不是因为代码有问题,而是因为两个人在同一瞬间触摸到了同一行。所以如果一个人单独测试的话,绝对不会重现。

概念图: 不是因为代码有问题,而是因为两个人在同一瞬间触摸到了同一行 · 库存变成-1。 · 丢失的更新(lost update) · select ... for update

为什么需要—库存变成负数的事故

select qty from stock where id = 1;      -- 1 이 남았다
-- (여기서 다른 사람도 똑같이 1 을 읽는다)
update stock set qty = qty - 1 where id = 1;

两个都读完“还剩1个了”,然后都扣除。库存变成-1。

读→判断→写之间别人可能会插手,这就是问题所在。这个被称为丢失的更新(lost update)

修复的方法有三个。

  1. select ... for update — 读的时候锁定那个行。等待剩下的
  2. 用一句短语结束update stock set qty = qty - 1 where id = 1 and qty > 0
  3. 乐观锁定 — 放在版本栏中where version = %s,如果是0行的话重新尝试

如果2号可以的话,2号是最便宜的。只有在需要阅读和判断的情况下才使用1号。

锁定状态将保持直到交易结束。

for update抓住的行动是commit伊娜rollback一直锁定。所以**在交易中不能调用外部API。**如果那个API需要3秒钟,那么该行将被锁定3秒钟。

交易很短。不锁定等待人或等待网络。

德德拉克——互相等待对方拥有的东西

A: id=1 잠금 → id=2 요청
B: id=2 잠금 → id=1 요청

两者都永远等待。PostgreSQL是deadlock_timeout(基本1秒)发现后面的这个循环,杀死一边。

ERROR:  deadlock detected
DETAIL:  Process 123 waits for ShareLock on transaction 773; blocked by process 122.
         Process 122 waits for ShareLock on transaction 774; blocked by process 123.

错误信息会准确地告诉谁在等什么。如果知道如何阅读这两行,找到原因会更快。

封锁的方法只有一个——让锁定的顺序都一样。如果总是按id升序锁定,就不能产生循环。无论排序标准是什么,重要的是大家都使用相同的标准

而且死亡毒素**完全无法消除。**所以应用程序是40P01遇到的话应该可以重新尝试。

不让无限等待

set lock_timeout = '3s';

如果不挂住这个,锁定等待就会咬住连接并拉长,如果那个连接吃完了线,**不是DB而是应用程序会停止运行。**症状不是“DB很慢”,而是“服务器没有响应”。

在迁移中特别重要。alter table要求ACCESS EXCLUSIVE,如果有一个长查询在前面,后面所有的查询都会排队。lock_timeout没有的话,服务会在那一刻停止。

谁在挡着谁

select pid, pg_blocking_pids(pid), query
from pg_stat_activity
where cardinality(pg_blocking_pids(pid)) > 0;

一条线上出现屏蔽关系。pg_locks比起直接加入,这个更快。

制作队列时 — SKIP LOCKED

如果很多工作人员要从同一个桌子上搬走工作,for update只要用一次,**步行者们就会排队。**因为第二个步行者要等第一个步行者抓住的行。

select id from jobs where state = 'ready'
order by id limit 10
for update skip locked;

skip locked**跳过锁定的工作。**第二个工作者没有等待,直接拿起下一个。没有专用队列中间件,在用DB创建工作队列时的核心一行。

应该对没有行动的东西进行锁定的时候

对于这个用户的结算一次只能结算一个“这样的规则没有锁定的行。这时使用**顾问锁定”。

select pg_try_advisory_lock(12345);   -- 얻으면 true, 아니면 즉시 false

注意两点。键空间是全局的——如果其他功能使用相同的数字,会发生冲突。而且绑定到会话中——在连接池中,在归还之前必须解锁。否则,下一个人会收到被锁定的连接。

整理

同步性问题因为很难重现所以很难。所以这个课程的实践都是实际启动两个会话让它们发生碰撞。在屏幕上直接看到等待和死锁是理解的大部分。

在现场

这个问题在开发环境中几乎无法重现。一个人点击的话总是正常,负载测试也不同商品购买的话不会重叠。实际上,就像限量销售或优惠券发放一样,在所有人都瞄准同一行的时候就会爆发。

因此,在故障后调查中,要怀疑锁定需要很长时间。因为数据库指标全部正常,应用程序日志中只是一串串的“库存扣除成功”记录。唯一的线索是结果数据的一个矛盾——库存为负数,或发行数量超过了限额。

在开始运营之前要确认的两件事是:列出同时触摸同一行的路径,并确定每个路径是否可以用一句短语结束,或者是否需要锁定。然后lock_timeout将设置为会话默认值,即使出错也不会导致连接崩溃。