前言
最近,有个朋友碰到了一个诡异的bug:更新数据时,明明where条件已经匹配到了,但是执行后数据并没有被更新,还是原来的值。
经过一番沟通分析,虽然最终解决了这个问题,但是我觉得这个问题在实际的业务开发中,遇到的几率还是蛮大的,所以和大家分享一下,这个问题产生的原因,以及对应的解决方案。
问题
朋友的需求其实很简单,每天晚上会有一个定时任务,对一些业务数据做统计及修正。
我以一个简单的表test,复现一下这个bug。
说明:事务隔离级别为repeatable read;如果是rc,不会产生下面的问题。
CREATE TABLE `test` (
`id` INT ( ) NOT NULL,
`num` INT ( ) DEFAULT NULL,
PRIMARY KEY ( `id` )
) ENGINE = INNODB;
INSERT INTO test ( id, num)
VALUES ( 1, 1 ),( 2, 1 ),( 3, 2 ),( 4, 2 );
下面,以代码的形式还原一下逻辑。
mysql> begin;
Query OK, 0 rows affected ( sec)
-- 步骤1:查询数据
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
| 4 | 2 |
+----+-----+
4 rows in set ( sec)
--步骤2:更新数据
mysql> update test set num = 3 where num = 2;
Query OK, 0 rows affected ( sec)
Rows matched: 0 Changed: 0 Warnings: 0
--步骤3:再次查询数据,更新没生效?
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
| 4 | 2 |
+----+-----+
4 rows in set ( sec)
大家可以看到,经过步骤2的更新,步骤3再次查询数据时,没有查到步骤2预计产生的结果。
分析
我们知道,在事务级别为可重复读的情况下,启动一个事务时,MySQL会启动一个视图,在事务执行期间,即使有其他事务修改了数据,事务中看到的数据仍然跟在启动时看到的一样。但是对于本事务中的更新操作,还是要认的。
比如步骤2,理论上步骤2更新之后,步骤3应该可以看到步骤2的执行结果。
那步骤2的问题出现在哪呢?为什么更新没有生效呢?
问题在于,update是当前读,读到的总是最新的数据,步骤2执行的时候,条件可能已经无法匹配到具体的数据了(虽然在当前事务中,看起来是匹配的)。
细心的朋友可能已经看到了,步骤2的执行结果rows affected返回的是0,说明当步骤2影响的行数是0。
总结一下具体的流程:
从图中可以看到,session2已经把num的值更新为了4,
当session1执行更新时,虽然从session1的角度看,where条件可以匹配到2行,但是此时num的值已经发生了变更,所以session1的update并没有影响到对应的数据。
既然session1的更新没有起效,自然第二次查询时,查到的数据与第一次的查询结果是一致的,所以产生了看起来没有更新的bug。
解决的办法其实也很简单,使用乐观锁,判断rows affected影响的行数是否与预期一致即可。
update ...set ... where id = xx and version = xx;
乐观锁,是我们提升系统性能的一个常用手段,不仅可以避免一些隐藏的bug,还可以提升锁的并发度。
总结
朋友遇见的这个bug,背后的原理其实并不难,如果知道update是当前读,并且加上一些预防手段的话,其实完全可以提前避开这个问题。
大家也可以检查一下自己的应用代码,有没有掉进这种坑。
如果本文对大家有帮助,帮忙点个赞关注吧