- 正常执行则提交事务
例子
// 定义事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName(“SomeTxName”);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// txManager,事务管理器
// 通过事务管理器开启一个事务
TransactionStatus status = txManager.getTransaction(def);
try {
// 完成自己的业务逻辑
}
catch (MyException ex) {
// 出现异常,进行回滚
txManager.rollback(status);
throw ex;
}
// 正常执行完成,提交事务
txManager.commit(status);
Spring事务抽象的关键就是事务策略的概念,事务策略是通过TransactionManager
接口定义的。TransactionManager
本身只是一个标记接口,它有两个直接子接口
-
ReactiveTransactionManager
,这个接口主要用于在响应式编程模型下,不是我们要讨论的重点 -
PlatformTransactionManager
,命令式编程模型下我们使用这个接口。
public interface PlatformTransactionManager extends TransactionManager {
// 开启事务
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
通常来说,我们不会直接实现这个接口,而是通过继承AbstractPlatformTransactionManager
,这个类是一个抽象类,主要用作事务管理的模板,这个抽象类已经实现了事务的传播行为以及跟事务相关的同步管理
TransactionDefinition
它的主要完成了对事务定义的抽象,这些定义有些是数据库层面本身就有的
- 在
AbstractPlatformTransactionManager
提供了四个常见的子类,其说明如下
Mysql事务
四大特性ACID
-
原子性 Atomicity
-
一致性 Consistency
-
隔离型 Isolation
-
持久性 Durability
事务的隔离级别
-
读未提交 ,存在脏读、不可重复读、幻读问题
-
读已提交,解决脏读问题,存在不可重复读、幻读问题
-
可重复读,解决脏读、不可重复读问题,存在幻读问题
-
可串行化,解决所有问题
不可重复读跟幻读的区别在于,「前者是数据发生了变化,后者是数据的行数发生了变化」。
保存点
我们可以在事务执行的过程中定义保存点,在回滚时直接指定回滚到指定的保存点而不是事务开始之初,有点像我们玩游戏的时候可以存档而不是每次都要重新再来
定义保存点的语法如下:
SAVEPOINT 保存点名称;
当我们想回滚到某个保存点时,可以使用下边这个语句(下边语句中的单词WORK
和SAVEPOINT
是可有可无的):
ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称;
Mysql几个重要知识点
-
「MySQL默认采用的是自动提交的方式」,也就是说如果不是显示的开始一个事务,则系统会自动向数据库提交结果。在当前连接中,还可以通过设置AUTOCONNIT变量来启用或者禁用自动提交模式。
-
MySQL的默认隔离级别是可重复读(REPEATABLE READ)
-
ACID
中的一致性是事务的最终目标,前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性
事务实现原理
-
InnoDB
是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正**「处理数据的过程是发生在内存中的」,「所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上」。而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB
存储引擎需要一条一条的把记录从磁盘上读出来么?不,那样会慢死,InnoDB
采取的方式是:「将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 *16* KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。」** -
我们还需要对MySQL中的日志有一定了解。MySQL的日志有很多种,如二进制日志(bin log)、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:「redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。」
-
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了**「缓存(Buffer Pool)」,Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:「当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。」**
-
InnoDB
存储引擎文件主要可以分为两类,表空间文件及重做日志文件(redo log file),表空间文件又可以细分为两类,共享表空间跟独立表空间。「undo log位于共享表空间中的undo段中」,每个表空间都被划分成了若干个页面,「凡是页面的读写都在buffer pool中进行,这意味着undo log也需要先写入到buffer pool,所以undo log的生成也需要持久化,也就是说undo log的生成需要记录对应的redo log」。(注意:不是所有的undo log的生成都会产生对应的redo log,对于操作临时表生成的undo log并不会生成对应的undo log,因为修改临时表而产生的undo日志
只需要在系统运行过程中有效,如果系统奔溃了,那么在重启时也不需要恢复这些undo
日志所在的页面,所以在写针对临时表的Undo页面
时,并不需要记录相应的redo日志
。)
持久性实现原理
通过前面的补充知识我们知道InnoDB引入了Buffer Pool
来优化读写的性能,但是虽然Buffer Pool
优化了性能,但同时也带来了新的问题:「如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证」。
基于此,redo log
就诞生了,「redo log是物理日志,记录的是数据库中数据库中物理页的情况」,redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。在概念上,innodb通过**「force log at commit」**机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。
看到这里可能有的小伙伴又会有疑问了,既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
这里我以文章开头的例子进行说明redo log
为何能保证持久性:
// 第一步:开始事务
start transaction;
// 第二步:A账户余额减少减少1000
update balance set money = money -500 where name= ‘A’;
// 第三步:B账户余额增加1000
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事务
commit;
❝ 这里需要对redo log的刷盘补充一点内容:
MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,「默认为1」。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。
-
当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()函数刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
-
当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer(内核缓冲区),而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
-
当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。
「可以看到设置为0或者2时,都有可能丢失1s的数据」
原子性实现原理
前面提到了,所谓原子性就是指整个事务是一个不可分隔的整体,组成事务的一组SQL要么全部成功,要么全部失败,要达到这个目的就意味着当某一个SQL执行失败时,我们要能够撤销掉其它SQL的执行结果,在MySQL中这是依赖undo log(回滚日志)
来实现。
undo log属于**「逻辑日志」(前面提到的redo log属于物理日志,记录的是数据页的情况),我们可以这么认为,「当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。」**
但执行发生异常时,会根据undo log中的记录进行回滚。undo log主要分为两种
-
insert undo log
-
update undo log
「insert undo log是指在insert 操作中产生的undo log」,因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。
「而update undo log记录的是对*delete 和update*操作产生的undo log」,该undo log可能需要提供MVCC机制,因此不能再事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
❝ 补充:purge线程两个主要作用是:清理undo页和清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操作实际上并不是真正的删除掉数据行,而是一种Delete Mark操作,在记录上标识Delete_Bit,而不删除记录。是一种"假删除",只是做了个标记,真正的删除工作需要后台purge线程去完成。
❞
这里我们就来看看insert undo log的结构,如下:
在上图中,undo type
记录的是undo log的类型,对于insert undo log
,该值始终为11(TRX_UNDO_INSERT_REC
),undo no
在一个事务中是从0
开始递增的,也就是说只要事务没提交,每生成一条undo日志
,那么该条日志的undo no
就增1。table id记录undo log所对应的表对象。如果记录中的主键只包含一个列,那么在类型为TRX_UNDO_INSERT_REC
的u ndo日志
中只需要把该列占用的存储空间大小和真实值记录下来,如果记录中的主键包含多个列(复合主键),那么每个列占用的存储空间大小和对应的真实值都需要记录下来(图中的len
就代表列占用的存储空间大小,value
就代表列的真实值),「在回滚时只需要根据主键找到对应的列然后删除即可」。end of record记录了下一条undo log在页面中开始的地址,start of record记录了本条undo log在页面中开始的地址。
对undo log有一定了解后,我们再回头看看文章开头的例子,分析下为什么undo log能保证原子性
// 第一步:开始事务