
简介这份PHP红包随机生成资源面向社交应用、电商平台开发者及PHP初学者用于解决红包金额随机分配且总额恒定的算法问题。包内共4个文件含3个php源码与1个txt说明文档压缩包约3KB其中php文件分别承担入口调用、红包逻辑封装与生成类实现txt则提供使用说明结构精简便于快速理解。资源围绕总金额、红包个数、单包上下限四个参数展开通过输入校验、循环随机取值与余额兜底分配保证每份金额落在设定区间且总和不变并采用mt_rand配合金额乘100再除100的方式规避浮点误差。已有493人学习下载适合需要快速落地红包功能的开发者参考也可作为概率分配与边界处理思路的练习素材便于在此基础上扩展大红包保底、比例控制等规则。1. 从一次线上事故说起PHP 随机红包到底难在哪年前帮一个做社群运营的朋友排查问题他们用 PHP 写了个发红包的小活动结果用户反馈“手气最佳永远是同一个人”“后面领的人全是 0.01”。代码翻出来一看核心逻辑就一行mt_rand(1, $remain)前面几个人把大头抢走后面的人自然只剩分币。这就是 PHP 随机红包金额生成最典型的翻车现场——看起来是随机实际上分布完全失控。所谓 PHP 随机红包本质是在给定总金额和总人数下生成一组满足约束的随机金额序列。约束有三条每份金额之和等于总额、每份不低于最小值通常 0.01 元、每份不超过剩余人均的两倍防止一人独吞。它适合做营销活动、群聊互动、积分拆分这类场景也常被拿来做算法练习题。但真放到线上并发、精度、分布均匀性三个问题会同时压过来不是随手写个rand就能交差的。下面把我拆过的几种实现和踩过的坑按能落地的顺序讲一遍。2. 三种主流切分算法从拍脑袋到可上线2.1 线段切割法最直观但最容易写错线段切割法的思路是把总金额想象成一条长度为 M 的线段随机切 N-1 刀得到 N 段。这是最符合直觉的做法也是很多教程首推的方案。?php function splitRedPacketByCut(float $total, int $count, float $min 0.01): array { // 金额转成分避免浮点误差 $totalFen (int)round($total * 100); $minFen (int)round($min * 100); // 每人至少 min剩余部分才拿来随机切 $remain $totalFen - $minFen * $count; if ($remain 0) { throw new InvalidArgumentException(总金额不足以满足最小金额约束); } // 在 [0, remain] 上随机取 count-1 个切点 $cuts []; for ($i 0; $i $count - 1; $i) { $cuts[] mt_rand(0, $remain); } sort($cuts); // 相邻切点差值就是每段随机部分再加上保底 min $result []; $prev 0; foreach ($cuts as $cut) { $result[] ($cut - $prev) $minFen; $prev $cut; } $result[] ($remain - $prev) $minFen; // 转回元 return array_map(fn($fen) $fen / 100, $result); }逻辑说明先把金额放大 100 倍转成整数分这是 PHP 处理金额的通用做法能绕开0.1 0.2 ! 0.3的浮点坑。切点排序后取相邻差值保证每段非负。参数方面$min控制保底金额$count是人数$total是总额。这个算法的问题是切点均匀分布会导致金额方差很大经常出现一个超大红包加一堆小金额体验上不够“像红包”。2.2 二倍均值法微信红包的经典近似二倍均值法的核心是每次随机范围控制在[min, 剩余人均 * 2]这样既保证后面的人有钱可分又让分布更平滑。?php function splitRedPacketByAvg(float $total, int $count, float $min 0.01): array { $totalFen (int)round($total * 100); $minFen (int)round($min * 100); $result []; for ($i 0; $i $count; $i) { $left $count - $i; // 还剩几个人 $remain $totalFen - array_sum($result); // 当前最大可领剩余人均的两倍且不能超过剩余总额减去后面人的保底 $maxFen (int)floor($remain / $left * 2); $maxFen min($maxFen, $remain - $minFen * ($left - 1)); if ($i $count - 1) { $amount $remain; // 最后一人拿走剩余 } else { $amount mt_rand($minFen, max($minFen, $maxFen)); } $result[] $amount; } return array_map(fn($fen) $fen / 100, $result); }逻辑说明$maxFen是当前这个人能领的上限取“剩余人均两倍”和“剩余总额减去后面所有人保底”的较小值这一步是防止最后一个人分不到钱的关键。参数$min同样控制保底。这个算法生成的金额分布更接近真实红包大额出现的概率被压下来了但仍有小概率前面的人偏大。2.3 预分配加洗牌分布最均匀的方案如果对分布均匀性要求高比如做抽奖类活动可以用预分配加洗牌。先生成一组符合约束的金额再打乱顺序。?php function splitRedPacketByShuffle(float $total, int $count, float $min 0.01): array { $totalFen (int)round($total * 100); $minFen (int)round($min * 100); $remain $totalFen - $minFen * $count; // 用指数分布生成权重再按权重分配剩余金额 $weights []; for ($i 0; $i $count; $i) { $weights[] -log(mt_rand(1, 999999) / 1000000); } $sum array_sum($weights); $result []; $used 0; for ($i 0; $i $count; $i) { if ($i $count - 1) { $fen $totalFen - $used; } else { $fen $minFen (int)floor($remain * $weights[$i] / $sum); $fen min($fen, $totalFen - $used - $minFen * ($count - $i - 1)); } $result[] $fen; $used $fen; } shuffle($result); // 打乱顺序避免大额集中在前面 return array_map(fn($fen) $fen / 100, $result); }逻辑说明指数分布生成的权重差异大但不极端分配出来的金额更分散。shuffle是点睛之笔把顺序随机化避免“先领的人占便宜”。参数上$min依然是保底mt_rand(1, 999999)的范围越大权重分布越平滑。三种算法对比算法分布均匀性实现难度适用场景线段切割差低内部测试、对体验无要求二倍均值中中社群红包、营销活动预分配洗牌好中高抽奖、需要公平感的场景3. 金额精度与并发安全两个必须处理的工程问题3.1 浮点数陷阱与整数分方案PHP 的浮点数遵循 IEEE 754 标准0.1 0.2结果是0.30000000000000004。红包金额如果全程用 float 运算最后array_sum很可能对不上总额出现“少一分”或“多一分”的玄学问题。常见做法是全程用整数分运算只在最终输出时除以 100。上面三个函数都遵循了这个原则。另外要注意round和floor的区别round四舍五入floor向下取整。在计算上限时用floor更安全避免超出剩余金额。?php // 错误示范浮点直接累加 $sum 0; foreach ($packets as $p) { $sum $p; // 可能得到 99.99999999999 } // 正确做法整数分累加 $sumFen 0; foreach ($packets as $p) { $sumFen (int)round($p * 100); } // 最后校验 $sumFen $totalFen3.2 高并发下的超发问题红包活动最怕超发。如果多个请求同时读取剩余金额、同时计算、同时写回就会出现“总共 100 元发出 120 元”的事故。这不是算法问题是并发控制问题。常见做法是用 Redis 的原子操作或者数据库行锁。以 Redis 为例把剩余金额和剩余人数放在一个 hash 里用 Lua 脚本保证读取和扣减的原子性。?php $lua LUA local key KEYS[1] local count tonumber(ARGV[1]) local totalFen tonumber(redis.call(HGET, key, remain_fen)) local leftNum tonumber(redis.call(HGET, key, left_num)) if leftNum 0 or totalFen 0 then return -1 end -- 这里简化处理实际应嵌入二倍均值逻辑 local minFen 1 local maxFen math.floor(totalFen / leftNum * 2) local amount math.random(minFen, math.max(minFen, maxFen)) if amount totalFen - minFen * (leftNum - 1) then amount totalFen - minFen * (leftNum - 1) end redis.call(HINCRBY, key, remain_fen, -amount) redis.call(HINCRBY, key, left_num, -1) return amount LUA; $result $redis-eval($lua, [$key, $count], 1);逻辑说明Lua 脚本在 Redis 中单线程执行天然保证原子性。KEYS[1]是红包的 keyARGV[1]是人数。返回 -1 表示已抢完。参数上要注意math.random在 Lua 中的范围是闭区间和 PHP 的mt_rand一致。提示Redis 集群模式下 Lua 脚本的 key 必须落在同一个 slot红包的 remain_fen 和 left_num 放在同一个 hash 里正好满足。4. 避坑与排查五条血泪经验4.1 最后一人分到 0 元现象跑完发现最后一个人金额是 0或者负数。原因前面的人把剩余金额领完了没有给后面的人留保底。常见于线段切割法没做保底处理或者二倍均值法上限计算漏了$remain - $minFen * ($left - 1)这个约束。解决每次计算上限时强制用min($maxFen, $remain - $minFen * ($left - 1))兜底。最后一人直接拿剩余但要确保剩余不小于$minFen。4.2 金额总和差一分钱现象array_sum($packets)和总额差 0.01。原因浮点累加误差或者floor取整时把零头丢了。解决全程整数分运算最后一人用总额 - 已分配补齐。校验时用整数分比较不要用浮点相等判断。4.3 手气最佳总是同一个人现象用户反馈先领的人金额明显偏大。原因算法本身没有打乱顺序先领的人处于“剩余金额多、剩余人数多”的状态随机上限高。解决用预分配加洗牌方案或者把生成好的金额数组shuffle后再按顺序发放。注意shuffle要在生成完所有金额之后做不能边生成边打乱。4.4 并发请求导致超发现象日志显示发出总额大于预算。原因多个进程同时读到相同的剩余金额各自计算后写回覆盖了彼此的扣减。解决用 Redis 原子操作或数据库SELECT ... FOR UPDATE加行锁。不要用“先查后改”的两步操作。压测时用ab或wrk模拟 100 并发观察总额是否守恒。4.5 mt_rand 种子被固定现象每次刷新页面红包金额序列完全一样。原因某些框架或测试环境会调用mt_srand固定种子导致mt_rand输出可预测。解决生产环境不要手动mt_srand。PHP 7.1 以后mt_rand的种子是自动随机的但如果代码里遗留了mt_srand(123)这类调用一定要删掉。涉及安全的场景建议用random_int。5. 进阶技巧用 random_int 和分布校验把红包做扎实前面用的mt_rand在普通活动里够用但如果红包金额涉及真实资金或者抽奖公平性建议换成random_int。它是密码学安全的随机数生成器不可预测代价是性能略低。对于红包这种每秒几千次调用的场景性能差异可以忽略。?php // 用 random_int 替换 mt_rand $amount random_int($minFen, max($minFen, $maxFen));另一个容易被忽略的点是分布校验。写完算法不要只看“能不能跑通”要跑一批数据看分布。我一般会生成 1000 组红包统计最大值、最小值、平均值和方差确认没有极端偏斜。?php $stats []; for ($i 0; $i 1000; $i) { $packets splitRedPacketByAvg(100, 10); $stats[] max($packets); } printf(最大值平均: %.2f, 最大值最大: %.2f, 最大值最小: %.2f\n, array_sum($stats) / count($stats), max($stats), min($stats));如果最大值平均接近 20人均 10 的两倍说明分布偏斜如果接近 15 左右说明比较健康。这个校验方法比肉眼观察靠谱得多。还有一个实用技巧是给红包加“手气最佳”标记。不要在生成金额时决定而是在发放完成后用max找出最大值对应的索引。注意如果有并列最大值取第一个即可避免争议。?php $bestIndex array_search(max($packets), $packets);最后说一个我自己的习惯每次上线红包功能前强制跑一遍“总额守恒 最小值校验 并发压测”三件套。总额守恒用整数分比对最小值校验遍历每个金额并发压测用 100 线程打 1000 次请求看总额是否漂移。这三步走完基本不会再出线上事故。从那以后我每次写金额相关的逻辑都先把浮点数换成整数分这个习惯帮我省了至少三次半夜爬起来修 bug 的机会。希望帮到你。本文还有配套的精品资源点击获取