spring事物失效场景
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 读已提交。
快照读和当前读:
快照读:
在可重复读的隔离级别下,事务启动的时候,就会针对当前库拍一个照片(快照),快照读读取到的数据要么就是拍照时的数据,要么就是当前事务自身插入/修改过的数据。
我们日常所用的不加锁的查询,都属于快照读
当前读:
与快照读相对应的就是当前读,当前读就是读取最新数据,而不是历史版本的数据,换言之,在可重复读隔离级别下,如果使用了当前读,也可以读到别的事务已提交的数据。
Mvcc: 多版本并发控制协议 ,可以理解为 数据库中的每一行数据背后有多条历史版本,我们通过navicat 查询的数据也就是我们定义的业务字段,其实每一行还隐藏了字段,如
DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR 三列:
DB_ROW_ID:该列占用 6 个字节,是一个行 ID,用来唯一标识一行数据。如果用户在创建表的时候没有设置主键,那么系统会根据该列建立主键索引。
DB_TRX_ID:该列占用 6 个字节,是一个事务 ID。在 InnoDB 存储引擎中,当我们要开启一个事务的时候,会向 InnoDB 的事务系统申请一个事务 id,这个事务 id 是一个严格递增且唯一的数字,当前数据行是被哪个事务修改的,就会把对应的事务 id 记录在当前行中。
DB_ROLL_PTR:该列占用 7 个字节,是一个回滚指针,这个回滚指针指向一条 undo log 日志的地址,通过这个 undo log 日志可以让这条记录恢复到前一个版本。
mvcc怎么来控制可重复读呢?
每当执行增删改的操作时候 会开启一个事务,在当前事务开启的一瞬间系统会创建一个数组,数组中保存了目前所有的活跃事务 id,所谓的活跃事务就是指已开启但是还没有提交的事务。当我们查询某条数据的时候,会查看这条数据的DB_TRX_ID,
如果这个值等于当前事务 id,说明这就是当前事务修改的,那么数据可见。
如果这个值小于数组中的最小值,说明当我们开启当前事务的时候,这行数据修改所涉及到的事务已经提交了,当前数据行是可见的。
如果这个值大于数组中的最大值,说明这行数据是我们在开启事务之后,还没有提交的时候,有另外一个会话也开启了事务,并且修改了这行数据,那么此时这行数据就是不可见的。
如果这个值的大小介于数组中最大值最小值之间(闭区间),且该值不在数组中,说明这也是一个已经提交的事务修改的数据,这是可见的。
如果这个值的大小介于数组中最大值最小值之间(闭区间),且该值在数组中(不等于当前事务 id),说明这是一个未提交的事务修改的数据,不可见。
通过这样控制来使数据在同一事务中多次读取一致。
在可重复读的情况下,update操作是一种会先读再写,读是当前读,即可以读到别的事务已提交的内容。