xiaoxiao
发布于 2024-02-06 / 32 阅读 / 0 评论 / 0 点赞

MariaDB (MySQL)可重复读在特定情况并发事务下的bug

这个是2023年生产问题之一,今天有空记录一下。

代码大概逻辑如下:

  1. 查询应用A的当前版本信息

  2. 把应用A当前版本字段全部设置为非当前版本

  3. 插入一条应用A当前版本的数据记录

  4. 再次查询应用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的旧版本数据没有刷新。因此第一次查询的旧数据和新插入的数据在最后都被查询出来


评论