競拍系統設計

作者:sengerlion

來源:SegmentFault 思否社區

技術:php、mysql、redis、laravel

業務對象:商品、場次、訂單

競拍過程:

一、實現商品、競拍場次和訂單的 CRUD;
二、定時將秒殺場次、商品、庫存等信息提前寫入 redis;
三、配置 Redis 持久化;
四、實現秒殺下單邏輯;
五、秒殺過程 redis 優化;
六、使用 golang 併發編程模擬秒殺。

一、實現商品、競拍場次和訂單的 CRUD

商品表:

 CREATE TABLE `goods` (
  `id` int(12) unsigned NOT NULL AUTO_INCREMENT COMMENT 'pk',
  `num` varchar(64) NOT NULL COMMENT '商品編號',
  `users_id` int(12) unsigned NOT NULL COMMENT '擁有者',
  `create_users_id` int(12) unsigned NOT NULL COMMENT '商品創建人',
  `contract_roles_id` int(10) unsigned NOT NULL COMMENT '商品合約級別外鍵',
  `name` varchar(255) NOT NULL COMMENT '商品名稱',
  `img` int(11) NOT NULL COMMENT '封面圖',
  `price` decimal(10,2) unsigned NOT NULL COMMENT '當前價格',
  `area_id` int(11) NOT NULL COMMENT '區域id',
  `trade_num` int(11) unsigned NOT NULL COMMENT '交易次數',
  `user_name` varchar(100) DEFAULT NULL COMMENT '收貨人名稱',
  `user_phone` varchar(11) DEFAULT NULL COMMENT '收貨人聯繫電話',
  `user_address` varchar(255) DEFAULT NULL COMMENT '收貨人地址',
  `express_id` int(11) DEFAULT NULL COMMENT '物流ID',
  `express_no` varchar(255) DEFAULT NULL COMMENT '物流單號',
  `is_auction` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否可競拍,1=》可 2=》不可',
  `status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '狀態1=>可交易 2=>待支付 3=>交易完成 4=>待發貨 5=》配送中 6=>完成 7 =>待收款',
  `next_time` timestamp NULL DEFAULT NULL COMMENT '下次最早顯示時間',
  `trade_time` timestamp NULL DEFAULT NULL COMMENT '下次可交易時間',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新時間',
  `deleted_at` timestamp NULL DEFAULT NULL COMMENT '刪除時間',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=111 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

競拍場次表:

CREATE TABLE `auctions` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `area` tinyint(4) NOT NULL COMMENT '拍賣區域,1=>新手區,2=>競拍區,3=>星級區',
  `name` varchar(64) DEFAULT NULL COMMENT '場次名稱',
  `start` time NOT NULL COMMENT '開始時間',
  `end` time NOT NULL COMMENT '結束時間',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新時間',
  `deleted_at` timestamp NULL DEFAULT NULL COMMENT '刪除時間',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='拍賣場次表';

訂單表:

CREATE TABLE `orders` (
  `id` int(12) unsigned NOT NULL AUTO_INCREMENT COMMENT 'pk',
  `serial_num` varchar(32) DEFAULT NULL COMMENT '流水號,沒交易前爲空',
  `goods_id` int(12) unsigned NOT NULL COMMENT '商品id',
  `sell_users_id` int(12) unsigned NOT NULL COMMENT '競拍商品擁有者id',
  `buy_users_id` int(12) unsigned DEFAULT NULL COMMENT '購買商品用戶id',
  `buy_price` decimal(10,2) NOT NULL COMMENT '購買價格-成本價格',
  `pay_time` datetime DEFAULT NULL COMMENT '支付時間',
  `status` char(5) NOT NULL COMMENT '狀態10000=>待支付  20000=>支付超時 30000=>確認支付 30001=>確認收款 40000=>賣家申訴中 40001=>買家申訴中 45000=>申訴完成 50000=>完成',
  `contract_roles_id` int(10) NOT NULL COMMENT '購買時商品合約外鍵',
  `charge_rate` decimal(10,4) unsigned DEFAULT NULL COMMENT '手續費',
  `remark` varchar(255) DEFAULT NULL COMMENT '備註-可以填寫申訴結果',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新時間',
  `deleted_at` timestamp NULL DEFAULT NULL COMMENT '刪除時間',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

二、定時提前寫入 redis

1、競拍場次時間是爲每天固定的三個時間,定時提前寫入並設置過期時間。

2、緩存數據結構設計有兩個版本:

a、第一個版本的數據結構設計在商品列表查詢時,無法排除自身商品信息並且分頁。

爲了滿足排除自身的商品功能和分頁,思考了一些實現方案:

(1) 完全放棄從緩存中獲取競拍商品信息,這樣增加數據庫壓力,同時無法使用競拍隨機碼。
(2)爲每個用戶單獨存放一個排除自身商品信息的集合,這樣會存放重複數據造成增加內容空間。
(3)查詢到 redis 有一個 SCAN 命令來迭代獲取數據,並可利用 glob 模式匹配,但是獲取數量無法確定而無法分頁。

以上(1)(3)點都被排除,我們從第(2)出發重新設計第二版數據結構,單獨存放商品數據和用戶可查詢的商品 id 集合來減少重複,但又會出現 keys 過多的情況,需要進行優化。

b. 第二個版本的數據結構設計

 key: prefix + area_id +users_id + auction_id+ start + end, 
 score:goods_id, 
 member:goods_id
 key: prefix + area_id + auction_id + goods_id + start + end, 
 value:goods
 key: random  
 value:1
  key: prefix + area_id + start + end  
  value:1

三、配置 Redis 持久化

持久化兩種模式都開啓:RDB(快照模式)+ AOF(日誌模式)
配置文件:save/append_only
區別:兩者數據保存間隔週期不同,RDB 存儲間隔大於 AOF 存儲間隔

四、實現秒殺下單邏輯

1、查詢場次和當前秒殺商品:

查詢 redis 中的緩存數據,當併發量大時可能出現:
緩存穿透:key 值不存在,重複請求壓垮數據庫 => 布隆過濾器或設置緩存爲空。
緩存擊穿:key 值存在但是失效,需重新請求數據庫造成併發問題 => SETNX 鎖
緩存雪崩:緩存重啓或集中失效,則都請求往 DB => 過期時間設置分散

2、正式競拍是單獨的秒殺下單功能。

3、具體的下單邏輯:
登錄校驗 => 秒殺過程校驗 => 通過隊列進行異步下單同時返回訂單號 orderSN
秒殺過程中校驗點如下:

秒殺時間:是否在秒殺時間內;
用戶是否在該區有可競拍商品
隨機碼:商品是否可秒殺;
是否已購買過:通過 redis 的 SETNX 設置 Key = 場次 id_商品 id_用戶 id 來判斷是否購買過。
秒殺庫存數量:在獲取對應庫存信息前,將隨機碼作爲 key 設置 SETNX 來實現併發鎖,設置超時時間,秒殺成功或失敗都釋放該鎖。

五、秒殺過程 redis 優化

因緩存數據結構的設計,可能會在 redis 存儲大量的 key,若通過 keys 命令查詢會是 O(n)複雜度,查詢會卡頓而緩慢,redis 有提供 scan 迭代來代替 keys,但是根據本項目無需使用它。

優化大致有兩個方面:

1、在提前將競拍信息寫入 redis 時,因 key 數量大,可採用 redis 的 pipeline 管道來提高寫入效率
2、儘可能將場次和開始結束時間返回前端讓其在查詢或競拍時傳給後端,後端拼接 key 值獲取數據的時間複雜度是 O(1)。

六、使用 golang 併發編程模擬秒殺

圖片請參考另外一篇文章:

https://segmentfault.com/a/1190000039349297

golang 併發調度項目碼雲:

https://gitee.com/jasonlxs/seckill/tree/master

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ey-672yhpr6EjRUiHhkdag