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。
实现可重复读
为什么表结构不支持可重复读?
因为表结构没有对应的行数据,也没有row trx_id,所以遵循当前读的逻辑。
每条记录更新时会记录一条回滚操作到回滚日志中。通过回滚操作可以得到上一个状态的值。
当系统里没有比这个回滚日志更早的视图时,回滚日志会被删除。
不同时刻启动的事务会有不同的视图,视图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。
InnoDB为每个事务构造视图,记录事务启动后未提交事务ID。
事务ID最小值记为低水位,事务ID最大值加1记为高水位。
低于低水位版本由已提交事务生成(可见),等于低水位版本由未提交事务生成(不可见),高于或者等于高水位版本由未开始事务生成(不可见)。当前事务算已提交事务。
基于当前读回溯版本来查询
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 对于读提交,每一个语句执行前都会创建一个新的视图,查询只承认在语句启动前已提交的数据。
start transaction with consistent snapshot;在读提交隔离级别下等效于普通的start transaction。
当前读,总是读取已经提交完成的最新版本。