下列解决方案可以满足:
- 秒杀
- 抢红包(需要增加一个红包预先拆分的逻辑)
等高并发场景。
一、削峰与异步化
对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。
利用Kafka消息队列缓存用户请求,后端的秒杀服务再按照自己的速率从消息队列拉取请求处理。
二、Redis实现高并发下的减库存操作
2.1 常见的错误逻辑
逻辑
- 假设库存是
num
; - 秒杀服务获取用户请求和
UserID
,先判断列表order:1
的长度(llen()
)是否超过库存num
; - 如果超过,返回“已经抢光了”;如果没有超过,则将 UserID
lpush
到 order:1 列表。
代码
<?php
$num = 10; //系统库存量
$user_id = \Session::get('user_id');//当前抢购用户id
$len = \Redis::llen('order:1'); //检查库存,order:1 定义为健名
if($len >= $num)
return '已经抢光了哦';
$result = \Redis::lpush('order:1',$user_id); //把抢到的用户存入到列表中
if($result)
return '恭喜您!抢到了哦';
超卖问题
在高并发的情况下,假设库存还剩1个,一定会出现多个线程/实例用llen()方法读取Redis的情况,这个时候它们都会认为还有库存,从而多个UserID被添加到列表,也就出现了超卖的Bug。
2.2 正确的逻辑
- 假设库存是10;
- 先往
goods_store:1列表
中LPUSH
10个1; - 每当获取一个用户请求,先用
RPOP
从goods_store:1列表
弹出一个数,然后判断这个数是否为0; - 如果是0,说明库存已经为空,返回“已经抢光了”,反之,将 UserID
lpush
到order:1 列表
。
分析
由于Redis的所有操作都是原子操作,而且单线程的Redis保证高并发下的请求都被串行化处理,所以goods_store:1列表
中的10个库存一定是一个个串行得被消费。
代码
$num=10; //库存
$len=\Redis::llen('goods_store:1'); //检查库存,goods_store:1 定义为健名
$count = $num-$len; //实际库存-被抢购的库存 = 剩余可用库存
for($i=0;$i<$count;$i++)
\Redis::lpush('goods_store:1',1);//往goods_store列表中,未抢购之前这里应该是默认滴push10个库存数了
/* 模拟抢购操作,抢购前判断redis队列库存量 */
$count=\Redis::lpop('goods_store:1');//lpop是移除并返回列表的第一个元素。
if(!$count)
return '已经抢光了哦';
系统再引导下单成功的客户进入下一步流程。并且系统需要将缓存中的数据同步到数据库对应的表中,比如商品表(改库存)、订单表(保存用户订单)。
三、MySQL实现秒杀
经过第一步Kafka消息队列的缓冲,数据库面临的压力是完全可控的,所以使用MySQL实现秒杀完全可行。只是在秒杀量比较大的时候,其总体效率肯定是比不上Redis的。思路如下:
(1)创建一个秒杀表 t_flash_sale,使用MySQL的自增主键特性,设置从1开始自增:
create table table1(id int auto_increment=1 primary key,...)
(2)获取一个用户请求则将UserID插入 t_flash_sale,获取返回的插入id;
(3)判断插入id是否大于库存num,大于则秒杀失败。
四、下单系统的水平可扩展架构
下单系统面临的一个普遍问题:
下一次单需要写很多数据库表,而且订单量比较大,因此用户的下单速度会比较慢。
解决方案的纲领:将下单逻辑和写数据库表的逻辑分开、异步化。具体架构如下:
用户层面的订单是一个整体,但是订单中心里会有很多不同的表,下单一次要更新很多表。
Buffer DB可以是MySQL数据库,保存订单;也可以是消息队列。