前言

因为自己对数据的可靠性,可用性方面特别感兴趣,所以在MySQL事务方面看了很多资料,也看了很多博客,所以想到自己也写一篇博客整理整理自己所学内容,尽量用自己的语言解释得通俗易懂。

事务经典场景

在很多介绍事务的博客都会代入这样一个场景,先简单说说:

A给B转账100,A少100,B多100。如果A少100后系统崩溃怎么办?B的钱多不了,这样金钱总数凭空少了100。这里就需要用到事务了。

什么是事务?

事务是恢复和并发控制的基本单位,事务有四个特性(ACID),原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。本文主要围绕这四个特性展开介绍。

原子性

原子性就是不可拆分的特性,要么全部成功然后提交(commit),要么全部失败然后回滚(rollback)。若开启事务,在上述场景就不会出现 A少100 成功,B多100 失败 这种情况。MySQL通过Redo Log重做日志实现了原子性,在将执行SQL语句时,会先写入redo log buffer,再执行SQL语句,若SQL语句执行出错就会根据redo log buffer中的记录来执行回滚操作,由此拥有原子性。

一致性

一致性指事务将数据库从一种状态转变为下一种一致的状态。比如有一个字段name有唯一索引约束,那么在事务前后都不能有重复的name出现违反唯一索引约束,否则回滚。在上述场景中即金钱总数总是200,不能凭空增加减少。MySQL通过undo Log实现一致性,执行SQL语句时,会先写入undo log再写入 redo log buffer。undo是逻辑日志,会根据之前的SQL语句进行相应回滚,比如之前是insert那么回滚时会执行一个delete,一个update会执行 一个相反的update。并且除了回滚,undo log还有一个作用是MVCC,当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可通过undo读取之前的行版本信息,实现非锁定读取。并且undo log也会产生redo log,因为undo log也需要持久性的保护。

隔离性

首先介绍如果没有隔离性会发生的4种情况

丢失更新

A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来,MySQL通过三级封锁协议的第一级解决了丢失更新,事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。

时间取款事务A转账事务B
T1开始事务
T2
开始事务
T3查询账户余额为1000元
T4
查询账户余额为1000元
T5
汇入100元把余额改为1100元
T6
提交事务
T7取出100元把余额改为900元
T8撤销事务
T9余额恢复为1000 元(丢失更新)
脏读

脏读主要是读取到了其他事务的数据,而其他事务随后发生回滚。MySQL通过三级封锁协议的第二级解决了脏读,在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。

时间取款事务A转账事务B
T1开始事务
T2
开始事务
T3查询账户余额为1000元
T4

T5
汇入100元把余额改为1100元
T6查询账户余额为1100元(脏读)
T7
撤销事务
T8汇入100元以为是1200元
不可重复读

不可重复读是读取到数据后,随后其他事务对数据发生了修改,无法再次读取。MySQL通过三级封锁协议的第三级解决了不可重复读。在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。

时间取款事务A转账事务B
T1开始事务
T2
开始事务
T3查询账户余额为1000元
T4

T5
汇入100元把余额改为1100元
T6查询账户余额为1100元(不可重复读)
T7
提交事务
T8提交事务
幻读

幻读是读取到数据后,随后其他事务对数据发生了新增,无法再次读取。在InnoDB引擎Repeatable Read的隔离级别下,MySQL通过Next-Key Lock以及MVCC解决了幻读,事务中分为当前读以及快照读。

1.快照读(snapshot read)  ------通过MVCC来避免幻读

简单的select操作(不包括 select ... lock in share mode, select ... for update)

2.当前读(current read)  ------通过Next-Key Lock 来避免幻读  Next-Key Lock即间隙锁(Gap Lock)+行锁 (Record Lock)

select ... lock in share mode

select ... for update

insert

update

delete

时间取款事务A转账事务B
T1开始事务
T2
开始事务
T3查询账户余额为1000元 RMB 100元美元
T4

T5
汇入100欧元
T6查询账户余额为1000元 RMB 100元美元 100欧元(幻读)
T7
提交事务
T8提交事务

事务有四个隔离级别

Read Uncommitted

解决了丢失更新

Read Committed

解决了丢失更新+脏读

Repeatable Read

解决了丢失更新+脏读+不可重复读 (Innodb下也解决了幻读,原理上文已说明)

Serializable

解决了丢失更新+脏读+不可重复读+幻读

从上至下,性能越差,安全性越优。

持久性

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。具体实现原理就是在事务commit之前会将,redo log buffer中的数据持久化到硬盘中的redo log file,这样在commit的时候,硬盘中已经有了我们修改或新增的数据,由此做到持久化。

总结

简单总结了一下MySQL事务,对于Redo Undo没有做到了如指掌的掌握所以介绍篇幅不太大,随着学习深入以后会进行相应补充。

参考资料

-----《MySQL技术内幕 InnoDB存储引擎》 第2版