在高并发场景下,如何保证数据的一致性和操作的原子性,是一个非常重要的课题。今天,我将和大家分享一个实际的开发案例: 通过Redisson分布式锁实现司机抢单功能,并探讨其背后的一些设计与实现细节。
场景介绍
在网约车平台中,多个司机可能会同时抢同一个订单。如果没有有效的并发控制机制,多个司机可能同时接到同一订单,导致系统的混乱和用户体验的下降。简单的数据库锁虽然可以解决这个问题,但在高并发环境下,数据库锁会造成系统性能下降,甚至可能导致数据库瓶颈。因此,引入 Redis分布式锁 是一种更高效的解决方案。
Redisson 是 Redis 官方推荐的 Java 客户端之一,它提供了丰富的分布式工具,其中的 RLock 分布式锁非常适合用来解决类似司机抢单这种高并发场景下的数据一致性问题。
代码实现
我们通过 Redisson
提供的分布式锁来实现 robNewOrder
方法,确保同一时间只有一个司机能够成功抢到订单。
@Override
public Boolean robNewOrder(Long driverId, Long orderId) {
// 使用 Redis 检查订单是否存在,减少对数据库的直接查询
String redisKey = RedisConstant.ORDER_ACCEPT_MARK + orderId;
if (!redisTemplate.hasKey(redisKey)) {
// 订单已不存在,抢单失败
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
// 获取分布式锁
RLock lock = redissonClient.getLock(RedisConstant.ROB_NEW_ORDER_LOCK + orderId);
try {
// 尝试获取锁,设置等待时间和自动释放时间
boolean flag = lock.tryLock(
RedisConstant.ROB_NEW_ORDER_LOCK_WAIT_TIME,
RedisConstant.ROB_NEW_ORDER_LOCK_LEASE_TIME,
TimeUnit.SECONDS
);
if (flag) {
// 锁获取成功,再次确认订单是否存在
if (!redisTemplate.hasKey(redisKey)) {
// 抢单失败
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
// 修改订单状态为“已接单”
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());
int rows = orderInfoMapper.updateById(orderInfo);
if (rows != 1) {
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
// 删除Redis中的订单标识
redisTemplate.delete(redisKey);
}
} catch (Exception e) {
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL, e);
} finally {
// 确保锁被释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return true;
}
代码解析
这段代码通过 Redis 和 Redisson 分布式锁来确保抢单操作的唯一性,以下是代码中的几个关键点:
-
Redis缓存标识判断
使用redisTemplate.hasKey(redisKey)
来判断订单是否存在于缓存中。通过缓存减少数据库访问频率,降低系统的整体负载。在并发抢单场景下,Redis可以大幅提升系统的响应速度。 -
分布式锁的获取与使用
通过redissonClient.getLock()
获取一个分布式锁,然后使用tryLock
尝试获取锁。这里我们为锁设置了 等待时间 和 租约时间。等待时间是指司机最多等多长时间尝试获取锁,租约时间则防止某个司机获得锁后一直不释放,导致锁资源被长期占用。 -
订单状态的更新
在成功获取锁后,通过数据库更新订单的状态为“已接单”,并将抢单的司机信息(如司机ID和接单时间)保存到订单中。这里使用了LambdaQueryWrapper
查询和更新订单,确保并发下订单状态的准确性。 -
锁的释放
无论抢单是否成功,或者是否抛出异常,最后都要在finally
块中释放锁,避免锁被长时间占用。通过lock.isHeldByCurrentThread()
确保只有当前持有锁的线程才能释放锁,防止锁误释放。
并发问题与解决方案
在高并发场景下,多个司机可能会同时尝试抢同一订单。为了解决可能的并发问题,以下几个设计点值得注意:
-
避免缓存击穿:通过 Redis 缓存订单的状态,减少并发请求对数据库的压力。使用 Redis 的高并发特性可以有效降低数据库的负载,防止缓存击穿导致的大量请求涌向数据库。
-
防止锁死:通过为锁设置租期(即
leaseTime
),防止锁被某个长时间不释放的线程占用。这样可以有效避免因异常或长时间任务导致的锁死。 -
锁的粒度:分布式锁的粒度直接影响到系统的并发性能。在这里,我们的锁是基于订单ID的,即每个订单对应一个锁,确保针对同一订单的操作是串行的,而不同订单的操作则可以并行处理,提升了系统的整体并发处理能力。
Redisson 分布式锁的优势
Redisson 提供了比手动使用 Redis 命令更简洁、强大的 API。它的分布式锁 RLock
实现了自动续期、可重入锁等功能,大大简化了分布式锁的使用难度。除此之外,Redisson 还提供了其他丰富的分布式工具,如信号量、计数器等,帮助开发者更高效地处理分布式环境下的并发问题。
总结
通过引入 Redisson 分布式锁,我们可以轻松地解决网约车司机抢单场景中的并发问题。分布式锁保证了同一时间只有一个司机能够抢到订单,从而保证了数据的一致性和操作的原子性。在设计和使用分布式锁时,合理设置锁的等待时间、租期以及锁的粒度,是避免性能问题和死锁的重要手段。
通过这一案例,我们可以看出 Redisson 在分布式系统中的强大作用,它不仅提供了分布式锁,还提供了许多其他分布式并发控制的工具,能够极大地简化高并发系统的开发。