秒杀系统

[TOC]

秒杀系统设计的注意点

  • 高性能。秒杀涉及高读和高写的支持,如何支撑高并发,如何抵抗高IOPS?核心优化理念其实是类似的:高读就尽量”少读”或”读少”,高写就数据拆分。本文将从动静分离、热点优化以及服务端性能优化 3 个方面展开
  • 一致性。秒杀的核心关注是商品库存,有限的商品在同一时间被多个请求同时扣减,而且要保证准确性,显而易见是一个难题。如何做到既不多又不少?本文将从业界通用的几种减库存方案切入,讨论一致性设计的核心逻辑
  • 高可用。大型分布式系统在实际运行过程中面对的工况是非常复杂的,业务流量的突增、依赖服务的不稳定、应用自身的瓶颈、物理资源的损坏等方方面面都会对系统的运行带来大大小小的的冲击。

高性能

1 动静分离

大家可能会注意到,秒杀过程中你是不需要刷新整个页面的,只有时间在不停跳动。这是因为一般都会对大流量的秒杀系统做系统的静态化改造,即数据意义上的动静分离。动静分离三步走:1、数据拆分;2、静态缓存;3、数据整合。

1.1 数据拆分

动静分离的首要目的是将动态页面改造成适合缓存的静态页面。因此第一步就是分离出动态数据,主要从以下 2 个方面进行:

  • 用户。用户身份信息包括登录状态以及登录画像等,相关要素可以单独拆分出来,通过动态请求进行获取;与之相关的广平推荐,如用户偏好、地域偏好等,同样可以通过异步方式进行加载
  • 时间。秒杀时间是由服务端统一管控的,可以通过动态请求进行获取

1.2 静态缓存

因此通常将静态数据缓存在 CDN

因此,将数据放到全国所有的 CDN 节点是不太现实的,失效问题、命中率问题都会面临比较大的挑战。更为可行的做法是选择若干 CDN 节点进行静态化改造,节点的选取通常需要满足以下几个条件:

  • 临近访问量集中的地区
  • 距离主站较远的地区
  • 节点与主站间网络质量良好的地区

基于以上因素,选择 CDN 的二级缓存比较合适,因为二级缓存数量偏少,容量也更大,访问量相对集中,这样就可以较好解决缓存的失效问题以及命中率问题,是当前比较理想的一种 CDN 化方案。部署方式如下图所示:

图片

​ MySQL主从复制,读写分离

主要有动态地址生成和接口放刷,双重MD5加密密码。

2 热点优化

热点分为热点操作和热点数据,以下分开进行讨论。

2.1 热点操作

零点刷新、零点下单、零点添加购物车等都属于热点操作。热点操作是用户的行为,不好改变,但可以做一些限制保护,比如用户频繁刷新页面时进行提示阻断。

2.2 热点数据

热点数据的处理三步走,一是热点识别,二是热点隔离,三是热点优化。

2.2.1 热点识别

热点数据分为静态热点和动态热点,具体如下:

  • 静态热点:能够提前预测的热点数据。大促前夕,可以根据大促的行业特点、活动商家等纬度信息分析出热点商品,或者通过卖家报名的方式提前筛选;另外,还可以通过技术手段提前预测,例如对买家每天访问的商品进行大数据计算,然后统计出 TOP N 的商品,即可视为热点商品
  • 动态热点:无法提前预测的热点数据。

因此秒杀系统需要实现热点数据的动态发现能力,一个常见的实现思路是:

  • 异步采集交易链路各个环节的热点 Key 信息
  • 聚合分析热点数据,达到一定规则的热点数据,通过订阅分发推送到链路系统,各系统根据自身需求决定如何处理热点数据,或限流或缓存,从而实现热点保护

2.2.2 热点隔离

热点数据识别出来之后,第一原则就是将热点数据隔离出来,不要让 1% 影响到另外的 99%,可以基于以下几个层次实现热点隔离:

  • 业务隔离。秒杀作为一种营销活动,卖家需要单独报名,从技术上来说,系统可以提前对已知热点做缓存预热
  • 系统隔离。系统隔离是运行时隔离,通过分组部署和另外 99% 进行分离,另外秒杀也可以申请单独的域名,入口层就让请求落到不同的集群中
  • 数据隔离。秒杀数据作为热点数据,可以启用单独的缓存集群或者DB服务组,从而更好的实现横向或纵向能力扩展

当然,实现隔离还有很多种办法。比如,可以按照用户来区分,为不同的用户分配不同的 Cookie,入口层路由到不同的服务接口中;再比如,域名保持一致,但后端调用不同的服务接口;又或者在数据层给数据打标进行区分等等,这些措施的目的都是把已经识别的热点请求和普通请求区分开来。

2.2.3 热点优化

热点数据隔离之后,也就方便对这 1% 的请求做针对性的优化,方式无外乎两种:

  • 缓存:热点缓存是最为有效的办法。如果热点数据做了动静分离,那么可以长期缓存静态数据

  • 限流:流量限制更多是一种保护机制。需要注意的是,各服务要时刻关注请求是否触发限流并及时进行review

    1、读限流:对读请求做限流保护,将超出系统承载能力的请求过滤掉
    2、读缓存:对读请求做数据缓存,将重复的请求过滤掉
    3、写限流:对写请求做限流保护,将超出系统承载能力的请求过滤掉
    4、写校验:对写请求做一致性校验,只保留最终的有效数据

  • 降级:

  • 削峰

  • 业务层面:弹出各种优惠券 跳转到其他页面,进行一个分流的过程

2.2.4 小结

数据的热点优化与动静分离是不一样的,热点优化是基于二八原则对数据进行了纵向拆分,以便进行针对性地处理。热点识别和隔离不仅对“秒杀”这个场景有意义,对其他的高性能分布式系统也非常有参考价值。

3 系统优化

image-20210327094954882

②LVS (Linux Virtual Server)

它是一种集群(Cluster)技术,采用 IP 负载均衡技术和基于内容请求分发技术。

调度器具有很好的吞吐率,将请求均衡地转移到不同的服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能的、高可用的虚拟服务器。

③Nginx

想必大家都很熟悉了,是一款非常高性能的 HTTP 代理/反向代理服务器,服务开发中也经常使用它来做负载均衡。

Nginx 实现负载均衡的方式主要有三种:

  • 轮询
  • 加权轮询
  • IP Hash 轮询

4 总结一下

性能优化需要一个基准值,所以系统还需要做好应用基线,比如性能基线(何时性能突然下降)、成本基线(去年大促用了多少机器)、链路基线(核心流程发生了哪些变化),通过基线持续关注系统性能,促使系统在代码层面持续提升编码质量、业务层面及时下掉不合理调用、架构层面不断优化改进。

一致性

秒杀系统中,库存是个关键数据,卖不出去是个问题,超卖更是个问题。秒杀场景下的一致性问题,主要就是库存扣减的准确性问题。

2 减库存的问题

防止超卖:

​ 1、利用数据库自带排他锁,当减库存的时候,进位where判断,只有库存余量大于0的时候才进行进库存; update goods set num = num - 1 WHERE id = 1001 and num > 0; 2、也可以可用乐观锁CAS版本号机制。select version from goods WHERE id= 1001;update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version);

避免了对数据库频繁的 IO 操作

这就是预扣库存。先扣除了库存,保证不超卖,然后异步生成用户订单,这样响应给用户的速度就会快很多;

那么怎么保证不少卖呢?用户拿到了订单,不支付怎么办?

我们都知道现在订单都有有效期,比如说用户五分钟内不支付,订单就失效了,订单一旦失效,就会加入新的库存,这也是现在很多网上零售企业保证商品不少卖采用的方案。

订单的生成是异步的,一般都会放到 MQ、Kafka 这样的即时消费队列中处理,订单量比较少的情况下,生成订单非常快,用户几乎不用排队。

图片

改进

图片

要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存。

有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“Buffer 库存”用来防止机器中有机器宕机的情况。

图片

防止重复消费:数据库加唯一索引:防止用户重复购买.秒杀订单表新增订单,使用唯一索引,由用户ID和商品ID组成

消息的消费结果如何返回给消息发送方:客户端轮询订单生成结果。

消息丢失:秒杀系统中,本来就是万中选一的,丢失无所谓。如果是重要的信息,我们可以从三个角度来避免。如果是发送者丢失,开启confirm机制,如果队列丢失,开始queue持久化和消息持久化。如果是消费者丢失,关闭自动ACK,当我们消费完之后,调用API给queue发送确认信息。

3 实际如何减库存

避免超卖:库存超卖的情况实际分为两种。对于普通商品,秒杀只是一种大促手段,即使库存超卖,商家也可以通过补货来解决;而对于一些商品,秒杀作为一种营销手段,完全不允许库存为负,也就是在数据一致性上,需要保证大并发请求时数据库中的库存字段值不能为负,一般有多种方案:

一是在通过事务来判断,即保证减后库存不能为负,否则就回滚;

二是直接设置数据库字段类型为无符号整数,这样一旦库存为负就会在执行 SQL 时报错;

三是使用 CAS业务手段保证商品卖的出去,技术手段保证商品不会超卖,库存问题从来就不是简单的技术难题,解决问题的视角是多种多样的。

4 一致性性能的优化

库存是个关键数据,更是个热点数据。对系统来说,热点的实际影响就是 “高读” 和 “高写”,也是秒杀场景下最为核心的一个技术难题。

4.1 高并发读

不同层次尽可能过滤掉无效请求,只在“漏斗” 最末端进行有效处理,从而缩短系统瓶颈的影响路径。

4.2 高并发写

高并发写的优化方式,一种是更换DB选型,一种是优化DB性能,以下分别进行讨论。

4.2.2 优化DB性能

库存数据落地到数据库实现其实是一行存储(MySQL),因此会有大量线程来竞争 InnoDB 行锁。但并发越高,等待线程就会越多,TPS 下降,RT 上升,吞吐量会受到严重影响——注意,这里假设数据库已基于上文【性能优化】完成数据隔离,以便于讨论聚焦 。

解决并发锁的问题,有两种办法:

1、应用层排队。

通过缓存加入集群分布式锁,从而控制集群对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用过多的数据库连接

2、数据层排队。

应用层排队是有损性能的,数据层排队是最为理想的。业界中,阿里的数据库团队开发了针对InnoDB 层上的补丁程序(patch),可以基于DB层对单行记录做并发排队,从而实现秒杀场景下的定制优化——注意,排队和锁竞争是有区别的,如果熟悉 MySQL 的话,就会知道 InnoDB 内部的死锁检测,以及 MySQL Server 和 InnoDB 的切换都是比较消耗性能的。

另外阿里的数据库团队还做了很多其他方面的优化,

COMMIT_ON_SUCCESSROLLBACK_ON_FAIL的补丁程序,通过在 SQL 里加入提示(hint),实现事务不需要等待实时提交,而是在数据执行完最后一条 SQL 后,直接根据 TARGET_AFFECT_ROW的结果进行提交或回滚,减少网络等待的时间(毫秒级)

4.3 小结

高读和高写的两种处理方式大相径庭。读请求的优化空间要大一些,而写请求的瓶颈一般都在存储层,优化思路的本质还是基于 CAP 理论做平衡。

高可用

集群 Web集群 读写分离 Redis集群,Nginx、LVS分流都给用上