同步秒杀实现:Redis在秒杀功能的实践

7,674 阅读5分钟

回顾目录

Redis在资源秒杀场景中的使用

业务概述

  • 秒杀资源:以周为时长的资源。
  • 每个页面都会有秒杀资源,数量在1~8份,以随机形式展示给访客。
  • 每周秒杀资源价格由数据部门计算定价,没有有一个时间点进行抢购,如:每周三10点。购买者抢购数量可以是 秒杀资源剩余资源中的任意数量。
  • 购买者是否有抢购秒杀资源的权限,由用户接口信息,账户信息,等权限接口等决定。
  • 购买者支付方式使用界面支付,系统生成购买者抢购支付加密信息,跳转支付页面,再支付界面后,异步回掉确定是否购买成功,如果购买失败需要及时退回秒杀资源库存,以供他人报买。

业务流程

时间轴 业务 流程节点备注
周一 生成资源数据 流程①
周三10:00前 check资源数据 流程②
周三10:00 购买者秒杀秒杀资源 流程③
周三10:00后 购买者退款 流程④
周日 本周资源抢购结束,生成外网展示信息 流程⑤

Redis节点说明

  • 通用redis:用于SSO做统一登录、以及非秒杀功能使用。
  • 缓存redis:用于存储购买者热身数据,抢购这查询信息的缓存。
  • 核心redis:负责资源库存剩余数量,秒杀秒杀资源抢占等核心业务实现,需要关闭redis的lru策略,程序控制内存中key的淘汰

Redis使用详情

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

    • 关键伪代码

      cacheRedis.setex(key,EXPIRE_TIME_7D,info);
      
    • 秒杀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+uid) * share; 
      
      if (inventory - coreRedis.lpush(key, values)) < 0) {
          coreRedis.lrem(key, share, values);
      }
      
      例如:id:1 秒杀资源有3份流量的库存, 
      当llen时发现秒杀资源在redis中没有数据,
      购买者20xxxxx1想买此资源3份流量,
      这时lpush后发现超卖,lrem退回库存。
      redis 127.0.0.1:6379> lrange XX_PRODUCT_1 0 -1
      1) "jali7xz20xxxxx1"
      2) "3whsh6b20xxxxx2"
      3) "3whsh6b20xxxxx2"
      4) "3whsh6b20xxxxx2"
      
    • 设计优点:核心命令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以下是没有任何问题的(第一期秒杀资源秒杀,在没有做redis多库负载切分,以及没有优化使用的情况下到了5W的QPS,没有出现超时链接,或者获取不到连接池资源的情况,也和没有使用事务以及采用的低复杂度命令实现有关
  • 像列表页缓存,切勿为了减少redis的开销,将数据库每一列放到redis中,在redis中查询汇总,例如:每个秒杀资源都放在redis中,秒杀资源页需要10次redis链接才能完成一次列表页的组装。这样做会将服务器的qps成几何倍数的扩大到与redis的qps中造成系统获取不到redis连接资源。
  • 如果redis只用作缓存数据,且追求极限性能,master可以关闭内存快照和日志记录,有slave节点完成。
  • 批量命令降低QPS可使用Redisson、Lettuce或lua脚本代替。