秒杀系统特点
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"
- }
- }
-
- 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()]);
- }
- }
-
-
- }