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

一. 测试工具

测试工具: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 等待获取锁时间(最大尝试获得锁的时间),超时返回false
               leaseTime – 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拿到锁,线程二没拿到锁

### 12306 算法实现 为了实现高效的机制,通常会采用Redis中的Bitmap数据结构来处理高并发场景下的资源分配问题。下面是一个简化版的Python脚本,该脚本展示了如何利用`requests`库模拟HTTP请求并结合多线程技术提高效率。 #### 使用 Redis Bitmap 进行库存管理 由于火车数量有限,在大量用户同时访问的情况下容易造成现象。因此可以借助于Redis提供的原子操作特性以及其内部高效的数据存储方式——BitMap来进行精准控制: ```python import redis from threading import Thread import time r = redis.Redis(host='localhost', port=6379, db=0) def set_ticket(ticket_id): """ 设置某张车的状态 """ key = f'ticket:{ticket_id}' if not r.getbit(key, 0): # 如果此位置为 False,则表示可售 r.setbit(key, 0, True) # 占位成功后将其置为True不可再售 return "success" else: return "fail" print(set_ticket('G4741')) # 测试函数调用 ``` 这段代码定义了一个名为 `set_ticket()` 的辅助函数,用来更新指定ID对应车次下某个座位是否已被占用的情况;当返回值为 `"success"` 表明占座成功反之则失败。 #### 构建 HTTP 请求发送模块 接下来构建一个能够向目标服务器发起GET/POST请求的功能组件,这里假设已经获取到了必要的Cookie信息以便绕过登录验证环节(实际应用中需合法合规地获得这些参数): ```python import requests session = requests.Session() headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', } cookies = {'cookie_name': 'your_cookie_value'} def check_availability(train_number): url = f'https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice?train_no={train_number}&from_station=&to_station=&date=' try: response = session.get(url=url, headers=headers, cookies=cookies) data = response.json() second_class_seat_info = data['data']['wz'] print(f'查询 {train_number} 次列车...') if int(second_class_seat_info) > 0: submit_order(train_number) except Exception as e: print(e) def submit_order(train_number): order_url = f'https://example.com/order/{train_number}' # 替换成真实的下单接口地址 payload = {"trainNo": train_number} try: resp = session.post(order_url, json=payload, headers=headers, cookies=cookies).json() if resp["status"] == "ok": print(f'{train_number} 订单创建成功') elif resp["message"]: print(resp["message"]) except Exception as ex: print(ex) ``` 上述两部分共同构成了整个系统的业务逻辑框架,其中包含了对特定班列二等座余量检测(`check_availability`) 和尝试提交订单 (`submit_order`)两个主要流程。需要注意的是这里的URL仅为示意性质,请替换为真实有效的API路径。 #### 多线程速执行过程 最后一步则是引入多线程机制进一步提升整体性能表现,使得同一时间内能有更多的实例参与到竞争当中从而增成功的几率: ```python threads = [] for i in range(5): # 创建五个工作线程 t = Thread(target=lambda: check_availability('G4741')) threads.append(t) t.start() for thread in threads: thread.join() # 等待所有子线程结束 ``` 以上即为基于 Python 编写的简易版本12306工具的核心组成部分[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值