【秒杀系统2】设计与实现下

秒杀前的流量控制

预约

开放预约获得资格后才能参与秒杀。
预约也有高并发所以需要一些设计:

  1. 预约管理后台。
  2. 预约短信和消息提醒。
  3. 面向终端的雨夜核心微服务,提供给用户预约和取消资格能力。
  4. 详情页在展示时获取预约信息的能力,比如商品是否预约,当前预约人数等等。
  5. 秒杀下单时检查预约资格的能力。
  6. 核心在两个维度:
    1. 预约活动和用户预约关系。所以需要2张表。预约活动表信息表,记录预约活动本身。预约活动开始和结束时间,
      预约活动对应的秒杀信息,预约商品信息等。另一张表 用户预约关系表,用户的ID,预约的活动ID,预约商品等。

预约系统优化

用户预约关系表量非常大。方法:

  1. 分库分表。用户预约关系表,写热点。
  2. 前置缓存。预约活动信息,读热点。

计算预约量:

  1. 用redis记录。
  2. 本地缓存累加,批量写入redis。

秒杀中流量控制-削峰

削峰

无损削峰:限流技术是有损。
有损削峰:验证码,问答题,以及一部消息队列。

加验证码的作用:本来需要0.1秒完成的操作,拉长到10秒或者更长时间。拉长了时间
降低的最高的峰值。

流量削峰

异步下单:
引入消息队列:kafka,RocketMQ,RabbitMQ

异步支付和订单不一致如何解决?

订单支付页,定时检查秒杀订单是否已经生成!WebSocket 等等。下单查库问题不大,比访问量第很多。

限流

秒杀中的流量控制-限流

自我保护的直接手段,整个链路都可以进行限流:逐级限流、分层过滤。

常用算法:令牌桶和漏铜。

图片

nginx限流

HttpLimitZone,HttpLimitRequest
HttpLimitZone: 用来限制一个客户端的并发连接数。
HttpLimitRequest: 通过漏桶算法进行限制用户的链接频率。

图片

limit_req_zone: 指令名称,关键字,只能在http块中使用。
$binary_remote_addr Nginx内置绑定变量,比如$remote_port是指客户端端口号。
zone=one:规则名称zai limit_req_zone申明过。
burst=2: 制定最大突然请求数,超过这个数目的请求会被延迟。
nodelay: 突发请求大于burst时候,立即返回503.不排队了。
rate:每秒允许通过的请求,每个IP。
zone=one:10m :1M可以支持 16000个链接。 10M就是160000个会话链接。

线程池限流

Java,Tomcat原生线程池,配置最大连接数,请求处理队列长度以及拒绝策略来达到限流的目的。

API限流

线程池限流是一种并发限流。并发恒定的情况下,处理速度越快。QPS越高。

大部分情况是根据QPS来限流。可以使用Google的RateLimiter开源包。基于令牌桶的算法实现。

开源的组件Sentinel整合了流量控制、流量整型、流量路由、熔断升级、系统自适应。

自定义限流

订单的重复下单问题。订单结算也的时候进行判断(生成ID服务)。下单的时候用这个订单来校验下单是否重复。
如果高并发回拖慢秒杀的速度,所以需要进行改造:

  1. OrderId预生成,放在队列里。缓存在本地。
    1
    2
    OrderIdList = new ConcurrentLinkedQueue();
    OrderItemList = new ConcurrentLinkedQueue();

100毫秒产生200个订单号。所以1秒内只能有2000个订单过来。类似令牌桶限流。更精细的可以控制某个商品的发放速度。

  1. 商品库存分发到本地。

限购、秒杀的库存与降级、热点

重点问题:

  1. 库存超卖,库存扣减的热点。

在库存服务里解决。

限购

全部流量不能直接打到库存服务。需要有个系统来承接大流量没并且脂肪商品库存和匹配的请求到库存服务。限购就是这样的角色。
限购之于库存,就像秒杀之于下单。欠着都是后者的滤网和保护伞。

限购: 做商品的限制购买。因为参加秒杀活动的商品都是爆品、稀缺品,所以为了让更多的用户参与进来,并让有限的投放量汇集
更多的人,所以往往会对商品的售卖做限制,一般限制的维度包括两个方面。

商品维度限购:每次参加秒杀的活动商品投放量。针对不同地区投放。

个人维度限购:校验个人是否能购买数量。

库存扣减

如何不超卖 -> 查询用户库存。

库存扣减必须实现原子性和一致性,如何实现呢?
两个操作

利用乐观锁

  1. 查询库存。
    乐观锁 version 每次扣减的时候带上这个版本号比如:

    1
    select stock,version from product where id = ? ;
  2. 扣减库存

    1
    update product set stock = stock - ?,version = version + 1 where id = ? and version = ?

利用数据库特性

数据库方案: 行锁。查询和扣减放在一个事务,for update。事务结束后进行释放锁。
通过SQL语句: where条件,保证库存不会被溅到0以下。

分布式锁

redis分布式锁可以实现。
弊端:需要超时机制。但是有超时机制,又占用时间,与高并发的秒杀系统是相悖的。不建议使用。

高并发扣减

流量洪峰来临时,TP99指标变差,CPU升高,IO等待边长。系统变得不稳定。需要对非核心服务进行降级,减轻
系统负担,这种降级一般是有损的。属于 ‘弃卒保帅’

秒杀的核心问题是解决单个商品搞并发读和写的问题。是典型的热点数据问题。我们需要响应的机制,避免热点数据打垮
系统。

Redis库存扣减。宁可少卖,不可超卖。

如何实现:

  1. 对缓存进行扣减。
    redis lua eval原子脚本:
    1
    2
    3
    4
    5
    local c_s = redis.call('get', KEYS[1])
    if (not c_s or tonumber(c_s) < tonumber(KEYS[2]) then
    return 0
    end
    redis.call('decrby',KEYS[1],KEYS[2])

下单失败需要还原数量。

问题:Redis挂了怎么办?
快速持久化扣减记录,采用WAL机制实现。保存到本地RockDB数据库。

降级

  • 写服务降级

Redis高并发扣减就是一种写降级

  • 读服务降级

在不影响正常购买的流程中。将一些无关紧要的信息隐藏。完成读降级。
例子:

问题:双11时登录后,切换几分钟后需要重新登陆。平时并不用重新登陆。

登录信息缓存失效时间减少。腾出缓存资源。
牢记:微服务资深所依赖的外服中间件或者系统跟本身可用性无关。

秒杀的防刷,风控,容灾

防刷

秒杀商品有限,防止黄牛
有效流量: 6:1:3

6: 黄牛用户
1: 错误用户
3: 正常请求

防刷方法有:

  1. Nginx限流机制。限制IP高频访问。
  2. Token机制,用来做鉴权。防止直接访问下单接口。在订单详情页时候获取Token,在下单时候带上原Token并再产生一个Token,每个环节进行校验。保证用户的操作是连续的。
    例子:在header_filter_by_lua_block 返回 的header里增加流程Token

风控

如果已经有风控系统,可以拿到黑名单列表。进行封闭。

容灾

防天灾,机房容灾。

异地双活:跨城市备份。有物理时延比较难实现。(招行实现)
同城双活:同城双活,物理距离比价近,延迟低。同城机房,同事承担部分流量。主机房承担写,部分读,备份机房部分读。