一、事务简介
1.事务ACID特性
- 原子性(Atomicity)
- 一致性(Consistent)
- 隔离性(Isolation)
- 持久性(Durable)
2.事务状态
- 活动的(active)
- 部分提交的(partially committed)
- 失败的(failed)
- 中止的(aborted)
- 提交的(committed)
二、事务隔离级别
1.事务并发执行遇到的问题
- 脏写:事务A修改了事务B已经修改但未提交的数据
- 脏读:事务A读取到事务B已经修改但未提交的数据
- 不可重复读:事务A内部的相同查询语句在不同时刻读出的结果不一致,可能因为每次其他事务对该数据进行修改并提交后,事务A都能查询得到最新值
- 幻读:事务A读取到了事务B提交的新增数据,比如一个事务先查询出0<id<10的记录为id=1、3、5、7、9,此时事务B又插入了id=8的记录,事务A再次执行相同查询,读到了id=8的记录
按照严重性:脏写 > 脏读 > 不可重复读 > 幻读
2.隔离级别
- 未提交读(READ UNCOMMITTED)
- 已提交读(READ COMMITTED)
- 可重复读(REPEATABLE READ)
- 可串行化(SERIALIZABLE)
不同的隔离级别,事务隔离越严格,并发副作用越小:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|
未提交读 | Possible | Possible | Possible | 已提交读 | Not Possible | Possible | Possible | 可重复读 | Not Possible | Not Possible | Possible | 可串行化 | Not Possible | Not Possible | Not Possible |
Mysql默认级别是可重复读(REPEATABLE READ),但是也可以解决幻读问题。用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别。
三、MVCC原理
MVCC(Multi-Version Concurrency Control),多版本并发控制机制,就是通过ReadView机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本。
Mysql在已提交读(READ COMMITTED)和可重复读(REPEATABLE READ)隔离级别下都实现了MVCC机制。
1.undo日志版本链
InnoDB为每条记录都添加trx_id和roll_pointer这两个隐藏字段
- trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
每次对一条记录更新后,都会将修改前的数据保留到undo日志中,就算是该记录的一个旧版本,并且通过两个隐藏字段trx_id和roll_pointer把所有undo日志串联起来形成一个历史记录版本链,版本链的头节点就是当前记录最新的值。
2.ReadView
在已提交读(READ COMMITTED)和可重复读(REPEATABLE READ)隔离级别下开启事务,执行查询sql时会生成当前事务的一致性视图ReadView。他们的区别就是生成时机不同:
- 已提交读(READ COMMITTED)每次读取数据前都生成一个ReadView;
- 可重复读(REPEATABLE READ)只在第一次读取数据时生成一个ReadView。
ReadView中主要包含:
- m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
- min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
- max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
- creator_trx_id:表示生成该ReadView事务的事务id。开启事务begin时不会分配事务id,只有执行第一个写操作事务才会分配事务id,只读操作事务id都默认为0。
比如有id为1,2,3这三个事务,id为3的事务提交了。那么此时ReadView:m_ids[1,2],min_trx_id=1,max_trx_id=4。
对于使用已提交读(READ COMMITTED)和可重复读(REPEATABLE READ)隔离级别的事务来说,需要判断版本链中的哪个版本是当前事务可见的。根据版本链对比规则,从版本链里的最新数据开始逐条跟ReadView对比从而得到结果。
对于使用未提交读(READ UNCOMMITTED)隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本;
对于使用可串行化(SERIALIZABLE)隔离级别的事务,规定使用加锁的方式来访问记录;
3.版本链对比规则
- 如果被访问版本的trx_id=creator_trx_id,表明当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id<min_trx_id,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id>=max_trx_id,表明被访问版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果min_trx_id<=被访问版本的trx_id<max_trx_id,需要判断trx_id是不是在m_ids列表中:
- 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被当前事务访问;
- 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被当前事务访问。
四、锁机制
1.锁分类
- 从性能上,分为乐观锁和悲观锁
- 乐观锁:用版本号version对比来实现
- 悲观锁:读锁,写锁都属于悲观锁
- 从对数据库操作的类型上,分为读锁和写锁
- 读锁(共享锁,S锁):阻塞写不阻塞读
- 写锁(排它锁,X锁):读和写都阻塞
- 从对数据操作的粒度上,分为表锁和行锁
- 表锁:对一个表加锁,锁的粒度比较粗
- 行锁:对一条记录加锁,锁的粒度比较细
2.表级别的锁
- 事务A给表加S锁,事务B可以获得该表的以及该表中的某些记录的S锁,但是不可以获得该表的以及该表中的某些记录的X锁
- 事务A给表加X锁,事务B不可以获得该表的以及该表中的某些记录的S锁和X锁
- 意向共享锁,简称IS锁。当事务A准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
- 意向独占锁,简称IX锁。当事务A准备在某条记录上加X锁时,需要先在表级别加一个IX锁。
- IS、IX锁是表级锁,为了之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁。IS锁和IX锁是兼容的,IX锁和IX锁是兼容的。
3.行级别的锁
InnoDB与MYISAM的不同:
3.1记录锁Record Locks
- 当事务A获取了记录的S锁后,事务B可以获取该记录的S锁,但不可以获取X锁
- 当事务A获取了记录的X锁后,事务B不可以获取该记录的S锁和X锁
3.2间隙锁Gap Locks
间隙锁在某些情况下可以解决可重复读(REPEATABLE READ)的幻读问题。
事务在第一次执行读取操作时,幻影记录还不存在,所以无法给幻影记录加上记录锁。间隙锁,锁的就是两个值之间的空隙,为了防止插入幻影记录。
比如表中有id尾1、2、3、10、20的五条数据,间隙就有 id 为 (3,10],(10,20],(20,+∞) 这三个区间。当事务查找8<id<15之间的数据时,不仅查出的记录会加记录锁, (3,10],(10,20]两个区间也会加间隙锁,所以此时想插入id为9的数据是会被阻塞的,直到拥有这个间隙锁的事务提交,才能插入id为9的数据到 (3,10]的区间。
每个索引页都有Infimum记录,表示该页面中最小的记录以及Supremum记录,表示该页面中最大的记录。所以(20,+∞) 这个区间就是指的id为20的记录到Supremum记录之间的间隙。
3.3临键锁Next-key Locks
临键锁就是记录锁与间隙锁的组合,既保护记录,又保护记录所在的间隙,防止插入新数据。
3.4插入意向锁Insert Intention Locks
事务在插入一条记录时需要判断插入位置是不是加了间隙锁。如果有间隙锁,插入操作是会被阻塞的,直到拥有这个间隙锁的事务提交。阻塞时事务需要加一把锁(插入意向锁),表示有意向在某个间隙中插入记录,但是在等待。
插入意向锁并不会阻止别的事务获取该记录上任何类型的锁。
3.5隐式锁
一般情况下INSERT操作是不加锁的。如果事务A插入了一条记录,此时还没有与该记录关联的锁,然后事务B想要给该记录加S锁或者X锁,会先看一下该记录的trx_id是否是当前的活跃事务,如果是就帮当前事务创建一个X锁,然后自己进入等待状态。
trx_id相当于对新插入的记录加了一个隐式锁,保护新插入的记录在本事务提交前不被别的事务访问。
|