Mysql事务失效总结

一、业务场景需求

当然如果不是单机服务还是比较推荐用redisson分布式锁来保证,效率会更优

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Serveice
public class ServiceOne{
//设置一把可重入的公平锁
private Lock lock=new ReentrantLock(true);

@Transacational(rollbackFor=Exception.class)
public Result func(Long seckillId,Long userId){
try{
lock.lock();

//执行数据库操作-查询商品库存数量
//如果库存数量不为0,则减少库存数量
//订单表中插入订单数据

}finally{
lock.unlock();
}

}
}

二、事务隔离机制

事务隔离级别决定了并发事务之间的可见性

图片

  • 脏读:一个事务读取到另一个事务未提交的更新数据
  • 幻读:一个事务读到另一个事务已提交的 insert 数据
  • 不可重复读(虚读):指一个事务读取到了另外一个事务提交的update的数据
  • 可重复读(默认隔离级别):指一个事务不能读取到了另外一个事务提交的update的数据

三、失效场景(出现超卖)

1. 事务提交在 unlock 之后(正常)

图片

因为事务以及提交了代表库存一定减下来了,而这个时候锁还没有释放其他进程也进不来,等unlock之后再进来一个线程执行查询数据库的操作,那么查询到的值一定是减去库存之后的值。

2. 事务提交在 unlock 之前(超卖)

图片

假设A,B 两个线程来下单。

A: 请求先拿到锁,然后查询出库存为1,可以下单,正常走了下单流程把库存减为 0 了,但是由于 A 先执行了 unlock 操作,释放了锁。

B :线程看到后马上就冲过来拿到了锁,并执行了查询库存的操作。

这个时候如果 A 线程还没来得及提交事务,所以 B 读取到的库存还是 1,如果程序没有做好控制,也走了下单流程,结果出现超卖。

3. @Transactional 默认只回滚 RuntimeException

Spring 事务默认只对 RuntimeExceptionError 进行回滚,若抛出 Exception 事务不会回滚,如果需要 Exception 也触发回滚,需要指定回滚类型

1
@Transactional(rollbackFor = Exception.class)

image-20230512120350456

4. 同类方法内部调用事务失效

在默认的代理模式下,只有目标方法由外部调用,才能被 Spring 的事务拦截器拦截。在同一个类中的两个方法直接调用,是不会被 Spring 的事务拦截器拦截

1
2
3
4
5
6
7
8
9
10
11
@Transactional(propagation = Propagation.REQUIRED)
public void save() {
insert();
userMapper.insert(new User("用户名2"));
throw new RuntimeException("save 抛异常了");
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insert() {
userMapper.insert(new User("用户名1"));
}

可以通过Spring 提供的 AopContext.currentProxy() 方法,可以获取当前类的代理对象,然后调用方法以触发事务管理,但只适用于 Spring 代理的 Bean,不能用于 new 直接创建的对象

1
2
3
# 1.需要`application.yml`中开启AOP代理暴露
spring.aop.proxy-target-class=true
spring.aop.auto=true
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class ServiceOne {
@Transactional
public void save() {
//2.通过当前对象代理来调用
((ServiceOne) AopContext.currentProxy()).insert();
}

@Transactional
public void insert() {
// 数据库操作
}
}

5. @Transactional 应用于非 public 方法

事务代理仅对 public 方法生效,虽然事务无效,但不会有任何报错,需确保方法为 public

6. 手动 catch 异常未抛出导致事务失效

1
2
3
4
5
6
7
8
9
10
@Transactional
public void func() {
try {
// 业务逻辑
} catch (Exception e) {
log.error("异常发生", e);
// 需要重新抛出异常,确保事务回滚
throw new RuntimeException(e);
}
}

7. 数据库引擎不支持事务

MySQL数据库默认为使用支持事务的Innodb引擎。一旦数据库引擎切换成不支持事务的Myisam,那事务就从根本上失效了。

1
2
3
#查看表是使用的引擎并修改
SHOW TABLE STATUS WHERE Name = 'your_table';
ALTER TABLE your_table ENGINE = InnoDB;

8. 事务传播机制配置不当

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Transaction(
[readOnly]//指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true

[propagation]
/*
PROPAGATION.REQUIRED(默认):如果当前没有事务,则创建一个新事务。如果当前存在事务,就加入该事务。该设置是最常用的设置。
PROPAGATION.SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务。如果当前不存在事务,就以非事务执行。
PROPAGATION.MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
PROPAGATION.REQUIRE_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
PROPAGATION.NOT_SUPPORTED:以非事务方式执行操作,如果当前事务存在,就把当前事务挂起。
PROPAGATION.NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION.NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按 REQUIRED 属性执行。
*/

[rollbackFor]//用于指定能够触发事务回滚的异常类型,可以指定多个异常类型

[noRollbackFor]//抛出指定的异常类型,不会滚事务,也可以指定多个异常类型

[isolation]//事务隔离级别
/*
Isolation.DEFAULT:使用底层数据库默认的隔离级别(Mysql默认可重读)
Isolation.READ_UNCOMMITTED:读取未提交数据(会出现脏读,不可重复读)基本不使用
Isolation.READ_COMMITTED:读取已提交数据(会出现不可重复读和幻读)
Isolation.REPEATABLE_READ:可重复读(会出现幻读)
Isolation.SERIALIZABLE:串行化
*/
)

三、总结

事务失效的12种场景:

  • 方法权限问题(@Transactional 仅作用于 public 方法)。

  • 方法被 finalstatic 修饰。

  • 内部方法调用不会触发事务代理。

  • 方法未被 Spring 管理(未通过 @Component@Service 注入)。

  • 线程池或异步任务调用事务失效。

  • 数据库表未使用事务支持的引擎(MyISAM)。

  • 事务传播特性错误配置。

  • catch 了异常但未重新抛出。

  • throw 了非 RuntimeException 异常。

  • 自定义回滚异常导致事务失效。

  • 嵌套事务回滚错误。

  • 事务未正确开启(未启用 @EnableTransactionManagement)。