1.场景描述
这是开发中的一个项目,有以下场景:
- 对于一条消息m,有若干个接收者(r1, r2)。
- 接收者(假设为r1)收到消息后,发送确认消息。
- 收到r1的确认消息后,服务器从消息m的接收者中删除对应的接收者(r1)。
- 服务器检查消息m的接收者队列,如果为空,则删除消息M
在极偶然的情况下,竟然发现,当r1,r2都发出了消息确认,而消息M的数据库记录中的接收者队列也确实是空,但是消息M竟然没被删除!
2.伪代码
服务器关于接收确认消息的处理伪代码大概如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 开启事务 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,即可对此条数据加一个“锁”
1select * 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.后记
这是笔者一点初步的个人理解,特此小结一下,以方便日后回看。
最后
以上就是满意丝袜最近收集整理的关于大坑后记: sql事务关于select for update那点事的全部内容,更多相关大坑后记:内容请搜索靠谱客的其他文章。
发表评论 取消回复