如果我们工作个三到五年,一定会在项目中遇到多线程、高并发需要加锁的场景,以及需要针对业务做异常处理;今天我以物流项目OMS中的一段代码,解析一波,有问题的请在评论区留言一起讨论
代码案例:
@Resource
private RedissonClient redissonClient;
@Transactional(rollbackFor = Exception.class)
public AjaxResult updateReviewStatusByIds(Map<String, Object> idsAndStatus) {
List<String> idsList = null;
try {
idsList = (ArrayList<String>) idsAndStatus.get("ids");
if (CollectionUtils.isEmpty(idsList)) {
throw new CustomException("请选择要操作的数据!", 520);
}
for (String orderCode : idsList) {
RLock lock = redissonClient.getLock("review:lock:" + orderCode);
if (!lock.tryLock(5, 10, TimeUnit.SECONDS)) {
throw new CustomException("选择的订单中订单号:" + orderCode + ",被其他操作锁定,请稍等重试!");
}
}
//根据入库单号集合查询
List<OmsGodownEntry> omsGodownEntryList = omsGodownEntryMapper.selectOmsGodownEntryByIds(idsList);
if (CollectionUtils.isEmpty(omsGodownEntryList)) {
throw new CustomException("订单号:" + idsList + "不存在", 520);
}
//1=复核 不允许取消
List<String> codes = omsGodownEntryList.stream().filter(v -> "1".equals(v.getSendBmsStatus())).map(OmsGodownEntry::getGodownEntryCode).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(codes)) {
throw new CustomException("以下订单状态不满足取消条件!" + codes, 520);
}
String createBy = SecurityUtils.getLoginUser().getUsername();
String createUserName = SecurityUtils.getLoginUser().getUser().getNickName();
List<OmsBasicLog> omsBasicLogs = new ArrayList<>();
List<String> omsGodownEntries = new ArrayList<>();
for (OmsGodownEntry omsGodownEntry : omsGodownEntryList) {
//未复核 跳过
if ("0".equals(omsGodownEntry.getSendBmsStatus())) {
continue;
}
OmsBasicLog omsBasicLog = new OmsBasicLog();
omsBasicLog.setType(7);
omsBasicLog.setCode(omsGodownEntry.getGodownEntryCode());
omsBasicLog.setContent("订单状态改变:已取消 ——>未复核");
omsBasicLog.setBeforeContent(JSONObject.toJSONString(idsAndStatus));
omsBasicLog.setCreateBy(createBy);
omsBasicLog.setCreateUserName(createUserName);
omsBasicLogs.add(omsBasicLog);
omsGodownEntries.add(omsGodownEntry.getGodownEntryCode());
}
if (CollectionUtils.isNotEmpty(omsGodownEntries)) {
GodownEntryInfo godownEntryInfo = buildGodownEntryInfo(omsGodownEntries, "0");
omsGodownEntryMapper.updateBmsStatusByCodeList(godownEntryInfo);
}
if (CollectionUtils.isNotEmpty(omsBasicLogs)) {
omsBasicLogService.batchInsertOmsBasicLog(omsBasicLogs);
}
return AjaxResult.success();
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
logger.error("操作失败,inputParam:{},errorMsg:{}", idsAndStatus, e.getMessage(), e);
return AjaxResult.error("操作失败,失败原因:" + e.getMessage());
} finally {
if (CollectionUtils.isNotEmpty(idsList)) {
for (String orderCode : idsList) {
RLock lock = redissonClient.getLock("review:lock:" + orderCode);
if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
}
代码功能解释
这段Java代码定义了一个名为 updateReviewStatusByIds 的方法,用于更新仓库入库单的审核状态。具体功能如下:
参数校验:检查传入的订单ID列表是否为空,如果为空则抛出异常。
**分布式锁:**为每个订单ID获取分布式锁,确保同一时间只有一个线程可以操作该订单。
查询订单:根据订单ID列表查询对应的仓库入库单。
状态校验:检查订单是否处于复核状态,如果是则不允许取消。
日志记录:记录订单状态变更的日志。
状态更新:更新订单的状态为“未复核”。
事务管理:使用 @Transactional 注解确保方法在事务中执行,遇到异常时回滚事务。
**异常处理:**捕获并记录异常,返回错误信息。
释放锁:在finally块中释放所有获取的锁。
控制流图
> 说明
A:方法开始。
B:检查订单ID列表是否为空。
C:如果为空,抛出异常。
D:为每个订单ID获取分布式锁。
E:检查是否成功获取锁。
F:如果获取锁失败,抛出异常。
G:根据订单ID列表查询对应的仓库入库单。
H:检查查询结果是否为空。
I:如果为空,抛出异常。
J:过滤出复核状态的订单。
K:检查是否存在复核状态的订单。
L:如果存在,抛出异常。
M:记录订单状态变更的日志。
N:更新订单状态为“未复核”。
O:批量插入日志。
P:返回成功结果。
Q:释放所有获取的锁。
R:方法结束
在这段代码中,RLock 是 Redisson 客户端提供的分布式锁接口。Redisson 是一个用于 Redis 的 Java 客户端,它提供了许多高级功能,包括分布式锁、分布式集合、分布式队列等。以下是这段代码中使用的技术及其简要介绍:
技术介绍
Redisson
for (String orderCode : idsList) {
RLock lock = redissonClient.getLock("review:lock:" + orderCode);
if (!lock.tryLock(5, 10, TimeUnit.SECONDS)) {
throw new CustomException("选择的订单中订单号:" + orderCode + ",被其他操作锁定,请稍等重试!");
}
}
获取锁:
通过 redissonClient.getLock 方法获取一个分布式锁实例。
尝试获取锁:使用 tryLock 方法尝试获取锁,等待最多 5 秒,如果获取成功,锁在 10 秒内有效。
释放锁:
在 finally 块中释放锁,确保即使发生异常也能释放锁。
总结
这段代码使用了 Redisson 客户端提供的 RLock 分布式锁,确保在分布式环境中对订单的修改操作是互斥的,从而避免了竞态条件和数据不一致的问题。Redisson 的高可靠性和易用性使得分布式锁的实现变得更加简单和高效。
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
@Transactional(rollbackFor = Exception.class)
public AjaxResult updateOmsGodownEntryOrderReviewStatusByIds(Map<String, Object> idsAndStatus) {
try {
// 业务逻辑
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
logger.error("操作失败,inputParam:{}, errorMsg:{}", idsAndStatus, e.getMessage(), e);
return AjaxResult.error("操作失败,失败原因:" + e.getMessage());
} finally {
// 释放锁
}
}
> 详细解释
捕获异常:
在 catch 块中捕获到任何异常时,调用 setRollbackOnly() 方法。
这样可以确保即使在 catch 块中进行了其他操作,事务仍然会被回滚。
记录日志:
使用 logger.error 记录异常信息,方便后续排查问题。
返回错误信息:
返回一个包含错误信息的 AjaxResult 对象,告知客户端操作失败的原因。
总结
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 是一个非常有用的工具,用于在捕获到异常或其他错误情况时显式地标记当前事务为只回滚状态。这确保了事务的一致性和数据的完整性,避免了部分操作成功、部分操作失败的情况。
不合理或者有问题的,请大家及时纠正,谢谢!!!!!