【并发场景问题】超卖、一人一单业务问题的解决方案

        超卖与一人一单问题是并发场景下常见的挑战,尤其是在库存管理、限时抢购等业务中。乐观锁和悲观锁是解决这类问题的两种主要方案,它们的核心思想和适用场景有所不同。

        在解决上述业务问题之前先来了解一下什么是乐观锁和悲观锁?

一、乐观锁

        乐观锁认为并发冲突概率较低,因此不主动锁定资源,而是在更新数据时检查资源是否被其他线程修改过。如果未被修改,则正常更新;如果已被修改,则放弃操作或重试。它是一种 "先做后验" 的策略。

        实现方式通常通过版本号机制实现:在表中增加version字段,每次更新时对比版本号,一致则更新并递增版本号,不一致则更新失败。

二、悲观锁

        悲观锁认为并发操作会频繁冲突,因此在操作数据时会直接锁定资源,阻止其他线程访问,直到当前操作完成并释放锁。它是一种 "先防后做" 的策略。例如Synchronized、Lock都属于
悲观锁。

        实现方式在数据库层面,通常使用for update语句实现行级锁;在 Java 代码中可配合事务(@Transactional)确保锁的有效性。

三、乐观锁与悲观锁的适用场景以及优缺点

        针对于不同的场景应该采用不同的策略来实现性能的最大化开发。

1.乐观锁适用场景以及优缺点

适用场景

  • 读多写少,并发冲突概率较低的场景。

  • 系统追求高吞吐量,能够容忍偶尔的更新失败。

  • 业务逻辑复杂,需要较长时间,不适合长时间持有锁。

优点

  • 性能好:没有锁的开销,大大提高了高并发下的吞吐量。

  • 无死锁风险。

缺点

  • 实现稍复杂。

  • 如果冲突频繁,重试次数会很多,可能降低体验。

  • 是一种“乐观”策略,无法保证每次请求都成功。

2.悲观锁适用场景以及优缺点

适用场景

  • 写操作非常频繁,冲突概率很高的场景。

  • 对数据一致性要求极高,不允许出现任何并发问题的场景。

  • 业务逻辑执行时间较长,需要在整个过程中保持锁定。

优点

  • 保证强一致性,实现起来简单直接。

  • 避免了任何并发冲突。

缺点

  • 性能开销大:加锁是数据库层面的操作,会阻塞其他等待锁的事务,导致系统吞吐量下降。

  • 死锁风险:如果多个事务相互等待对方持有的锁,容易引发死锁。

  • 不适用于高并发读的场景,会严重影响读性能。

四、超卖业务问题解决示例

业务说明:对于活动报名业务,在高并发场景下可能会出现报名人数超出活动的人数限制,因此采用乐观锁来解决该业务问题。

        没必要在数据库中添加version字段,我们可以使用已有字段来代替,如下采用 ParticipantCount < ParticipantLimit 即可。

/**
     * 活动报名
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public 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)
    @Override
    public 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. 构建用户报名分布式锁key
        String 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测试:对于同一位用户只有第一次报名成功,其他均失败。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汤姆大聪明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值