Repeatable Read隔离级别下,并发事务可能产生Update丢失的现象:
https://stackoverflow.com/questions/53562850/mysql-repeatable-read-isolation-level-and-lost-update-phenomena
根据测试,这种情况下丢失的Update语句会返回affected rows为0(即使重新Select可以看到满足Update条件的数据行数不为0)。解决办法:
- 每次执行修改语句时,不要丢弃affected rows返回值,而是判断一下返回值是否大于0(目前主要发现Update,但是既然这么重要的场景也被看做“实现细节”,建议能判断的都判断一下)
- 在可能出现竞争写的场合下,使用SELECT…FOR UPDATE语句,以保证计算结果符合预期
24/07/04更新:
根据链接中的问题描述在数据库(5.7和8.0都试了)重新验证,发现并不能出现我所说的affected rows为0的情况,多次尝试后,终于复现了当时说的情况,这里记录一下:
准备工作
CREATE TABLE test_lost_update (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
`key` VARCHAR(2),
val VARCHAR(2)
);
INSERT INTO test_lost_update (`key`, val) VALUES ('k1','v1'),('k2','v2');
看一下结果SELECT * FROM test_lost_update;
打开两个连接(DataGrip可以右键数据源点击new Query,注意START TRANSACTION之前可能要手动调整 Tx:Auto 为 Tx:Manual):
第一个连接执行(注释的序号是操作步骤的顺序,注意两个连接是穿插执行的):
-- 1
START TRANSACTION;
SELECT * FROM test_lost_update WHERE `key`='k1';
-- 4
UPDATE test_lost_update SET val = 'v4' WHERE `key`='k1';
SELECT * FROM test_lost_update;
COMMIT;
第二个连接执行:
-- 2
START TRANSACTION;
SELECT * FROM test_lost_update WHERE `key`='k1';
-- 3
UPDATE test_lost_update SET `key` = 'k3' WHERE `key`='k1';
COMMIT;
可以发现,第4步的update返回确实是0,而且执行第4步的select的时候能查到对应条件确实有数据的,并且这一步的update和select可以反复执行,执行一次惊你一次!更不用说你的Java程序会有什么想法了。
看来所谓 Repeatable Read,并不包括 Consistant View Between Read And Write (我乱造的短语)!
观察发现,关键点在于第一个事务的Update语句要去查询一个已经被另一个事务更新了的字段(并且已经Commit)。
这可能和MVCC+乐观锁实现的方式有关,具体原理待调查补充(待续)。