## 抽獎
抽獎類似秒殺,但又不同,雖然抽獎針對的是單個活動,但是有不同的獎品,不同的庫存。
也類似群紅包,有概率的成分,但獎品不是臨時制成的而是提前設置好概率而已,并且不是每個人都能中獎。
----
### 活動初始化:
每日 0點初始化:
1. 活動數據 放到 redis
key: activity.1 data: dataJson
2. 獎品庫存 放到 redis
key: activity:prize:stock.1 data: (total - issued_num)
key: activity:prize:day_stock.1 data: (day_max_issued_num - 今日已發放數量)
> 相當于有兩個庫存
3. 獎品列表 放到 redis
key: activity:prizes.1 data: dataJson (id,status,probability,type,attr_json)
> 這些數據如果后臺有更新時,需要有相應的機制同步跟更新 redis
------
### 偽代碼
```
if 活動時間
if 用戶今日參與次數
INCR(k) < num
允許參與 搖獎
概率計算 (
1. 取得全部 有庫存 且 今日發放次數沒超限 的獎品
2. 根據概率搖獎
// 獎品列表如果沒有,那么中獎概率一直都是0
)
if 未中獎
參與記錄落盤(未中獎)
else 中獎
獎品發放
if 目標獎品是否有庫存 && 是否發放超限
if 扣減庫存:兩個庫存扣減成功
參與記錄落盤(中獎)
else
重新搖獎(注意恢復庫存扣減)
else
重新搖獎
```
> 每日發放數量有限,其實也相當于一個庫存,每日庫存
----
### 分析
其實單純用數據庫也能控制不超賣,但數據庫在并發場景下性能非常差,update都是鎖,所以主要目的是將請求擋在mysql之上,盡量不與數據庫直接操作,只在真正要落盤時才去讀寫數據庫。
秒殺方案中,如果 redis 掛了怎么辦,其實沒問題,掛了只是 程序活數據 崩了,這和 程序執行過程中的上下文 沒什么區別,我們 業務數據只要完整落盤了 就沒問題,就是安全的,加載到 redis 中的只是程序程序運行中要使用到的數據而已,可以叫 程序數據 或者是 方案數據 ,運行數據 都行,這并不是業務數據,理解這點很重要。類似mq消息也是這樣,只要業務數據安全,業務就是安全的。
redis 掛了就是服務不可用了,活動就不能進行,就需要暫停活動,暫停業務了,等人工修復完成了才能繼續活動。
`INCR`/`DECR`相當于mysql的 update 和 select 并且是加鎖了的,相當于兩條命令合成一條原子性命令了。
----
### 方案實現
上面使用incr太麻煩了,還要處理”減回來“,如果要操作多個key更麻煩,還是使用執行lua腳本的方式更好,簡單直觀。
**表定義:**
```sql
# 抽獎活動 表
DROP TABLE IF EXISTS `s_luck_activity`;
CREATE TABLE `s_luck_activity` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '活動標題',
`start_time` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '活動有效期:開始',
`end_time` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '活動有效期:結束',
`desc` varchar(500) NOT NULL DEFAULT '' COMMENT '活動備注',
`create_time` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '創建時間',
`update_time` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '更新時間',
`no_prize` smallint(1) NOT NULL DEFAULT 0 COMMENT '活動不中獎概率(1 ~ 10000)',
`rule_json` varchar(500) NOT NULL DEFAULT '' COMMENT '規則配置',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '活動狀態:0-正常,1-停用',
PRIMARY KEY (`id`)
) ENGINE=innodb AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='抽獎活動 表';
-- 抽獎活動 獎品 表
DROP TABLE IF EXISTS `s_luck_prize`;
CREATE TABLE `s_luck_prize` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`activity_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '活動id',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '獎品名稱',
`img` varchar(255) NOT NULL DEFAULT '' COMMENT '獎品圖片',
`desc` varchar(500) NOT NULL DEFAULT '' COMMENT '獎品描述',
`total` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '總數量',
`day_max_issued_num` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '每日最多發放數量',
`issued_num` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '已發放數量',
`probability` tinyint(1) NOT NULL DEFAULT 0 COMMENT '獎品中獎概率(1 ~ 100)',
`type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '獎品類型:0-普通獎品,1-紅包(中獎后會自動發放)',
`attr_json` text NULL COMMENT '屬性,普通紅包沒有這個',
`create_time` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '創建時間',
`update_time` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '更新時間',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '獎品狀態:0-正常,1-下架',
PRIMARY KEY (`id`)
) ENGINE=innodb AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='抽獎活動 獎品 表';
# 抽獎記錄 表
DROP TABLE IF EXISTS `s_luck_record`;
CREATE TABLE `s_luck_record` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`activity_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '活動id',
`user_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '用戶id',
`prize_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '獎品id',
`create_time` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '創建時間',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '活動狀態:0-未中獎,1-中獎',
PRIMARY KEY (`id`)
) ENGINE=innodb AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='抽獎記錄 表';
```
**lua腳本(解決并發問題,保證不超賣的核心):**
lua腳本執行方式保證了批量的命令被整體的串行化了,所以不會有并發問題,本質上可以理解為用了鎖,鎖的本質就是串行排隊。
為什么 lua可以做到批量命令的串行?
> 不是lua提供了批量命令的串行,而是 `redis.exex` 命令以 lua腳本為參數了而已,本質上 `redis.exex` 是一個命令,和其他命令一樣,其本身就是串行的,而現在這個命令就是以 批量命令為參數
```lua
-- lua腳本: 抽獎功能
-- last update: 2019-08-16 18:51:52
-- 獎品:單個獎品 庫存
local prizeSurplus
-- 獎品:單個獎品 每日 發放數量
local prizeIssuedDay
-- 活動:單個活動 每日每用戶 中獎次數
local activityWinningDay
local day = tostring(ARGV[1])
local userId = tonumber(ARGV[2])
local activityId = tonumber(ARGV[3])
local prizeId = tonumber(ARGV[4])
-- 活動:沒人每日中獎次數限額
local activityWinningDayQuota = tonumber(ARGV[5])
-- 獎品每日發放限額
local prizeIssuedDayQuota = tonumber(ARGV[6])
-- 獎品:單個獎品 庫存 key
local prizeSurplusKey = "luck:prizeSurplus:" .. prizeId
-- 獎品:單個獎品 每日 發放數量 key
local prizeIssuedDayKey = "luck:prizeIssuedDayKey:" .. day .. '_' .. prizeId
-- 活動:單個活動 每日每用戶 中獎次數 key
local activityWinningDayKey = "luck:activityWinningDay:" .. day .. '_' .. activityId .. '_' .. userId
------------------------------------------[1]------------------------------------------
-- 活動: 初始化 每日中獎次數
activityWinningDay = redis.call("GET", activityWinningDayKey);
if activityWinningDay == false then
activityWinningDay = 0
end
activityWinningDay = tonumber(activityWinningDay)
-- 活動: 判斷每日中獎次數是否超額
if activityWinningDayQuota > 0 then
if activityWinningDay >= activityWinningDayQuota then
-- 活動: 每日中獎次數超額
return 1
end
end
------------------------------------------[2]------------------------------------------
-- 獎品: 初始化 今日已發放數量
prizeIssuedDay = redis.call("GET", prizeIssuedDayKey);
if prizeIssuedDay == false then
prizeIssuedDay = 0
end
prizeIssuedDay = tonumber(prizeIssuedDay)
-- 獎品: 判斷今日已發放數量
if prizeIssuedDayQuota > 0 then
if prizeIssuedDay >= prizeIssuedDayQuota then
-- 獎品: 超過每日最大發放數量
return 2
end
end
------------------------------------------[3]------------------------------------------
-- 獎品: 初始化 庫存
prizeSurplus = redis.call("GET", prizeSurplusKey);
if prizeSurplus == false then
-- 獎品: 庫存還沒準備
return 3
end
prizeSurplus = tonumber(prizeSurplus)
-- 獎品: 判斷獎品是否有庫存
if prizeSurplus <= 0 then
-- 獎品: 沒有庫存了
return 4
end
------------------------------------------[4]------------------------------------------
-- 獎品: 扣減庫存
redis.call("DECR", prizeSurplusKey);
-- 獎品: 今日已發放數量 +1
redis.call("INCR", prizeIssuedDayKey);
-- 活動: 增加中獎次數 +1
redis.call("INCR", activityWinningDayKey);
return 0
```
**抽獎邏輯:**
```php
<?php
namespace app\includes\luck;
// todo: 解決更新活動,獎品數據時 如何更新正在進行的活動數據
/**
* 紅包基礎類
*/
class Luck
{
protected $db;
protected $redis;
protected $userId;
protected $activityId;
protected $activity;
protected $prizeList;
protected $day;
protected $time;
public function __construct($activityId)
{
$this->db = $GLOBALS['db'];
$this->redis = $GLOBALS['redis'];
$this->userId = $_SESSION['user_id'];
$this->activityId = $activityId;
$this->day = local_date('Y-m-d');
$this->time = time();
// 用戶參與抽獎時,實例化此類時,活動數據就從redis加載到此類中作為屬性了(私有內存數據,程序數據),與所有外部隔離
// 除了庫存外,不會再從外部讀任何數據了
// 即使想更新redis也請在 此類被初始化之前,否則這里過后是不會再讀活動數據了,就沒有機會再干預正在進行的抽獎了
// 當然如果外部修改了庫存,則情況會復雜一些,后面再詳細討論
$this->activity = $this->redis->get('luck:activity:' . $this->activityId);
$this->prizeList = $this->redis->get('luck:prizes:' . $this->activityId);
}
public function setActivityId($activityId)
{
$this->activityId = $activityId;
}
// 初始化活動,給定時任務用的
public function init()
{
$this->_loadData();
}
// 緩存腳本
public function _scriptLoad()
{
$scriptStr = file_get_contents(__DIR__ . '/luck.lua');
$tag = $this->redis->script('load', $scriptStr);
// 這個 $tag 其實時 scriptStr 哈希
$this->redis->set('luck:script_luck_lua_tag', $tag);
return $tag;
}
// 執行lua腳本
public function _evalSha($prizeId)
{
$tag = $this->redis->get('luck:script_luck_lua_tag');
// 現在需要調試lua代碼,等上線時再打開
// if (!$tag || !$this->redis->script('exists', $tag)[0]) {
$tag = $this->_scriptLoad();
// }
return $this->redis->evalSha($tag, [
$this->day,
$this->userId,
$this->activityId,
$prizeId,
$this->activity['rule_data']['u_d_winning_num'], // 活動每日最大參與次數
$this->prizeList[$prizeId]['day_max_issued_num'], // 獎品每日最大發放數量
], 0);
}
// 搖獎
public function shake()
{
$db = $this->db;
$time = $this->time;
if (true !== ($_check = $this->_checkActivityQualification())) {
return $_check;
}
$res = [];
// 搖獎
$_prizeList = $this->_getPrizeList();
// 獎品完了,提示沒中獎就行了
if (empty($_prizeList)) {
return ['error' => 6, 'message' => '很遺憾,未中獎'];
}
$_probabilityRes = $this->_probability($_prizeList);
// 是否中獎標記
$_isLuck = false;
if (!$_probabilityRes) {
// 沒中
$res = ['error' => 6, 'message' => '很遺憾,未中獎'];
} else {
$prizeId = $_probabilityRes['id'];
// 扣減庫存
// 扣減成功 中獎
$code = $this->_evalSha($prizeId);
$scriptRes = $this->_parseScriptRes($code);
if ($scriptRes['error'] === 0) {
// 這里才是真正的中獎
$_isLuck = true;
} else {
$res = $scriptRes;
}
}
if ($_isLuck) {
// 中獎
// 開啟事務,數據落盤(更新獎品的已發放數量 +1)
// 參與人很多,中獎概率很小,所以這里訪問量不大,直接落盤應該沒事,后面有性能問題再用MQ去落盤就行了
$errMsg = '';
try {
$db->startTrans();
$db->lock(true)->getRow("SELECT id FROM s_luck_prize WHERE id = {$prizeId} ");
$db->autoExecute('s_luck_prize', [
'issued_num' => ['exp', '`issued_num` + 1'],
], 'UPDATE', " id = {$prizeId} ");
$db->commit();
} catch (Exception $e) {
$db->rollback();
$errCode = $e->getCode();
$errMsg = $e->getMessage();
}
$num = count($list);
if ($errMsg == '') {
$res = ['error' => 0, 'message' => '恭喜,您中獎了', 'data' => $this->prizeList[$prizeId]];
} else {
// 更新db數據失敗,應該不會出現這種清空,redis庫存扣減成功了,db落盤失敗了,上線了不允許出現這種問題
$res = ['error' => $errCode, 'errMsg' => $errMsg];
// 嚴重錯誤:做個日志
trace($res, '>luck-error');
}
}
$insertData = [
'activity_id' => $this->activityId,
'user_id' => $this->userId,
'prize_id' => $prizeId,
'create_time' => $time,
'status' => $_isLuck ? 1 : 0,
];
if ($_isLuck) {
// 中獎時,所中獎品,隨之快照落盤,因為后面獎品可能會被編輯更新,所以必須進行快照
$insertData['prize_json'] = json_encode($this->prizeList[$prizeId], JSON_UNESCAPED_UNICODE);
}
// 記錄抽獎記錄,順序插入性能應該也不是問題,(后面有性能問題時可以移到MQ中去)
// db性能問題主要是更新
$db->autoExecute('s_luck_record', $insertData, 'INSERT'); // 寫入抽獎記錄
return $res;
}
// 解析lua腳本返回的狀態碼
public function _parseScriptRes($code)
{
$_list = [
0 => '庫存扣減成功', // 只有這種狀態才認為是本次真正中獎了
1 => '活動: 超過每日中獎次數限制',
2 => '獎品: 超過每日最大發放數量',
3 => '庫存還沒準備', // 活動沒有初始化好
// 好巧不巧,中的獎品沒有庫存了,這種情況很少,相當于是兩個人都中同一個獎品了,但獎品只剩下一個,第二個人就自認倒霉了
4 => '來遲一步,獎品派發完了',
];
if ($code === false) {
// 腳本錯誤
return ['error' => 444, 'message' => '服務忙,請稍后再試'];
}
if (isset($_list[$code])) {
return ['error' => $code === 0 ? $code : (500 + $code), 'message' => $_list[$code]];
}
}
// 裝載數據
public function _loadData()
{
$this->_loadActivityData();
$this->_loadPrizeData();
}
// 裝載活動數據
public function _loadActivityData()
{
$activity = $this->db->lock(true)->getRow("SELECT * FROM s_luck_activity WHERE id = {$this->activityId} AND state = 1 AND status = 0 ");
if ($activity) {
$activity['rule_data'] = json_decode($activity['rule_json'] ?: '{}', true);
// 活動信息緩存
$this->redis->set('luck:activity:' . $this->activityId, json_encode($activity, JSON_UNESCAPED_UNICODE));
}
}
// 裝載獎品數據
public function _loadPrizeData()
{
$_prizeList = [];
$prizeList = $this->db->lock(true)->getAll("SELECT * FROM s_luck_prize WHERE activity_id = {$this->activityId} AND status = 0 ");
foreach ($prizeList as $item) {
$item['attr_data'] = json_decode($item['attr_json'] ?: '{}', true);
$_prizeList[$item['id']] = $item;
// 每個獎品的庫存 入庫
$prizeSurplus = $item['total'] - $item['issued_num']; // 總數 - 已發放
$this->redis->set("luck:prizeSurplus:" . $item['id'], $prizeSurplus);
// 注意: 每日已發放不能初始化
}
if ($_prizeList) {
// 獎品列表緩存
$this->redis->set('luck:prizes:' . $this->activityId, json_encode($_prizeList, JSON_UNESCAPED_UNICODE));
}
}
// 清除活動緩存數據
public function clean()
{
$this->redis->del('luck:activity:' . $this->activityId);
$this->redis->del('luck:prizes:' . $this->activityId);
// 活動參與計數暫時不清除(如果活動參與計數被清空,會導致計數重置,跳過參與次數攔截)
// 獎品庫存數據暫時不清除(如果庫存被清空了,會導致提示活動未準備)
}
// 檢測活動參與資格
public function _checkActivityQualification()
{
$time = $this->time;
if (empty($this->activity) || empty($this->prizeList)) {
return ['error' => 1, 'message' => '活動暫未開始'];
}
if ($this->activity['start_time'] > $time) {
return ['error' => 2, 'message' => '活動暫未開始'];
}
if ($this->activity['end_time'] < $time) {
return ['error' => 3, 'message' => '活動已經結束'];
}
if ($this->activity['status'] != 0 || $this->activity['state'] != 1) {
return ['error' => 4, 'message' => '活動已經結束'];
}
// todo:參與資格檢測
$participationKey = 'luck:participation:' . $this->day . '_' . $this->activityId . '_' . $this->userId;
// 注意: 這樣有并發問題
// if ($this->redis->get($participationKey) >= 5) {
// return ['error' => 5, 'message' => '今天的抽獎機會用完了,請明天再來'];
// }
// 0 為不限制
$u_d_participation = $this->activity['rule_data']['u_d_participation'];
// incr = set && get 是原子性的 也就沒有并發問題了
if ($u_d_participation != 0 && ($this->redis->incr($participationKey) > $u_d_participation)) {
// 參與額度用完了, 不應該計數, 注意減回來
$this->redis->decr($participationKey);
return ['error' => 5, 'message' => '今天的抽獎機會用完了,請明天再來'];
}
return true;
}
// 取得參與搖獎的獎品列表
public function _getPrizeList()
{
// 獎品滿足條件:總庫存 > 0 && 今日已發放數量 < 每日最多發放數量
$_prizeList = [];
foreach ($this->prizeList as $item) {
$prizeSurplusKey = "luck:prizeSurplus:" . $item['id'];
$prizeIssuedDayKey = "luck:prizeIssuedDayKey:" . $this->day . '_' . $item['id'];
$prizeIssuedDay = $this->redis->get($prizeIssuedDayKey) ?: 0; // 沒開始發時未0
// 注意: 這里其實有并發問題,不過沒事,這里只是初步的攔截,真正搖獎在lua腳本中,是串行的,所以最終是沒有并發問題的
// 排除沒有庫存的
if ($this->redis->get($prizeSurplusKey) > 0 && $prizeIssuedDay < $item['day_max_issued_num']) {
$_prizeList[] = $item;
}
}
return $_prizeList;
}
// 概率計算
public function _probability($prizes)
{
$_probability = [];
foreach ($prizes as $item) {
$_probability[] = $item['probability'];
}
// 最后加上活動的不中獎概率
$_probability[] = $this->activity['no_prize'];
$index = $this->_get_rand($_probability); // 獲取中獎 index
if ($index == (count($_probability) - 1)) {
return false; // 沒中獎
}
return $prizes[$index]; // 返回中獎商品
}
/**
* 概率算法
* proArr array(10, 20, 30, 40)
*/
public function _get_rand($proArr)
{
$result = '';
$proSum = array_sum($proArr);
foreach ($proArr as $key => $proCur) {
$randNum = mt_rand(1, $proSum);
if ($randNum <= $proCur) {
$result = $key;
break;
} else {
$proSum -= $proCur;
}
}
unset($proArr);
return $result;
}
}
```
**上面的概率算法其實存在問題:**
概率算法有問題,如果幾個連著的獎品的中獎率一樣,則相同的后面的一個不會中獎,因為遍歷就決定了返回順序,用于是第一個就返回了,后面的沒有機會,可以解決這個問題,利用:array_count_values array_rand 可以做到,出現相同值的項時,在進行一次隨機
概率算法雖然有缺陷,但我們也可以繞過這個缺陷,_probability() 里面 自己將 獎品列表 shuffle 打亂一次既可,這樣就相當于提前有一次隨機了,也算是公平了(而不是從數據庫查出來的順序或者其他情況決定,這是算法之外不公平的根本原因),并且考慮到 中 不中獎的概率很大,可以把 它放在最前面,也能提高效率
----
### 參考資料
[Redis Lua實戰 - 簡書](https://www.jianshu.com/p/366d1b4f0d13)
[利用Redis和Lua的原子性實現搶紅包功能 - 簡書](https://www.jianshu.com/p/b58ed2fe6976?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation)
[42丨如何使用Redis來實現多用戶搶票問題 | 極客時間](https://time.geekbang.org/column/article/132851)
[(3) 秒殺系統優化方案之緩存、隊列、鎖設計思路 - 唐成勇 - SegmentFault 思否](https://segmentfault.com/a/1190000008888926)
[(3) Redis使用lua腳本 - 飛鴻影下 - SegmentFault 思否](https://segmentfault.com/a/1190000016753833)
[(3) php和redis設計秒殺活動 - - SegmentFault 思否](https://segmentfault.com/a/1190000019778733)
[(3) 【redis進階(1)】redis的Lua腳本控制(原子性) - 菜問專欄 - SegmentFault 思否](https://segmentfault.com/a/1190000019676878)
[Redis遞增遞減功能 - 亻也倔強小男人 - CSDN博客](https://blog.csdn.net/qq_25218095/article/details/79723531)
[(3) Redis 中 Lua 腳本的應用和實踐 - 燕南飛和你一起聊技術 - SegmentFault 思否](https://segmentfault.com/a/1190000018070172)
[事務(transaction) — Redis 命令參考](http://redisdoc.com/topic/transaction.html#redis)
[redis中如何保證原子性_Redis](https://www.sohu.com/a/324014689_120047065#cmid=249183)
> 糾錯,redis事務并不是原子性的,單個命令是串行的原子的(lua腳本除外,也不是原子性的)
[高性能分布式鎖-redisson的使用 - webwangbao - 博客園](https://www.cnblogs.com/webwangbao/p/9247318.html)
[分布式鎖設計與實現](https://mp.weixin.qq.com/s?__biz=MzIwNTI2ODY5OA==&mid=2649938438&idx=1&sn=ec19c1f1cdd161dad8d5cf8fd89637f4&chksm=8f3509b3b84280a5c6750343a094657817b72b92f70787253e1accfa636e61db1316c79d0955&scene=21#wechat_redirect)
[從Redis異步到反應式架構 - 知乎](https://zhuanlan.zhihu.com/p/77328969)
----
### 思考:活動進行中修改活動數據
假設活動正在進行中,活動、獎品數據已經裝載到redis了,此時更新活動、獎品數據會發生什么:
1. 更新活動數據(名稱,規則配置,狀態等)
2. 更新獎品數據(總數,概率,名稱,下架狀態,刪除)
3. 新增獎品
所有更新操作都是,先更新數據庫,再更新redis(假設redis都能更新成功,不成功也有補償機制保證最新數據一定會從db刷到redis)
這里問題的關鍵在于,活動進行中可能也有db落盤,也有redis更新,而我們更新活動數據也會有db更新和redis操作,這之間可能出現并問題,導致設計的并發方案出問題
中獎后,活動數據和獎品數據需要隨之快照,不能只存獎品id因為獎品后面可能會更新
另外,活動獎品數據都不會物理刪除的
用戶參與抽獎時,實例化 `Luck類` 時,活動數據就從redis加載到此類中作為屬性了(私有內存數據,程序數據),與所有外部隔離
除了庫存外,不會再從外部讀任何數據了
即使想更新redis也請在 此類被初始化之前,否則這里過后是不會再讀活動數據了
當然如果外部修改了庫存,則情況會復雜一些,后面再詳細討論
#### 相對先后性理論
>[tip] 任何事物都有個相對先后性,通常以最后一次確認為證,當然最大程度的確認能保證最大程度的準確性,但這也是有極限和代價的,這里最大程度的確認就是 `luck類` 實例化時從redis加載進來的數據,和 搖獎時 時間狀態等再判斷了,畢竟到這里不能再查數據庫了(整個抽獎就是要設計成不查數據庫),所以這是這里為 **“最大程度的確認”** 所能盡的最大努力了(也是最后的努力)。
>
> 如果redis數據裝載進來后,已經執行到抽獎概率算法那里時(假設執行了幾秒),活動剛好到期了,這里也無視,仍是以當前活動是有效的為準。這是基本的事實,任何討論都應該基于這個事實,否則再無法進行下去,就又回到了時間極限的問題上去了。
----
last update: 2019-08-16 18:51:52
- 開始
- 公益
- 更好的使用看云
- 推薦書單
- 優秀資源整理
- 技術文章寫作規范
- SublimeText - 編碼利器
- PSR-0/PSR-4命名標準
- php的多進程實驗分析
- 高級PHP
- 進程
- 信號
- 事件
- IO模型
- 同步、異步
- socket
- Swoole
- PHP擴展
- Composer
- easyswoole
- php多線程
- 守護程序
- 文件鎖
- s-socket
- aphp
- 隊列&并發
- 隊列
- 講個故事
- 如何最大效率的問題
- 訪問式的web服務(一)
- 訪問式的web服務(二)
- 請求
- 瀏覽器訪問阻塞問題
- Swoole
- 你必須理解的計算機核心概念 - 碼農翻身
- CPU阿甘 - 碼農翻身
- 異步通知,那我要怎么通知你啊?
- 實時操作系統
- 深入實時 Linux
- Redis 實現隊列
- redis與隊列
- 定時-時鐘-阻塞
- 計算機的生命
- 多進程/多線程
- 進程通信
- 拜占庭將軍問題深入探討
- JAVA CAS原理深度分析
- 隊列的思考
- 走進并發的世界
- 鎖
- 事務筆記
- 并發問題帶來的后果
- 為什么說樂觀鎖是安全的
- 內存鎖與內存事務 - 劉小兵2014
- 加鎖還是不加鎖,這是一個問題 - 碼農翻身
- 編程世界的那把鎖 - 碼農翻身
- 如何保證萬無一失
- 傳統事務與柔性事務
- 大白話搞懂什么是同步/異步/阻塞/非阻塞
- redis實現鎖
- 淺談mysql事務
- PHP異常
- php錯誤
- 文件加載
- 路由與偽靜態
- URL模式之分析
- 字符串處理
- 正則表達式
- 數組合并與+
- 文件上傳
- 常用驗證與過濾
- 記錄
- 趣圖
- foreach需要注意的問題
- Discuz!筆記
- 程序設計思維
- 抽象與具體
- 配置
- 關于如何學習的思考
- 編程思維
- 談編程
- 如何安全的修改對象
- 臨時
- 臨時筆記
- 透過問題看本質
- 程序后門
- 邊界檢查
- session
- 安全
- 王垠
- 第三方數據接口
- 驗證碼問題
- 還是少不了虛擬機
- 程序員如何談戀愛
- 程序員為什么要一直改BUG,為什么不能一次性把代碼寫好?
- 碎碎念
- 算法
- 實用代碼
- 相對私密與絕對私密
- 學習目標
- 隨記
- 編程小知識
- foo
- 落盤
- URL編碼的思考
- 字符編碼
- Elasticsearch
- TCP-IP協議
- 碎碎念2
- Grafana
- EFK、ELK
- RPC
- 依賴注入
- 科目一
- 開發筆記
- 經緯度格式轉換
- php時區問題
- 解決本地開發時調用遠程AIP跨域問題
- 后期靜態綁定
- 談tp的跳轉提示頁面
- 無限分類問題
- 生成微縮圖
- MVC名詞
- MVC架構
- 也許模塊不是唯一的答案
- 哈希算法
- 開發后臺
- 軟件設計架構
- mysql表字段設計
- 上傳表如何設計
- 二開心得
- awesomes-tables
- 安全的代碼部署
- 微信開發筆記
- 賬戶授權相關
- 小程序獲取是否關注其公眾號
- 支付相關
- 提交訂單
- 微信支付筆記
- 支付接口筆記
- 支付中心開發
- 下單與支付
- 支付流程設計
- 訂單與支付設計
- 敏感操作驗證
- 排序設計
- 代碼的運行環境
- 搜索關鍵字的顯示處理
- 接口異步更新ip信息
- 圖片處理
- 項目搭建
- 閱讀文檔的新方式
- mysql_insert_id并發問題思考
- 行鎖注意事項
- 細節注意
- 如何處理用戶的輸入
- 不可見的字符
- 抽獎
- 時間處理
- 應用開發實戰
- python 學習記錄
- Scrapy 教程
- Playwright 教程
- stealth.min.js
- Selenium 教程
- requests 教程
- pyautogui 教程
- Flask 教程
- PyInstaller 教程
- 蜘蛛
- python 文檔相似度驗證
- thinkphp5.0數據庫與模型的研究
- workerman進程管理
- workerman網絡分析
- java學習記錄
- docker
- 筆記
- kubernetes
- Kubernetes
- PaddlePaddle
- composer
- oneinstack
- 人工智能 AI
- 京東
- pc_detailpage_wareBusiness
- doc
- 電商網站設計
- iwebshop
- 商品規格分析
- 商品屬性分析
- tpshop
- 商品規格分析
- 商品屬性分析
- 電商表設計
- 設計記錄
- 優惠券
- 生成唯一訂單號
- 購物車技術
- 分類與類型
- 微信登錄與綁定
- 京東到家庫存系統架構設計
- crmeb
- 命名規范
- Nginx https配置
- 關于人工智能
- 從人的思考方式到二叉樹
- 架構
- 今日有感
- 文章保存
- 安全背后: 瀏覽器是如何校驗證書的
- 避不開的分布式事務
- devops自動化運維、部署、測試的最后一公里 —— ApiFox 云時代的接口管理工具
- 找到自己今生要做的事
- 自動化生活
- 開源與漿果
- Apifox: API 接口自動化測試指南