秒杀系统特点
1、抢购人数远多于库存,读写并发巨大。
2、库存少,有效写少。
3、写需强一致性,商品不能卖超。
4、读强一致性要求不高。
5、稳定性难:高并发下,某个小依赖可能直接造成雪崩、流量预期难精确,过高也造成雪崩。分布式集群,机器多,出故障的概率高。
6、准确性难:库存、抢购成功数,创建订单数之间的一致性。
7、高性能难:有限成本下需要做到极致的性能。
秒杀系统——架构原则
1、稳定性:减少第三方依赖,同时自身服务部署也需做到隔离。压测、降级、限流方案、确保核心服务可用。需健康度检测机制,整个链路避免单点。
2、高性能:缩短单请求访问路径,减少IO。减少接口数,降低吞吐数据量,请求次数减少。
秒杀服务核心实现
1、怎样设计秒杀服务:满足基本需求,做到单服务极致性能。请求链路流量优化,从客户端到服务端每层优化。稳定性建设。
2、基本需求:扣库存、查库存、排队进度。(做到单服务极致性能)。查订单详情、创建订单,支付订单。(库存少抢购人数远多于库存,读写并发高)
基本需求——扣库存方案
1、下单减库存?
并发请求——>创建订单——>扣库存——>支付 这种流程不会超卖,但问题是如果有人恶意下单不支付,占用库存。
2、支付减库存?
并发请求——>创建订单——>支付——>扣库存 这种流程是支付一次扣一次库存,如果用户把商品买完了,别的用户下不了订单或者订单超卖。
3、预扣库存?
并发请求——>扣库存——>创建订单——>支付——>10分钟内不支付取消订单,加库存。
采用预扣库存方案比较好。
wxml
<view>
<l-countdown time-type="second" time="{{expire_time}}" bind:linend="changeBtn"/>
<view><image src="{{Detail.image}}" bindtap="image"></image></view>
<view>{{Detail.store_product.store_name}}</view>
<view>{{Detail.price}}</view>
<view>
<l-button disabled="{{ disabled }}" bind:lintap="buyGoods" type="error" data-id="{{Detail.id}}">立即秒杀</l-button>
</view>
</view>
js
// pages/Detail/Detail.js
Page({
/**
* 页面的初始数据
*/
data: {
expire_time:0,
disabled:false,
expire_time:'',
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
let that = this
let sid = options.id
wx.request({
url: 'http://www.wej.com/index.php/api/seckill/goodsDetail', //仅为示例,并非真实的接口地址
data: {
sid
},
header: {
'content-type': 'application/json' // 默认值
},
success (res) {
console.log(res.data)
let newdate = Math.round(new Date().getTime() / 1000).toString()
let expire_time = res.data.data.start_time - newdate
console.log(expire_time)
that.setData({
Detail:res.data.data,
expire_time:expire_time
})
if(expire_time > 0){
that.setData({
disabled:true
})
}else{
that.setData({
disabled:false
})
}
}
})
},
buyGoods(c){
clearTimeout(this.TimeID);
this.TimeID = setTimeout(() => {
let goods_id = c.currentTarget.dataset.id
wx.request({
url: 'http://www.wej.com/index.php/api/seckill/snap_up', //仅为示例,并非真实的接口地址
data: {
goods_id
},
header: {
'content-type': 'application/json' // 默认值
},
success (res) {
console.log(res.data)
}
})
}, 1000);
},
changeBtn(){
console.log('秒杀开始')
this.setData({
disabled:false
})
},
image(c){
wx.previewImage({
current: this.data.Detail.image, // 当前显示图片的 http 链接
urls: [this.data.Detail.image] // 需要预览的图片 http 链接列表
})
},
})
json
{
"usingComponents": {
"l-countdown":"/dist/countdown",
"l-button":"/dist/button"
}
}
<?php
namespace App\Http\Controllers;
use App\Models\AddressInfo;
use App\Models\StoreOrder;
use App\Server\Snowflake;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
class Seckill extends Controller
{
/**
* 数据预热
* @return \Illuminate\Http\JsonResponse
*/
public function activity()
{
$result = \App\Models\Seckill::with('StoreProduct')
->get()->toArray();
foreach ($result as $val){
//生成对应商品库存队列
$goods = "activity_goods_".$val['product_id'];
for ($i=0; $i < $val['stock']; $i++) {
Redis::lpush($goods,1);
}
}
return response()->json(['code' => 20000, 'msg' => '查询成功', 'data' => $result]);
}
/**、
* 秒杀列表
* @return \Illuminate\Http\JsonResponse
*/
public function activityList()
{
$result = \App\Models\Seckill::with('StoreProduct')
->get()->toArray();
return response()->json(['code' => 20000, 'msg' => '查询成功', 'data' => $result]);
}
/**
* 秒杀商品详情
* @return \Illuminate\Http\JsonResponse
*/
public function goodsDetail()
{
$goods_id = request()->get('sid');
$result = \App\Models\Seckill::with(['StoreProduct'])
->where('product_id',$goods_id)
->first();
return response()->json(['code' => 20000, 'data' => $result, 'msg' => '查询成功']);
}
/**
* 校验库存
* @return \Illuminate\Http\JsonResponse
*/
public function snap_up(){
//用户ID
$userID = 1;
//商品ID
$goodsID = request()->get('goods_id');
//对应商品库存队列
$goods = "activity_goods_".$goodsID;
//对应商品抢购成功用户集合 {1,3,4}
$robSuccessUser = "success_user".$goodsID;
//进行判断当前用户是否在抢成功的队列里面
$result = Redis::sismember($robSuccessUser,$userID);
//如果你在这里面,就抢完了
if ($result) {
//如果抢购成功 返回状态码,进行下单
return response()->json(['code' => 20000, 'data' => '', 'msg' => '已经抢购过了']);
}
//减库存,把队列里面的数据从左边 头
$count = Redis::lpop($goods);
if (!$count) {
//如果抢购成功 返回状态码,进行下单
return response()->json(['code' => 20001, 'data' => '', 'msg' => '已经抢光了哦']);
}
//把当前这个秒杀的uid存储到抢购成功的队列里set
$success = Redis::sadd($robSuccessUser, $userID);
if(!$success){
//已经在成功队列里了,加回库存,防止的是同个用户并发请求
Redis::lpush($goods, 1);
//如果抢购成功 返回状态码,进行下单
return response()->json(['code' => 20002, 'data' => '', 'msg' => '已经抢购过了']);
}
//如果抢购成功 返回状态码,进行下单
return response()->json(['code' => 20000, 'data' => '', 'msg' => '秒杀成功']);
}
/**
* 生成订单
* @return false|\Illuminate\Http\JsonResponse|string
*/
public function createOrder(){
//用户ID
$userID = request()->get('userID');
//商品ID
$goodsID = request()->get('goods_id');
//地址ID
$address_id = request()->get('address_id');
//对应商品抢购成功用户集合
$robSuccessUser = "success_user".$goodsID;
//进行判断当前用户是否在抢成功的队列里面
$result = Redis::sismember($robSuccessUser,$userID);
//如果你在这里面,就抢完了
if (!$result) {
//如果抢购成功 返回状态码,进行下单
return response()->json(['code' => 20003, 'data' => '', 'msg' => '手慢了!']);
}
DB::beginTransaction();
try{
//减库存
$shopData = \App\Models\Seckill::with('StoreProduct')
->where('product_id',$goodsID)
->first()->toArray();
$shopStock = \App\Models\Seckill::where('product_id',$goodsID)->update(['stock' => $shopData['stock'] - 1]);
if (!$shopStock) return json_encode(['code' => 50000,'msg' => '下单失败','data' => []]);
//地址
$address_info = AddressInfo::find($address_id)->toArray();
//生成订单
Snowflake::machineId($userID);
$order_id = substr(date('Ymd'),2).'-'.Snowflake::createOnlyId();
$data = [
'order_id' => $order_id,
'uid' => $userID,
'real_name' => $address_info['real_name'],
'user_phone' => $address_info['phone'],
'user_address' => $address_info['detail'],
'pay_price' => $shopData['store_product']['price'],
'pay_time' => time()
];
$orderAdd = StoreOrder::insert($data);
if (!$orderAdd) return json_encode(['code' => 50000,'msg' => '下单失败','data' => []]);
DB::commit();
//下单成功,跳转支付页面
return response()->json(['code' => 20000, 'data' => '', 'msg' => '下单成功!']);
}catch (\Exception $e){
DB::rollBack();
return response()->json(['code' => 50000, 'data' => '', 'msg' => $e->getMessage()]);
}
}
}