事务特性之可重复读
可重复读RR(Repeatable Read),说的是,在一个事务内,第一次执行select语句,查到的是所有已提交事务的数据,此后该事务内的所有查询,都会被“冻结”为第一次select那一个时刻的“快照”版本,即使已经有别的事务修改并提交了一些改动
按照多数说法,RR可以实现可重复读
,也能避免幻读
但这个特性只限于对这部分数据只有纯select的情况,如果做了update,那情况就变得有点耐人寻味了(想要保证100%可重复读,就不能在本事务内对数据做修改)
事实说话
先来点现象说明一下
step-1 建表,并插入一条数据
CREATE TABLE `read_repeatable` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`age` int DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
insert into read_repeatable (name,age) values ("用户1",10);
step-2 在本地工具上,开两个连接,比如我用dbeaver
step-3 在“事务1”这个连接里,开启事务,并执行一次select
begin;
select * from read_repeatable where id = 1;
(结果太简单就不贴图了)
step-4 在“事务2”这个连接里,执行sql(不用开事务,让它默认提交)
update read_repeatable set age = 15 where id = 1;
刷新下数据,可以看到age已经被改成了15
step-5 在“事务1”内,再次执行一下select语句
select * from read_repeatable where id = 1;
由于“事务1”此前已经执行过select,所以它后续的查询都被“冻结”,所以遵循可重复读
规则,得到结果仍是age=10这个版本
step-6 在“事务1”内,执行update语句,但只更新name字段
update read_repeatable set name = 'vip用户1' where id = 1;
step-7 可以先自己思考一下,这时候“事务1”再查询,会得到什么结果?当然name=“vip用户1”这个是不出意外的,但是age呢?如果age = 10,那么就得到了一条“不三不四”的数据(name是新数据,age又是老数据)。如果age = 15,那么看起来就违背了“可重复读”这个特性了,所以mysql到底是怎么去抉择的呢?
step-8 在“事务1”内,再次执行select
select * from read_repeatable where id = 1;
总结
通过上述实验,证明了:在有update的情况下,即便隔离级别设置的是RR,也会出现“不可重复读”这种现象
But,上述结论就是一坨答辩般的八股文,如果不能从原理上解释,对后端程序员来说没有任何意义(面试官换个角度问仍然答不上来)
原理解析
首先要了解MVCC机制了,这里我就不多写了,网上资料很多,假如不了解,需要先了解一下才能接着往下读
假设上述实验的“事务1”和“事务2”对应的的DB_TRX_ID分别是100和101
当“事务1”执行select之后,这个事务可见数据的事务id列表就确定了(可见数据不包含事务101),当“事务2”对数据做了修改后,该数据最新版本的DB_TRX_ID就是101
,对“事务1”(100
)不可见,因此能达到“可重复读”的效果。
当“事务1”执行 update
语句,因为只修改了name字段,所以其余字段就原封不动的复制过来(因为写操作,所以不遵循MVCC的限制,直接使用最新值
,这也叫当前读
),所以age就复制的事务101修改后的值(age=15),同时该数据最新的版本的DB_TRX_ID变成了100
,所以对于“事务1”自身而言,本身就是可见的。
同理,假如“事务2”也insert了一条数据,然后“事务1”,执行了一次update,并且更新到了“事务2”新增的这数据,此后“事务1”再次select,也能将其查出来,所以看起来就出现了幻读
。具体就不演示了,原理一样,有兴趣的可以自己试试
最后
其实这就是由MVCC机制本身引起的,所以能从底层把这个原理想通后,对MVCC也会有更进一步的理解
了解这个特性后,在工作中,遇到一些场景,也最好能去思考一下,毕竟程序员还是应该以思考为主,CV为辅