事务特性

ACID:原子性、一致性、隔离性、持久性

  • 原子性:一个事务要么全部成功,要么全部失败
  • 一致性:事务提交前后,数据库保持一致性状态
  • 隔离性:一个事务所做的修改,其他事务不可见
  • 持久性:事务一旦提交,所做修改永久保存到数据库中

关系:

原子性 + 隔离性  ---> 一致性   ---> 结果正确

                     持久性  ----> 应对数据库崩溃

并发一致性

当多个事务并发执行时会导致事务不能保证一致性,导致结果出错。

丢失修改、读脏数据、不可重复读、幻读

  • 丢失修改:对于同一数据,T1 修改数据后,T2 又修改数据,T1修改读数据被覆盖
  • 读脏数据:T1修改数据后,T2读该数据,T1又回滚数据,T2读脏数据
  • 不可重复读:T1读数据后,T2对该数据进行修改,T1再读该数据,读得的数据值不一致
  • 幻读:T1读数据(例如count统计表中行数),T2插入一行数据,导致数据不一致

三级封锁协议

  1. 一级封锁:当更新数据时,立即加X锁,直到事务结束。 ==> 解决了丢失修改
  2. 二级封锁:在1上,当读数据时,立即加S锁,读完立即释放锁。 ==> 解决了读脏数据
  3. 三级封锁:在2上,当读数据时,立即加S锁,直到事务结束。 ==> 解决了不可重复读

两段锁协议

加锁和解锁分成两个阶段。一个阶段加锁,一个阶段解锁。保证可串行性化调度。

隔离级别

  • 未提交读:事务的修改未提交前,其他事务可见 读:不加锁; 更新 : 行级共享锁
  • 提交读: 事务的修改在未提交前,其他事务不可见 读:行级共享锁; 更新: 行级排他锁
  • 可重复读: 一个事务中的多次读结果一致 读:行级共享锁; 更新:行级排他锁
  • 可串行化:事务串行执行 读:表级共享锁; 更新:表级排他锁

解决的问题

隔离级别 脏读 不可重复读 幻读
未提交读 - - -
提交读 - -
可重复读 -
可串行化

mysql事务隔离级别的实现

MVCC(多版本并发控制)

  • 版本号

1.系统版本号

一个递增的数字,每开始一个新事务,系统版本号自动递增

2.事务版本号

事务开始的系统版本号

  • 隐藏列

MVCC的每行记录后面都保存着两个隐藏的列

  1. 创建版本号,创建一个数据行的快照时的版本号
  2. 删除版本号,

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有三种行锁的算法:

  1. Record Lock:单个行记录上的锁,锁定记录上的索引

  2. Gap Lock:间隙锁,锁定索引之间的间隙,但是不包含索引本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。

  3. Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

参考: https://juejin.im/post/5cd8283ae51d453a907b4b29