競拍系統(tǒng)設(shè)計(jì)(秒殺系統(tǒng)知識遷移)
作者:sengerlion
來源:SegmentFault 思否社區(qū)
自從上次整理了秒殺系統(tǒng)的文章(php+golang商品秒殺)后,知識遷移一新項(xiàng)目,商品競拍。
技術(shù):php、mysql、redis、laravel
業(yè)務(wù)對象:商品、場次、訂單
競拍過程:
一、實(shí)現(xiàn)商品、競拍場次和訂單的CRUD;
二、定時(shí)將秒殺場次、商品、庫存等信息提前寫入redis;
三、配置Redis持久化;
四、實(shí)現(xiàn)秒殺下單邏輯;
五、秒殺過程redis優(yōu)化;
六、使用golang并發(fā)編程模擬秒殺。
一、實(shí)現(xiàn)商品、競拍場次和訂單的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 '商品創(chuàng)建人',
`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 '當(dāng)前價(jià)格',
`area_id` int(11) NOT NULL COMMENT '區(qū)域id',
`trade_num` int(11) unsigned NOT NULL COMMENT '交易次數(shù)',
`user_name` varchar(100) DEFAULT NULL COMMENT '收貨人名稱',
`user_phone` varchar(11) DEFAULT NULL COMMENT '收貨人聯(lián)系電話',
`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 '狀態(tài)1=>可交易 2=>待支付 3=>交易完成 4=>待發(fā)貨 5=》配送中 6=>完成 7 =>待收款',
`next_time` timestamp NULL DEFAULT NULL COMMENT '下次最早顯示時(shí)間',
`trade_time` timestamp NULL DEFAULT NULL COMMENT '下次可交易時(shí)間',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
`updated_at` timestamp NULL DEFAULT NULL COMMENT '更新時(shí)間',
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '刪除時(shí)間',
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 '拍賣區(qū)域,1=>新手區(qū),2=>競拍區(qū),3=>星級區(qū)',
`name` varchar(64) DEFAULT NULL COMMENT '場次名稱',
`start` time NOT NULL COMMENT '開始時(shí)間',
`end` time NOT NULL COMMENT '結(jié)束時(shí)間',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
`updated_at` timestamp NULL DEFAULT NULL COMMENT '更新時(shí)間',
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '刪除時(shí)間',
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 '購買價(jià)格-成本價(jià)格',
`pay_time` datetime DEFAULT NULL COMMENT '支付時(shí)間',
`status` char(5) NOT NULL COMMENT '狀態(tài)10000=>待支付 20000=>支付超時(shí) 30000=>確認(rèn)支付 30001=>確認(rèn)收款 40000=>賣家申訴中 40001=>買家申訴中 45000=>申訴完成 50000=>完成',
`contract_roles_id` int(10) NOT NULL COMMENT '購買時(shí)商品合約外鍵',
`charge_rate` decimal(10,4) unsigned DEFAULT NULL COMMENT '手續(xù)費(fèi)',
`remark` varchar(255) DEFAULT NULL COMMENT '備注-可以填寫申訴結(jié)果',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
`updated_at` timestamp NULL DEFAULT NULL COMMENT '更新時(shí)間',
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '刪除時(shí)間',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
二、定時(shí)提前寫入redis
1、競拍場次時(shí)間是為每天固定的三個(gè)時(shí)間,定時(shí)提前寫入并設(shè)置過期時(shí)間。
2、緩存數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)有兩個(gè)版本:
a、第一個(gè)版本的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)在商品列表查詢時(shí),無法排除自身商品信息并且分頁。
-
不同區(qū)域可秒殺的用戶set (判斷用戶所屬競拍區(qū)) key: prefix + area_id + start + end + auctions_id,
value:uid -
不同區(qū)域下的商品信息zset (可支持分頁) key: prefix + area_id + start + end + auctions_id,
score:goods_id,
member:goods_detail -
庫存字面量 key: random
value:1 -
是否購買占位 key: prefix + area_id + start + end
value:1
為了滿足排除自身的商品功能和分頁,思考了一些實(shí)現(xiàn)方案:
(1) 完全放棄從緩存中獲取競拍商品信息,這樣增加數(shù)據(jù)庫壓力,同時(shí)無法使用競拍隨機(jī)碼。
(2)為每個(gè)用戶單獨(dú)存放一個(gè)排除自身商品信息的集合,這樣會存放重復(fù)數(shù)據(jù)造成增加內(nèi)容空間。
(3)查詢到redis有一個(gè)SCAN命令來迭代獲取數(shù)據(jù),并可利用glob模式匹配,但是獲取數(shù)量無法確定而無法分頁。
以上(1)(3)點(diǎn)都被排除,我們從第(2)出發(fā)重新設(shè)計(jì)第二版數(shù)據(jù)結(jié)構(gòu),單獨(dú)存放商品數(shù)據(jù)和用戶可查詢的商品id集合來減少重復(fù),但又會出現(xiàn)keys過多的情況,需要進(jìn)行優(yōu)化。
b.第二個(gè)版本的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
-
用戶可查詢的商品id的zset (判斷用戶是否有可競拍商品)
key: prefix + area_id +users_id + auction_id+ start + end,
score:goods_id,
member:goods_id
-
商品信息string (可支持分頁)
key: prefix + area_id + auction_id + goods_id + start + end,
value:goods
-
庫存字面量
key: random
value:1
-
是否購買占位
key: prefix + area_id + start + end
value:1

配置文件:save/append_only
區(qū)別:兩者數(shù)據(jù)保存間隔周期不同,RDB存儲間隔大于AOF存儲間隔
緩存穿透:key值不存在,重復(fù)請求壓垮數(shù)據(jù)庫 => 布隆過濾器或設(shè)置緩存為空。
緩存擊穿:key值存在但是失效,需重新請求數(shù)據(jù)庫造成并發(fā)問題 => SETNX鎖
緩存雪崩:緩存重啟或集中失效,則都請求往DB => 過期時(shí)間設(shè)置分散
登錄校驗(yàn) => 秒殺過程校驗(yàn) => 通過隊(duì)列進(jìn)行異步下單同時(shí)返回訂單號orderSN
秒殺過程中校驗(yàn)點(diǎn)如下:
用戶是否在該區(qū)有可競拍商品
隨機(jī)碼:商品是否可秒殺;
是否已購買過:通過redis的SETNX設(shè)置Key=場次id_商品id_用戶id來判斷是否購買過。
秒殺庫存數(shù)量:在獲取對應(yīng)庫存信息前,將隨機(jī)碼作為key設(shè)置SETNX來實(shí)現(xiàn)并發(fā)鎖,設(shè)置超時(shí)時(shí)間,秒殺成功或失敗都釋放該鎖。
優(yōu)化大致有兩個(gè)方面:
1、在提前將競拍信息寫入redis時(shí),因key數(shù)量大,可采用redis的pipeline管道來提高寫入效率
2、盡可能將場次和開始結(jié)束時(shí)間返回前端讓其在查詢或競拍時(shí)傳給后端,后端拼接key值獲取數(shù)據(jù)的時(shí)間復(fù)雜度是O(1)。
圖片請參考另外一篇文章:
https://segmentfault.com/a/1190000039349297
重磅!交流群已成立
公眾號運(yùn)營至今,離不開小伙伴們的支持。
為了給小伙伴們提供一個(gè)互相交流的技術(shù)平臺,特地開通了公眾號交流群。 群里有不少技術(shù)大神,不時(shí)會分享一些技術(shù)要點(diǎn),更有一些資源收藏愛好者不時(shí)分享一些優(yōu)質(zhì)的學(xué)習(xí)資料。(免費(fèi),不賣課?。?/span> 需要進(jìn)群的朋友,可長按掃描下方二維碼,備注:GitHub科技
▲長按掃碼
