抽奖
最后更新于:2022-04-02 04:33:02
## 抽奖
抽奖类似秒杀,但又不同,虽然抽奖针对的是单个活动,但是有不同的奖品,不同的库存。
也类似群红包,有概率的成分,但奖品不是临时制成的而是提前设置好概率而已,并且不是每个人都能中奖。
----
### 活动初始化:
每日 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
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
';