mysql事务详解
以下都是针对InnoDb引擎, 描述的都是单机事务,说明一下个人对事务的一些理解。
事务的四大特性
这四个特性并不是平级的关系。准确的说, 一致性是最基本的属性,其他的属性都是为了保证一致性而存在的。
Atomicity 原子性
整个事务是一个整体,是程序运行时不可分割的最小工作单位。
一个事务中的所有操作要么全部执行成功,要么全部都不执行。任何一条语句执行失败,都会导致事务回滚。
实现方式:undo log,当执行失败的时候,使用undo log进行回滚。
Isolation 隔离性
一个事务所做的修改在该事务未提交之前,对其他事务不可见。
为什么会出现隔离性的需求?
本质是因为并发, 试想一下, 如果没有并发,所有事务都是串行执行的,那么也就不会存在隔离性的问题了。所以mysql提出了隔离级别的概念, 针对不同的隔离级别,进行不同的处理, 这在下文的隔离级别会有详细描述。
那么, 如何解决并发造成的数据一致性问题?
第一个想法自然是加锁,给数据加锁,相当于让各个事务串行执行,自然就不存在数据一致性的问题了。同时为了提高加锁的效率和粒度,mysql针对锁提出了不同的类别,比如间隙锁、排他锁等。
加锁会造成一个比较大的问题, 就是并发效率不高,即使在锁上面进行优化, 也只能治标不治本,所以要寻找一种不加锁的方式, 答案就是数据冗余, mysql基于此实现了mvcc(多版本并发控制), 下文会有详细描述。
实现方式:锁和mvcc
Durability 持久性
数据一旦提交,结果就是永久性的,并不因为宕机、重启等情况丢失。
一般理解就是写入硬盘保存成功。
实现方式:redo log。数据写入磁盘是通过redo log进行的, redo log存储的是对于每个页修改的物理描述。
Consistency 一致性
数据库的记录总是从一个一致性状态转变成另一个一致性状态。
这里的一致性是语义上的一致性, 并不是语法上的一致性, 可以理解为需要达到用户期望的状态。
比如经典的银行转账例子,若A转账200给B,则需要执行如下三步:
- 检查A账户余额是否大于200
- 从A账户减去200
- 将B账户增加200
用户期望的状态就是A减去200,同时B增加200。
在事务的概念中,以上三个步骤必须处于同一个事务中,若不是处于同一事务中,当程序执行到3的时候系统异常了,就会导致用户的钱被扣。在真正的银行事务中,不仅需要强事务来保证一致性,而且会有一个日终对账系统,来计算每天所有的资金流向,保证整个资金流动值为0。
实现方式:通过回滚,恢复以及并发情况下的隔离从而实现的一致性,可以理解为其他三个特性是为了保持一致性。
事务的隔离级别
首先介绍一下关于数据不一致的几个概念:
- 脏读:事务A读到了事务B未提交的数据, 此时事务A读到的数据就是一个脏数据
- 不可重复读:在同一个事务中, 前后两次相同的查询, 返回的结果不一样, 针对的是update的操作
- 幻读: 在同一个事务中,前后两次相同的查询, 返回的结果不一样, 针对的是insert和delete的操作
隔离级别总共分为四大类(注意与上面数据不一致类型的区分):
- 未提交读
- 提交读
- 可重复读
- 串行读
针对每个隔离级别是否会出现脏读、不可重复读、幻读的情况如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | 可能 | 可能 | 可能 |
提交读 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行读 | 不可能 | 不可能 | 不可能 |
未提交读 Read Uncommited
一个事务所做的修改在该事务未提交前对其他事务可见。
造成的结果就是会形成脏读, 因此一般不采用。
提交读 Read Commited
只能读取到其他事务已经提交的数据。
即一个事务所做的修改在该事务未提交之前对其他事务不可见。
此级别能够解决脏读的问题,但是有可能会产生不可重复读的问题。
Orcale默认数据隔离级别就是提交读。
可重复读 Repetable Read
保证在同一个事务中,两次读取到的数据是一致的。
该级别能解决数据不一致中的不可重复读
的问题,但无法解决幻读(注意此处指的是一般意义上的概念,mysql InnoDB在该级别下是不存在幻读的,具体原因见后面分析)。
mysql InnoDb引擎默认的数据库隔离级别就是可重复读。
串行读
顾名思义,所有的读操作都是串行的,这样做肯定不会存在数据一致性问题, 但无疑效率是最低的。
Spring中的@Transactional注意事项
该注解是Spring中用来控制事务的注解,在使用时有以下几点需要注意:
- @Transactional 注解默认回滚
RuntimeExcepption
异常,注意此处即使异常被处理(即catch捕获了该类型的RuntimeException异常),Spring仍然会回滚事务 - @Transactional注解有一个参数rollbackFor可用来指定当自定义Exception发生时,也会进行事务回滚
- @Transactional注解只能作用于public方法上,虽然也能写在非public上且不会报错,但实际上是不起作用的
Spring中事务的传播级别
以下例子中用到的表结构:
1 | create table user1 ( |
在Spring中定义的事务的传播级主要有如下几种,现就其中几个主要的说明如下
PROPAGATION_REQUIRED
按需加载。Spring默认的传播级别。
当上下文中已经存在事务,则加入到该事务执行。若上下文不存在事务,则新建一个事务执行。
此处上下文可以理解为@Transactional注解作用的地方,当该注解作用于方法时,上下文从方法最先开始的地方开启,也就是说在刚进入方法时,首先就会开启一个事务。
以下分几种情况进行说明:
(1)当外部方法未开启事务时, 若外部方法抛出异常, 如何运行?
User1Service代码
1 |
|
User2Service代码
1 |
|
测试代码:
1 | public void t1() { |
执行结果: user1, user2 均插入成功
分析: 因为外围方法未开启事务,所以程序执行到两个addUser方法时都会开启一个新的事务,这两个事务是相互独立的,分别执行自己的逻辑,插入成功,外部方法异常不影响内部.
(2)当外部方法未开启事务时, 若内部方法抛出异常, 如何运行?
User1Service代码:
1 |
|
User2Service代码:
1 |
|
测试代码:
1 | public void t1() { |
结果: user1插入成功, user2插入失败
分析: 还是因为外部方法未开启事务, 所以user1, user2会开启两个独立的事务, 互不影响。 所以user1插入成功, user2插入失败。
(3)当外部方法开启事务时, 若内部方法或者外部方法抛出异常, 如何运行?
User1Service和User2Service代码同之前, 只是在测试方法上也加上@Transactional注解。
测试方法代码如下:
1 |
|
结果: user1, user2都未插入
分析: 外部方法开启事务,内部两个方法都会加入到该事务中成为一个事务,外部方法回滚,则所有回滚
PROPAGATION_NESTED
事务之间是嵌套的。若上下文存在事务,则嵌套执行,若不存在事务,则新建事务执行。
原理:子事务是父事务的一部分,当进入子事务之前,会先在父事务建立一个回滚点save point,然后执行子事务。待子事务执行结束,再执行父事务。
总结起来就是: 子事务的执行不会影响父事务,但父事务的执行会影响子事务。
举个例子, 若methodA以PROPAGATION_REQUIRED修饰, methodB以PROPAGATION_NESTED修饰, 并且在methodA中调用methodB, 此时A为父事务, B为子事务。 其执行情况如下:
异常情况 | 执行结果 |
---|---|
A抛异常,B正常 | A, B 都回滚 |
A正常, B异常 | B先回滚, A再正常提交 |
A,B都抛异常 | A , B 都回滚 |
问题1: 若子事务回滚,会发生什么?
若子事务回滚,则父事务会回滚到之前建立的save point,然后执行其他业务逻辑,父事务之前的操作不受影响,更不会自动回滚。所以父事务不受子事务的影响。
问题2:若父事务回滚,子事务会回滚吗?
答:子事务会回滚
问题3:子事务和父事务谁先提交?
答:子事务先提交
PROPAGATION_SUPPORTS
支持当前事务,若没有当前事务,则以非事务执行
PROPAGATION_MANDATORY
使用当前事务,注意这是强制性的(mandatory),因为当发现当前没有事务时,直接抛出异常
PROPAGATION_REQUIRES_NEW
不管上下文中是否存在事务,每次都新建一个独立事务。
若上下文中存在事务,则先会将上下文事务挂起,然后执行本方法事务,待本方法事务执行完毕之后,继续执行上下文事务。
若上下文未开启事务,其行为逻辑与requires级别一致,即新建一个自己独立的事务。
当外部方法开启事务时,requires_new修饰的内部方法依然会单独开启独立事务,且与外部方法的事务相互独立,内部方法之间、内部方法和外部方法的事务均相互独立,互不影响。
PROPAGATION_NOT_SUPPORTED
以非事务方式执行代码,若当前存在事务,则将当前事务挂起
PROPAGATION_NEVER
以非事务执行,该处语气比上一个更强,因为当发现当前存在事务时,则直接抛出异常