文章目錄
- 抽獎核心算法
- 生成抽獎大轉盤
- 抽獎接口實現
抽獎核心算法
- 我們可以根據
單商品庫存量/總商品庫存量
得到每個商品被抽中的概率,可以想象這樣一條0-1
的數軸,數軸上的每一段相當于一種商品,概率之和為1
。
- 抽獎時,我們會生成
U(0,1)
上的一個隨機數,這個數位于哪個線段上就對應著抽中了對應的商品。 - 構建線段,時間復雜度
O(n)
。 - 用二分查找算法查找隨機數位于哪一段,時間復雜度
O(logn)
,采集k
個樣本需要再乘以k
。
- 接下來介紹二分查找區間算法:
N
個點把實數域分割成N+1
段,target
是隨機生成的實數target
應該落在哪一段上?- 定義
array[i-1] < target < array[i]
為落在第i
條線段上,代表第i
個獎品被抽中
// BinarySearch 查找 >= target 的最小元素下標,arr單調遞增(不能存在重復元素)
// 如果target比arr的最后一個元素還大,返回最后一個元素下標
func BinarySearch(arr []float64, target float64) int {if len(arr) == 0 {return -1}left := 0right := len(arr)for left < right {// 通用條件if target <= arr[left] {return left}if target > arr[right-1] {return right}// len(arr) == 2, mid在left和right之間, 選擇left的概率值if left == right-1 {return right}// len(arr) >= 3mid := (left + right) / 2if target < arr[mid] {right = mid} else if target == arr[mid] {return mid} else {left = mid // NOTE: 這里不是找直接數值,而是區間}}return -1
}
生成抽獎大轉盤
- 首先看看我們對于抽獎大轉盤所設計的 mysql 數據庫表結構
-- ----------------------------
-- DataBase
-- ----------------------------
CREATE DATABASE lottery;use lottery;-- ----------------------------
-- Table structure for inventory
-- ----------------------------
DROP TABLE IF EXISTS `inventory`;
CREATE TABLE `inventory` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT "獎品id, 自增",`created_at` DATETIME(3) NULL DEFAULT CURRENT_TIMESTAMP COMMENT "創建時間",`updated_at` DATETIME(3) NULL DEFAULT NULL COMMENT "更新時間",`deleted_at` DATETIME(3) NULL DEFAULT NULL COMMENT "刪除時間",`name` varchar(20) NOT NULL COMMENT "獎品名稱",`description` varchar(100) NOT NULL DEFAULT "" COMMENT "獎品描述",`picture` varchar(200) NOT NULL DEFAULT "0" COMMENT "獎品圖片",`price` int(11) NOT NULL DEFAULT "0" COMMENT "價值",`count` int(11) NOT NULL DEFAULT "0" COMMENT "庫存量",PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COMMENT="獎品庫存表";insert into `inventory` (id,name,picture,price,count) values (1,'謝謝參與','img/face.png',0,0);
insert into `inventory` (name,picture,price,count) values ('籃球','img/ball.jpeg',100,1000),('水杯','img/cup.jpeg',80,1000),('電腦','img/laptop.jpeg',6000,200),('平板','img/pad.jpg',4000,300),('手機','img/phone.jpeg',5000,400),('鍋','img/pot.jpeg',120,1000),('茶葉','img/tea.jpeg',90,1000),('無人機','img/uav.jpeg',400,100),('酒','img/wine.jpeg',160,500);-- ----------------------------
-- Table structure for order
-- ----------------------------
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT "訂單id, 自增",`created_at` DATETIME(3) NULL DEFAULT CURRENT_TIMESTAMP COMMENT "創建時間",`updated_at` DATETIME(3) NULL DEFAULT NULL COMMENT "更新時間",`deleted_at` DATETIME(3) NULL DEFAULT NULL COMMENT "刪除時間",`gift_id` int(11) NOT NULL COMMENT "商品id",`user_id` int(11) NOT NULL COMMENT "用戶id",`count` int(11) NOT NULL DEFAULT "1" COMMENT "購買數量",PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=189549 DEFAULT CHARSET=utf8mb4 COMMENT="訂單表";
- 上面這一段,在數據量不大的時候還是可以的,但是數據量一大,在千萬級以上大表的場景下就不行啦,會導致長時間的阻塞,而且讀出來內存也不夠
-
V2是對其優化,主要是一個分頁查詢思路,每次把一頁的數據放入一個channel中,然后用一個go協程每次從channel中讀出數據寫入redis
-
對于每個商品的 count 字段,每次被抽中都應該 count 對應的值減1,但是在高并發的情況下mysql可能扛不住這么大并發量下的頻繁寫入,考慮先記錄在redis里面,真正的減1操作是在redis里面實現的。
-
在前端頁面初始化的時候我們需要把整個大轉盤的頁面通過后端服務器返回的數據去渲染出整個大轉盤出來,所以需要一開始通過
InitInventory
函數獲得所有獎品的初始庫存,存入redis。
- 看看前端的代碼:
<body><div class="center" id="my-lucky"></div><script>var giftMap = new Map(); //維護獎品ID和轉盤里獎品index的對應關系$(document).ready(function () {$.ajax({type: "GET",url: "api/v1/gifts",success: function (data) {console.log(data)let gifts = data["data"]var prizes=new Array();$.each(gifts,function(index,gift){giftMap[gift.Id]=index;prizes[index]= { background: '#e9e8fe', fonts: [{ text: gift.Name }], imgs:[{src:gift.Picture,top:30,width:80,height:80}] };})// 直接使用luch-canvas抽獎插件 https://100px.net/usage/js.htmlconst myLucky = new LuckyCanvas.LuckyWheel('#my-lucky', {width: '600px',height: '600px',blocks: [{ padding: '10px', background: '#869cfa' }],prizes: prizes,buttons: [{ radius: '40%', background: '#617df2' },{ radius: '35%', background: '#afc8ff' },{radius: '30%', background: '#869cfa',pointer: true,fonts: [{ text: '抽獎', top: '-10px' }]},],start: function() {$.ajax({type: "GET",url: "api/v1/lucky",success: function (giftId) {if(giftId=="0"){alert("抽獎結束")}else{myLucky.play();idx=giftMap[giftId];myLucky.stop(idx);}}}).fail(function (result, result1, result2) {alert("出錯了");});},end: function(prize) { // 游戲停止時觸發alert('恭喜中獎: ' + prize.fonts[0].text)}})}}).fail(function (result, result1, result2) {$('#my-lucky').html("數據加載失敗");});});</script>
</body>
- 我們可以看到前端的代碼邏輯是先放一個空的div,然后頁面加載好之后通過js代碼發起一個請求去請求"/gifts"這個接口獲得數據渲染生成大轉盤。
- 這里有個小細節,我們后端返回給前端gifts數據的時候要記得抹掉敏感信息,也就是說我們的抽獎概率是通過商品的庫存量來決定的,我們不希望前端拿到json字符串后看到庫存量,所以我們的gifts返回給前端的時候記得把所有的庫存量都置為0。
type Inventory struct {ID uint `gorm:"column:id"`Name string `gorm:"column:name"`Description string `gorm:"column:description"`Picture string `gorm:"column:picture"`Price int `gorm:"column:price"`Count int `gorm:"column:count"`
}
- 上面這段代碼就是從redis上獲取所有獎品的庫存量,其中商品id作為key的時候會統一加一個前綴prefix
抽獎接口實現
- 當我們按下抽獎這個按鈕后,前端會用js代碼請求"/lucky"接口,由后端返回本次抽獎抽中了哪個商品id
- ids和probs兩個是一一對應的,一個存的是獎品id一個存的是獎品的庫存量
- 如果獎品已經count為0了,說明已經抽沒了,不應該再參與抽獎
- 為什么要給前端傳一個0,因為這個0有特殊的意思,前端收到0后會告訴用戶抽獎已經結束
- 有可能同時對一個庫存量為1的商品去執行減1操作,會導致庫存量為負數,這個時候我們會執行新一輪的抽獎,重新再抽一遍,如果執行指定次數后還是失敗,則會返回最后一行代碼,謝謝參與
- redis Decr 是支持原子性支持并發的