【排坑】多线程事务引发的问题(二)

原创
2018/04/12 14:56
阅读数 34

【前提】:TestReqJsonProcess位于Service层,其中doProcess方法用于更新指定主键的remark数据。后期新需求要求在执行所有更新Service时调用三方接口,并将remark转义为中文作为参数传输给三方。类似TestReqJsonProcess的Service有很多个,计划利用注解注入BusiOptUtil工具类来实现。

【代码】:

@Service
@Transactional
public class TestReqJsonProcess implements JsonProcessInterface {
	private Logger logger = LoggerFactory.getLogger( getClass() );

	@PersistenceContext
	private EntityManager em;
	
	@Autowired
	private BusiOptUtil busiOptUtil;
	
	@Autowired
	private RTmAppMain rTmAppMain;
	
	@Override
	public void doProcess( HttpServletRequest request ) throws ProcessException {
		logger.debug("--------------执行开始-主方法用于更新保存appNo为主键的单条数据--------------");
		try {
			String appNo = request.getAppNo();
			String remark = request.getRemark();
			
			//appNo 作为TmAppMain表的主键,使用JPA方式读取数据库
			QTmAppMain qTmAppMain = QTmAppMain.tmAppMain;
			TmAppMain tmAppMain = new JPAQuery( em ).from( qTmAppMain )
					.where( qTmAppMain.appNo.eq( appNo ) )
					.singleResult( qTmAppMain );
			
			logger.debug("主方法首次从数据库获取remark="+tmAppMain.getRemark());
			
			//更新remark
			tmAppMain.setRemark(remark);
			
			logger.debug("主方法首次更改remark="+tmAppMain.getRemark());
			
			//@Autowired BusiOptUtil 调用三方接口传输数据的工具类
			//方法内为匹配三方接口的参数要求,更新了remark参数用于示例
			busiOptUtil.opt(appNo);
			
			logger.debug("主方法显示主方法remark="+tmAppMain.getRemark());
			
			//更新后保存
			rTmAppMain.save(tmAppMain);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		logger.debug("--------------执行结束-主方法用于更新保存appNo为主键的单条数据--------------");
	}
}
@Component
public class BusiOptUtil {

	Logger logger = LoggerFactory.getLogger( this.getClass() );
	
	@PersistenceContext                              
	private EntityManager em;
	
	public void opt(String appNo){
		
		QTmAppMain qTmAppMain = QTmAppMain.tmAppMain;
		TmAppMain tmAppMain = new JPAQuery( em ).from( qTmAppMain ).where( 
				qTmAppMain.appNo.eq( appNo ))
				.singleResult( qTmAppMain );
		
		logger.debug("工具类里从数据库获取remark="+tmAppMain.getRemark());
		
		if(StringUtils.isNotBlank(tmAppMain.getRemark())){
			tmAppMain.setRemark("中国");
		}else{
			tmAppMain.setRemark("");
		}
		
		logger.debug("工具类里更改remark="+tmAppMain.getRemark());
		
		//省略调用三方接口,将tmAppMain转化为JSON字符串传输
	}
}
请求参数:
appNo  = 123456
remark = China

【结果】 :appNo为123456这个单号记录,保存在数据库中remark值为"中国",并非“China”

【原因】:TestReqJsonProcess类上面有@Transactional声明式事务。在首次查询完数据库没有commit之前,数据会存在缓存中(主键查询才会缓存),当再次通过相同主键去查询,会优先检查缓存中是否已经存在,存在将不会再去数据库查询。所以就会出现上述的情况,问题比较容易理解,但实际场景中很容易忽略。(示例中事务使用 Spring 提供的默认事务传播行为  PROPAGATION_REQUIRED )

【扩展】:顺便整理下事务使用过程遇到的问题,使用事务默认的传播行为PROPAGATION_REQUIRED ,方法A和方法B同时通过相同主键获取同一数据对象。

(一)单线程事务或嵌套事务

1、方法A有事务,方法B无事务,A中调用B,B先commit,结束后A再commit;

2、方法A有事务,方法B有事务,A中调用B,B先commit,结束后A再commit;

都不会报错。B若无事务,A的事务将传播到B方法;若B有事务,B将加入A方法现有的事务;当B方法commit后,A方法commit时发现对象没有改变,A方法就不会再commit

(二)多线程事务并发

多线程事务要考虑Spring框架提供的事务隔离级别

ISOLATION_DEFAULT
ISOLATION_READ_UNCOMMITTED
ISOLATION_READ_COMMITTED
ISOLATION_REPEATABLE_READ
ISOLATION_SERIALIZABLE

后四种隔离级别具体隔离何种数据读取,我们使用默认的 ISOLATION_DEFAULT ,这个默认隔离级别是与具体的数据库相关的,采取的是具体数据库的默认隔离级别,不同的数据库是不一样的。

SELECT @@global.tx_isolation;#查询Mysql全局事务隔离级别
SELECT @@session.tx_isolation;#查询当前连接上的事务隔离级别
SELECT @@tx_isolation; #查询下一个未开始的事务隔离级别
-------------------------------------
REPEATABLE-READ 

查询结果为可重复读,该种情况会出现幻读。 InnoDB和Falcon存储引擎通过多版本并发控制机制解决了该问题。数据库表加了乐观锁JPA_VERSION(Spring中映射对象使用@Version支持乐观锁)。

方法A有事务,方法B有事务,线程1执行A获取对象并线程睡眠,此时线程2执行B获取相同对象并先commit,待A线程恢复后commit;

会报错,Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction

 

展开阅读全文
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部