什么是事务?
- 事务是一个不可分割的工作单元,工作单元要么工作完成,要么什么也不做。
- 从应用层面来说一个事务对应了一个完整的业务功能。
- 从数据库层面的来讲事务就是由一批DML语句构成。
事务的分类
MySQL的InnoDB存储引擎支持扁平事务、带有保存点的事务、链事务、分布式事务。
- 扁平事务(Flat Transactions)
扁平事务应用最为广泛,实现最为简单,扁平事务的所有操作都是在同一个层级,这些操作要么全部成功,要么全部回滚,不能存在部分提交或者部分回滚的的场景。
- 带保存点的扁平事务(Flat Transactions with Sacepoint)
扁平事务的限制就在于不能部分回滚或者提交,而有的场景是这么做是代价非常大的。比如我们举个例子:
我们玩生存类游戏,如果我们意外失败就必须从出生地开始玩,那么这会是让人崩溃的,我们希望有一个游戏存档,如果游戏失败我们可以从最近的一个存档重新加载游戏。
带保存点的扁平事务就是,除了支持扁平事务的操作外,允许事务执行过程中回滚到该事务较早的一个状态,而这个较早的状态就是保存点来记录的。
- 链事务(Chained Transaction)
链事务是一种保存点事务的变种,两者的最大区别是,带保存点的事务可以回滚到较早前的任意保存点,而链式事务只能回滚到最近一个保存点;带保存点的事务因为需要回滚到任意保存点,固其事务执行期间所占用的资源是不会被释放的,而链事务则在执行完成当前节点后会释放掉不需要的资源,并将下一个节点需要的资源隐士传递下去。链事务可以参考Flink流式计算的Checkpoint机制,两者非常的相似。
- 嵌套事务(Nested Transaction)
嵌套事务顾名思义,事务结构看上去就像一棵树,根节点就是一个顶层事务,所有的叶子节点都是扁平事务(也就是说叶子节点才是真正干活儿的),事务的嵌套层级不受限制。子事务可以提交也可以回滚,但是其提交不会立即生效,只有在顶层事务提交之后所有子事务才会被真正的提交。
- 分布式事务(Distributed Transactions)
分布式事务是指一个在分布式环境下运行的扁平事务,在本章中主要介绍本地事务,分布式事务我会在后续章节是介绍。
事务的ACID特性
- A(Atomicity)原子性:整个事务操作是一个完整的不可分割的整体,只有事务中的所有操作都执行成功,事务才算执行成功,否则就要回滚到事务执行前的状态,即要么全部都做,要么全都不做。
例如:转账场景,自己账户扣除转账额度与对方账户收到转账这两个操作必须是原子的。
- C(Consistency)一致性:事务将数据库从一种状态转变为另一种状态,在事务执行前和事务执行后,数据库的完整性约束没有被破坏。
例如:用户表的用户ID列有unique约束,即用户ID不可重复,如果事务执行插入了一样的用户ID,那么就产生了不一致的状态。
- I(Isolation)隔离性:隔离性(又称并发控制)非常好理解,就是两个事务之间不能相互影响,即当前事务提交之前所作出的修改对其他事务都不可见,上一章我们讲到了MySQL锁,它就可以起到控制并发的作用。
- D(Durability)持久性:持久性是指事务一旦提交,其结果就是永久性的,即使是服务器宕机,数据也必须能够得到恢复,除了硬件故障,数据物理损坏,否则必须保证事务执行结果的永久性,也就是保证高可靠性(High Reliablility)。
事务如何实现
事务的原子性、一致性、持久性通过redo log与undo log来完成,事务的隔离性由锁与MVCC来完成。
Redo log(重做日志)
Redo log是用来实现事务的持久性,为了更好的读写性能,InnoDB会将数据缓存在内存中,对磁盘数据的修改也会落后于内存,如果进程或系统崩溃,则数据面临丢失的风险,这时重做日志就起到了保证数据的一致性与持久性作用。重做日志主要记录了以页为单位的数据修改信息,其结构如下:
- 重做日志在Buffer中是连续写入的,Buffer中的数据会适时地刷新到物理文件中;
- 文件顺序写入,每个事务的重做日志追加到文件末尾;
- 单个文件大小固定,写满以后会切回到文件组的下一个文件;
- 重做日志文件组的文件个数是固定的,写完最后一个文件则继续回到第一个文件开始写入;
- 每个重做文件有固定2K的文件头,文件头的之后是以一个个512bytes的Block,每个Block有16bytes的头尾信息;
- 重做日志有一个全局的日志序列号(LSN:Log Sequence Number),单调递增,表示事务写入的重做日志的字节总量,也就是一个日志偏移量。
Undo log(回滚日志)
重做日志记录了事务的行为,可以在需要的时候对页进行“重做”,但是事务有时是需要被回滚的,当语句执行失败或者用户请求回滚,就可以通过undo log将数据回滚到修改前的样子,undo log是存储了行记录的变更。其主要包含两类undo log:
- insert undo log:insert操作时产生,只对当前事务本身可见,在事务提交之后可直接删除。
- update undo log:delete与update操作产生,需要提供历史版本,为后续章节要讲到的MVCC服务,其交由purge线程统一删除。
- undo log需要通过group commit 操作将数据fsync到磁盘,以保证事务的持久性。
下面是一个事务与undo log的关系结构:
事务隔离
事务在并发场景下很难保证事务的隔离性一致性,主要有以下一些事务的并发一致性问题。
事务并发问题
- 脏读(Dirty Read):事务A读取了另外一个并行事务B未提交的数据。
- 不可重复读(Non-Repeatable Read):在解决脏读问题之后,能够保证读事务读取到的数据都是持久的数据,如果事务A多次读取同一数据,正好在两次读取之间,另外一个并行事务B提交了这一数据的修改,这就导致事务A多次读取到的同一数据内容不一样。
- 幻读(Phantom):与不可重复读类似,事务A多次查询一个范围,另外一个并行事务B向该范围内插入或删除了数据并提交,当事务A再次查询时发现记录变多或者丢失。
- 更新丢失(Lost Updates):两个事务A和B修改了同一数据,由于未提交事务之间看不到对方的修改,因此都以一个旧的前提去更新了同一数据。
- 写偏差(Write Skew):与更新丢失类似,都是写前提被改变,写偏差则是事务A读取某些数据,作为另一些写入的前提条件(更新丢失是针对同一数据),但这时另外一个事务B对事务A已读取的数据做了修改并提交,从而导致事务A做了错误的commit操作。
- 读偏差(Read Skew):如事务A读取某两个数据求和,事务B在事务A读取期间对已读取数据做了增减,此时事务A求和得到的结果就会与实际的结果不一致。
针对上面的并发问题,InnoDB存储引擎通过MVCC(当然MVCC本质上也是一种乐观锁)与锁(关于锁的介绍可以阅读我的上一篇文章)来解决事务的隔离性一致性问题。
事务隔离级别
事务隔离级别是MySQL对ACID的实现程度上的分级,分为了四个等级,等级越高数据库越安全,每种隔离级别解决了不同事务并发一致性的问题,具体如下:
- READ UNCOMMITTED(读未提交):这是一个最差的隔离级别,该级别下事务可以读到其它事务未提交的数据,也就是说在该事务隔离级别下会发生上述的所有并发一致性问题。
- READ COMMITTED(读已提交):事务只能读取到已提交的修改,也就是说多个并发的事务之间的修改是相互不可见的,该事务隔离级别解决了脏读问题。
- REPEATABLE READ(可重复读):该级别保证同一个事务中多次读取同一数据的结果是一致的,该级别是InnoDB默认的隔离级别,该隔离级别解决了脏读与不可重复读问题,但是仍可能出现幻读的情况(InnoDB存储引擎在该隔离级别下使用了Next-Key Lock解决了幻读问题)。
- SERIALIZABLE(串行化):强制事务串行化执行,没有并发,那么并发问题自然就不存在了,当然在该级别下的事务性能非常低。
关于事务隔离的实现会在后续文章详细讲解,本文不在展开。
事务的执行过程
- 查询数据,若数据不存在于buffer,则从磁盘加载;
- 数据更新前,先将当前数据记录到undo log重,以便后续可能出现的回滚做准备;
- 更新Buffer Pool中的数据;
- 将更新的数据写入到Redo Log Buffer中;
- 准备提交事务,调用fsync将Redo Log Buffer的数据写入到redo log文件中,状态记为prepared;
- 准备提交事务,binlog写入到磁盘中;
- binlog写入成功后,将redo log的状态更新为commit;
binlog的开启时会存在一个内部XA的问题(binlog是在MySQL层,而redo log在存储引擎层),这里引入了2PC(二阶段提交):
- prepare阶段:redo log持久化到磁盘,同时设置状态为prepared,binlog此时不错任何操作。
- commit阶段:存储引擎释放锁,是否回滚段,然后binlog持久化到磁盘,然后存储引擎层提交,更改redo log的状态为commit。