如何设计秒杀系统(三)
实施方案
减库存逻辑
减库存的方式:
- 下单减库存
控制精准且简单,不会出现超卖,但是会有人故意刷单,下完单不付款。 - 付款减库存
当并发比较高时可能会出现下单后无法付款的情况。 - 预扣库存
超时释放库存,实现比较复杂。也有可能出现刷单,对于这种情况可以利用反作弊措施来制止(例如识别打标或者为某些类别设置最大购买件数)。
针对秒杀这个业务场景,使用下单减库存方式更为合适。
下单减库存如何保证数据一致性:
- 应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;
- 直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错;
- 使用 CASE WHEN 判断语句
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
性能优化
如果秒杀商品的减库存逻辑非常单一,比如没有复杂的 SKU 库存和总库存这种联动关系,可以放到Redis等缓存中
如果直接操作数据库,会带来很多的锁竞争,因为热点的几行数据拖垮整个数据库。解决办法是可以把热点数据进行分库,但是这个会带来维护上的困难,且即使是单库也并没有解决并发锁的问题。要解决并发锁问题,有两种方案:
- 应用层排队
按照商品维度设置队列,减少同一个机器对同一行记录操作的并发度,同时也能控制单个商品占用数据连接的数量。 - 数据库层排队
可以收用一些开源的组件,如阿里在InnoDB层上的补丁程序(patch)
兜底方案
高可用建设
架构阶段:架构阶段主要考虑系统的可扩展性和容错性,要避免系统出现单点问题。例如多机房单元化部署,即使某个城市的某个机房出现整体故障,仍然不会影响整体网站的运转。
编码阶段:编码最重要的是保证代码的健壮性,例如涉及远程调用问题时,要设置合理的超时退出机制,防止被其他系统拖垮,也要对调用的返回结果集有预期,防止返回的结果超出程序处理范围,最常见的做法就是对错误异常进行捕获,对无法预料的错误要有默认处理结果。
测试阶段:测试主要是保证测试用例的覆盖度,保证最坏情况发生时,我们也有相应的处理流程。
发布阶段:发布时也有一些地方需要注意,因为发布时最容易出现错误,因此要有紧急的回滚机制。
运行阶段:运行时是系统的常态,系统大部分时间都会处于运行态,运行态最重要的是对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排查问题。
故障发生:故障发生时首先最重要的就是及时止损,例如由于程序问题导致商品价格错误,那就要及时下架商品或者关闭购买链接,防止造成重大资产损失。然后就是要能够及时恢复服务,并定位原因解决问题。
降级
所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。
降级方案可以这样设计:当秒杀流量达到 5w/s 时,把成交记录的获取从展示 20 条降级到只展示 5 条。“从 20 改到 5”这个操作由一个开关来实现,也就是设置一个能够从开关系统动态获取的系统参数。
两部分,一部分是开关控制台,它保存了开关的具体配置信息,以及具体执行开关所对应的机器列表;另一部分是执行下发开关数据的 Agent,主要任务就是保证开关被正确执行,即使系统重启后也会生效。
限流
- 客户端限流
优点:可以限制无用请求的发出
缺点:阈值不好确定 - 服务端限流
好处是可以根据服务端的性能设置合理的阈值,而缺点就是被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。
拒绝服务
当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求。
举例:
在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码,在 Java 层同样也可以设计过载保护。
总结
网站的高可用建设是基础,可以说要深入到各个环节,更要长期规划并进行体系化建设,要在预防(建立常态的压力体系,例如上线前的单机压测到上线后的全链路压测)、管控(做好线上运行时的降级、限流和兜底保护)、监控(建立性能基线来记录性能的变化趋势以及线上机器的负载报警体系,发现问题及时预警)和恢复体系(遇到故障要及时止损,并提供快速的数据订正工具等)等这些地方加强建设,每一个环节可能都有很多事情要做。