5分钟了解系统架构设计(6)
最近梳理了之前学习的架构设计相关的一些课程学习总结,将其整理成了一个大纲脑图,以每篇5分钟系列展现出来,希望对你有所帮助。
秒杀抢购,是近年来电商系统的常见考点,本篇,我们聚焦此类问题的回答思路。
注意,本篇内容和上一篇高性能系统架构的设计思路相辅相成,建议先阅读上一篇再阅读本篇内容。
首先,我们需要明确基本的回答套路:
明确需求阶段 => 分阶段梳理难点 => 针对难点的解决方案设计
下面,我们就按照这介个点段分别来看看。
1、明确需求阶段
以某东预约抢购系统为例,实现一个预约抢购系统大致分为四个阶段:
-
商品预约:用户进入商品详情页面,获取购买资格,并等待商品抢购倒计时。
-
等待抢购:等待商品抢购倒计时,直到商品开放抢购。
-
商品抢购:商品抢购倒计时结束,用户提交抢购订单,排队等待抢购结果,抢购成功后,扣减系统库存,生成抢购订单。
-
订单支付:等待用户支付成功后,系统更新订单状态,通知用户购买成功。
2、商品预约阶段
预约的目的:电商平台为了方便流量运营,通过先预约再抢购的方式预热商品,并根据预约量调整运营策略。
预约阶段难点:如何在高并发的情况下,让每个用户都能得到抢够资格。解决方案:基于Redis实现分布式锁(这也是最常用的方式)
实现要点:加锁过程:在加锁的过程中,实际是给Key键设置一个值,为避免思索,还要给Key设置一个过期时间。
SET lock_key unique_value NX PX 10000
-
lock_key 就是 key 键;
-
unique_value 是客户端生成的唯一的标识;
-
NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
-
PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
解锁过程:将lock_key键删除,但要保证是加锁的客户端来删除,并且还要保证原子性操作。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
3、等待抢购阶段
等待抢购阶段难点:流量突增,商品详情页的度请求量剧增,如何做好商品详情页的流量控制?
解决方案:通过 前端页面静态化 和 服务端限流 来控制。
实现要点:
页面静态化:提前将抢购商品的详情页面做静态化,并将该静态页面放到距离用户最近的CDN节点中,这样用户一访问就会自动缓存改静态页面的静态资源文件。
服务端限流:在商品详情页的后端系统入口层(如Nginx)配置限流算法,比如Nginx的限流模块可以做到限制单位时间内所有IP的请求数量 和 限制单位时间内单个IP的请求数量。
4、商品抢购阶段
商品抢购阶段难点:短时间之内提交订单的写流量非常高,如何保证后端服务和DB的稳定性?
解决方案:流量削峰、扣减库存、分库分表
实现要点:
流量削峰:引入消息队列做异步化,然后在抢购系统的后端服务中,启动若干个队列处理消息队列中的提单请求,再执行校验库存、下单等逻辑。
扣减库存:基于Redis分布式锁实现库存的扣减,避免数据不一致。但需要考虑Redis的单点问题 以及 Redis哨兵模式下主从切换带来的数据不一致的问题。
分库分表:对订单表分库分表,通过对用户 ID 字段进行 Hash 取模,实现分库分表,提高系统的并发能力。
5、订单支付阶段
订单支付阶段难点:订单支付完成后,一般会由支付平台回调系统接口,更新订单状态。在支付回调成功之后,抢购系统还会通过异步通知的方式,实现订单更新之外的非核心业务处理。如果服务宕机,就会存在数据丢失的可能。
解决方案:可靠消息传递机制,即先做本地消息存储,再通过异步重试机制实现消息的补偿。
实现要点:
-
在更新状态的同时,插入一个消息,之后再返回第三方支付操作成功的结果。
-
通过数据库中的消息驱动,异步推送到其他系统,完成后续的工作。
参考资料
李运华,《从0开始学架构》
刘海丰,《架构设计面试精讲》
潘新宇,《23讲搞定后台架构实战》