27

一个极简、高效的秒杀系统(战略设计篇)

 4 years ago
source link: http://dockone.io/article/9975
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

2018年,楼主所在业务线开始发力探索线上O2O业务,但楼主所在公司并非纯电商公司,电商体系标配的商品中心、库存中心、活动系统都处于萌芽阶段不成气候。10月中旬,业务决定搞一波双十一大促提升品牌知名度。整个大促最核心功能点就是造一个秒杀系统,为整个活动会场积蓄流量。作为该业务线营销工作负责人,这个秒杀系统设计任务自然就落到楼主身上。时间紧任务重,从需求提出到上线,2周时间紧锣密鼓开发,多次代码写到凌晨两点。值得欣慰的是,最终这个系统完美支撑了大促,帮助业务拿到显著超出预期的结果。时光飞逝,一晃一年多过去了,楼主19年初也转战另一条新业务线负责导购交易,这篇文章就当是对这个念念不忘的秒杀系统一个总结,祭奠那段写代码不知疲倦激情满满的岁月。

友情提示:网上搜秒杀系统架构,大多一上来就开始讲CDN静态资源缓存、Nginx请求拦截、缓存设计、消息队列削峰、请求限流等一系列技术点。看似面面俱到考虑周全,但楼主真忍不住想问,以当前业务体量,真有必要搞那么多招式吗?以上技术点本文通通不涉及!阅读本文不需要高深的知识,楼主就想从纯后端角度,带领大家看一看如何快速落地一个秒杀系统。

这一篇题目标注「战略篇」,事实上来源就是楼主做秒杀需求的技术方案。内容过于真实,楼主摘取最核心的内容贴出来。从一个亲历者角度来复盘一个秒杀系统是如何设计并落地的。

业务需求

产品需求

此处应该贴产品需求文档;我简单概述下:双十一业务要上线活动大促,需要通过秒杀这种玩法来为整个会场蓄水流量。秒杀活动的玩法是:指定几个商品,商品库存有限,同时每个商品还限制单个用户累计只能买N件。

业务流程

秒杀活动整体业务流程

QbiEBzJ.png!web

通过对需求分析,我们提炼出秒杀活动的三大子流程,即:(运营)创建秒杀活动、(运营/买家)查看秒杀活动、(买家)参与秒杀活动;有了这个整体把握,我们再针对每一个子流程,分析该场景下需要编排哪些产品功能。

创建秒杀活动

创建秒杀活动:做的事情很简单,需要配置好活动场次,每个场次又需配置参与活动的秒杀商品;这个功能点,主要是面向运营,为了方便运营完成活动配置编辑,附带着还需提供活动查询能力:如查看活动列表、活动详情、发布/禁用活动等mis接口。

eiyeYja.png!web

查看秒杀活动

查看秒杀活动:主要功能为支持秒杀会场的活动列表、秒杀商品列表、秒杀商品详情等活动页面;这些都是直接面向C端用户的系列读接口,承载流量会很高。

3aMJRnE.png!web

参与秒杀活动

查看秒杀活动:这一块核心述求是要能正确高效完成库存扣减,严格保证不能出现超卖!从功能点上来看,C端买家下单实现秒杀商品库存扣减,如果买家在规定时间未完成支付或拍下后取消订单,需及时释放用户下单锁定的库存,也就是要回库存。

EjaAJj6.png!web

小节

这一章首先通过需求分析提炼,建立需求整体大局观(创建秒杀活动、查看秒杀活动、参与秒杀活动)。然后通过拆解需求三大业务子流程,明确各子流程功能点,进一步分而治之。明确业务流程和场景,有了清晰的产品功能认识,我们就可以开展下一步概要设计了。

概要设计

E-R 关系图

E-R关系图,用于指导如何建立领域模型。从E-R图上我们能看出,几个比较重要的领域模型:如活动、活动商品,系统真正编码落地的时候,就紧紧围绕这些领域模型去建模,做到代码和领域模型的表达是一致的。

6Nvae2a.png!web

产品边界

概要设计的目的是为了明确产品功能和系统边界,通过领域驱动的界限上下文图,能清晰地看出完成当前需求需要参与协作的团队,以及团队与团队之间任务划分边界。活动上下文是我们关注的重点,同时也应该看到,我们需要商品团队、交易团队的协作。

AJrEVrr.png!web

接口定义

通过概要设计划清了系统边界,每部分每个团队应该做什么就容易确定了,Api定义呼之欲出。

配置活动

1、定义

  • 设置活动、活动商品、活动库存、开始结束时间等配置
  • 系统需为每场活动分配全局唯一活动id:若提交的数据带有活动id,则表示更新

2、接口变更:新增接口

3、API:POST http://${domain}/api/v1/activity/save

4、输入:

{

"activityName": "双十一秒杀第一场",

"startTime": 1540174800000, // 活动开始时间

"endTime": 1541988000000, // 活动结束时间

"itemLine": [

{

  "itemId": 123, // 活动商品id

  "itemType": 7, // 活动商品类型

  "itemTitle": "这是商品标题",

  "subTitle": "这是商品副标题",

  "itemImage": "这是图片链接",

  "salePrice": 66800, // 商品原价(单位:分)

  "activityPrice": 100, // 活动价

  "quota": 3, // 单个用户商品抢购件数限制

  "stock": 100 // 商品活动库存

}

],

"activityRuleConfigs": [ // 活动规则列表

{

  "configKey": "city", // 城市规则:在规则列表的城市可看到活动

  "configValue": "17,5,10,2,3,4,11"

}

]

} 

5、输出:

{

"traceId": "2910c88d0d4f45d5fe299f0c5829d72c",

"code": "SERVICE_RUN_SUCCESS",

"msg": "服务运行成功",

"status": 10000,

"success": true

} 

活动列表

1、定义:返回已创建的全部活动简要信息(不包含活动商品及销量)

2、接口变更:新增接口

3、API:POST http://${domain}/api/v1/activity/list

4、输入:无

5、输出:

{

"traceId": "2910c88a0d4f45d5be290f0c5829d72c",

"success": true,

"status": 10000,

"msg": "OK",

"code": "SUCCESS",

"data": [

{

  "activityId": 1,

  "activityName": "双十一秒杀第一场",

  "startTime": 1541901600000,

  "endTime": 1741951999000,

  "enabled" true

}

]

} 

活动详情

1、定义:返回指定活动详细信息(包含活动商品及其销量)

2、接口变更:新增接口

3、API:GET http://${domain}/api/v1/activity/detail

4、输入:activityId=1

5、输出:

{

"traceId": "889924ef8e6241a7a766107f38c5e0c0",

"success": true,

"status": 10000,

"msg": "OK",

"code": "SUCCESS",

"data": {

"activityId": 1,

"activityName": "双十一秒杀第一场",

"startTime": 1541901600000,

"endTime": 1741951999000,

"enabled" true

"items": [

  {

    "itemId": 53725,

    "itemType": 1,

    "itemTitle": "x商品",

    "subTitle": "x商品副标题",

    "itemImage": "http://img.xxxx.com/static/do1_QtSq1m2xM7VL6zEI4sUH",

    "itemPrice": 19800,

    "activityPrice": 4800,

    "quota": 3,

    "stock": 50,

    "sold": 0

  },

  {

    "itemId": 53724,

    "itemType": 1,

    "itemTitle": "y商品",

    "subTitle": "y商品副标题",

    "itemImage": "http://img.xxxx.com/static/MrcNjUeeoOG24zZH7nR.png",

    "itemPrice": 42800,

    "activityPrice": 17000,

    "quota": 3,

    "stock": 50,

    "sold": 0

  }

]

}

} 

活动商品详情

1、定义:返回活动商品详细信息(包含活动商品销量、活动信息)

2、接口变更:新增接口

3、API:GET http://${domain}/api/v1/activity/itemDetail

4、输入:activityId=1&itemId=53725

5、输出:

{

"traceId": "a77edf653da644959d331b7b55607958",

"success": true,

"status": 10000,

"msg": "OK",

"code": "SUCCESS",

"data": {

"itemId": 53724,

"itemType": 1,

"itemTitle": "x商品",

"subTitle": "商品副标题",

"itemImage": "http://img.xxxx.com/static/do1_QtSq1m2xM7VL6zEI4sUH",

"itemPrice": 42800,

"activityPrice": 17000,

"quota": 3,

"stock": 50,

"sold": 0,

"activity": {

  "activityId": 1,

  "activityName": "双十一秒杀第一场",

  "startTime": 1541901600000,

  "endTime": 1741951999000,

  "enabled" true

}

}

} 

扣库存

1、定义:扣活动库存、扣用户参与抢购资格(注:不局限Http接口,可采用Dubbo调用;此处仅方便演示)

2、接口变更:新增接口

3、API:POST http://${domain}/api/v1/stock/reduce

4、输入:

{

"activityId": 1,

"buyerId": "buyer_001",

"itemId": 53724,

"orderId": "20191111123456789",

"orderTime": 1541901700000,

"quantity": 1

} 

5、输出:

{

"traceId": "f689852f113e413d9940ce24020e7083",

"success": true,

"status": 10000,

"msg": "OK",

"code": "SUCCESS",

"data": true

} 

回库存

1、定义:回商品活动库存、回用户参与资格;可看做是扣库存的逆向操作;(注:笔者在真正实现时,未采用Http接口,而是通过监听订单MQ来异步回库存)

2、接口变更:新增接口

3、API:POST http://${domain}/api/v1/stock/cancelReduce

4、输入:

{

"activityId": 1,

"orderId": "20191111123456789"

} 

5、输出:

{

"traceId": "5342243fd424468ab9ad13d03ffcdc62",

"success": true,

"status": 10000,

"msg": "OK",

"code": "SUCCESS"

} 

详细设计

系统流程

1、创建秒杀活动

VVzIzqe.png!web

2、查看秒杀活动

eUZ3umB.png!web

3、参与秒杀活动

7rMRZz2.png!web

数据模型

纵观整个需求的核心,就是如何做好库存扣减。在系统落地上,楼主采用了业界较为广泛的Redis + Lua脚本方式来实现库存扣减控制。

活动配置表结构

QfQvM3U.png!web

说明:

  • activity_catalog这个Hash结构,用来配置活动信息(活动id、名称、开始/结束时间、活动准入规则等);全部活动共用这一个结构
  • activity_items:$活动id这个Hash结构,用来配置指定活动的商品信息(活动库存、限购量、活动价等);每一个活动都有一个这样的结构

库存扣减核心表结构

vmU3QbE.png!web

说明:

  • buyer_hold:$活动id:$商品id,这个Hash结构,用来记录买家在某个活动拍下活动商品的数量
  • item_salse:$活动id,这个Hash结构,用来记录活动商品销量
  • stock_reduce_flow$活动id,这个Hash结构,称为库存扣减流水表。用来记录「哪个活动(activityId)哪个买家(buyerId)在何时(orderTime)下了哪个订单(orderId)拍下哪个商品(itemId)多少件(quantity)」这一库存扣减流水

为什么是这三个结构?

  • buyer_hold:$活动id:$商品id,可以知道用户已拍下多少件,就能做到控制用户累计只能买N件;
  • 通过 item_salse:$活动id 能知道商品已售出多少件,再结合商品的库存限制,就有办法去控制库存避免超卖;同时也能给C端透出商品秒杀进度。
  • 通过库存扣减流水stock_reduce_flow$活动id,在做回库存的时候,就能依据这一流水,知道去回哪个商品的库存、以及回哪个买家的已拍下数量。

放张图,直观感受下上面罗列的五种数据结构;眼精的同学肯定会发现,其中有四个Hash结构的key都带上了{seckill_$活动id}的前缀。为何要这样特殊处理?其实楼主也在《 这就是你要找的分布式锁 》这篇中有所提及,原因在于Redis集群环境下Lua脚本操作的key,必需限制这些key落在同一个slot中,否则运行会报错Lua script attempted to access a non local key in a cluster node . channel;对此,Redis就提供了HashTag的方案,HashTag是用{和}包裹的一个子串,相同HashTag子串,会落到同一个slot中。

7jyYnya.png!web

库存扣减Lua脚本伪代码演示

扣库存:扣用户抢购资格、扣商品库存、记录库存扣减流水

hincrby buyer_hold:$活动id:$商品id  $买家id  $抢购数量

hincrby item_sales:$活动id  $商品id   $抢购数量

hset stock_reduce_flow:$活动id  $订单id  $json化库存扣减流水

回库存:回用户抢购资格、回商品库存、删除库存扣减流水

hincrby buyer_hold:$活动id:$商品id $买家id -1*$抢购数量

hincrby item_sales:$活动id  $商品id -1*$抢购数量

hdel stock_reduce_flow:$活动id $订单id

总结

至此,我们就就从业务流程、产品流程、系统流程,由整体到局部,完成了整个秒杀系统的需求分析和接口定义;Talk is cheap, Show me your code,在下一篇《一个极简、高效的秒杀系统(实战篇)》楼主会再结合源码,一步步揭开整个秒杀系统的面纱;最后的最后,写作不易,人过留名燕过留声,如果有收获,点个赞呗~

原文链接: https://blog.csdn.net/caiguoxi ... 66073 ,作者:温柔一cai刀


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK