• 【Spring】事务(transactional)之初步理解


    一、 场景

    有一个定时器,做数据库表数据同步。

    把数据库A的表table(DB_A_TABLE)同步到数据库B的表table(DB_B_TABLE)。

    对于DB_A_TABLE的每行数据要做一定的逻辑处理转换到DB_B_TABLE,这转换过程中可能会出异常,所以此行记录无法同步,要记录这行数据到log文件或者保存到DB_B的某张表记录下来。但别的正确转换的数据,要正常的同步。

    EX:

      DB_A_TABLE 有8行记录[1,2,3,4,5,6,7,8],其中[3,6,7]无法被正确转换,[5]能正确转换但无法保存进DB_B_TABLE。

      那么,[1,2,4,8]要正确的同步到DB_B_TABLE。 [3,5,6,7]要记录到DB_B_ERROR。

    二、 事务

    针对以上场景,个人的做法是:

    针对每行记录,进行逻辑转换、数据保存,这一系列的操作都在一个事务。

    即,对每行记录的同步,都用各自的事务去控制。

    那么针对以上的ex,就是有8个事务。

    (传播行为都默认是PROPAGATION_REQUIRED,可能更好的是用PROPAGATION_NESTED/ PROPAGATION_REQUIRES_NEW)

    三、code demo

    // 定时器 timmer.class
    public class SyncDataTimmer {
        @Log
        private Logger log;
        @Autowired
        private DealService dealService;
        // @Transactional(readOnly=false,rollbackFor=Exception.class)
        @Scheduled(cron = "0 0 4 * * ?")
        public void syncData() {
            Map<String,Object> condition = new HashMap<String,Object>();
            List<DbBtable> allData = dealService.getBankDefaultRepaymentInfo(condidtions);
            if (ObjectHelper.isNotEmpty(allData)) {
                for (DbBtable row : allData) {
                    row.setXXX(...); 
                    try {
                      // 针对每行数据的操作
                        dealService.save(row);
                    } catch (Exception e) {
                      // 记录到log或者记录到数据库表
                        log.error("错误信息:", e);
                    }
                }
            }
        }
    }
    // Service处理 DealService.class
    public class DealService {
        @Override
        @Transactional(rollbackFor=Exception.class)
        public void save(DbBtable dto ) throws Exception {
            String table001Id = dto.getTable001Id();
            
            if(ObjectHelper.isNotEmpty(table001Id)){
                Table001 table001 = this.findById(table001Id);
            if(ObjectHelper.isNotEmpty(table001)) this.syncDataTemplate(table001,dto);//逻辑处理,可能异常
                else throw new Exception("根据id="+ table001Id +",未找到记录!");
            }else{
                throw new Exception("表001的Id为空");
            }
        }
    
       @Transactional(rollbackFor=Exception.class)
       public void syncDataTemplate (Table001 table001,DbBtable dto ) throws Exception {
        //可能进行表数据查询、保存等
        //省略...
       }
    }

    说明:

      1、 Spring默认只会回滚RuntimeException运行时异常。而CheckedException不会回滚,所以要用rollbackFor指定。(若异常定义好,就不需要指定rollbackFor)

      2、 Demo中是把要同步的数据获取写在了timmer.class中,针对timmer.class也是一个定时器,所以也可以用事务Transactional。

      结合场景和demo,如果由timmer.class创建事务,又未指定事务的传播行为,那么默认是PROPAGATION_REQUIRED。则后面操作全部在一个事务中,无法达到想要的结果。

      所以,(不知道怎么表达,现在理解也不深刻,先这么理解着)

      针对场景和demo,不要在顶层方法创建事务,即SyncDataTimmer.class 的syncData()不用事务。而是在每行记录的顶层处理方法DealService.Save() 创建事务,然后传播行为默认ROPAGATION_REQUIRED,那么可以针对每行数据进行控制。

    为什么不在synData()创建事务?

      因为,我没有修改事务传播行为,都用的默认的ROPAGATION_REQUIRED。导致,DealService.Save()会用同一个事务,即[1,2,3,4,5,6,7,8]都在一个事务中,被一起提交/回滚。

    更好的传播方式?

      针对场景和demo,可能的传播行为可以是:

      1) PROPAGATION_NESTED:和他的父事务是相依的,他的提交是要等和他的父事务一块提交的。也就是说,如果父事务最后回滚,NESTED也要回滚的。

      (hibernate并不支持NESTED!最好确认所用hibernate版本是否支持,我查的hibernate4说不支持。)

      2) PROPAGATION_REQUIRES_NEW:另起一个事务,将会与他的父事务相互独立;

        假设ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,

          ServiceB.methodB的事务级别为PROPAGATION_REQUIRES_NEW,

        那么当执行到ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起,

          ServiceB.methodB会起一个新的事务,等待ServiceB.methodB的事务完成以后,ServiceA.methodA才继续执行。

        (对code demo,可能timmer.syncData()需要事务,所以REQUIRES_NEW更适合。)

        PROPAGATION_REQUIRES_NEW与PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。

        因为ServiceB.methodB是新起一个事务,那么就是存在两个不同的事务。

        如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB是不会回滚的。

        如果ServiceB.methodB失败回滚,如果他抛出的异常被ServiceA.methodA捕获,ServiceA.methodA事务仍然可能提交。

    针对deme的修改

        Timmer.class可以启用事务, 而把调用的传播行为定义为REQUIRES_NEW

        @Transactional(rollbackFor=Exception.class,propagation=Propagation.REQUIRES_NEW)
        public void save(DbBtable dto ) throws Exception {
            ...
        }

    此时,timmer与service是完全独立的。且service的事务在timmer之前提交。所以,REQUIRES_NEW 的话,当timmer方法错误时,无法全部回滚。

    当用NESTED的话,service是timmer的子事务,在timmer提交时一起提交。

    四、 扩展

    1、事务传播行为

    传播行为

    含义

    MANDATORY

    表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常

    NESTED

    表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务。

    NEVER

    表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常

    NOT_SUPPORTED

    表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager

    REQUIRED(默认)

    表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务

    REQUIRED_NEW

    表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager

    SUPPORTS

    表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行

    2、事务的隔离级别ISOLATION

    隔离级别

    含义

    DEFAULT

    使用数据库默认的事务隔离级别

    READ_UNCOMMITTED

    事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据

        会产生脏读,不可重复读和幻读

    READ_COMMITTED

    保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。

        避免脏读但会不可重复读和幻读

    REPEATABLE_READ

    这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。
    它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。

    SERIALIZABLE

    花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。
    除了防止脏读,不可重复读外,还避免了幻像读

    隔离级别

    脏读

    不可重复读

    幻读

    READ_UNCOMMITTED

    Y

    Y

    Y

    READ_COMMITTED

    N

    Y

    Y

    REPEATABLE_READ

    N

    N

    Y

    SERIALIZABLE

    N

    N

    N

    脏读: 指当一个事务A正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中(事务A未提交)。

        这时,另外一个事务B也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据(A事务可能回滚), 那么事务B读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。

    不可重复读: 指在一个事务A内,多次读同一数据。在这个事务A还没有结束时,另外一个事务B也访问该同一数据并修改。
        那么,事务A中的两次读数据之间,由于事务B的修改,那么事务A两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

    幻读: 指当事务不是独立执行时发生的一种现象,例如事务A对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。

        同时,事务B也修改这个表中的数据,这种修改是向表中插入一行新数据。

        那么,可能就会发生操作事务A的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

    不可重复读 与 幻读 的区别

        不可重复读:可能是第一次读取total=100,第二次读取total=200。因为修改引起。

        幻读:则是条数不一样,因为有新插入或者删除的记录。因为新增/删除引起。

    针对oracle

        Oracle数据库缺省的事物隔离级别已经保证了避免脏读和不可重复读

        但可能会幻读,避免幻读 需要加表级锁,Oracle缺省行级锁。在基于Spring的事物配置中一定要慎重使用ISOLATION_SERIALIZABLE的事物隔离级别。

        这种配置会使用表级锁,对性能影响巨大。

        一般没有特殊需要的话,配置为使用数据库缺省的事物隔离级别便可

     

    参考

    Spring事务异常回滚,捕获异常不抛出就不会回滚

    Spring中的事务管理实例详解

    浅谈Spring事务隔离级别

    解惑 spring 嵌套事务

    Spring事务的隔离级别

  • 相关阅读:
    [ARC080D] Prime Flip
    硬币游戏
    点分治
    OneInDark对众数的爱
    [CF838D] Airplane Arrangements
    网络流总结(转载)
    二分图最大权完美匹配(KM)
    网络流
    FWT
    FFT & NTT
  • 原文地址:https://www.cnblogs.com/VergiLyn/p/5998285.html
Copyright © 2020-2023  润新知