大坑后记: sql事务关于select for update那点事

本文通过一个具体的场景,探讨了在处理数据库事务时,由于并发导致的消息记录未被正确删除的问题。分析指出,即使在事务中,简单的select后再update也可能引发不一致性。解决方案是使用`select for update`来锁定数据,确保事务处理过程的安全性。文章以实例解析了`select for update`如何避免并发更新带来的问题,并总结了这一经验教训。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.场景描述

这是开发中的一个项目,有以下场景:

  1. 对于一条消息m,有若干个接收者(r1, r2)。
  2. 接收者(假设为r1)收到消息后,发送确认消息。
  3. 收到r1的确认消息后,服务器从消息m的接收者中删除对应的接收者(r1)。
  4. 服务器检查消息m的接收者队列,如果为空,则删除消息M

在极偶然的情况下,竟然发现,当r1,r2都发出了消息确认,而消息M的数据库记录中的接收者队列也确实是空,但是消息M竟然没被删除!

2.伪代码

 服务器关于接收确认消息的处理伪代码大概如下:

// 开启事务
begin_transaction()
// 从db中找到消息m
// select * from M where id = some_id
m = find(some_id)
// 从db中找到消息m的接收者
receivers = find_receivers(m)
// 从db中删除消息m的接收者r
del_receiver(m, r) 
// 从receivers中删除该接收者r
receivers = remove(receivers, r)
if (receivers.empty()) {
    // 如果接收者队列为空,即没有接收者了,则删除消息m
    del_m(m)
}
commit_transaction()

3.疑问

如果简单的把事务当成锁的话,那上面的逻辑无懈可击,因为最后一个确认消息到达后,消息的接收者队列必然会清空,消息肯定会被删除。但残酷的现实表示,不一致状态还是出现了:消息的接收者队列是空的,但是消息没有被删除。

发生这种情况的唯一解释就是,r1, r2的确认消息同时到达,而该事务同时进行。在查找m的接收这队列时,两个事务返回的接收者队列都是[r1, r2]。然后r1的事务删除了r1从而留下了接收者队列为[r2],而r2的事务删除了r2留下了接收者队列为[r1]。两者都不为空,从而无法进入if语句删除消息m。

4.解决办法

发生这个问题的根本原因就是,就算是在事务里,先select再根据结果进行update这种行为也是不安全的。原因就是,当select数据后,此时另一个事务如果改变了该数据,对原事务来说,select的结果也是没有变化的。以这种数据进行后续处理,则整个业务逻辑就变得不安全了。

经过一系列的搜索,终于找到了解药,那便是select for update。从db中select消息m的时候,在select语句后面添加for update,即可对此条数据加一个“锁”

select * from M where id = some_id for update

回到场景中来,此时,r1,r2的确认消息同时到来,事务同时开始。r1开始执行select for update语句找到m,数据库会将m加锁。当r2也想select for update m的时候,它就会被阻塞,直到r1的事务完成后,锁被释放,r2的事务才开始。这样,消息m必然在最后一个确认者确认后进入if语句删除自己。

5.后记

这是笔者一点初步的个人理解,特此小结一下,以方便日后回看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值