手动控制spring事务
用spring的时候,什么时候需要手动控制事务?在方法前加@Transaction不行么?
有这么一种场景,用户在web页面点击一个按钮,后台需要几分钟或几小时的长时间执行一段程序。这时如果页面不响应,用户感觉就一直卡在这里了。一种方法是起一个线程来跑这个程序,让请求立即返回。但这与我们平时的代码不太一样。平时的代码如果有事务直接放在Service的方法上就行了,再这个真正的执行是在一个线程里。而这里得不到外面的开的事务。所以就在执行方法里手动打开与关闭事务。
关于长时任务,在这篇文章中有介绍 《Java耗时任务怎么不重复执行》。这里就不多说了,看以下代码,根据经验1000行以下就直接执行,以上就开起一个线程跑数据:
@Transactional public String processDatas(ImportVo importVo,List<Map<String,String>> data) { String rstInfo=null; WorkState startState=new WorkState(WorkState.STATE.DOING.getCode(),new Date(),null); setWorkSate(importVo.getServiceId(),importVo.getGradeId(),JSONObject.toJSONString(startState)); if(data.size()<1000 ) { try { int sucNum=importAnswers(importVo); rstInfo="成功导入"+sucNum+"份数据"; }catch(Exception e) { log.error(e.getMessage(),e); rstInfo=e.getMessage(); setWorkSate(importVo.getServiceId(),importVo.getGradeId(),JSONObject.toJSONString(new WorkState(WorkState.STATE.FAIL.getCode(),new Date(),e.getMessage()))); } }else { String key="TaskKey"; LongTimeWorkThread ltw=new LongTimeWorkThread() { @Override public void doWork() { try { int sucNum=importAnswers(importVo); }catch(Exception e) { log.error(e.getMessage(),e); setWorkSate(importVo.getServiceId(),importVo.getGradeId(),JSONObject.toJSONString(new WorkState(WorkState.STATE.FAIL.getCode(),new Date(),e.getMessage()))); } } @Override public Object finish() { distributeWorkService.clearKey(key); return null; } }; StartInfo startInfo=distributeWorkService.checkStart(key, ltw); if(startInfo.isThisStartFlag()) { rstInfo="启动成功,预计需要:"+data.size()/10+"秒"; }else { SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); rstInfo="启动失败,现在执行的是"+sdf.format(startInfo.getStartTime())+"开始的任务"; } } return rstInfo; }
setWorkSate方法是设置这个任务的状态,方便用户了解任务的执行情况,前面两个是业务键,作为任务的主键。表结构如下:
CREATE TABLE `import_work` ( `id` bigint(20) NOT NULL, `activity_id` bigint(20) DEFAULT NULL, `grade_id` int(11) DEFAULT NULL, `msg` varchar(512) DEFAULT NULL COMMENT '成功,失败信息', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
这个processDatas也是有事务的。但在线程中的importAnswers方法就没有事务了。可以这样手动控制事务:
1.注入
@Autowired
private DataSourceTransactionManager dstManager
2.开启事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); // 事物隔离级别,开启新事务,这样会比较安全些。
TransactionStatus transaction= dstManager.getTransaction(def); // 获得事务状态
3.提交事务
dstManager.commit(transaction);
4.回滚事务
dstManager.rollback(transaction);
我的项目中对数据源重新配置过了,配置方法见http://www.highersoft.net/html/notice/notice_447.html,所以第1步注入有问题。所以我的注入方法是这样的:
//DynamicDataSourceConfiguration是数据源配置类 @Configuration
@Autowired
private DynamicDataSourceConfiguration dynamicDataSourceConfiguration;
开启事务时,这样取事务管理对象:
PlatformTransactionManager dataSourceTransactionManager=dynamicDataSourceConfiguration.platformTransactionManager();
其它的一样。
如果在这个手工事务里想做个数据库操作让用户立刻可以看见,但又不想影响之前开的事务(之前开的事务未提交,不能让用户看见)。可以在方法中间再加一个方法,再开一个手工事务,代码同上,就可以了。
上面记录得有点乱,最近又遇到这个问题。对于大文件的导入还是用手工事务的方式,不过这次用简化的方法。直接取spring管理的org.springframework.jdbc.datasource.DataSourceTransactionManager,代码模板是这样。
@Autowired private DataSourceTransactionManager transactionManager; public String importFile(String fileName,Integer rewriteType) { DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); // 事物隔离级别,开启新事务,这样会比较安全些。 TransactionStatus transaction = transactionManager.getTransaction(def); // 获得事务状态 try{ //业务处理 transactionManager.commit(transaction); }catch(Exception e) { transactionManager.rollback(transaction); log.error(e.getMessage(),e); } return "成功导入"; }当然,业务处理这块可能比较麻烦,Exception处理也有些技巧。文件导入批量更新可参考《批量插入,用mybatis还是jdbc》