一、 场景
有一个定时器,做数据库表数据同步。
把数据库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的事物隔离级别。
这种配置会使用表级锁,对性能影响巨大。
一般没有特殊需要的话,配置为使用数据库缺省的事物隔离级别便可。