之前面试的时候还真的有一个面试官问过我:
什么是事务?
事务,说人话就是:
把多个数据库操作打包成一个"不可分割的整体",要么全成功,要么全失败并回滚到最初状态,绝对不会出现"只做了一半"的情况。
以生活中最经典的银行转账举个例子那是再合适不过了。
且看:
A给B转100块钱,这一笔转账业务,其实是由两步独立的数据库操作组成的:
- A的账户扣100块
- B的账户加100块
这两步必须同时成功、一起生效。
如果A扣了钱B没收到,或者B收到了钱A没扣,那银行就乱套了。
事务的四大特性
用银行转账的例子加深理解:
| 特性 | 解释 | 银行转账例子 |
|---|---|---|
| 原子性(Atomicity) | 要么全做,要么全不做,就像"原子"一样不可分割 | A扣钱和B加钱必须同时成功,不能A扣了B没加 |
| 一致性(Consistency) | 事务前后,数据要保持"一致",总金额不变 | 转账前A有1000,B有0,总和是1000;转账后A有900,B有100,总和还是1000 |
| 隔离性(Isolation) | 多个事务同时执行时,互相不干扰,就像"隔离开"一样 | 两个转账操作同时改A的账户,不能互相影响,不然钱就不对了 |
| 持久性(Durability) | 事务提交后,数据就永久保存了,就算数据库挂了也不会丢 | 转账成功后,就算银行服务器断电重启,A和B的钱数也不会变 |
隔离级别
为什么要有隔离级别呢?
如果多个事务同时执行,没有隔离的话,会出现3个问题:
- 脏读
脏读就是读到了别人还没提交的数据。
比如A转给B100块,还没提交,B就查到自己多了100块,结果A回滚了,B白高兴一场。

- 不可重复读
不可重复读的意思是同一个事务里,两次读同一条数据的结果是不一样的。
比如A查自己有1000块,中间B转给了A100块提交了,A再查就变成1100了。

- 幻读
幻读就是同一个事务里,两次查的数量不一样。
比如A查账户里有10条记录,中间B插了一条提交了,A再查就变成11条了,就像"幻觉"一样。

这么看,不可重复读 和幻读 是由别的事务提交后导致的错误,而脏读是读到了别人还没提交的临时数据。
那不可重复读和幻读两者的区别在哪呢?
不可重复读是盯着一条记录看,发现这条记录的内容变了。
而幻读是盯着一堆记录数看,发现总数量变了。
类比一下就是不可重复读是你看自己的银行卡余额,钱数变了;而幻读是你数银行有多少张卡,卡的数量变了。
| 隔离级别 | 解决的问题 | 解释 |
|---|---|---|
| 读未提交(Read Uncommitted) | 啥都解决不了 | 能读到别人没提交的数据,脏读、不可重复读、幻读都有,一般不用 |
| 读已提交(Read Committed) | 脏读 | 只能读到别人提交后 的数据,但会有不可重复读、幻读 |
| 可重复读(Repeatable Read) | 脏读、不可重复读 | Spring默认的隔离级别。 同一个事务里,两次读同一个数据一样 ,但会有幻读(MySQL InnoDB用MVCC解决了幻读) |
| 串行化(Serializable) | 所有问题 | 事务一个接一个执行,就像"排队"一样,性能最差,一般不用 |
传播行为
传播行为就是:
当一个事务方法调用另一个事务方法时,这两个事务该怎么协作?是直接加入原事务?还是新建一个独立的事务?还是先挂起原事务,等新事务执行完再继续?
以下是3 个常用的传播行为:
| 传播行为 | 解释 | 适用场景 |
|---|---|---|
| REQUIRED(默认) | 有事务就加入,没有就新建一个事务 | 最常用,比如转账服务 |
| REQUIRES_NEW | 新建一个事务,挂起原事务,等新事务执行完再继续原事务 | 比如记录日志,不管主事务成功失败,日志都要记录 |
| NESTED | 嵌套事务,有一个保存点: 1. 子事务回滚:只回滚自己,不影响父事务和其他子事务 2. 父事务回滚:带着所有子事务一起回滚 | 系统批量发福利:某一个用户的福利发放失败,不影响其他用户;但整体回滚,所有用户的福利都会被收回 |
小贴士
- Spring AOP 是基于代理的,
@Transactional只能用在 public 方法上,不然不生效。不管是 JDK 动态代理还是 CGLIB 代理,Spring 都只保证 public 方法的事务生效,非 public 的写了也白写。 - 内部调用不会走代理类,直接调用
this.method(),所以,同一个类里方法调用,@Transactional不生效。 - Spring默认只回滚非受检异常 (RuntimeException、Error),受检异常 (Exception)不回滚。如果要所有异常都回滚,需要在
@Transactional里加rollbackFor = Exception.class。 - MySQL的MyISAM引擎不支持事务,只有InnoDB支持。