本文目录导读:

在PHP项目中实现“普通红包”(即定额或随机金额的平均分配,常指微信那种每个红包金额随机但总和固定的玩法)通常涉及两个核心算法:金额分配 和 并发安全(避免超发或漏发)。
下面为你提供一份完整的实现方案,包含核心算法、数据库设计和关键代码示例。
核心算法:二倍均值法
这是最流行的随机红包算法,它的核心思想是:*每次抢到的金额 = 随机区间[0.01 , 剩余金额 / 剩余数量 2]**
这个算法可以保证每次抢到的金额期望值相等,且不会出现最后金额过大的情况。
算法示例 (PHP):
<?php
/**
* 生成红包金额数组 (普通红包/随机红包)
* @param int $totalAmount 总金额 (单位:分,避免浮点数误差)
* @param int $num 红包个数
* @return array 返回每个红包的金额 (单位:分)
* @throws Exception
*/
function generateRedPacket($totalAmount, $num) {
if ($totalAmount <= 0 || $num <= 0) {
throw new Exception("金额或数量必须大于0");
}
if ($totalAmount < $num) {
throw new Exception("总金额不能小于人数(每人至少1分钱)");
}
$packets = [];
$remainingAmount = $totalAmount;
$remainingNum = $num;
for ($i = 0; $i < $num; $i++) {
if ($i == $num - 1) {
// 最后一个人:拿走所有剩余
$amount = $remainingAmount;
} else {
// 二倍均值算法
$max = (int)($remainingAmount / $remainingNum * 2);
// 保证至少有1分钱
$amount = rand(1, $max);
}
$packets[] = $amount;
$remainingAmount -= $amount;
$remainingNum--;
}
return $packets;
}
// 示例:发100元(10000分)给10个人
$amountList = generateRedPacket(10000, 10);
print_r($amountList);
// 输出类似: [1234, 876, 1567, ...] 总和为10000
?>
重要说明:
- 单位统一用 分 (例如100元=10000分),避免
float运算的精度丢失。 rand(1, $max)确保每人至少1分钱。
数据库设计
保证业务逻辑和资金安全,至少需要两张表:
红包总单表 (red_packet)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int/bigint | 主键,红包ID |
| total_amount | int | 总金额 (单位:分) |
| total_num | int | 总个数 |
| remaining_amount | int | 剩余金额 (分) |
| remaining_num | int | 剩余个数 |
| status | tinyint | 状态:0-未发完,1-已抢完 |
| created_at | datetime | 创建时间 |
红包领取记录表 (red_packet_log)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| red_packet_id | int | 红包ID |
| user_id | int | 用户ID |
| amount | int | 抢到的金额 (分) |
| created_at | datetime | 领取时间 |
核心业务逻辑(并发安全)
在高并发场景下(如秒杀、群红包),必须使用数据库行锁或Redis分布式锁,下面以MySQL行锁为例:
发红包逻辑 (伪代码):
<?php
// 1. 用户发起发红包请求 (假设已校验余额)
$totalAmount = 10000; // 100元
$num = 10;
$packets = generateRedPacket($totalAmount, $num); // 预生成所有红包金额
// 2. 开启事务
$db->beginTransaction();
try {
// 3. 扣用户余额 (可加锁)
$db->exec("UPDATE user_balance SET balance = balance - $totalAmount WHERE user_id = ?");
// 4. 插入红包总单
$db->insert("INSERT INTO red_packet (total_amount, total_num, remaining_amount, remaining_num, status) VALUES (?,?,?,?,?)", [$totalAmount, $num, $totalAmount, $num, 0]);
$redPacketId = $db->lastInsertId();
// 5. 预插入所有红包记录 (状态为0:未领取)
foreach ($packets as $amount) {
$db->insert("INSERT INTO red_packet_log (red_packet_id, user_id, amount, status) VALUES (?, 0, ?, 0)", [$redPacketId, $amount]);
}
$db->commit();
echo "红包已发出,ID: " . $redPacketId;
} catch (Exception $e) {
$db->rollback();
echo "发红包失败: " . $e->getMessage();
}
?>
抢红包逻辑 (高并发核心):
关键在于 原子性操作:先锁定,再更新,最后返回。
<?php
/**
* 抢红包(保证并发安全)
* @param int $redPacketId 红包ID
* @param int $userId 用户ID
* @return array [success, amount, msg]
*/
function grabRedPacket($redPacketId, $userId) {
$db = new PDO(...);
$db->beginTransaction();
try {
// 1. 检查用户是否已经抢过该红包 (可先查索引)
$row = $db->query("SELECT id FROM red_packet_log WHERE red_packet_id = ? AND user_id = ? AND status = 1", [$redPacketId, $userId])->fetch();
if ($row) {
$db->rollback();
return ['success' => false, 'msg' => '你已经抢过这个红包了'];
}
// 2. 核心:使用 FOR UPDATE 锁住红包总单行记录
$packet = $db->query("SELECT id, remaining_num, remaining_amount, status FROM red_packet WHERE id = ? FOR UPDATE", [$redPacketId])->fetch();
if (!$packet || $packet['status'] == 1) {
$db->rollback();
return ['success' => false, 'msg' => '红包已被抢完'];
}
// 3. 从预生成的记录中找到一条未被领取的记录 (可以用 limit 1 for update)
$log = $db->query("SELECT id, amount FROM red_packet_log WHERE red_packet_id = ? AND status = 0 LIMIT 1 FOR UPDATE", [$redPacketId])->fetch();
if (!$log) {
// 理论上不会发生,但安全校验
$db->rollback();
return ['success' => false, 'msg' => '红包已被抢完'];
}
$amount = $log['amount'];
// 4. 更新红包总单 (扣减剩余金额和剩余个数)
$affected = $db->exec("UPDATE red_packet SET remaining_num = remaining_num - 1, remaining_amount = remaining_amount - ? WHERE id = ? AND remaining_num > 0", [$amount, $redPacketId]);
if ($affected == 0) {
$db->rollback();
return ['success' => false, 'msg' => '红包已被抢完'];
}
// 5. 更新红包领取记录 (标记为用户领取)
$db->exec("UPDATE red_packet_log SET user_id = ?, status = 1 WHERE id = ? AND status = 0", [$userId, $log['id']]);
// 6. 如果剩余个数为0,更新状态
if ($packet['remaining_num'] - 1 == 0) {
$db->exec("UPDATE red_packet SET status = 1 WHERE id = ?", [$redPacketId]);
}
$db->commit();
return ['success' => true, 'amount' => $amount, 'msg' => '恭喜,你抢到了' . ($amount/100) . '元'];
} catch (Exception $e) {
$db->rollback();
// 记录日志
return ['success' => false, 'msg' => '服务器繁忙,请稍后重试'];
}
}
?>
优化建议:使用Redis解决更高并发
如果QPS非常高(例如春晚红包),MySQL的FOR UPDATE行锁会成为瓶颈,推荐采用 Redis + Lua 方案:
- Redis List:发红包时,将
generateRedPacket()生成的每个金额(字符串)LPUSH到一个以red_packet_id为key的List中。 - Lua脚本:使用
EVAL执行原子操作:RPOP取出一个金额 ->SADD记录已抢用户 -> 返回金额,如果List为空或用户已存在,返回nil。
- 算法:二倍均值法是首选,简单稳定。
- 安全性:所有金额操作使用分(int)而非小数(float)。
- 高并发:首选MySQL行锁(
SELECT...FOR UPDATE),极致性能要求用Redis+Lua。 - 幂等性:抢红包接口必须保证用户只能抢一次(通过唯一索引或
SADD检查)。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播