嗯,说到PHP抽奖程序,很多新手可能觉得不就是个rand()或者array_rand()的事嘛?但说实话,这玩意儿背后的坑多得能摔死人。我自己做了五年Web开发,电商抽奖模块也折腾过不少次,今天就跟大伙聊聊这里头的门道。记得有次半夜调试一个高并发抽奖的bug,差点把测试环境搞崩,最后发现是随机种子没处理好——这种坑咱就别再踩了。

先唠唠抽奖的几种基本类型吧。其实常见的主要是三类:概率抽奖、固定奖品池和权重分配。概率抽奖最简单,就像抓阄,每个奖独立算概率,适合那种“谢谢参与”和实物奖混着的场景。固定奖品池呢,好比一箱子奖品抽完为止,一般用于限量活动。而权重分配是我个人比较偏爱的,因为它可控性强,能精细调整每个奖的中奖几率。话说回来,选哪种逻辑得看业务需求——别像我之前那样,明明该用权重却偷懒用了array_rand(),结果用户投诉中奖率太低,差点被运营同事追杀。
接下来咱直接上代码。先看个最简单的概率抽奖示例:
function simpleLottery($prizes) {
$rand = mt_rand(1, 100); // 生成1-100的随机数
$rangeStart = 0;
foreach ($prizes as $prize => $prob) {
if ($rand > $rangeStart && $rand <= $rangeStart + $prob) {
return $prize; // 命中概率区间则返回奖品
}
$rangeStart += $prob;
}
return '谢谢参与'; // 兜底返回
}
// 奖品配置:奖品名 => 概率(百分比)
$prizeConfig = [
'iPhone' => 5, // 5%几率
'红包10元' => 15,
'优惠券' => 30,
'谢谢参与' => 50 // 注意总概率需为100%
];
$result = simpleLottery($prizeConfig);
这段代码的思路是给每个奖品划个概率区间,然后看随机数落在哪个区间。嗯...这里要注意两点:一是概率总和得是100%,二是mt_rand()的均匀性其实不如random_int(),不过大部分场景够用了。我个人其实不太爱用mt_rand(),倒不是它不好,而是以前在权重计算上栽过跟头——等下会提到。
但实际项目中更常用的是权重算法,因为运营同学总想动态调整概率。来看个带权重的版本:
function weightedLottery($prizes) {
$totalWeight = array_sum($prizes);
$rand = mt_rand(1, $totalWeight);
$current = 0;
foreach ($prizes as $prize => $weight) {
$current += $weight;
if ($rand <= $current) {
return $prize;
}
}
}
// 奖品池:奖品名 => 权重(无需总和为100)
$prizePool = [
'iPhone' => 50, // 权重越高中奖几率越大
'红包10元' => 150,
'优惠券' => 300,
'谢谢参与' => 500
];
用权重的好处是改起来灵活,比如想降低iPhone中奖率,直接把50改成30就行,不用重新计算百分比。不过这里有个隐藏坑点:权重必须是整数!之前我偷懒用了浮点数,结果因为精度问题导致某些奖永远抽不中...
说到坑,不得不提我亲身经历的一次事故。某次电商大促做抽奖,需求是100%中奖但奖品数量有限。我用了固定奖品池模式,代码长这样:
function fixedPoolLottery() {
$pool = [
'iPhone', 'iPhone', 'iPhone', // 3台iPhone
'红包', '红包', '红包', '红包', '红包', // 5个红包
// ... 其他奖品
];
if (empty($pool)) {
return '奖品已发完';
}
$index = array_rand($pool);
$prize = $pool[$index];
unset($pool[$index]); // 抽中的奖品从池中移除
return $prize;
}
看起来没问题对吧?结果活动上线十分钟iPhone就没了——因为并发请求下多个用户同时抽奖,都判断池子非空后同时执行了array_rand()。最后超发了三台iPhone!这事儿教训就是:抽奖逻辑不仅是技术问题,还得考虑业务规则。后来我们改用了Redis原子操作保证并发安全:
$redis = new Redis();
// 用LPOP原子性地取出奖品
$prize = $redis->lPop('prize_pool');
if (!$prize) {
$prize = '奖品已发完';
}
当然这只是解决方案之一,数据库事务或者队列也行,看具体架构了。
另外提一嘴,抽奖公平性其实涉及很多细节。比如避免重复中奖,我们通常会在用户中奖后立即标记状态,而不是等前端请求领取。还有随机数的质量——虽然mt_rand()够用,但对安全要求高的场景建议用random_int()。说到这个,我记得有次用户投诉说某个时间段从没人中大奖,一查发现是随机种子被固定了...所以现在我都习惯在入口处调用srand()。
最后啰嗦一句:抽奖逻辑就像炒菜,火候(随机性)差了要么糊要么生。技术实现上要严谨,但也得兼顾用户体验。比如当中奖率极低时,可以考虑增加保底机制;又比如权重计算最好后台可配置,免得每次改概率都要重新发布代码。
总之吧,抽奖代码写得好不好,直接关系到用户骂不骂你——这话虽然糙,但理不糙。时间关系,异步处理和降级方案下次再聊,各位要是遇到具体问题欢迎交流。


评论