MySQL事务

事务是在存储引擎层实现的。

ACID特性

1.原子性
事务中的所有操作要么全部提交成功,要么全部失败回滚。
2.一致性
数据库总是从一个一致性状态转换到另外一个一致性状态。
3.隔离性
两个事务之间的隔离程度,与隔离级别有关。
4.持久性
事务提交后所做的修改会永久保存到数据库中。

隔离性与隔离级别

隔离级别越高,效率越低。
1.读未提交(不用)
事务还没提交时,它做的变更能被别的事务看到。
2.读已提交
事务提交后,它做的变更才会被其他事务看到。
3.可重复读(默认事务隔离级别)
在同一个事务中执行相同的查询结果一致。
4.串行化(不用)
强制事务串行执行,写会加写锁,读会加读锁。

隔离级别

查询结果

读已提交

V1是1,V2和V3都是2

可重复读

V1和V2都是1,V3是2

在可重复读的隔离级别下,视图是在事务启动时创建的,整个事务执行期间都用这个视图。在读已提交的隔离级别下,视图是在每个SQL语句开始执行时创建的。

事务启动时刻

CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB;
INSERT INTO t (id, k) VALUES (1, 1), (2, 2);

begin/start transaction不是事务的起点,第一个操作InnoDB表的语句,事务才真正启动。
start transaction with consistent snapshot 马上启动事务。
事务B查到的k的值是3,事务A查到的k的值是1。

实现可重复读

为什么表结构不支持可重复读?
因为表结构没有事务id,所以遵循当前读。

undo log

每条记录更新时会记录一条回滚操作到回滚日志中。通过回滚操作可以得到上一个状态的值。
当系统里没有比这个回滚日志更早的视图时,回滚日志会被删除。

不同时刻启动的事务会有不同的视图,视图A、B、C里面记录的值分别是1、2、4。
同一个记录在系统中可以存在多个版本即数据库的多版本并发控制(MVCC)。

当前读比一致性读快得多案例

session B更新完100万次,生成了100万个回滚日志(undo log)。
当前读直接读到1000001这个结果,速度快;一致性读,从1000001开始,依次执行undo log,执行了100万次以后,才将1这个结果返回。
undo log里记录的是“把2改成1”,“把3改成2”这样的操作逻辑,画成减1方便看图。

MySQL有两个视图的概念:
1.用查询语句定义的虚拟表。创建视图的语法是create view …。
InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。

快照在MVCC里是怎么工作的?

在可重复读隔离级别下,事务在启动时就拍了个整库的快照。
InnoDB里面每个事务有唯一的事务ID即 transaction id,在事务开始时向InnoDB的事务系统申请的,按申请顺序严格递增。
每行数据有多个版本,每个版本都有transaction id。
一个记录被多个事务连续更新后的状态

图中虚线框里是同一行数据的4个版本,当前最新版本是V4,k的值是22,它是被transaction id 为25的事务更新的,因此它的row trx_id也是25。
语句更新会生成undo log(回滚日志)。3个虚线箭头,就是undo log。

按事务id从小到大排序
已提交事务->未提交事务->已提交事务->未提交事务(包含当前事务在内)->...
InnoDB在事务启动时构造视图,记录未提交事务和当前事务的id列表。
行记录版本是当前事务id,或者行记录版本不是视图内未提交事务id且不超过当前事务id,可见;其他,不可见。

基于当前读回溯版本来查询
1.事务A开始前,只有一个活跃(未提交)事务ID是99;
2.事务A、B、C的版本号分别是100、101、102,当前只有这四个事务;
3.3个事务开始前,(1,1)这一行数据的row trx_id是90。
事务A的视图数组就是[99,100], 事务B的视图数组是[99,100,101], 事务C的视图数组是[99,100,101,102]。
跟事务A查询有关的操作(事务B不马上提交,事务C马上提交):

事务C把数据从(1,1)改成了(1,2),新版本row trx_id是102,90成为历史版本。
事务B把数据从(1,2)改成了(1,3),新版本row trx_id是101,102成为历史版本。
读数据都是从当前版本读起,事务A查询语句流程:
1 找到(1,3)的时候,row trx_id=101比100大,不可见;
2 找到上一个历史版本,row trx_id=102比100大,不可见;
3 找到上一个历史版本,(1,1)的row trx_id=90,比99小,可见。
虽然这一行数据被修改过,但是事务A查询时看到这行数据的结果都是一致的。

基于当前读已提交的数据来更新

事务C更新后马上提交。
如果事务B在更新之前查询数据,那么查询返回的k=1。
事务B更新时当前读拿到的数据是(1,2),更新后生成了新版本的数据(1,3),这个新版本的row trx_id是101。
事务B查询时最新版本号是101,事务版本号也是101,查询得到的k=3。
除了update语句外,select语句加锁后也是当前读。即事务A的查询语句加上lock in share mode(读锁,S锁,共享锁)或 for update(X锁,排他锁)。

事务C’更新后没有马上提交,加了行锁,提交前事务B的更新语句发起了,阻塞等到事务C’释放行锁,才能继续更新。

读提交和可重复读区别

1. 针对普通查询,可重复读是快照读,读提交是当前读,读取被更新很多次的同一条记录时读提交远快于可重复读。
2. 针对事务启动时刻,可重复读是执行第一条sql语句或者start transaction with consistent snapshot;而读提交是start transaction。
3. 针对幻读,可重复读中普通查询没有该问题,当前读通过间隙锁阻塞其他事务插入新记录来解决,读提交有该问题,可重复读有间隙锁而读提交没有。可重复读没有真正解决幻读问题,因为更新了其他事务的新插入记录后在下次普通查询就可以读到。
4. 针对加锁,读提交行锁在语句执行完成后释放“不满足条件的行”,不需要等到事务提交,比可重复读的加锁范围更小且加锁时间更短;可重复读遵守两阶段锁协议,加锁在事务提交或者回滚的时候才释放。

参考资料

MySQL 实战 45 讲

posted on   王景迁  阅读(14)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

导航

统计

点击右上角即可分享
微信分享提示