PHP + Redis 实现抽奖算法(ThinkPHP5)
在使用 ThinkPHP5 和 Redis 实现抽奖算法时,我们可以结合 Redis 的高性能数据结构来管理奖品的库存、用户的抽奖机会,并确保并发情况下的公平性和一致性。下面是一个实现抽奖算法的示例,涵盖了以下步骤:
- 设置奖品池:将奖品和奖品数量存储在 Redis 中。
- 用户抽奖:根据抽奖概率,从奖品池中抽取奖品。
- 中奖处理:扣减奖品库存并记录中奖信息。
- 避免重复中奖:确保用户在抽奖期间不会重复抽中相同的奖品。
1. 初始化奖品池及其中奖概率
首先,在初始化奖品池时,为每个奖品设置库存和中奖概率。这里的中奖概率总和可以小于或大于100%。
在 Redis 中创建一个哈希表来存储奖品及其库存。可以在 ThinkPHP 的控制器中初始化奖品池。
use think\facade\Cache;
class LotteryController {
// 初始化奖品池
public function initPrizePool() {
$prizes = [
'prize1' => ['quantity' => 10, 'probability' => 50], // 奖品1,库存10个,中奖概率50%
'prize2' => ['quantity' => 5, 'probability' => 30], // 奖品2,库存5个,中奖概率30%
'prize3' => ['quantity' => 1, 'probability' => 15], // 奖品3,库存1个,中奖概率15%
'prize4' => ['quantity' => 50, 'probability' => 5] // 奖品4,库存50个,中奖概率5%
];
foreach ($prizes as $prize => $data) {
Cache::store('redis')->hSet('prize_pool', $prize, $data['quantity']);
Cache::store('redis')->hSet('prize_probabilities', $prize, $data['probability']);
}
return json(['status' => 'Prize pool initialized']);
}
}
2. 高并发下用户抽奖逻辑
编写抽奖逻辑,随机抽取奖品,加入 Redis 锁的获取和释放逻辑:
use think\facade\Cache;
class LotteryController {
// 用户抽奖
public function draw() {
$userId = session('user_id'); // 获取当前用户ID
$lockKey = "lock:draw"; // 锁的键
$lockTimeout = 5; // 锁定时间(秒)
// 尝试获取锁
if (Cache::store('redis')->set($lockKey, $userId, ['nx', 'ex' => $lockTimeout])) {
try {
// 检查用户是否已经中奖,防止重复抽奖
if (Cache::store('redis')->hExists('user_prizes', $userId)) {
return json(['status' => 'fail', 'message' => 'You have already won a prize!']);
}
// 获取奖品池
$prizePool = Cache::store('redis')->hGetAll('prize_pool');
if (empty($prizePool)) {
return json(['status' => 'fail', 'message' => 'No prizes available']);
}
// 获取奖品的概率列表
$prizeProbabilities = Cache::store('redis')->hGetAll('prize_probabilities');
// 计算总概率
$totalProbability = array_sum($prizeProbabilities);
// 生成一个随机数
$rand = mt_rand(1, $totalProbability);
// 根据随机数选择奖品
$cumulativeProbability = 0;
$selectedPrize = null;
foreach ($prizeProbabilities as $prize => $probability) {
$cumulativeProbability += $probability;
if ($rand <= $cumulativeProbability) {
$selectedPrize = $prize;
break;
}
}
// 确认有选中奖品
if (!$selectedPrize) {
return json(['status' => 'fail', 'message' => 'Sorry, better luck next time']);
}
// 检查奖品库存并更新
$remaining = Cache::store('redis')->hIncrBy('prize_pool', $selectedPrize, -1);
if ($remaining < 0) {
// 如果库存不足,恢复库存并返回失败信息
Cache::store('redis')->hIncrBy('prize_pool', $selectedPrize, 1);
return json(['status' => 'fail', 'message' => 'Sorry, all prizes are gone']);
}
// 记录用户中奖信息
Cache::store('redis')->hSet('user_prizes', $userId, $selectedPrize);
return json(['status' => 'success', 'message' => 'Congratulations, you won!', 'prize' => $selectedPrize]);
} finally {
// 释放锁
Cache::store('redis')->del($lockKey);
}
} else {
return json(['status' => 'fail', 'message' => 'System busy, please try again later']);
}
}
}
3. 解释并发处理逻辑
-
分布式锁:在抽奖逻辑的开始,我们使用 Redis 的
set
命令来获取一个分布式锁,确保当前操作的唯一性。锁的nx
参数表示“仅在键不存在时设置键”,ex
参数设置锁的超时时间(这里是5秒)。 -
获取锁成功:如果当前用户成功获取了锁,则继续执行抽奖逻辑。在抽奖结束后,无论成功与否,都需要释放锁。
-
获取锁失败:如果获取锁失败,说明有另一个用户正在执行抽奖操作,当前用户会收到系统繁忙的提示。
-
库存处理:在确定用户抽中某个奖品后,使用 Redis 的
hIncrBy
命令减少该奖品的库存。如果库存不足,会回滚该操作并返回失败信息。
4. 调用抽奖
当用户调用 draw()
方法时,系统会根据设定的百分比概率和库存情况随机选择一个奖品。
5. 可扩展性
-
抽奖概率控制:在实际场景中,你可以通过调整
getRandomPrize()
函数中的逻辑来实现,例如通过给奖品分配比例、权重等。 -
多用户并发:Redis 的原子操作能够确保在高并发情况下奖品库存的正确性,因此在并发抽奖场景中也可以安全使用。
-
过期管理:可以为奖品池设置过期时间,避免长期占用内存。用户中奖信息也可以设置过期时间,防止数据堆积。
总结
通过引入 Redis 分布式锁,确保了在高并发情况下抽奖的原子性和安全性。这样可以有效防止多个用户同时抽中同一个奖品导致库存不足的问题,并且提高了系统的稳定性。