10万QPS的秒杀系统的设计探索

概要

提示:这里可以添加技术概要

例如:

在用户基数越来越大的情况下,系统通常都会遇到高并发情况,,这对系统的设计提出了更高的要求。本次示例,以抢购优惠券为例子,来探究单机情况下的QPS。

整体架构流程

提示:这里可以添加技术整体架构

整体流程如下:
1.抢购之前,系统启动后,把优惠券信息加载到缓存中。
2.开始抢购
3.异步处理抢购订单

技术细节

1.缓存预热:
这里,采用来完成优惠券缓存预热,实际生产中可根据需求自行选择各种方式来完成此步骤
提示:这里因为数据库中只放了一条优惠券数据,所以采用了查询所有的方法

@Component
public class CouponInitRunner implements ApplicationRunner {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private CouponInfoService couponInfoService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //容器初始化之后,把优惠券信息存入到redis中
        List<CouponInfo> list = couponInfoService.findAll();
        for (int i = 0; i <list.size() ; i++) {
            CouponInfo couponInfo = list.get(i);
            stringRedisTemplate.opsForValue().set("seckill:coupon:stock:" + couponInfo.getId(), couponInfo.getStock().toString());
        }

    }
}

2.抢购流程
抢购流程
3.关键代码如下:
3.1 抢购代码

@Service
public class CouponSeckillServiceImpl implements ICouponSeckillService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
   private static final DefaultRedisScript<Long> SECKILL_LUA;
    @Autowired(required = false)
    private IdCreaterUtils idCreater;

    @Autowired
    private RabbitTemplate rabbitTemplate;
    static {
        SECKILL_LUA = new DefaultRedisScript<>();
        SECKILL_LUA.setLocation(new ClassPathResource("seckillCoupon.lua"));
        SECKILL_LUA.setResultType(Long.class);
    }

    /**
     * 优惠券秒杀
     * @param couponId
     * @return
     */
    @Override
    public Result seckillCoupon(String userId,Long couponId) {
        //1.首先判断用户信息和优惠券信息。比如用户id是否为空,券id是否正确,是否到了券的抢购时间
        if (StringUtils.isBlank(userId)){
             return new Result(false, StatusCode.ERROR, "用户信息错误");
        }
        //2.业务幂等性。判断该用户是否已经抢购过了
        //3.抢购优惠券。抢到优惠券
        //4.扣减优惠券库存。抢到优惠券之后,就要把优惠券的库存-1
        //第2步和第3步以及第4步骤,为了防止超卖以及出现多次抢券的问题,采用lua脚本保证数据的原子性

        long seckilledOrderId = idCreater.nextId();//通过雪花算法的ID生成器生成秒杀到的订单id
        Long result = stringRedisTemplate.execute(
                SECKILL_LUA,//lua脚本
                Collections.emptyList(),//key。因为key我们采用在lua脚本中定义了,这里不用传
                couponId.toString(), userId.toString(), String.valueOf(seckilledOrderId) //传递给lua脚本的参数
        );
        int seckill_resultVal = result.intValue();
        // 2.判断结果是否为0
        if (seckill_resultVal != 0) {
            // 不为0 ,代表没有购买资格
            if(seckill_resultVal==1){
                new Result(false, StatusCode.ERROR, "库存不足");
            }else{
                new Result(false, StatusCode.ERROR, "不能重复抢购");
            }

        }
        //5.抢购成功,生成订单信息,用mq发送消息,异步处理
        SeckilledOrderVO  orderVO=new SeckilledOrderVO(seckilledOrderId,couponId,userId);
        rabbitTemplate.convertAndSend("seckillFanoutExchange",  JSON.toJSONString(orderVO));
        //6.返回信息给用户,也就是订单id
        return new Result(true, StatusCode.OK, String.valueOf(seckilledOrderId));
    }
}

3.2 lua脚本如下

-- 1.参数列表
local couponId = ARGV[1]
local userId = ARGV[2]
-- 2.数据key
local stockKey = 'seckill:coupon:stock:' .. couponId
local orderKey = 'seckill:coupon:order:' .. couponId

-- 3.脚本业务
-- 判断库存是否充足
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 库存不足,返回1
    return 1
end
-- 4.判断用户是否下单
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 存在,说明是重复下单,返回2
    return 2
end
-- 5扣库存
redis.call('incrby', stockKey, -1)
-- 6.抢购成功(保存抢购成功的用户)
redis.call('sadd', orderKey, userId)

return 0

4.压测结果
线程组
请求路径
压测结果

小结

提示:这里可以添加总结

本次压测,redis和rabbitMQ都是部署在本地的单机版本,在实际中,可以采用集群模式,来提高这两个中间件的并发能力。
演示配置

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值