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

来源:互联网 发布:怎么测试网络延迟 编辑:程序博客网 时间:2024/05/16 15:17

一、 场景

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

把数据库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.classpublic 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.classpublic 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的事物隔离级别。

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

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

原创粉丝点击