这个是2023年生产问题之一,今天有空记录一下。
代码大概逻辑如下:
查询应用A的当前版本信息
把应用A当前版本字段全部设置为非当前版本
插入一条应用A当前版本的数据记录
再次查询应用A的当前版本信息
以上代码在手动提交的可重复读事务里面一起执行。
理论上在这个事务步骤4的查询只能查到步骤3插入的那一条数据,但是查到了两条记录。程序里因为使用了getOne方法,导致条数没有对上报错了。
不要吐槽为什么要再次查找,而不是复用之前插入的数据,很多人同时一起开发出现这种问题很正常。
我准备了复现的sql,感兴趣可以复现一下。
表结构和初始化数据(必须要有一条数据)
CREATE TABLE test (
id int auto_increment NOT NULL,
app_name varchar(100) NOT NULL,
version varchar(100) NOT NULL,
is_current TINYINT DEFAULT 0 NOT NULL,
CONSTRAINT test_PK PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_general_ci;
CREATE INDEX test_app_name_IDX USING BTREE ON xxl_job.test (app_name);
INSERT INTO test (app_name, version, is_current) VALUES('test_app', uuid(), 1);
程序逻辑对应的SQL
start transaction;
select * from test where app_name = 'test_app' and is_current = 1;
UPDATE test set is_current = 0 where app_name = 'test_app';
INSERT INTO test (app_name, version, is_current) VALUES('test_app', uuid(), 1);
# 程序在这一步干了不少事情比较慢,但是sleep这么久主要是为了手动在客户端工具复现容易
select sleep(5);
select * from test where app_name = 'test_app' and is_current = 1;
commit;
问题解决方法
增加一个update_time字段,更新的时候把更新时间设置进去就行。推测问题的实际原因为MVCC的优化逻辑导致,在update的时候是当前读,所以没有更新到一条数据,导致MVCC的旧版本数据没有刷新。因此第一次查询的旧数据和新插入的数据在最后都被查询出来