java 开发中,如果一个请求需要操作多个数据表(增删改),为了确保操作的原子性,数据的一致性,一般需要引入spring事物注解@Transactional。
事物特性:ACID (原子性 一致性 隔离性 持久性)
失效场景1: 访问权限不支持
java 中访问权限有 private ,default, protect, public ,要想事物生效,spring 要求被代理的方法必须是public修饰的。
失效场景2:方法用final 修饰
被final 修饰的方法表示该方法不可被子类重写。 spring 事务底层使用了 aop,通过 jdk 动态代理或者 cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用 final 修饰了,那么在它的代理类中,就无法重写该方法,无法实现事物。
失效场景3:方法内部调用
例如: updateStatus 事物并不会生效,updateStatus会被当成add方法的this调用,并没有加入spring 代理中。
@Service public class UserService { @Autowired private UserMapper userMapper; public void add(UserModel userModel) { userMapper.insertUser(userModel); updateStatus(userModel); } @Transactional public void updateStatus(UserModel userModel) { doSameThing(); } }
解决办法: 使用AopContext.currentProxy() 获取代理对象 ((UserService)AopContext.currentProxy()).updateStatus(userModel);
失效场景4:多线程调用
只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
失效场景5:数据库表不支持事物
如果表引擎是myisam 则不支持事物,mysql5之后innodb 取而代之。
失效场景6:try catch 吞并了异常或抛出了其他异常
spring 事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的 Exception(非运行时异常),它不会回滚。
@Slf4j @Service public class UserService { @Transactional public void add(UserModel userModel) throws Exception { try { saveData(userModel); updateData(userModel); } catch (Exception e) { log.error(e.getMessage(), e); throw new Exception(e); } } }
扩展:
事物的传播特性:
(1)REQUIRED 如果当前上下文中存在事务,则加入该事务,如果不存在事务,则创建一个事务,这是默认的传播属性值。
(2)SUPPORTS 如果当前上下文中存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
(3)MANDATORY 当前上下文中必须存在事务,否则抛出异常。
(4)REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
(5)NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
(6)NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
(7)NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务
脏读,不可重复读,幻读:
(1)脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据)。
(2)不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。
(3)幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。
事物隔离级别:
mysql 默认的隔离级别是 Repeatable Read 可重复读 ,oracle 默认的隔离级别是 Read Committed 读已提交。