#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 节点完成。
  • Java

    Java,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台的总称。用 Java 实现的 HotJava 浏览器(支持 Java applet)显示了 Java 的魅力:跨平台、动态的…

    380 引用 • 6 回帖
感谢    赞同    分享    收藏    关注    反对    举报    ...