競拍系統設計和核心資料結構

tieniu6636發表於2022-09-06
技術: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;



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

2、快取資料結構設計有兩個版本:

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

  • 不同區域可秒殺的使用者set (判斷使用者所屬競拍區)

    key: prefix + area_id + start + end + auctions_id,value:uid
  • 不同區域下的商品資訊zset (可支援分頁)

    key: prefix + area_id + start + end + auctions_id,score:goods_id,member:goods_detail
  • 庫存字面量

    key: randomvalue:1
  • 是否購買佔位

    key: prefix + area_id + start + endvalue:1

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

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

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

b.第二個版本的資料結構設計

  • 使用者可查詢的商品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: randomvalue: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)。




來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70018409/viewspace-2913554/,如需轉載,請註明出處,否則將追究法律責任。

相關文章