高并发抢票时,防止机器人刷票的令牌大闸,减轻服务器的压力(防刷+限流)

1. 为什么要引入令牌大闸?


场景1:分布式锁和限流都不能解决机器人刷票的问题,1000个请求抢票,900个限流快速失败,另外100个有可能是同一个在刷库。

引入令牌,令牌中记录用户信息,会进行校验用户是否拿过令牌,如果拿过令牌,那么几秒内不允许再获得令牌

场景2:没有余票时,需要查库存才能知道没票,会影响性能,不如查询令牌余票来的快

令牌的数量是和票数是相关的,令牌可以和票数相等,那么通过查询令牌就可以知道是否还有余票,会减少查询数据库,减少IO压力

2. 增加秒杀令牌表来维护令牌信息


增加一张表,表的创建SQL代码如下所示:

drop table if exists `sk_token`;  
create table `sk_token` (  
  `id` bigint not null comment 'id',  
  `date` date not null comment '日期',  
  `train_code` varchar(20) not null comment '车次编号',  
  `count` int not null comment '令牌余量',  
  `create_time` datetime(3) comment '新增时间',  
  `update_time` datetime(3) comment '修改时间',  
  primary key (`id`),  
  unique key `date_train_code_unique` (`date`, `train_code`)  
) engine=innodb default charset=utf8mb4 comment='秒杀令牌';

利用代码生成器生成相应的文件

3. 初始化车次信息时初始化令牌信息


在SkTokenService中实现genDaily方法

/**  
 * 初始化  
 */  
public void genDaily(Date date, String trainCode) {  
    LOG.info("删除日期【{}】车次【{}】的令牌记录", DateUtil.formatDate(date), trainCode);  
    SkTokenExample skTokenExample = new SkTokenExample();  
    skTokenExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);  
    skTokenMapper.deleteByExample(skTokenExample);  
  
    DateTime now = DateTime.now();  
    SkToken skToken = new SkToken();  
    skToken.setDate(date);  
    skToken.setTrainCode(trainCode);  
    skToken.setId(SnowUtil.getSnowflakeNextId());  
    skToken.setCreateTime(now);  
    skToken.setUpdateTime(now);  
	  //计算该车次共有多少个座位
    int seatCount = dailyTrainSeatService.countSeat(date, trainCode);  
    LOG.info("车次【{}】座位数:{}", trainCode, seatCount);  
	  //查询该车次共有多少个车站
    long stationCount = dailyTrainStationService.countByTrainCode(date, trainCode);  
    LOG.info("车次【{}】到站数:{}", trainCode, stationCount);  
  
    // 3/4需要根据实际卖票比例来定,一趟火车最多可以卖(seatCount * stationCount)张火车票  
    int count = (int) (seatCount * stationCount); // * 3/4);  
    LOG.info("车次【{}】初始生成令牌数:{}", trainCode, count);  
    skToken.setCount(count);  
  
    skTokenMapper.insert(skToken);  
}

然后在生成每日数据时加入该方法即可

//生成该车次的车站数据  
dailyTrainStationService.genDaily(date,train.getCode());  
//生成该车次的车厢数据  
dailyTrainCarriageService.genDaily(date,train.getCode());  
//生成该车次的座位数据  
dailyTrainSeatService.genDaily(date,train.getCode());  
//生成该车次的余票数据  
dailyTrainTicketService.genDaily(dailyTrain,date,train.getCode());  
LOG.info("生成日期【{}】车次【{}】的信息结束", DateUtil.formatDate(date), train.getCode());  
//生成令牌余量数据  
skTokenService.genDaily(date,train.getCode());

4. 增加校验秒杀令牌功能


在执行核心业务之前加上下面代码

//校验令牌容量  
boolean validSkToken=skTokenService.validSkToken(req.getDate(),req.getTrainCode(), req.getMemberId());  
if(validSkToken){  
    LOG.info("令牌校验通过");  
}else{  
    LOG.info("令牌校验不通过");  
    throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_SK_TOKEN_FAIL);  
}

其对应逻辑:先从redis缓存中查询令牌余量,如果存在缓存(60s过期),则直接从缓存中查询令牌余量,
如果余量大于0,则获取令牌,同时更新缓存中令牌余量
如果不存在缓存,则从数据库中查询

/**  
 * 校验令牌  
 */  
public boolean validSkToken(Date date, String trainCode, Long memberId) {  
    LOG.info("会员【{}】获取日期【{}】车次【{}】的令牌开始", memberId, DateUtil.formatDate(date), trainCode);  
  
    // 需要去掉这段,否则发布生产后,体验多人排队功能时,会因拿不到锁而返回:等待5秒,加入20人时,只有第1次循环能拿到锁  
    // if (!env.equals("dev")) {  
    //     // 先获取令牌锁,再校验令牌余量,防止机器人抢票,lockKey就是令牌,用来表示【谁能做什么】的一个凭证  
    //     String lockKey = RedisKeyPreEnum.SK_TOKEN + "-" + DateUtil.formatDate(date) + "-" + trainCode + "-" + memberId;  
    //     Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 5, TimeUnit.SECONDS);    
    //     if (Boolean.TRUE.equals(setIfAbsent)) {    //         LOG.info("恭喜,抢到令牌锁了!lockKey:{}", lockKey);  
    //     } else {    //         LOG.info("很遗憾,没抢到令牌锁!lockKey:{}", lockKey);  
    //         return false;    //     }    // }  
    String skTokenCountKey = RedisKeyPreEnum.SK_TOKEN_COUNT + "-" + DateUtil.formatDate(date) + "-" + trainCode;  
    Object skTokenCount = redisTemplate.opsForValue().get(skTokenCountKey);  
    if (skTokenCount != null) {  
        LOG.info("缓存中有该车次令牌大闸的key:{}", skTokenCountKey);  
        Long count = redisTemplate.opsForValue().decrement(skTokenCountKey, 1);  
        if (count < 0L) {  
            LOG.error("获取令牌失败:{}", skTokenCountKey);  
            return false;  
        } else {  
            LOG.info("获取令牌后,令牌余数:{}", count);  
            redisTemplate.expire(skTokenCountKey, 60, TimeUnit.SECONDS);  
            // 每获取5个令牌更新一次数据库  
            if (count % 5 == 0) {  
                skTokenMapperCust.decrease(date, trainCode, 5);  
            }  
            return true;  
        }  
    } else {  
        LOG.info("缓存中没有该车次令牌大闸的key:{}", skTokenCountKey);  
        // 检查是否还有令牌  
        SkTokenExample skTokenExample = new SkTokenExample();  
        skTokenExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);  
        List<SkToken> tokenCountList = skTokenMapper.selectByExample(skTokenExample);  
        if (CollUtil.isEmpty(tokenCountList)) {  
            LOG.info("找不到日期【{}】车次【{}】的令牌记录", DateUtil.formatDate(date), trainCode);  
            return false;  
        }  
  
        SkToken skToken = tokenCountList.get(0);  
        if (skToken.getCount() <= 0) {  
            LOG.info("日期【{}】车次【{}】的令牌余量为0", DateUtil.formatDate(date), trainCode);  
            return false;  
        }  
  
        // 令牌还有余量  
        // 令牌余数-1  
        Integer count = skToken.getCount() - 1;  
        skToken.setCount(count);  
        LOG.info("将该车次令牌大闸放入缓存中,key: {}, count: {}", skTokenCountKey, count);  
        // 不需要更新数据库,只要放缓存即可  
        redisTemplate.opsForValue().set(skTokenCountKey, String.valueOf(count), 60, TimeUnit.SECONDS);  
        skTokenMapper.updateByPrimaryKey(skToken);  
        return true;  
    }  
  
    // 令牌约等于库存,令牌没有了,就不再卖票,不需要再进入购票主流程去判断库存,判断令牌肯定比判断库存效率高  
    // int updateCount = skTokenMapperCust.decrease(date, trainCode, 1);  
    // if (updateCount > 0) {    //     return true;    // } else {    //     return false;    // }}

  • 30
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值