Redis在秒杀功能的实践

#Redis在留链展位秒杀场景中的使用

业务概述

  • CPT展位:以周为时长的小区吊顶展位。
  • 北京每个小区都会有CPT展位,数量在1~8份,以随机形式展示给访客。
  • 每周CPT展位价格由运营统一定价,每周三10点经纪人秒杀抢购。经纪人抢购数量可以是展位剩余资源中的任意数量。
  • 经纪人是否有抢购展位的权限,由uc接口信息,组队盘信息,lmall链家个人账户信息,是否在“小黑屋”,是否是买卖经纪人等决定。
  • 经纪人支付方式使用lmall收银台界面支付,系统生成经纪人抢购支付加密信息,跳转Lmall支付,再Lmall支付后,异步回掉确定是否购买成功,如果购买失败需要及时退回展位库存,以供他人报买。

业务流程

时间轴 业务 流程节点备注
周一 生成展位数据 流程①
周三10:00前 check业务数据 流程②
周三10:00 经纪人秒杀展位 流程③
周三10:00后 经纪人退款 流程④
周日 本周展位报买结束,生成三端展示信息 流程⑤

业务具体细节

  • 小区(展位)多:有12000左右小区。
  • 每个展位可购买数量少:每个小区的展位,数量在1~8个, 购买数量无限制
  • 各商品针对人群区分度很高且具报买资格经纪人数量不多:比如远洋山水小区吊顶展位,远洋山水一店A店、远洋山水北门店A店、远洋山水景山店A店、远洋山水中街店A店...店组下所有经纪人可以报买,平均每个小区吊顶展位可报买人数在60人。
  • 北京实际参与报买经纪人在1万人左右

Redis节点说明

  • 通用redis:用于shiro做统一登录、以及非秒杀展位业务功能使用。
  • 缓存redis:用于存储经纪人热身数据(店组维护盘、uc信息、Lmall个人账号信息等),经纪人查询信息的缓存。
  • 核心redis:负责展位库存剩余数量,秒杀展位抢占等核心业务实现,需要关闭redis的lru策略,程序控制内存中key的淘汰,由OP负责监控redis内存大小。

Redis使用详情

  • 缓存redis-数据热身 流程①② (牛奶供给降级策略)

    • 缓存经纪人UC数据、维护盘数据、Lmall数据

    • 根据所使用数据的更新度级别:

      • 设置刷新UC、维护盘信息任务的corn为:40 10 4,15 * * ?
      • 设置刷新lmall个人账户信息的corn为:10 0/8 * * * ?
    • 关键伪代码

      cacheRedis.setex(key,EXPIRE_TIME_7D,info);
      复制代码
    • 设计优点: 秒杀功能对经纪人的校验与uc、组队盘、lmall接口为弱依赖,数据已在流程③之前缓存到本地,这种设计在本次dubbo迁移zk发挥重要作用,周三uc迁移时,关闭刷新缓存任务,大部分经纪人使用迁移前的数据,完成秒杀校验。

  • 缓存redis-列表页不固定参数缓存 流程③ (借鉴spring-data-redis)

    • 秒杀qps峰值在1w左右,但是超过60%的qps请求的是展位列表方法,所以需要增加可购买展位缓存。

    • 关键伪代码

      生成rediskey, objects包括ucid、用户输入入参、分页信息等等
      public static String builder(String prefix, Object... objects) {
          String input = JSONObject.toJSONString(Arrays.asList(objects));
          String output = Util.md5_16(input);
          return prefix+output;
      }
      cacheRedis.setex(key,EXPIRE_TIME_2S,info);
      复制代码
    • 设计优点:借鉴spring-data-redis将入参通用为objects...序列化,然后将JsonString Md5压缩为16位,这里主要由于在秒杀开始时,redis数据会出现大量缓存列表数据,redis储存100w个value长度为32位,key长度为16位的数据时,需要使用个130MB内存,如果key的长度为32位时需要160MB左右的内存,所以压缩key的长度在这种场景很有必要。

  • 核心redis-展位秒杀 流程③

    • 每个展位拥有自己的队列,完成多队列,低队列长度的秒杀。

    • 关键伪代码

      String key = PURCHASING_PRODUCT + productId;
      Long count = coreRedis.llen(key);
      判断count是否大于库存
      判断count+用户欲购买展位数量(share)是否大于库存
      
      String[] values = (uuid+ucid) * share; 
      
      if (inventory - coreRedis.lpush(key, values)) < 0) {
          coreRedis.lrem(key, share, values);
      }
      
      例如:id:1 展位有3份流量的库存, 
      当llen时发现展位在redis中没有数据,
      经纪人20244816想买此展位3份流量,
      这时lpush后发现超卖,lrem退回库存。
      redis 172.30.0.20:6379> lrange DMP_PRODUCT_1 0 -1
      1) "jali7xz20243386"
      2) "3whsh6b20244816"
      3) "3whsh6b20244816"
      4) "3whsh6b20244816"
      复制代码
    • 设计优点:核心命令llen、lpush的时间复杂度都是O(1)、lrem时间复杂度是O(N),官方lrem给出的复杂度是O(N)但我觉得在这种使用场景下lrem的复杂度应该无极限接近于O(count),但是将补偿操作封装为原子性,且支持多次、幂等执行。曾经也想过用一些getset,setnx,pipelin、将库存缓存到队列然后pop、事务等实现秒杀。但是性能、或者鲁棒性在这种场景下都没有以上设计表现出色,而且这种方式在支付失败,或者查询到未支付的情况下立刻幂等lrem展位队列的订单,其他有资格购买的经纪人可以继续购买。

Redis线上使用情况

  • 缓存redis (图片来源地址:github)

cache redis

  • 核心redis

cache redis

Redis使用总结

  • 使用一主一从,rdb为备份策略的redis架构,QPS在8W以下是没有任何问题的(第一期CPT展位秒杀,在没有做redis多库负载切分,以及没有优化使用的情况下到了5W的QPS,没有出现超时链接,或者获取不到连接池资源的情况,也和没有使用事务以及采用的低复杂度命令实现有关
  • 像列表页缓存,切勿为了减少redis的开销,将数据库每一列放到redis中,在redis中查询汇总,例如:每个展位都放在redis中,展位页需要10次redis链接才能完成一次列表页的组装。这样做会将服务器的qps成几何倍数的扩大到与redis的qps中造成系统获取不到redis连接资源
  • 如果redis只用作缓存数据,且追求极限性能,master可以关闭内存快照和日志记录,有slave节点完成。

猜你喜欢

转载自juejin.im/post/5aa241c4518825558453983d