【仿12306项目】通过加“锁”,解决高并发抢票的超卖问题

ops/2025/2/3 12:06:30/

文章目录

  • 一. 测试工具
  • 二. 超卖现象演示
  • 三. 原因分析
  • 四. 解决办法
    • 方法一:加synchronized锁
      • 1. 单个服务节点情况
      • 2. 增加服务器节点,分布式环境synchronized失效演示
    • 方法二:使用Redis分布式锁锁解决超卖问题
    • 方法三:使用Redisson看门狗解决锁超时的问题
      • 1. 添加Redisson依赖
      • 2. 使用Redisson看门狗
      • 3. 结果
    • 方法四:Redis红锁解决单机Redis宕机问题

一. 测试工具

测试工具:JMeter 参考文档
5.6.3版本下载链接:下载链接

二. 超卖现象演示

在这里插入图片描述

以一等座票为例,设置有8张票,此时开启500个线程同时购票:

在这里插入图片描述
两秒内请求全部发送完成。此时查看数据库车票表:
在这里插入图片描述

显示为负数,超卖了55张!!

三. 原因分析

问题代码段:

        // 查出余票记录,需要得到真实的库存DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);LOG.info("查出余票记录:{}", dailyTrainTicket);// 扣减余票数量,并判断余票是否足够reduceTickets(req, dailyTrainTicket);

若某时刻余票为1,如果这时候有多个线程并发进入到这里,那么它们同时查数据库,均可查到有余票1,并且都能校验为余票足够,从而执行下面的购票逻辑

因此为了避免这样的问题产生,应该对这一段代码加锁,限制并发访问

四. 解决办法

方法一:加synchronized锁

1. 单个服务节点情况

在上述问题代码段产生的方法上加上synchronized

public synchronized void doConfirm(ConfirmOrderDoReq req) {

再次执行500个线程同时购票:
在这里插入图片描述

优点:可以看到,此时成功解决超卖问题,也没有重卖
不足:由于上锁,相当于是一个个的执行,会导致售票效率降低!响应较慢!(线程花了十多秒才结束)

2. 增加服务器节点,分布式环境synchronized失效演示

在这里插入图片描述

增加一个服务节点,模拟分布式环境,再次测试

在这里插入图片描述

可以看到,超卖了一张票。
因此,分布式环境下,synchronized会失效! 只能解决单机锁问题,不能解决多机锁问题

方法二:使用Redis分布式锁锁解决超卖问题

1. 添加Redis分布式

在问题代码段的方法中,添加Redis分布式锁:

public void doConfirm(ConfirmOrderDoReq req) {String key = req.getDate() + "-" + req.getTrainCode();// 设置超时时间为5秒Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(key, key, 5, TimeUnit.SECONDS);if (Boolean.TRUE.equals(setIfAbsent)) {LOG.info("恭喜,抢到锁了!");} else {// 只是没抢到锁,并不知道票抢完了没,所以提示稍候再试LOG.info("很遗憾,没抢到锁!");throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);}try{...}finally{LOG.info("购票流程结束,释放锁!lockKey:{}", lockKey);redisTemplate.delete(lockKey);LOG.info("购票流程结束,释放锁!");}
}

将key设置为日期和车次的拼接,意为只有同一日期同一车次之间进行上锁

2. 结果

解决超卖问题,但有时还会剩票没卖完。因此redis分布式锁也会导致效率降低,同时,由于设置了timeout为5秒,若购票业务或者其他业务耗时超过5秒,则此时会提前释放锁,造成并发问题

方法三:使用Redisson看门狗解决锁超时的问题

由于redis分布式锁存在业务结束时间比设置的超时时间长的问题,因此可以引入守护线程,在一定时间的时候,延长超时时间,同时守护线程会随着主线程终止而终止,也不会无限延长终止时间

1. 添加Redisson依赖

            <!--至少3.18.0版本,才支持spring boot 3--><!--升级到3.20.0,否则打包生产会报错:Could not initialize class org.redisson.spring.data.connection.RedissonConnection--><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.21.0</version></dependency>

2. 使用Redisson看门狗

修改上锁与解锁内容,使用Redission自带的方法实现看门狗:

public void doConfirm(ConfirmOrderDoReq req) {// 获取分布式String lockKey = RedisKeyPreEnum.CONFIRM_ORDER + "-" + DateUtil.formatDate(req.getDate()) + "-" + req.getTrainCode();RLock lock = null;try {// 使用redisson,自带看门狗lock = redissonClient.getLock(lockKey);/**waitTime – the maximum time to acquire the lock 等待获取锁时间(最大尝试获得锁的时间),超时返回falseleaseTime – lease time 锁时长,即n秒后自动释放锁time unit – time unit 时间单位*/// boolean tryLock = lock.tryLock(30, 10, TimeUnit.SECONDS); // 不带看门狗boolean tryLock = lock.tryLock(0, TimeUnit.SECONDS); // 带看门狗if (tryLock) {LOG.info("恭喜,抢到锁了!");// 可以把下面这段放开,只用一个线程来测试,看看redisson的看门狗效果// for (int i = 0; i < 30; i++) {//     Long expire = redisTemplate.opsForValue().getOperations().getExpire(lockKey);//     LOG.info("锁过期时间还有:{}", expire);//     Thread.sleep(1000);// }} else {// 只是没抢到锁,并不知道票抢完了没,所以提示稍候再试LOG.info("很遗憾,没抢到锁");throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);}// 业务逻辑...}catch (InterruptedException e){LOG.error("购票流程异常", e);}finally {LOG.info("购票流程结束,释放锁!");if (null != lock && lock.isHeldByCurrentThread()) {lock.unlock();}}}

3. 结果

设置多线程进行模拟抢票,实验多次后,发现第一次抢票始终是会还剩下一些票,多次抢票后票数为0,且不会出现超卖。

方法四:Redis红锁解决单机Redis宕机问题

以上带看门狗的锁存在以下问题:
如果Redis节点宕机挂了,同时又有一个线程拿到了某个锁,那么此时如果Redis节点自动切换,又一个线程进来就有可能拿到同一个锁,因为此时Redis中没有该锁的key,违反了锁的互斥性,就会出现一些问题。
此时就可以利用Redis红锁来解决单机Redis宕机问题

Redis红锁用于Redis多节点情况。通常设有奇数个节点的Redis服务,每个线程在进入上锁代码段时,只有拿到半数以上的Redis的锁,才算拿到锁。

例如:
有 A B C D E 五个节点的Redis服务
线程1:拿到ABC
线程2:拿到DE
则此时仅算线程1拿到锁,线程二没拿到锁


http://www.ppmy.cn/ops/155301.html

相关文章

C++ 写一个简单的加减法计算器

************* C topic&#xff1a;结构 ************* Structure is a very intersting issue. I really dont like concepts as it is boring. I would like to cases instead. If I want to learn something, donot hesitate to make shits. Like building a house. Wh…

【力扣每日一题】LeetCode 2412: 完成所有交易的初始最少钱数

LeetCode 2412: 完成所有交易的初始最少钱数 题目解析 问题描述 给定一个二维数组 transactions&#xff0c;每个元素 transactions[i] [costi, cashbacki] 表示一个交易。对于每笔交易&#xff0c;要求你完成该交易时有足够的初始资金 money&#xff0c;并且交易会减少或增…

React 前端框架开发详细操作

一、引言 在当今的 web 开发领域,React 作为一款流行的前端框架,以其高效的组件化开发模式、虚拟 DOM 带来的高性能以及灵活的生态系统,受到了广大开发者的青睐。无论是开发小型的单页应用还是大型的企业级项目,React 都能展现出强大的能力。本文将详细介绍 React 前端框架…

CSS 中调整元素大小的全面指南

CSS 中调整元素大小的全面指南 1. 原始尺寸&#xff08;固有尺寸&#xff09;示例代码&#xff1a;图像的固有尺寸 2. 设置具体的尺寸示例代码&#xff1a;设置固定宽度和高度 3. 使用百分比示例代码&#xff1a;使用百分比设置宽度 4. 使用百分比作为外边距和内边距示例代码&a…

AI技术路线(marked)

人工智能&#xff08;AI&#xff09;是一个非常广泛且充满潜力的领域&#xff0c;它涉及了让计算机能够执行通常需要人类智能的任务&#xff0c;比如感知、推理、学习、决策等。人工智能的应用已经渗透到各行各业&#xff0c;从自动驾驶到医疗诊断&#xff0c;再到推荐系统和自…

第38周:猫狗识别 (Tensorflow实战第八周)

目录 前言 一、前期工作 1.1 设置GPU 1.2 导入数据 输出 二、数据预处理 2.1 加载数据 2.2 再次检查数据 2.3 配置数据集 2.4 可视化数据 三、构建VGG-16网络 3.1 VGG-16网络介绍 3.2 搭建VGG-16模型 四、编译 五、训练模型 六、模型评估 七、预测 总结 前言…

VScode+Latex (Recipe terminated with fatal error: spawn xelatex ENOENT)

使用VSCode编辑出现Recipe terminated with fatal error: spawn xelatex ENOENT问题咋办&#xff1f; 很好解决&#xff0c;大概率的原因是因为latex没有添加到系统环境变量中&#xff0c;所有设置的编译工具没有办法找到才出现的这种情况。 解决方法&#xff1a; winR 然后输…

Qpython+Flask监控添加发送语音中文信息功能

对QpythonFlask实现对小孩学习的监控-CSDN博客中html页面进行改造&#xff0c;利用Ajax&#xff0c;提交一段文字&#xff0c;发送到数据库&#xff0c;再在服务器&#xff0c;发送该段文件给手机端&#xff0c;然手机端TTS朗读出来&#xff0c;增加了父母监控小孩学习&#xf…