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操作是一种会先读再写,读是当前读,即可以读到别的事务已提交的内容。

 

posted @ 2021-10-09 15:47  山阴路的秋天  阅读(97)  评论(0编辑  收藏  举报