在共享经济和出行服务的背景下, 抢单 成为了司机端应用中至关重要的功能之一。为了提高用户体验,确保系统能够高效、准确地处理抢单请求,必须解决在高并发场景下可能出现的数据不一致问题。本文将带大家一起探讨如何在 Java 中实现司机抢单逻辑,并介绍解决并发问题的方案。
一、司机抢单的基础实现
我们首先来看一个基本的抢单逻辑实现。这个版本通过 Redis 缓存减少对数据库的压力,并对订单状态进行更新。
@Override
public Boolean robNewOrder(Long driverId, Long orderId) {
// 1. 判断订单是否存在,通过Redis,减少数据库压力
String redisKey = RedisConstant.ORDER_ACCEPT_MARK + orderId;
if (Boolean.FALSE.equals(redisTemplate.hasKey(redisKey))) {
// 如果Redis中没有订单,表示该订单已被抢走,抛出异常
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
// 2. 司机抢单,修改订单状态为“已接单”
LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderInfo::getId, orderId);
OrderInfo orderInfo = orderInfoMapper.selectOne(wrapper);
orderInfo.setStatus(OrderStatus.ACCEPTED.getStatus());
orderInfo.setDriverId(driverId);
orderInfo.setAcceptTime(new Date());
// 3. 更新订单状态
int rows = orderInfoMapper.updateById(orderInfo);
if (rows != 1) {
// 更新失败,抛出异常
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
// 4. 删除Redis中的标识
redisTemplate.delete(redisKey);
return true;
}
解析:
- 订单存在性检查:通过 Redis 来判断订单是否可被抢,减少了直接访问数据库的开销。使用 Redis 的好处在于其高性能和快速响应,适合用于高并发场景。
- 订单状态更新:通过数据库操作将订单状态修改为“已接单”。
- Redis标志删除:抢单成功后,删除订单在 Redis 中的标记,确保其他司机无法再次抢单。
这种实现方法在普通业务场景下足够有效,但在高并发情况下,多个司机同时抢同一个订单时,可能会出现 数据竞争 问题,导致订单被多次更新。
二、并发问题:如何解决多司机同时抢单?
在高并发情况下,多个司机可能会同时尝试抢同一个订单。此时,如果不加控制,可能会导致 订单状态 在短时间内被多个司机同时修改,最终导致 数据不一致。为了解决这个问题,我们可以引入 乐观锁。
三、通过乐观锁解决并发问题
乐观锁 的设计思路是,在更新数据时,检查记录是否在此期间被其他请求修改。如果数据状态发生了变化,更新操作将失败,这样可以避免多个司机同时抢单导致的竞态问题。
@Override
public Boolean robNewOrder1(Long driverId, Long orderId) {
// 1. 判断订单是否存在,通过Redis减少数据库压力
String redisKey = RedisConstant.ORDER_ACCEPT_MARK + orderId;
if (Boolean.FALSE.equals(redisTemplate.hasKey(redisKey))) {
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
// 2. 司机抢单,修改订单状态为“已接单”,使用乐观锁
LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderInfo::getId, orderId);
wrapper.eq(OrderInfo::getStatus, OrderStatus.WAITING_ACCEPT.getStatus()); // 仅允许状态为“等待接单”的订单
// 创建一个新的订单对象,只更新需要的字段
OrderInfo orderInfo = new OrderInfo();
orderInfo.setStatus(OrderStatus.ACCEPTED.getStatus());
orderInfo.setDriverId(driverId);
orderInfo.setAcceptTime(new Date());
// 3. 更新订单信息(乐观锁确保订单状态没有被其他请求修改)
int rows = orderInfoMapper.update(orderInfo, wrapper);
if (rows != 1) {
// 更新失败,表示订单状态已经被其他司机抢走
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
// 4. 删除Redis中的标志,防止其他司机重复抢单
redisTemplate.delete(redisKey);
return true;
}
改进点:
- 状态判断:在修改订单之前,增加了订单状态的条件判断,确保只有状态为“等待接单”的订单才能被更新。如果多个司机同时抢同一个订单,只有一个抢单请求会成功更新状态,其他请求会因为订单状态变化而失败。
- 乐观锁机制:通过在更新时检查订单状态的方式实现了乐观锁。在高并发场景下,这种机制能够有效防止订单被多次抢单,确保订单状态更新的安全性。
四、Redis分布式锁的进一步优化
尽管乐观锁可以在多数场景下解决并发问题,但如果并发量极高,可能仍存在极少数的边界问题。例如,多个请求几乎同时执行状态检查,虽然乐观锁能减少问题发生的概率,但仍无法彻底解决 竞态条件。
为此,我们可以引入 Redis分布式锁 来进一步增强并发控制。在抢单操作开始时,为订单加上分布式锁,确保同一时间只有一个司机能够执行抢单操作。
// 1. 尝试获取分布式锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(redisKey, driverId, 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(lock)) {
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL); // 其他司机已抢单
}
// 2. 执行抢单逻辑...
// 3. 释放锁
redisTemplate.delete(redisKey);
分布式锁优势:
- 保证在抢单过程中,只有一个线程可以执行订单更新操作,彻底杜绝并发问题。
- 锁的设置有过期时间,避免因异常未释放锁而导致订单无法处理。
五、总结
抢单作为出行服务中的重要环节,涉及到高并发情况下的数据一致性问题。通过基础的抢单实现,我们能够理解如何利用 Redis 缓存提升系统性能。但在高并发场景下,需要引入 乐观锁 和 分布式锁 机制,来确保订单状态更新的安全性和准确性。
通过合理的锁机制设计,我们不仅可以提升系统的并发处理能力,还能够保证 数据一致性,从而为用户提供流畅、稳定的抢单体验。