背景介绍
事务的特点是ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性).
隔离性的意思就是两个事务各执行各的。
谈到隔离性必须提数据库的四个问题和解决对应问题的隔离级别,
场景/问题 解决方案/隔离级别
读未提交(read uncommitted )
脏读(dirty read) 读提交(read committed)
不可重复读(non-repeatable read) 可重复读(repeatable read)
幻读(phantom read) 串行化(serializable )
查看当前数据库的隔离级别
Oracle 默认是读提交
MySQL 是可重复读
查看当前的隔离级别:
show variables like 'tx_isolation'; 5.6之前用这个
show variables like 'transaction_isolation'; 5.7及以后
设置MYSQL的隔离级别
set session transaction isolation level read uncommitted;
set session transaction isolation level read committed;
set session transaction isolation level repeatable read;
set session transaction isolation level serializable;
读提交和当前读又是什么意思
当前如果有一个A事务要更新某一行,并且另外一个B事务拥有这一行的行锁,那当前A事务就会被锁住,等等到B事务提交才能继续。
等B事务已经提交了,理论上B事务的提交时间是A启动之后的,应该看不到B的修改(一致性读),但是实际上某些场景,可以读到B的修改。这就是当前读。
一个观察当前读的小实验
0.先确认当前数据库的隔离级别
设置成读提交
set session transaction isolation level read committed;
1. 建表语句
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),(3,3);
2. 执行顺序
A | B | C |
set session transaction isolation level read committed; start transaction with consistent snapshot ; select k from t where id =1; | ||
set session transaction isolation level read committed; start transaction with consistent snapshot ; | ||
update t set k=k+1 where id =1; | ||
update t set k=k+1 where id =1; select k from t where id =1; //?? | ||
select k from t where id =1; //??? | ||
commit; | ||
select k from t where id =1; select k from t where id =1 for update ; commit; |
3. 实验分析
实验完成了吧,说明C 的 commit 在AB读之前,所以对他们都有影响,AB在读的时候都会进行当前读。
另外B的提交后于A的读,所以B的update 对A 不可见。
但是如果 select for update ; 就会触发当前读。
哈哈哈
可重复读是什么
可重复读是,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据
是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的(这一点读提交级别就能保证了)。
可重复读的使用场景大致就是 类似对账操作,当一个事务开始的时候要获取一个逻辑上不变的静态快照,让这个操作不受操作启动后其他的修改操作的影响。
可重复读的复现和实验观察
把数据的隔离级别设置成为可重复读,把上面的执行流程重新操作一遍,就会有不一样的结果
update t set k=id;
A | B | C |
set session transaction isolation level repeatable read; start transaction with consistent snapshot ; | ||
set session transaction isolation level repeatable read; start transaction with consistent snapshot ; | ||
update t set k=k+1 where id =1; | ||
update t set k=k+1 where id =1; select k from t where id =1; //?? | ||
select k from t where id =1; //??? | ||
commit; | ||
select k from t where id =1; select k from t where id =1 for update ; commit; |
可重复读隔离级别的实现逻辑
MYSQL 里面有一个MVCC 多版本并发控制的解决方案。
首先,InnoDB 里面每一个事务都会有一个全局唯一,按顺序单调递增的事务ID transaction id。
每一行数据是有多个版本的,每次事务更新数据,都有一个数据版本,并关联上本次修改操作的事务ID,计做 row trx_id。
另外,当前事务A启动的时候,会对整个数据库的所有事务进行分类,分别是:
基本原则是,事务启动时已经commit的就可见,还没commit的就不可见。
用逻辑来实现就是:
1. A (id=15)启动时就已经commit过的事务,(<=10 的,和13),肯定可见的。
2. A (id=15)启动时已经启动,但是还没有commit的事务,组成了一个数组,最低点的事务Id叫低水位,最大的叫做高水位。[ 11 ,12,14 ]
3. 如果A查询到的数据的版本在高低水位之间,那就看在不在集合里面,在上面集合里的
A启动之后开始的事务,大于高水位,肯定不可见。(>15的所有事务)
解析: 如果当前A读取到的某行记录上面的有10,11,12,13,14,16,17的更改,那么 10小于低水位11肯定可见;17大于高水位14 肯定不可见;11,12在集合里面,不可见,13不在集合里面,可见。
undo log
那么,如果确定了哪些应该可见那些不可见又怎么加载数据呢:
undo log就是每次数据更新的时候生成的回滚日志,是逻辑日志,可以关联数据的多个版本,用于版本回退。
innoDB在事务提交的时候,先将事务的所有日志写入磁盘(redo log file 和undo log file)。
启动时确定的这个所有事务的分类就是形成了一个稳定的一致性视图 read view。
也就是说,不同时候启动的事务是通过回滚日志来实现的,回滚日志会一直保留到日志对应时间之前的开始的事务都已经全部提交才能进行清理。
所以说长事务不仅会增加锁竞争,还会导致数据库里会有大量的回滚日志 undo log.
MYSQL 5.5之前回滚日志是和数据放在一起的(ibdata文件),有可能数据几十G,回滚段几百G.
当前读是什么
可重复读隔离级别中的可重复读本身是一致性读。
但是就算是可重复读隔离级别,更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)
如果selecte 加锁的话也是当前读。当前读会读到所有已经commit的数据。
select k from t where id=1 lock in share mode; //共享锁
select k from t where id=1 for update; //排他锁
幻读是什么
幻读也在可重复读的隔离级别才会发生。
同一次事务,但是前后两次查询同一范围,后一次看到第一次没有看到的行。
可重复读在普通查询进行快照读,只有在当前读财汇出现幻读。
幻读专门指新插入的行。
怎么解决幻读
幻读之所以会发生是因为 ,行锁只能锁住行,即使把所有的行记录都上锁,也阻止不了新插入的记录。解决幻读就需要使用到 间隙锁。
解决幻读就要用升级到串行化隔离级别,写有写锁,读有读锁,读写冲突时后访问的事务需要等待(前一个事务提交)。