当前位置: 首页 > news >正文

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

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

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

一、乐观锁

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

        实现方式通常通过版本号机制实现:在表中增加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测试:对于同一位用户只有第一次报名成功,其他均失败。

http://www.xdnf.cn/news/1420795.html

相关文章:

  • 大型语言模型监督微调(SFT)
  • openharmony之sandbox沙箱机制详解
  • K8S网络组件Calico深度解析
  • Python OpenCV图像处理与深度学习:Python OpenCV图像几何变换入门
  • 深入解析 Go 程序逆向风险与防护策略
  • 如何对嵌入式软件进行单元测试
  • 无重复字符的最长子串,leetCode热题100,C++实现
  • Vue3响应式陷阱:如何避免ref解构导致的响应式丢失
  • 电动自行车车架前叉振动、组合件冲击、把立管弯曲强度试验机等机械强度性能测试项目解析
  • 【趣味阅读】Python 文件头的秘密:从编码声明到 Shebang
  • Spring事务管理策略对比与性能优化实践指南
  • AI辅助论文写作,在《人工智能生成合成内容标识办法》出台后的新规则
  • HarmonyOS 应用开发深度实践:深入 Stage 模型与 ArkTS 声明式 UI
  • Linux - JDK安装
  • 刷题之链表oj题目
  • 突破超强回归模型,高斯过程回归!
  • 大语言模型对齐
  • VMware pro16(许可证)+centos 7超详细安装教程
  • MQ使用场景分析
  • 【RK3576】【Android14】PMIC电源管理
  • DVWA靶场通关笔记-SQL Injection Blind(SQL盲注 Impossible级别)
  • kubectl-etcd
  • 【C++】内存管理机制:从new到delete全解析
  • 植物中lncRNA鉴定和注释流程,代码(包含Identified,Classification,WGCNA.....)
  • 开发中使用——鸿蒙CoreSpeechKit语音识别
  • 基于MCP架构的OpenWeather API服务端设计与实现
  • C#在物联网GPS经纬度转换为百度地图地址
  • 亚马逊云代理商:如何选择适合的AWS EC2实例类型?
  • CVE Push Service | 高危漏洞实时情报自动化推送工具
  • Vue基础知识-使用监视属性watch和计算属性computed实现列表过滤+排序