开发总结Mysql事务失效总结
DreamCollector一、业务场景需求
当然如果不是单机服务还是比较推荐用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();
}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 事务默认只对 RuntimeException
或 Error
进行回滚,若抛出 Exception
事务不会回滚,如果需要 Exception
也触发回滚,需要指定回滚类型
1
| @Transactional(rollbackFor = Exception.class)
|

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
| 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() { ((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
方法)。
方法被 final
或 static
修饰。
内部方法调用不会触发事务代理。
方法未被 Spring 管理(未通过 @Component
、@Service
注入)。
线程池或异步任务调用事务失效。
数据库表未使用事务支持的引擎(MyISAM
)。
事务传播特性错误配置。
catch
了异常但未重新抛出。
throw
了非 RuntimeException
异常。
自定义回滚异常导致事务失效。
嵌套事务回滚错误。
事务未正确开启(未启用 @EnableTransactionManagement
)。