秒杀系统的设计
之前写过一篇关于 促销系统的设计 中提到了秒杀/直减/聚划算,但在实际工作中,并没有真的做过秒杀系统,所以假想了一个简单的秒杀系统来”解解馋“,促销思路依旧顺延之前的文章设计。
分析
秒杀时大量的流量涌入,秒杀开始前频繁刷新查询,如果大量的流量瞬间冲击到数据库的话,非常容易造成数据库的崩溃。所以秒杀的主要工作就是对流量进行层层筛选最后让尽可能少且平缓的流量进入到数据库。
通常的秒杀是大量的用户抢购少量的商品,类似这样的需求只需要简单的进行库存缓存,就能在实际创建订单前过滤大量的流量。
但但但是,只是这样的话好像没什么挑战力呀!稍微加大一下难度,假设我们的秒杀是像抢小米手机一样,100 万人抢 10 万台手机呢?小米抢购时的排队是一种方法(虽然体验不太好),后续将会按照这种思路进行我们的秒杀设计。
提到小米就不得不说一下,其让我知道了什么是「运气也是实力的一部分!」
前端限流大法 : random(0, 1) ? axios.post : wait(30, '抢完啦!')
下面开始从一些代码的细节进行分析,原则上是对原有业务逻辑尽可能小的改动。另外后文中没有什么服务熔断,多级缓存等高级的玩法,只是比较简单的业务设计。
开始
运营人员在后台将一个变体添加到秒杀促销,并设置秒杀的库存/秒杀折扣率/开始时间和结束时间等,我们能够得到类似这样的数据。
// promotion_variant (促销和变体表「sku」的一个中间表) { 'id': 1, 'variant_id': 1, 'promotion_id': 1, 'promotion_type': 'snap_up', 'discount_rate': 0.5, 'stock': 100, // 秒杀库存 'sold': 0, // 秒杀销量 'quantity_limit': 1, // 限购 'enabled': 1, 'product_id': 1, 'rest': { variant_name: 'xxx', // 秒杀期间变体名称 image: 'xxx', // 秒杀期间变体图片 } }
首先便是在秒杀促销创建成功后将促销的信息进行缓存
# PromotionVariantObserver.php public function saved(PromotionVariant $promotionVariant) { if ($promotionVariant->promotion_type === PromotionType::SNAP_UP) { $seconds = $promotionVariant->ended_at->getTimestamp() - time(); \Cache::put( "promotion_variants:$promotionVariant->id", $promotionVariant, $seconds ); } }
下单
已有的下单接口,接收到变体信息后,并不知道当前变体列表哪些参与了促销,这里的判断操作是需要大量的数据库查询操作的。
所以此处为秒杀编写一个新的 api ,前端检测到当前变体处于秒杀促销时则切换到秒杀下单 api 。
当然依旧使用原有的下单 api ,前端传递一个标识也是没有问题的。
需要解释的一点时,下单通常分为两步
第一步是 「结账( checkout )」生成一个结账订单,用户可以为结账订单选择地址、优惠卷、支付方式 等。
第二步是 「确认 ( confirm )」,此时订单将变成确认状态,对库存进行锁定,且用户可以进行支付。通常如果在规定时间内没有支付,则取消该订单,并解锁库存。
所以在第一步时就会对用户进行过滤和排队处理,防止后续的选择地址、优惠卷等操作对数据库进行冲击。
# CheckoutController.php /** * @param Request $request * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response * @throws StockException */ public function snapUpCheckout(Request $request) { $variantId = $request->input('variant_id'); $quantity = $request->input('quantity', 1); // 加锁防止超卖 $lock = \Cache::lock('snap_up:' . $variantId, 10); try { // 未获取锁的消费者将阻塞在这里 $lock->block(10); $promotionVariant = \Cache::get('promotion_variants:' . $variantId); if ($promotionVariant->quantity < $quantity) { $lock->release(); throw new StockException('库存不足'); } $promotionVariant->quantity -= $quantity; $seconds = $promotionVariant->ended_at->getTimestamp() - time(); \Cache::put( "promotion_variants:$promotionVariant->id", $promotionVariant, $seconds ); } catch (LockTimeoutException $e) { throw new StockException('库存不足'); } finally { optional($lock)->release(); } CheckoutOrder::dispatch([ 'user_id' => \Auth::id(), 'variant_id' => $variantId, 'quantity' => $quantity ]); return response('结账订单创建中'); }
可以看到在秒杀结账 api 中,并没有涉及到数据库的操作。并且通过 dispatch 将创建订单的任务分发到队列,用户按照进入队列的先后顺序进行对应时间的排队等待。
现在的问题是,订单创建成功后如何通知客户端呢?
客户端通知
这里的方案无非就是轮询或者 websocket, 这里选择对服务器性能消耗较小的 websocket ,且使用 laravel 提供的 laravel-echo ( laravel-echo-server ) 。 当用户秒杀成功后,前端和后端建立 websocket 链接,后端结账订单创建成功后通知前端可以进行下一步操作。
后端
后端接下来要做的就是在 「CheckoutOrder」Job 中的订单创建成功后,向 websocket 对应的频道中发送一个 「OrderChecked 」事件,来表明结账订单已经创建完成,用户可以进行下一步操作。
# Job/CheckoutOrder.php // ... public function handle() { // 创建结账订单 // ... // 通知客户端. websocket 编程本身就是以事件为导向的,和 laravel 的 event 非常契合。 event(new OrderChecked($this->data->user_id)); } // ...
# Event/OrderChecked.php class OrderChecked implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; private $userId; /** * Create a new event instance. * * @param $userId */ public function __construct($userId) { $this->userId = $userId; } /** * App.User.{id} 是 laravel 初始化时,默认的私有频道,直接使用即可 * @return \Illuminate\Broadcasting\Channel|array */ public function broadcastOn() { return new PrivateChannel('App.User.' . $this->userId); } }
假设当前抢购的用户 id 是 1,总结一下上面的代码就是向 websocket 的私有频道「App.User.1」 推送一个 「OrderChecked」 事件。
前端
下面的代码是使用 vue-cli 工具初始化的默认项目。
// views/products/show.vue <script> import Echo from 'laravel-echo' import io from 'socket.io-client' window.io = io export default { name: 'App', methods: { async snapUpCheckout () { try { // await post -> snap-up-checkout this.toCheckout() } catch (error) { // 秒杀失败 } }, toCheckout () { // 建立 websocket 连接 const echo = new Echo({ broadcaster: 'socket.io', host: 'http://api.e-commerce.test:6001', auth: { headers: { Authorization: 'Bearer ' + this.store.auth.token } } }) // 监听私有频道 App.User.{id} 的 OrderChecked 事件 echo.private('App.User.' + this.store.user.id).listen('OrderChecked', (e) => { // redirect to checkou page }) } } } </script>
laravel-echo 使用时需要注意的一点,由于使用了私有频道,所以 laravel-echo 默认会向服务端api /broadcasting/auth
发送一条 post 请求进行身份验证。 但是由于采用了前后端分类而不是 blade 模板,所以我们并不能方便的获取 csrf token 和 session 来进行一些必要的认证。
因此需要稍微修改一下 broadcast 和 laravel-echo-server 的配置
# BroadcastServiceProvider.php public function boot() { // 将认证路由改为 /api/broadcasting/auth 从而避免 csrf 验证 // 添加中间件 auth:api (jwt 使用 api.auth) 进行身份验证,避免访问 session ,并使 Auth::user() 生效。 Broadcast::routes(["prefix" => "api", "middleware" => ["auth:api"]]); require base_path('routes/channels.php'); }
// laravel-echo-server.json // 认证路由添加 api 前缀,与上面的修改对应 "authEndpoint": "/api/broadcasting/auth"
库存解锁
在已经为该订单锁定”库存“的情况下,用户如果断开 websocket 连接或者长时间离开时需要将库存解锁,防止库存无意义占用。
这里的库存指的是缓存库存,而非数据库库存。这是因为此时订单即使创建成功也是结账状态(未选择地址,支付方式等),在个人中心也是不可见的。只有当用户确认订单后,才会将数据库库存锁定。
所以此处的理想实现是,用户断开 websocket 连接后,将该订单锁定的库存归还。且结账订单创建后再创建一个延时队列对长时间未操作的订单进行库存归还。
但但但是,laravel-echo 是一个广播系统,并没有提供客户端断开连接事件的回调,有些方法可以实现 laravel 监听的客户端事件,比如在 laravel-echo-server 添加 hook 通知 laravel,但是需要修改 laravel-echo-server 的实现,这里就不细说了,重点还是提供秒杀思路。
总结
上图为秒杀系统的逻辑总结。至此整个秒杀流程就结束了,总的来说代码量不多,逻辑也较为简单。
从图中可以看出,整个流程中,只有在 queue 中才会和 mysql 交互,通过 queue 的限流从而最大限度的适应了 mysql 的承受能力。在 mysql 性能足够的情况下,通过大量的 queue 同时消费订单,用户是完全感知不到排队的过程的。
有问题或者有更好的思路欢迎留言讨论呀~
更多Laravel相关技术文章,请访问Laravel教程栏目进行学习!
以上是秒杀系统的设计的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

4月17日消息,HMD携手知名啤酒品牌喜力以及创意公司Bodega,联袂推出了一款别具一格的翻盖手机——无聊手机(TheBoringPhone)。这款手机不仅在设计上充满新意,更在功能上返璞归真,旨在引领人们回归真实的人际交往,享受与朋友畅饮的纯粹时光。无聊手机采用了独特的透明翻盖设计,展现出一种简约而不失优雅的美感。其内部配备了2.8英寸QVGA显示屏,外部则是一块1.77英寸的显示屏,为用户提供了基本的视觉交互体验。在摄影方面,虽然仅搭载了30万像素的摄像头,但足以应对日常的简

3月4日消息,酷比魔方将于3月5日推出“小酷平板2Lite”平板电脑,首发价649元。据悉,新款平板搭载紫光展锐T606处理器,采用12nm工艺,由两颗1.6GHz的ArmCortex-A75CPU和六颗ArmCortex-A55处理器组成。屏幕采用的是10.95英寸IPS护眼屏,分辨率为1280x800,亮度高至350尼特。影像方面,小酷平板2Lite后置1300万像素主摄,前置500万像素自拍镜头,另支持4G上网/通话、蓝牙5.0、Wi-Fi5。此外,官方宣称,这款平板电脑&l

4月26日消息,中兴5G随身Wi-FiU50S目前已经正式开售,首发899元。外观设计上,中兴U50S随身Wi-Fi简约时尚,易于手持和包装。其尺寸为159/73/18mm,携带方便,让您随时随地畅享5G高速网络,实现畅行无阻的移动办公与娱乐体验。中兴5G随身Wi-FiU50S该设备支持先进的Wi-Fi6协议,峰值速率高达1800Mbps,依托骁龙X55高性能5G平台,为用户提供极速的网络体验。不仅支持5G双模SA+NSA网络环境和Sub-6GHz频段,实测网速更可达惊人的500Mbps,轻松满

4月3日消息,台电即将推出的M50Mini平板电脑是一款功能丰富、性能强大的设备。这款8英寸小平板新品搭载了8.7英寸的IPS屏幕,为用户提供了出色的视觉体验。其金属机身设计不仅美观,还增强了设备的耐用性。在性能方面,M50Mini搭载了紫光展锐T606八核处理器,拥有两个A75核心和六个A55核心,确保了流畅且高效的运行体验。同时,该平板还配备了6GB+128GB的存储方案,并支持8GB内存扩展,满足了用户对于存储和多任务处理的需求。在续航上,M50Mini配备了5000mAh的电池,支持Ty

7月12日消息,荣耀MagicV3系列今日正式发布,搭载全新荣耀视力舒缓绿洲护眼屏,在屏幕本身具备高规格和高素质的同时,还开创性的引入AI主动式护眼技术。据悉,传统的缓解近视的方式是“近视镜”,近视眼镜度数均匀分布,保证了视线中心区域成像在视网膜之上,但周边区域成像在视网膜后,视网膜感应到成像在后,促进眼轴向后生长,从而使度数加深。目前主要的缓解近视发展的方式之一是“离焦镜”,其中心区域度数正常,周边区域通过光学设计分区调整,从而使周边区域成像落在视网膜前,

在工作中,ppt是职场人士常常使用的办公软件。一个完整的ppt必须有一个好的结束页。不同的职业要求赋予不同的ppt制作特点。关于结束页的制作,如何才能设计的比较吸引人呢?下边我们一起看一看,如何设计ppt结束页吧!ppt结束页的设计可以在文字和动画方面进行一些调整,根据需要选择简洁或炫目的风格。接下来,我们将重点关注如何通过创新的表达方式来打造出符合要求的ppt结束页。那我们开始今天的教程吧。1、对于结束页的制作上,使用图片中的任何文字都可以,结束页重要的是表示我的演示结束了。2、除了这些文字,

2月22日消息,华为Pocket2折叠旗舰今日正式登场,采用灵巧身型设计,推出大溪地灰、洛可可白、芋紫、雅黑四款配色。据介绍,华为Pocket2首创超冷立体散热系统,业界首创中框VC+立体散热结构,并且采用业界最高导热石墨烯材料,等效导热系数达1800W/m·K,整体导热面积提升80%。对于大家关心的折痕问题,华为Pocket2搭载业界首创玄武水滴铰链,屏幕长时间使用依然平整,双力臂杠杆齿轮,轻巧开合。通信方面,华为Pocket2支持超强灵犀通信,并且还是首款支持双向北斗卫星消息的小折叠。操

7月29日消息,荣耀X60i手机今日正式开售,首发1399元。设计上,荣耀X60i手机采用居中挖孔直屏设计,四边近乎无界的超窄边框,极大地拓宽了视野边界。荣耀X60i参数显示屏:6.7英寸高清显示屏电池:5000mAh大容量电池处理器:天玑6080处理器(台积电6nm,2x2.4G的A76+6×2G的A55)系统:MagicOS8.0系统其他功能:5G信号增强灵动胶囊屏下指纹双MIC降噪知识问答摄影能力:后置双摄系统:5000万像素主摄200万像素辅助镜头前置自拍镜头:800万像素价格:8GB
