【并发场景问题】超卖、一人一单业务问题的解决方案
超卖与一人一单问题是并发场景下常见的挑战,尤其是在库存管理、限时抢购等业务中。乐观锁和悲观锁是解决这类问题的两种主要方案,它们的核心思想和适用场景有所不同。
在解决上述业务问题之前先来了解一下什么是乐观锁和悲观锁?
一、乐观锁
乐观锁认为并发冲突概率较低,因此不主动锁定资源,而是在更新数据时检查资源是否被其他线程修改过。如果未被修改,则正常更新;如果已被修改,则放弃操作或重试。它是一种 "先做后验" 的策略。
实现方式通常通过版本号机制实现:在表中增加version
字段,每次更新时对比版本号,一致则更新并递增版本号,不一致则更新失败。
二、悲观锁
悲观锁认为并发操作会频繁冲突,因此在操作数据时会直接锁定资源,阻止其他线程访问,直到当前操作完成并释放锁。它是一种 "先防后做" 的策略。例如Synchronized、Lock都属于
悲观锁。
实现方式在数据库层面,通常使用for update
语句实现行级锁;在 Java 代码中可配合事务(@Transactional
)确保锁的有效性。
三、乐观锁与悲观锁的适用场景以及优缺点
针对于不同的场景应该采用不同的策略来实现性能的最大化开发。
1.乐观锁适用场景以及优缺点
适用场景
读多写少,并发冲突概率较低的场景。
系统追求高吞吐量,能够容忍偶尔的更新失败。
业务逻辑复杂,需要较长时间,不适合长时间持有锁。
优点
性能好:没有锁的开销,大大提高了高并发下的吞吐量。
无死锁风险。
缺点
实现稍复杂。
如果冲突频繁,重试次数会很多,可能降低体验。
是一种“乐观”策略,无法保证每次请求都成功。
2.悲观锁适用场景以及优缺点
适用场景
写操作非常频繁,冲突概率很高的场景。
对数据一致性要求极高,不允许出现任何并发问题的场景。
业务逻辑执行时间较长,需要在整个过程中保持锁定。
优点
保证强一致性,实现起来简单直接。
避免了任何并发冲突。
缺点
性能开销大:加锁是数据库层面的操作,会阻塞其他等待锁的事务,导致系统吞吐量下降。
死锁风险:如果多个事务相互等待对方持有的锁,容易引发死锁。
不适用于高并发读的场景,会严重影响读性能。
四、超卖业务问题解决示例
业务说明:对于活动报名业务,在高并发场景下可能会出现报名人数超出活动的人数限制,因此采用乐观锁来解决该业务问题。
没必要在数据库中添加version字段,我们可以使用已有字段来代替,如下采用 ParticipantCount < ParticipantLimit 即可。
/*** 活动报名*/@Transactional(rollbackFor = Exception.class)@Overridepublic void registerActivity(Long activityId, Long userId) {// 1. 查询活动信息Activity activity = baseMapper.selectById(activityId);// 2. 查询用户是否报名LambdaQueryWrapper<ActivityUser> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(ActivityUser::getActivityId, activityId).eq(ActivityUser::getUserId, userId);Long count = activityUserMapper.selectCount(queryWrapper);if (count > 0) {throw new ActivityException("用户已报名");}// 3. 检查人数限制if (activity.getParticipantCount() >= activity.getParticipantLimit()) {throw new ActivityException("活动已报满");}// 4. 更新活动报名人数(乐观锁解决并发超卖问题)LambdaUpdateWrapper<Activity> updateWrapper = new LambdaUpdateWrapper<>();updateWrapper.eq(Activity::getActivityId, activityId).lt(Activity::getParticipantCount, activity.getParticipantLimit()).setSql("participant_count = participant_count + 1");int update = baseMapper.update(updateWrapper);if (update == 0) {throw new ActivityException("活动已报满");}// 5. 记录报名信息ActivityUser activityUser = ActivityUser.builder().activityId(activityId).userId(userId).build();activityUserMapper.insert(activityUser);}
采用jmeter测试,数据库限制活动报名人数50,jmeter设置1s请求100次模拟高并发测试:
相比于无防护,并没有超卖问题出现。
五、一人一单业务问题解决示例
业务说明:继续采用上述业务场景,超卖问题已经解决,但是仍存在一个用户多次报名问题,因此采用悲观锁策略,也就是采用redisson的分布式锁来解决业务问题。
为防止在锁的执行时间内,有其余线程等待锁并在锁释放后获取锁再次执行业务,因此在锁内仍需添加双重检查机制。
/*** 活动报名 - 超卖、一人一单问题解决*/@Transactional(rollbackFor = Exception.class)@Overridepublic void registerActivity(Long activityId, Long userId) {// 1. 先检查用户是否已报名LambdaQueryWrapper<ActivityUser> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(ActivityUser::getActivityId, activityId).eq(ActivityUser::getUserId, userId);Long count = activityUserMapper.selectCount(queryWrapper);if (count > 0) {throw new ActivityException("用户已报名");}// 2. 构建用户报名分布式锁keyString userActivityLockKey = "lock:user:activity:" + userId + ":" + activityId;RLock userActivityLock = redissonClient.getLock(userActivityLockKey);try {// 3. 尝试获取锁boolean isLocked = userActivityLock.tryLock(3, 10, TimeUnit.SECONDS);if (!isLocked) {throw new ActivityException("系统繁忙,请稍后重试");}// 4. 获取锁后再次检查用户是否已报名(双重检查)count = activityUserMapper.selectCount(queryWrapper);if (count > 0) {throw new ActivityException("用户已报名");}// 5. 查询活动信息Activity activity = baseMapper.selectById(activityId);if (activity == null) {throw new ActivityException("活动不存在");}// 6. 检查活动状态是否为已审核if (!StatusEnum.approved.getCode().equals(activity.getStatus())) {throw new ActivityException("活动未审核通过,无法报名");}// 7. 检查人数限制if (activity.getParticipantCount() >= activity.getParticipantLimit()) {throw new ActivityException("活动已报满");}// 8. 更新活动报名人数(乐观锁解决并发超卖问题)LambdaUpdateWrapper<Activity> updateWrapper = new LambdaUpdateWrapper<>();updateWrapper.eq(Activity::getActivityId, activityId).lt(Activity::getParticipantCount, activity.getParticipantLimit()).setSql("participant_count = participant_count + 1");int update = baseMapper.update(updateWrapper);if (update == 0) {throw new ActivityException("活动已报满");}// 9. 记录报名信息ActivityUser activityUser = ActivityUser.builder().activityId(activityId).userId(userId).build();activityUserMapper.insert(activityUser);} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new ActivityException("系统繁忙,请稍后重试");} finally {// 10. 释放锁if (userActivityLock.isHeldByCurrentThread()) {userActivityLock.unlock();}}}
继续采用原来的jmeter测试:对于同一位用户只有第一次报名成功,其他均失败。