客是为了自己平时自己查方便当然也是记录平时开发过程中遇到的坑
以便自己越挫越勇,也能像项目中的其他哥们一样早点变成大神。。。。。
*一:什么是事务?
我理解的就是完成一件事情所定义的一系列操作,这些操作要么都成功,要么都失败。生活中,比如那个老掉牙的例子,““取钱””这个可以叫做一个事务,它大体就包含两个操作,ATM出钱和银行卡里扣钱,这两个操作要么都成功要么都失败。事务概念里面还有4个重要特性记住ACID就可以了,满足ACID就可以成为事务。分别是原子性,一致性,隔离性,持久性。
1.原子性。指事务是由一系列动作构成,这些动作要么全部参与,要么全部不参与。是一个整体,不可以再分割;
2,一致性。一旦事务完成,结果应该是一致的状态,要么都成功要么都失败;
3.隔离性。一个事务在执行的时候不会被别的事务所干扰;
4.持久性。事务一旦执行对数据中的数据的影响是永久的;
*二、spring事务的7种传播行为
网上随便一搜各种例子多得很,要是有哪个不理解的话,建议可以去搜一搜喔。一般项目里面用的就是(REQUIRED、REQUIRES_NEW)两种了。现在我们项目组就是用的这两种 。
![在这里插入图片描述
三、spring事务的5种隔离级别
四、spring是如何支持事务的*
两种方式:编程式事务和声明式事务
4.1编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。但是这种有个弊端哦就是你自己需要手动写代码实现,实现doInTransaction()这个方法,所以,一般不推荐使用,也不符合spring的思想,因为它直接耦合代码,但各有利弊。
4.2 声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。
**
4.2.1 基于Transactional注解的事务管理
**
之前上一家公司就是用的这个方式,思路就是配置文件配好了,写业务方法的时候给需要用到事务的方法或者类上面加个@Ttansaction注解就可以了 ,个人觉得这个方法没有配置文件的方式好用,因为有时候可能会忘记啥的,就像下面这样配就可以了
1.配置
<tx:annotation-driven transaction-manager="transactionManager"/>
<mvc:annotation-driven/>
2.配置后只需要在要处理的地方加上Transactional注解,而且Transactional注解的方式可以应用在类上,也可以应用在方法上,当然只针对public方法。
3.具体业务代码:
@Transactional
public int insertUser(User user) {
int i = userMapper.insertUser(user);
if (i > 0) {
System.out.println("success");
}
int j = 10 / 0;
return i;
}
**
*4.2.2 基于配置文件的事务管理
这种方式就是像我之前在三的时候说事务的传播性的时候,(https://img-blog.csdnimg.cn/20190625183357708.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3MTQ3MDUy,size_16,color_FFFFFF,t_70)事先把某些要用到事务的方法配置好,写业务方法的时候 按照配置好的方法名字去给业务方法取名字就可以了,比较方便。所以个人建议用此种方式
五、spring实现事务的基本原理
spring的事务实现代码层面主要是通过AOP来实现的,而Spring的AOP机制就是采用动态代理的机制来实现切面编程。
关于java动态代理机制,这篇博客不错,从头到尾看下来,应该会有大致的理解
https://blog.csdn.net/u011784767/article/details/78281384
* 六、具体开发中是怎么用到的
讲一下现在所在公司的两个典型例子吧
第一个需求,每天定时下载代扣文件且包含处理存量数据,并且有入库操作。(存量:就是当今天正常下载的时候如果发现有存在昨天下载失败的数据,那当天也需要下载昨天失败的数据),而且当天需要下载的数据和处理存量的数据要互不影响(例如假如今天除了下载正常需要下载的数据之外还需要下载昨天,前天和大前天的失败数据,而且这几天的数据成功与否要需要互不影响)。
解决方法就是把下载任务写在一个大的业务方法里面,然后把存量数据个正常数据分开,正常数据下载和存量数据下载事务的传播级别都是使用REQUIRED_NEW
因为,spring事务的事务级别如果是REQUIRED_NEW是需要在一个被代理的方法上起作用的问题,项目组有一个大牛专门写了一个去开启新事务的公用类。这个大牛特别牛,项目组很多底层公共的代码都是出自他之手。
a.以下代码是开启新事务的公共类,每次用的时候 ,只需要注入TxNewServiceImpl这个bean然后调用它的doInTxNew就可以了
/**
- 用来解决spring事务的REQUIRED_NEW需要在一个被代理的方法上起作用的问题。
- 之前的解决办法是调用另外一个service的xxxTxNew方法,在它的xxxTxNew方法中做业务处理,
- 这样导致业务逻辑分散到多个类中,并且不方便使用上下文。
- 注意:
-
- 慎用REQUIRED_NEW事务,使用REQUIRED_NEW事务时,一定要考虑在此事务中更新的记录,
- 是否在外层事务中也有更新操作。如果外层事务中也有更新操作,那么会产生死锁!!!
-
- 慎用REQUIRED_NEW事务,使用REQUIRED_NEW事务时,一定要考虑在此事务中更新的记录,
- 是否在外层事务中也有更新操作。如果外层事务中也有更新操作,那么会产生死锁!!!
-
- 慎用REQUIRED_NEW事务,使用REQUIRED_NEW事务时,一定要考虑在此事务中更新的记录,
- 是否在外层事务中也有更新操作。如果外层事务中也有更新操作,那么会产生死锁!!!
*/
@Service
public class TxNewServiceImpl{
public T doInTxNew(Callback callback){
return callback.execute();
}
}
b.这是项目里面配置事务的配置文件
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED" rollback-for="Throwable" isolation="READ_COMMITTED"/>
<tx:method name="save*" propagation="REQUIRED" rollback-for="Throwable" isolation="READ_COMMITTED"/>
<tx:method name="create*" propagation="REQUIRED" rollback-for="Throwable" isolation="READ_COMMITTED"/>
<tx:method name="update*" propagation="REQUIRED" rollback-for="Throwable" isolation="READ_COMMITTED"/>
<tx:method name="*TxNew" propagation="REQUIRES_NEW" rollback-for="Throwable" isolation="READ_COMMITTED"/>
<tx:method name="delete*" propagation="REQUIRED" rollback-for="Throwable" isolation="READ_COMMITTED"/>
<tx:method name="*" propagation="SUPPORTS" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 切面 -->
<aop:config>
<aop:pointcut id="serviceOperation" expression="execution(* *****..*Impl.*(..))" />
<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice" />
</aop:config>
<tx:annotation-driven transaction-manager="transactionManager" />*
c.这就是下载文件那个大的的业务方法了
public void downloadWithhold() {
List<ChkacctTaskFlowBean> needReloadBean = chkacctTaskFlowMapper
.queryByTypeAndStaus(CheckAccountFileType.WITHHOLD.getCode());
if (CollectionUtils.isNotEmpty(needReloadBean)) {
for (ChkacctTaskFlowBean chkacctTaskFlowBean : needReloadBean) {
txNewServiceImpl.doInTxNew(() -> {
logger.info("*****:{}",
DateUtil.format(chkacctTaskFlowBean.getCkhDate(), DateUtil.SHORT_DATE_FORMAT));
downloadStockWithhold(chkacctTaskFlowBean); // 处理存量数据
return null;
});
}
}
ChkacctTaskFlowBean taskFlowBean = new ChkacctTaskFlowBean();
Date beforDate = DateUtil.addDay(new Date(), -1); // 昨天
SimpleDateFormat sdt = new SimpleDateFormat(DateUtil.SHORT_DATE_FORMAT);
String temp = sdt.format(beforDate);
Date date = null;
try {
date = sdt.parse(temp);
} catch (Exception e1) {
logger.info("日期转换异常:" + e1.getMessage());
}
taskFlowBean.setChkAcctType(CheckAccountFileType.WITHHOLD.getCode());
taskFlowBean.setCkhDate(date);
ChkacctTaskFlowBean bean = chkacctTaskFlowMapper.queryByDateAndType(taskFlowBean);
if (bean != null && bean.getStatus().equals(LoanCheckTaskStatus.FAIL.getCode())) {
return;
} else if (bean != null && bean.getStatus().equals(LoanCheckTaskStatus.SUCCESS.getCode())) {
return;
} else {
txNewServiceImpl.doInTxNew(() -> {
downloadNormalWithhold(taskFlowBean); //
DateUtil.format(taskFlowBean.getCkhDate(), DateUtil.SHORT_DATE_FORMAT));
return null;
});
}
}
private void downloadStockWithhold(ChkacctTaskFlowBean chkacctTaskFlowBean) {
boolean isHandle = true;
try {
downWithholdFile(isHandle, chkacctTaskFlowBean);
} catch (Exception e) {
String errorMessage = e.getMessage();
DateUtil.format(chkacctTaskFlowBean.getCkhDate(), DateUtil.SHORT_DATE_FORMAT), errorMessage);
if (errorMessage.length() > 500) {
errorMessage.substring(0, 500);
}
chkacctTaskFlowBean.setRemark(errorMessage);
insertRecord(isHandle, chkacctTaskFlowBean);
}
}
public void downloadNormalWithhold(ChkacctTaskFlowBean taskFlowBean) {
boolean isHandle = false;
try {
DateUtil.date2Str(taskFlowBean.getCkhDate(), DateUtil.SHORT_DATE_FORMAT));
downWithholdFile(isHandle, taskFlowBean); // 下载
} catch (Exception e) {
,DateUtil.date2Str(taskFlowBean.getCkhDate(), DateUtil.SHORT_DATE_FORMAT), e.getMessage());
String errorMsg = e.getMessage();
if (errorMsg.length() > 500) {
errorMsg = errorMsg.substring(0, 500);
}
taskFlowBean.setRemark(errorMsg);
taskFlowBean.setStatus(LoanCheckTaskStatus.FAIL.getCode());
insertRecord(isHandle, taskFlowBean);
}
}
第二个现在很典型用到
是在做凭证转换项目的时候的一个需求了。用户点击前端有个重新生成并发送按钮。后台数据需要重新去生成,然后再把生成的新数据,通过异步调用重发接口进行重发。
解决思路是,先写一个重新生成并发送大的业务方法,然后因为异步接口的调用的数据是生成的新数据,所以接口的异步调用和数据的重新生成两个事务是需要隔离开的,不能够反正一个事务里面。而且两个方法里面都有对数据库的操作,所以需要分别分开去提交,故采用的事务的隔离级别就要是REQUIRES_NEW了
a.配置文件如下
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="create*" propagation="REQUIRED" />
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="do*" propagation="REQUIRED" />
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="up*" propagation="REQUIRED" />
<tx:method name="close*" propagation="REQUIRED" />
<tx:method name="run*" propagation="REQUIRED" />
<tx:method name="txNew*" propagation="REQUIRES_NEW" rollback-for="Throwable" isolation="READ_COMMITTED"/>
<tx:method name="*" read-only="true" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceOperation" expression="execution(* ****.*.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation" />
</aop:config>
b.业务方法如下:
/**
* @param idfyDto
* @param acctDate
* @throws Exception
* @author
* @date 2019-06-12
* 重新生成并发送
*/
public void doHandelBizReCreate(SapVoucherIdfy idfyDto, Date acctDate) throws Exception {
SapVoucherIdfy idfyEntity = cfvtVoucherRecordService.txNewHandelBizReCreateAndSendVoucher(idfyDto, acctDate); // 重新生成数据
cfvtToFvtVoucherSyncService.txNewHandelBizVoucherSendToFvt(idfyEntity); //
}
public SapVoucherIdfy txNewHandelBizReCreateAndSendVoucher(SapVoucherIdfy idfyDto, Date acctDate) throws BusinessException {
logger.info("handelBizReCreateAndSendVoucher开始:{}", idfyDto.getIdfyId());
Date currentTime = CommHelper.getCurrentDate();
SapVoucherIdfy idfyEntity = new SapVoucherIdfy();
idfyEntity.setDataid1(CommHelper.getUUID());
// 接口类型
idfyEntity.setDjlx(idfyDto.getDjlx());
idfyEntity.setZzdeIntBusTy(idfyDto.getZzdeIntBusTy());
String refSeqNo = IdGenerator.generatorPrimaryKey(Constants.FINANCE_INTERFACE_CODE);
idfyEntity.setIdfyId(IdGenerator.generatorPrimaryKey1(""));
idfyEntity.setZzprDocRef2(refSeqNo);
idfyEntity.setZzdeTrTime(idfyDto.getZzdeTrTime());
idfyEntity.setPreparedDate(currentTime);
idfyEntity.setAcctDate(idfyDto.getAcctDate());
idfyEntity.setBizSysSrc(idfyDto.getBizSysSrc());
idfyEntity.setTradeType(idfyDto.getTradeType());
idfyEntity.setProcessNo(idfyDto.getProcessNo());
idfyEntity.setBatchNo(idfyDto.getBatchNo());
idfyEntity.setOrigIdfyId(idfyDto.getIdfyId());
idfyEntity.setCreateId(CommHelper.getSystemUser());
idfyEntity.setCreateTime(currentTime);
idfyEntity.setUpdateId(CommHelper.getSystemUser());
idfyEntity.setUpdateTime(currentTime);
sapVoucherIdfyDao.insertSelective(idfyEntity);
SapVoucherHead headerDto = sapVoucherHeadDao.selectByIdfyId(idfyDto.getIdfyId());
SapVoucherHead header = new SapVoucherHead();
BeanUtils.copyProperties(headerDto, header);
header.setIdfyId(idfyEntity.getIdfyId());
header.setDataid1(idfyEntity.getDataid1());
header.setDataid2(CommHelper.getUUID());
header.setXblnr(IdGenerator.generatorVoucherId(Constants.FINANCE_INTERFACE_CODE));
header.setCreateTime(currentTime);
header.setOrigHeadId(headerDto.getHeadId());
header.setHeadId(idfyEntity.getIdfyId());
if (acctDate != null) {
header.setBldat(acctDate);
}
sapVoucherHeadDao.insertSelective(header);
List<SapVoucherItem> itemDtos = sapVoucherItemDao.selectByHeadId(headerDto.getHeadId());
for (SapVoucherItem itemDto : itemDtos) {
SapVoucherItem item = new SapVoucherItem();
BeanUtils.copyProperties(itemDto, item);
String itemId = IdGenerator.generatorPrimaryKey1("");
item.setItemId(itemId);
item.setHeadId(header.getHeadId());
item.setDataid1(idfyEntity.getDataid1());
item.setDataid2(header.getDataid2());
item.setDataid3(CommHelper.getUUID());
item.setCreateTime(currentTime);
sapVoucherItemDao.insertSelective(item);
}
idfyDto = new SapVoucherIdfy();
idfyDto.setIdfyId(headerDto.getIdfyId());
idfyDto.setSendStatus(SendStatusEnum.INUSED.name());
idfyDto.setUpdateTime(CommHelper.getCurrentDate());
// idfyDto.setOrigUpdateTime(idfyDto.getUpdateTime());
int i = sapVoucherIdfyDao.updateWithOldUpdateTimeByPrimaryKeySelective(idfyDto);
if (i <= 0) {
throw new BusinessException("");
}
logger.info("handelBizReCreateAndSendVoucher结束:{}", idfyEntity.getIdfyId());
return idfyEntity;
}
@Override
public void txNewHandelBizVoucherSendToFvt(SapVoucherIdfy sapVoucherIdfy) throws Exception {
logger.info("txNewHandelBizVoucherSendToFvt() 上送凭证到FVT请求对象开始======== IdfyId为:{} ",sapVoucherIdfy.getIdfyId() );
SapVoucherHead sapVoucherHead = sapVoucherHeadDao.selectByIdfyId(sapVoucherIdfy.getIdfyId());
SapVoucherIdfyDTO sapVoucheridfyDTO = getSapVoucherIdfyDTO(sapVoucherIdfy);
SapVoucherHeadDTO sapVoucherHeadDTO = getSapVoucherHeadDTO(sapVoucherHead);
AcctVoucherSyncToSapReq req = new AcctVoucherSyncToSapReq(sapVoucheridfyDTO, sapVoucherHeadDTO);
AcctVoucherSyncToSapRep rep = acctVoucherSyncToSapService.sendToSAP(req);
Date currentDate = CommHelper.getCurrentDate();
SapVoucherIdfy dto = new SapVoucherIdfy();
dto.setIdfyId(sapVoucherIdfy.getIdfyId());
dto.setSendNum((sapVoucherIdfy.getSendNum() == null ? 0 : sapVoucherIdfy.getSendNum()) + 1);
dto.setSendStatus(SendStatusEnum.SENT.name()); // 已发送
dto.setSendTime(currentDate);
dto.setUpdateTime(currentDate);
dto.setNotifyTime(currentDate);
if(StringUtils.isBlank(sapVoucherIdfy.getReturnStatus())){
sapVoucherIdfy.setReturnStatus(SapReturnStatusEnum.AWAIT_INQUIRY.name());
}
if (rep == null || StringUtils.isBlank(rep.getBody())) {
if ( !sapVoucherIdfy.getReturnStatus().equals(SapReturnStatusEnum.POST_FAILED)
|| !sapVoucherIdfy.getReturnStatus().equals(SapReturnStatusEnum.POST_SUCCESS)) {
dto.setReturnStatus(SapReturnStatusEnum.ACCEPT_FAILED.name()); // 判断是否为终极状态了再更新是为了防止异步接口比同步接口返回得快的情况
}
} else {
JSONObject body = JsonUtil.parse(rep.getBody());
if (body != null) {
String status = body.getString(RECV_STATUS);
dto.setReturnTs(CommHelper.format(currentDate, DateType.YYYYMMDDHHMMSS.getText()));
if (OK.equalsIgnoreCase(status)) {
if (!sapVoucherIdfy.getReturnStatus().equals(SapReturnStatusEnum.POST_FAILED)
|| !sapVoucherIdfy.getReturnStatus().equals(SapReturnStatusEnum.POST_SUCCESS)) {
dto.setReturnStatus(SapReturnStatusEnum.ACCEPTED.name()); // 已接收
}
} else {
dto.setPostStatus(status);
dto.setVoucherStatus(VoucherStatusEnum.SEND_FAILED.name());
if (!sapVoucherIdfy.getReturnStatus().equals(SapReturnStatusEnum.POST_FAILED)
|| !sapVoucherIdfy.getReturnStatus().equals(SapReturnStatusEnum.POST_SUCCESS)) {
dto.setReturnStatus(SapReturnStatusEnum.POST_FAILED.name()); // 过账失败
}
String[] trimStrs = CommHelper.trimStrs(parseMessage(body).toString(), 1000);
if (trimStrs != null && trimStrs.length > 0)
dto.setReturnMsg(trimStrs[0]);
}
}
}
sapVoucherIdfyDao.updateByPrimaryKeySelective(dto); // 更新凭证的状态
// todo 插入数据至日志记录表
CfvtApiLog cfvtApiLog = new CfvtApiLog ();
cfvtApiLogDao.selectByPrimaryKey(dto.getIdfyId());
cfvtApiLog.setCreateTime(currentDate);
cfvtApiLog.setBusinessCode(dto.getIdfyId());
cfvtApiLog.setRemark("同步发送接口日志记录");
cfvtApiLog.setRequestParam(JsonUtil.toJSONString(req));
cfvtApiLog.setResponse(JsonUtil.toJSONString(rep));
cfvtApiLog.setServiceCode("sendToSAP");
cfvtApiLog.setCreator("sys");
cfvtApiLog.setState("success");
cfvtApiLogDao.insertSelective(cfvtApiLog);
}
七、总结**
在使用配置文件配置的时候 ,有一个坑
这两个不一样哦 ,前面一个方法名必须是以TxNew结尾,后面一个方法名必须是TxNew开头哦,不然到时候匹配不到的。
嘻嘻嘻 好了 大概就是这么多了。
长这么大 第一次写博客 鉴于技术和文笔有限,所以恳请各位大佬多多海涵~