什么是事务?
事务是一个操作序列,要么全部执行成功,要么全部执行失败。事务有四个重要特性,称为 ACID
特性:
- Atomicity(原子性):事务中的所有操作要么全部完成,要么全部不完成。
- Consistency(一致性):事务完成后,数据要处于一致的状态。
- Isolation(隔离性):一个事务的执行不能被其他事务干扰。
- Durability(持久性):事务完成后,数据应该永久保存
补充:
undo_log
表保证事务 原子性(A) 和 一致性(C )redo_log
表保证事务 持久性(D)- 隔离级别 保证事务 隔离性(I)
事务的隔离级别
-
读未提交(read Uncommited)
在该隔离级别,所有的事务都可以读取到别的事务中未提交的数据,会产生脏读问题,在项目中基本不怎么用, 安全性太差;脏读:所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。
也就是说,当前事务读到的数据是别的事务想要修改成为的但是没有修改成功的数据。 -
读已提交(read commited)
处于READ COMMITTED
级别的事务可以看到其他事务对数据的修改。也就是说,在事务处理期间,如果其他事务修改了相应的数据,那么同一个事务的多个 SELECT 语句可能返回不同的结果。在一个事务内,能看到别的事务提交的数据。出现 不可重复读。不可重复读:事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变 了,然后事务A再次读取的时候,发现 数据不匹配,就是所谓的不可重复读了。
-
可重复读(Repeatable read)
这是 MySQL 的默认隔离级别,它确保了一个事务中多个实例在并发读取数据的时候会读取到一样的数据;不过理论上,这会导致另一个棘手的问题:幻读 。幻读:事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读,简单来说就是突然多了几行数据。
为了解决幻读问题,MySQL引入了两种不同的MVCC实现方式:基于快照的MVCC 和 基于原始行的MVCC。
- 基于快照的MVCC:该方式会为每个事务创建一个快照,事务开始时记录数据库的当前版本号,当事务再次访问该行数据时,会检查当前版本号是否与快照版本号一致,如果不一致则会进行回滚或重新读取数据。
- 基于原始行的MVCC:该方式会为每行数据创建一个版本链表,每次更新操作都会创建一个新的版本号,并将旧版本号链接到新版本号上。当事务需要读取数据时,会检查当前版本号是否在版本链表中,如果在则读取最新版本的数据,避免幻读问题。
-
可串行化
有效避免“脏读”、“不可重复读”、“幻读”,不过效率特别低。
不可重复读和幻读比较:
- 不可重复读针对的是
update
或delete
,是由于数据发生改变导致的- 幻读针对的
insert
,是由于行数发生改变导致的。
spring事务的实现方式
事务就是一系列的操作原子执行。Spring事务机制主要包括声明式事务和编程式事务。
- 编程式事务:通过编程的方式管理事务,手动去开启、提交、回滚事务、这种方式带来了很大的灵活性,但很难维护。
- 声明式事务:将事务管理代码从业务方法中分离出来,通过aop进行封装。Spring声明式事务使得我们无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。使用 @Transactional 注解开启声明式事务。
事务失效的8种情况
-
非
public
修饰的方法
@Transactional 注解只能在在public修饰的方法下使用。 -
自调用(Self-Invocation)
自调用指的是一个类的方法在调用同一个类的另一个方法,事务管理会失效。类内部非直接访问带注解标记的方法 B,而是通过类普通方法 A,然后由 A 调用 B。 自己调用自己
@Service public class Demo { public void A() { this.B(); } @Transactional public void B() { ...... } }
解决方法:
解决方法是将这些方法拆分到不同的类中,或者通过 Spring 的代理来调用这些方法。
在该Service类中使用AopContext.currentProxy()
获取代理对象
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableTransactionManagement
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
通过调用代理对象中被增强过的方法实现事务
((ServiceA)AopContext.currentProxy()).doSave(user);
或者
-
数据库不支持事务
MySQL中,MyISAM引擎不支持事物,InnoDB 支持事物 -
异常类型不匹配
@Transactional 注解默认只管理运行时异常(如RuntimeException及其子类)和错误(如Error)。当抛出未被捕获的运行时异常时,Spring 会触发事务回滚操作,将之前的操作撤销;对于受检异常(即必须被捕获或声明抛出的异常,例如IOException,SQLException等),除非你明确配置事务管理器去处理它们,否则事务不会回滚。
可以通过rollbackFor
和noRollbackFor
属性来指定需要回滚或不需要回滚的异常类型。 -
传播属性设置问题
记忆方法:-
两个
REQUIRED
:一定有事务- 带NEW:总是自己建自己的事务。
- 不带NEW:有就加入,没有才建。
-
两个
SUPPORTS
- 带NOT:直接不用。
- 不带NOT:有就用,没有就拉到。
-
MANDATORY
:强制的意思,必须用,语气强烈,没有就异常。 -
NEVER
:从不,就不用,语气强烈,有就异常。 -
NESTED
:嵌套的意思,有,建嵌套事务。没有,新建普通事务。
-
- 传播属性设置不当导致不走事务
@Transactional 默认的事务传播机制是:REQUIRED
,若指定成了NOT_SUPPORTED
、NEVER
事务传播机制,则事物不生效,如:@Transactional(propagation = Propagation.NOT_SUPPORTED)
-
捕获异常未抛出
//自己消化掉了异常 @Transactional public void A(){ try{ ...... }catch(Exception e){ // 未抛异常 } }
-
Bean没有纳入Spring IOC容器管理
使用spring事务的前提是:对象要被spring管理,需要创建bean实例。如果类没有加@Controller、@Service、@Component、@Repository等注解,即该类没有交给spring去管理,那么它的方法也不会生成事务。// 注释调@Component,该类没被Spring管理,事物也是不生效的 public class Demo { @Transactional(rollbackFor = Exception.class) public void A() { ...... } }
-
事务方法内启动新线程进行异步操作
主线程没有拿到子线程的异常,所以代理类以为正常执行了@Transactional() public int transfer2(String from,String to, int money){ accountDao.decrMoney(from,money); new Thread(()->{ int c = 5/0; accountDao.addMoney(to,money); }).start(); return 1; }