【前提】: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