事务特性
ACID:原子性、一致性、隔离性、持久性
- 原子性:一个事务要么全部成功,要么全部失败
- 一致性:事务提交前后,数据库保持一致性状态
- 隔离性:一个事务所做的修改,其他事务不可见
- 持久性:事务一旦提交,所做修改永久保存到数据库中
关系:
原子性 + 隔离性 ---> 一致性 ---> 结果正确
持久性 ----> 应对数据库崩溃
并发一致性
当多个事务并发执行时会导致事务不能保证一致性,导致结果出错。
丢失修改、读脏数据、不可重复读、幻读
- 丢失修改:对于同一数据,T1 修改数据后,T2 又修改数据,T1修改读数据被覆盖
- 读脏数据:T1修改数据后,T2读该数据,T1又回滚数据,T2读脏数据
- 不可重复读:T1读数据后,T2对该数据进行修改,T1再读该数据,读得的数据值不一致
- 幻读:T1读数据(例如count统计表中行数),T2插入一行数据,导致数据不一致
三级封锁协议
- 一级封锁:当更新数据时,立即加X锁,直到事务结束。 ==> 解决了丢失修改
- 二级封锁:在1上,当读数据时,立即加S锁,读完立即释放锁。 ==> 解决了读脏数据
- 三级封锁:在2上,当读数据时,立即加S锁,直到事务结束。 ==> 解决了不可重复读
两段锁协议
加锁和解锁分成两个阶段。一个阶段加锁,一个阶段解锁。保证可串行性化调度。
隔离级别
- 未提交读:事务的修改未提交前,其他事务可见 读:不加锁; 更新 : 行级共享锁
- 提交读: 事务的修改在未提交前,其他事务不可见 读:行级共享锁; 更新: 行级排他锁
- 可重复读: 一个事务中的多次读结果一致 读:行级共享锁; 更新:行级排他锁
- 可串行化:事务串行执行 读:表级共享锁; 更新:表级排他锁
解决的问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | - | - | - |
提交读 | - | - | |
可重复读 | - | ||
可串行化 |
mysql事务隔离级别的实现
MVCC(多版本并发控制)
- 版本号
1.系统版本号
一个递增的数字,每开始一个新事务,系统版本号自动递增
2.事务版本号
事务开始的系统版本号
- 隐藏列
MVCC的每行记录后面都保存着两个隐藏的列
- 创建版本号,创建一个数据行的快照时的版本号
- 删除版本号,
MVCC具体实现
1.select:满足以下两个条件innodb会返回该行数据:
(1)该行的创建版本号小于等于当前版本号,用于保证在select操作之前所有的操作已经执行落地。
(2)该行的删除版本号大于当前版本或者为空。删除版本号大于当前版本意味着有一个并发事务将该行删除了。
2.insert:将新插入的行的创建版本号设置为当前系统的版本号。
3.delete:将要删除的行的删除版本号设置为当前系统的版本号。
4.update:不执行原地update,而是转换成insert + delete。将旧行的删除版本号设置为当前版本号,并将新行insert同时设置创建版本号为当前版本号。
例子
以下表格仅为示意图,方便理解
1). insert操作(事务版本为1):事务1,插入两行数据
id | data | 创建版本号 | 删除版本号 |
---|---|---|---|
1 | 事务1.1 | 1 | - |
2 | 事务1.2 | 1 | - |
2). delete操作(事务版本为2):事务2,删除id为1数据
id | data | 创建版本号 | 删除版本号 |
---|---|---|---|
1 | 事务1.1 | 1 | 2 |
2 | 事务1.2 | 1 | - |
3). update操作(事务版本为3):事务3,更新id为2数据,=>(delete+insert)
id | data | 创建版本号 | 删除版本号 |
---|---|---|---|
1 | 事务1.1 | 1 | 2 |
2 | 事务1.2 | 1 | 3 |
2 | 事务3 | 3 | - |
4). select操作(事务版本为4):事务4,查询所有行,返回结果如下,
id | data | 创建版本号 | 删除版本号 |
---|---|---|---|
2 | 事务3 | 3 | - |
5). 两个事务:事务5(查询id=2),事务6(更新id=2)
事务5未执行,事务6执行完
id | data | 创建版本号 | 删除版本号 |
---|---|---|---|
1 | 事务1.1 | 1 | 2 |
2 | 事务1.2 | 1 | 3 |
2 | 事务3 | 3 | 6 |
2 | 事务6 | 6 | - |
事务5执行完,第4行创建版本号6>事务版本号5,第3行创建版本号3<=事务版本号5<=删除版本号,故返回为
id | data | 创建版本号 | 删除版本号 |
---|---|---|---|
2 | 事务3 | 3 | 6 |
快照读与当前读
通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据。这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)。
- 快照读
当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。
- 当前读
对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。 读取的是最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update;
delete;
Next-Key Lock
InnoDB有三种行锁的算法:
Record Lock:单个行记录上的锁,锁定记录上的索引。
Gap Lock:间隙锁,锁定索引之间的间隙,但是不包含索引本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。