以下都是针对InnoDb引擎, 描述的都是单机事务,说明一下个人对事务的一些理解。

事务的四大特性

这四个特性并不是平级的关系。准确的说, 一致性是最基本的属性,其他的属性都是为了保证一致性而存在的。

Atomicity 原子性

整个事务是一个整体,是程序运行时不可分割的最小工作单位。

一个事务中的所有操作要么全部执行成功,要么全部都不执行。任何一条语句执行失败,都会导致事务回滚。

实现方式:undo log,当执行失败的时候,使用undo log进行回滚。

Isolation 隔离性

一个事务所做的修改在该事务未提交之前,对其他事务不可见。

为什么会出现隔离性的需求?

本质是因为并发, 试想一下, 如果没有并发,所有事务都是串行执行的,那么也就不会存在隔离性的问题了。所以mysql提出了隔离级别的概念, 针对不同的隔离级别,进行不同的处理, 这在下文的隔离级别会有详细描述。

那么, 如何解决并发造成的数据一致性问题?

第一个想法自然是加锁,给数据加锁,相当于让各个事务串行执行,自然就不存在数据一致性的问题了。同时为了提高加锁的效率和粒度,mysql针对锁提出了不同的类别,比如间隙锁、排他锁等。

加锁会造成一个比较大的问题, 就是并发效率不高,即使在锁上面进行优化, 也只能治标不治本,所以要寻找一种不加锁的方式, 答案就是数据冗余, mysql基于此实现了mvcc(多版本并发控制), 下文会有详细描述。

实现方式:锁和mvcc

Durability 持久性

数据一旦提交,结果就是永久性的,并不因为宕机、重启等情况丢失。

一般理解就是写入硬盘保存成功。

实现方式:redo log。数据写入磁盘是通过redo log进行的, redo log存储的是对于每个页修改的物理描述。

Consistency 一致性

数据库的记录总是从一个一致性状态转变成另一个一致性状态。

这里的一致性是语义上的一致性, 并不是语法上的一致性, 可以理解为需要达到用户期望的状态。

比如经典的银行转账例子,若A转账200给B,则需要执行如下三步:

  1. 检查A账户余额是否大于200
  2. 从A账户减去200
  3. 将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
2
3
4
5
6
7
8
9
10
create table user1 (  
id int unsigned not null auto increment,
name varchar(32) not null,
primary key(id)
) engine=InnoDB;
create table user2 (
id int unsigned not null auto increment,
name varchar(32) not null,
primary key(id)
) engine=InnoDB

在Spring中定义的事务的传播级主要有如下几种,现就其中几个主要的说明如下

PROPAGATION_REQUIRED

按需加载。Spring默认的传播级别。

当上下文中已经存在事务,则加入到该事务执行。若上下文不存在事务,则新建一个事务执行。

此处上下文可以理解为@Transactional注解作用的地方,当该注解作用于方法时,上下文从方法最先开始的地方开启,也就是说在刚进入方法时,首先就会开启一个事务。

以下分几种情况进行说明:

(1)当外部方法未开启事务时, 若外部方法抛出异常, 如何运行?

User1Service代码

1
2
3
4
5
6
7
8
9
10
@Service 
public class User1Service extends ServiceImpl<User1Mapper, User1Entity> {
@Resource
private User1Mapper user1Mapper;

@Transactional(propagation = Propagation.REQUIRED)
public void addUser1(User1Entity user1Entity) {
user1Mapper.insert(user1Entity);
}
}

User2Service代码

1
2
3
4
5
6
7
8
9
10
@Service 
public class User2Service extends ServiceImpl<User2Mapper, User2Entity> {
@Resource
private User2Mapper user2Mapper;

@Transactional(propagation = Propagation.REQUIRED)
public void addUser2(User2Entity user2Entity) {
user2Mapper.insert(user2Entity);
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
public void t1() {     
User1Entity user1Entity = new User1Entity();
user1Entity.setId(100);
user1Entity.setName("sju");
user1Service.addUser1(user1Entity);

User2Entity user2Entity = new User2Entity();
user2Entity.setId(200);
user2Entity.setName("fdf");
user2Service.addUser2(user2Entity);
throw new RuntimeException();
}

执行结果: user1, user2 均插入成功

分析: 因为外围方法未开启事务,所以程序执行到两个addUser方法时都会开启一个新的事务,这两个事务是相互独立的,分别执行自己的逻辑,插入成功,外部方法异常不影响内部.

(2)当外部方法未开启事务时, 若内部方法抛出异常, 如何运行?

User1Service代码:

1
2
3
4
5
6
7
8
9
10
@Service 
public class User1Service extends ServiceImpl<User1Mapper, User1Entity> {
@Resource
private User1Mapper user1Mapper;

@Transactional(propagation = Propagation.REQUIRED)
public void addUser1(User1Entity user1Entity) {
user1Mapper.insert(user1Entity);
}
}

User2Service代码:

1
2
3
4
5
6
7
8
9
10
11
@Service 
public class User2Service extends ServiceImpl<User2Mapper, User2Entity> {
@Resource
private User2Mapper user2Mapper;

@Transactional(propagation = Propagation.REQUIRED)
public void addUser2(User2Entity user2Entity) {
user2Mapper.insert(user2Entity);
throw new RuntimeException();
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
public void t1() {     
User1Entity user1Entity = new User1Entity();
user1Entity.setId(100);
user1Entity.setName("sju");
user1Service.addUser1(user1Entity);

User2Entity user2Entity = new User2Entity();
user2Entity.setId(200);
user2Entity.setName("fdf");
user2Service.addUser2(user2Entity);
}

结果: user1插入成功, user2插入失败

分析: 还是因为外部方法未开启事务, 所以user1, user2会开启两个独立的事务, 互不影响。 所以user1插入成功, user2插入失败。

(3)当外部方法开启事务时, 若内部方法或者外部方法抛出异常, 如何运行?

User1Service和User2Service代码同之前, 只是在测试方法上也加上@Transactional注解。

测试方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Transactional 
public void t1() {
User1Entity user1Entity = new User1Entity();
user1Entity.setId(100);
user1Entity.setName("sju");
user1Service.addUser1(user1Entity);

User2Entity user2Entity = new User2Entity();
user2Entity.setId(200);
user2Entity.setName("fdf");
user2Service.addUser2(user2Entity);
throw new RuntimeException();
}

结果: 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

以非事务执行,该处语气比上一个更强,因为当发现当前存在事务时,则直接抛出异常