如果我们工作个三到五年,一定会在项目中遇到多线程、高并发需要加锁的场景,以及需要针对业务做异常处理;今天我以物流项目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块中释放所有获取的锁。
控制流图
flowchart TD
A[开始] --> B{订单ID列表为空?}
B -->|Yes| C[抛出异常:请选择要操作的数据!]
B -->|No| D[获取分布式锁]
D --> E{获取锁失败?}
E -->|Yes| F[抛出异常:订单被其他操作锁定,请稍等重试!]
E -->|No| G[查询订单]
G --> H{订单不存在?}
H -->|Yes| I[抛出异常:订单号不存在]
H -->|No| J[过滤复核状态订单]
J --> K{存在复核状态订单?}
K -->|Yes| L[抛出异常:订单状态不满足取消条件!]
K -->|No| M[记录日志]
M --> N[更新订单状态]
N --> O[批量插入日志]
O --> P[返回成功]
P --> Q[释放锁]
Q --> R[结束]
F --> Q
I --> Q
L --> Q
C --> Q
> 说明
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
简介:Redisson 是一个用于 Redis 的 Java 客户端,它不仅提供了基本的 Redis
操作,还封装了许多高级功能,使其在分布式系统中更加易用。 主要功能: 分布式锁:包括公平锁、可重入锁、多锁等。 分布式集合:如
Set、List、Map 等。 分布式队列:如 BlockingQueue、Deque 等。 分布式原子操作:如
AtomicLong、AtomicDouble 等。 分布式计数器:如 CountDownLatch、Semaphore 等。 优点:
高性能:利用 Redis 的高性能特性。 易用性:提供了丰富的 API 和数据结构,简化了分布式系统的开发。 可靠性:支持 Redis
集群模式,提高了系统的可用性和可靠性。 RLock 简介:RLock 是 Redisson
提供的分布式锁接口,用于在分布式环境中实现互斥锁。 主要方法: tryLock():尝试获取锁,立即返回结果。 tryLock(long
waitTime, long leaseTime, TimeUnit
unit):尝试获取锁,等待指定时间,如果获取成功,则锁在指定时间内有效。 unlock():释放锁。 使用场景:
并发控制:在分布式系统中,多个节点可能同时访问同一资源,使用分布式锁可以确保同一时间只有一个节点可以操作该资源。
防止竞态条件:避免多个节点同时修改同一数据导致的不一致问题。 代码中的使用 在提供的代码中,RLock
用于确保对订单的修改操作是互斥的,防止多个请求同时修改同一个订单。具体使用如下:
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()
在 Spring 框架中TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 用于显式地标记当前事务为只回滚状态。这意味着无论事务后续的操作如何,最终都会回滚事务,而不会提交。
具体作用
标记事务为回滚:
当调用 setRollbackOnly() 方法后,Spring 事务管理器会将当前事务标记为只回滚状态。
即使后续的代码没有抛出异常,事务也不会被提交,而是会被回滚。
确保事务一致性:
在捕获到异常或其他错误情况时,调用 setRollbackOnly() 可以确保事务的一致性,避免部分操作成功、部分操作失败的情况。
这对于维护数据的完整性和一致性非常重要。
避免隐式回滚:
默认情况下,Spring 事务管理器会在遇到运行时异常(RuntimeException)时自动回滚事务。
但是,对于某些特定的异常(如 CheckedException),默认情况下事务不会自动回滚。在这种情况下,手动调用 setRollbackOnly() 可以确保事务回滚。
代码示例
在提供的代码中,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(); 是一个非常有用的工具,用于在捕获到异常或其他错误情况时显式地标记当前事务为只回滚状态。这确保了事务的一致性和数据的完整性,避免了部分操作成功、部分操作失败的情况。
不合理或者有问题的,请大家及时纠正,谢谢!!!!!