运用的技术
1、redis链表(预库存)
2、rabbitMq(记录秒杀成功的订单数据)
3、php
两个方案
1、mysql 开启事务,生成排他锁🔒
2、redis链表+Mq (推荐)
方案一:mysql开启事务,生成排他锁
tp5.1框架代码
了解tp5.1的两个数据库方法
setInc():将数字字段值增加
setDec():将数字字段值减少
通过事务实现排他锁,可以防止超卖
这里是先修改商品库存 -1 后再添加到订单表
他的流程就是以下:
秒杀请求 -> 启动事务 -> 修改商品 -> 生成排他锁 -> 创建订单 -> 提交事务 -> 释放排他锁 -> 处理下一个秒杀请求
public function order(Request $request)
{
// 商品id
$id = $request->param('id');
// 模拟购买用户的手机号
$mobile = '13888888888';
// 实际业务处理
try {
// 启动事务
Db::startTrans();
// 下面setDec 会产生排他锁,10个并发过来,这里会产生排队的,库存-1
$res = Db::name('goods')->where('id', $id)->setDec('num');
// 另外知识:如果把上面那句注释掉,这里就是共享锁了,10个并发,他们可能读到同一个数据
// 所以这里必须要先修改后查询,才会有排他锁
$num = Db::name('goods')->where('id', $id)->value('num');
if ($num < 0) {
return json(['status' => 'fail', 'msg' => '秒杀失败']);
}
if ($res) {
// 秒杀成功的用户,开始存进订单表
Db::name('orders')->insert(['mobile' => $mobile, 'goods_id' => $id]);
// 提交事务,释放排他锁
Db::commit();
return json(['status' => 'success', 'msg' => '秒杀成功']);
}
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
}
}
方案二:redis链表+Mq (推荐)
1、利用链表的特性,先把商品存进链表,因为链表是会取出来后就不会存在的了,可以利用这个特性来做个预库存
2、等获取链表数据为空的时候,就代表秒杀失败了
3、获取到数据,也把用户的顺序记录进另一条链表中,后续再添加进订单,也可以减少这一步操作,看情况而定
首先我们来看看链表的特性
lpush goods 商品id1 模拟商品的id为1的库存 有5个
127.0.0.1:6379> lpush goods 商品id_1
(integer) 1
127.0.0.1:6379> lpush goods 商品id_1
(integer) 2
127.0.0.1:6379> lpush goods 商品id_1
(integer) 3
127.0.0.1:6379> lpush goods 商品id_1
(integer) 4
127.0.0.1:6379> lpush goods 商品id_1
(integer) 5
127.0.0.1:6379> lrange goods 0 -1
1) "\xe5\x95\x86\xe5\x93\x81id_1"
2) "\xe5\x95\x86\xe5\x93\x81id_1"
3) "\xe5\x95\x86\xe5\x93\x81id_1"
4) "\xe5\x95\x86\xe5\x93\x81id_1"
5) "\xe5\x95\x86\xe5\x93\x81id_1"
127.0.0.1:6379>
如果这时候有人秒杀成功了,就会在队列中取出一个
lpop 删除左边第一个节点,并将其返回,可以看到,现在还剩四个,那么就可以保证了不会超卖了
127.0.0.1:6379> lpop goods
"\xe5\x95\x86\xe5\x93\x81id_1"
127.0.0.1:6379> lrange goods 0 -1
1) "\xe5\x95\x86\xe5\x93\x81id_1"
2) "\xe5\x95\x86\xe5\x93\x81id_1"
3) "\xe5\x95\x86\xe5\x93\x81id_1"
4) "\xe5\x95\x86\xe5\x93\x81id_1"
127.0.0.1:6379>
当lpop到没有数据的时候,就提示秒杀已经没有库存了。
那么现在开始上代码;他的流程以下:
秒杀来了->通过商品id获取链表中的商品id->这里是生成订单数据并且推送mq和存储redis链表的,但这里只是说思路,所以就通过商品id+手机号存储进链表 && 推送mq -> 消费者(mq消费者可以多个)开始消费订单数据
public function order(Request $request)
{
$mobile = $request->input('mobile');
$goods_id = $request->input('goods_id');
// 从链表中取出来
$goods_id = $this->redis->lpop('这里是商品id');
if ($goods_id) {
// 之所以在redis也存储,是为了避免mq生产失败,还有个备用方案
$this->redis->lpush('orders', $mobile . '==' . $goods_id);
// 存放抢购成功的用户到mq中,然后消费者就是负责生成订单(同步到mysql)的,生成订单之后就去支付
// 我问了老师,说如果有人支付失败,或者取消订单的,可以后续随便在秒杀订单中找一笔来让用户支付。
// 他说但是一般情况下不会有以上情况的,因为这样就不是秒杀了。可以参考京东的秒杀。
$this->sendOrder(json_encode(['mobile' => $mobile, 'goods_id' => $goods_id]));
return json(['status' => 'success', 'msg' => '秒杀成功']);
} else {
return json(['status' => 'fail', 'msg' => '秒杀失败']);
}
}
public function sendOrder($data)
{
$connection = new AMQPStreamConnection(config('rabbitmq.rabbit-server'), config('rabbitmq.rabbit-port'), config('rabbitmq.rabbit-user'), config('rabbitmq.rabbit-password'));
$channel = $connection->channel();
$queue_name = config('rabbitmq.rabbit-queue');
$channel->queue_declare($queue_name, false, false, false, false);
$msg = new AMQPMessage($data);
$channel->basic_publish($msg, '', $queue_name);
$channel->close();
$connection->close();
}
如果后续有人退款,或者支付失败的,那么就可以相应的在redis的链表中,加上商品预库存即可,商品预库存就是我刚刚lpush那样子添加即可。
大概的秒杀思路就是如此。