1. 项目概述:PHP抢红包系统的核心逻辑
去年春节前接手一个电商平台的节日营销项目,需要开发一套高并发的抢红包系统。当时用原生PHP+Redis实现了一套方案,单机轻松扛住了每秒3000+的请求量。今天就把这套经过实战检验的方案拆解给大家,重点分享几个关键设计点:
- 红包金额分配的算法选择(为什么不用简单随机数)
- 并发控制的三种实现方式对比
- Redis原子操作的实际应用技巧
- 防刷策略的七层过滤机制
这个方案特别适合中小型项目快速落地,所有代码都可以直接移植到现有系统中。下面我会用最贴近生产环境的代码示例,手把手演示如何从零搭建这套系统。
2. 红包系统的核心设计
2.1 金额分配算法
常见的随机分配算法有:
- 普通随机法(会导致最后一个人可能拿到极大/极小值)
- 二倍均值法(金额波动更合理)
- 线段切割法(最适合微信这类大平台)
我们选择改进版的二倍均值法,核心逻辑:
php复制function divideRedPacket($totalAmount, $totalPeople) {
$result = [];
$restAmount = $totalAmount;
$restPeople = $totalPeople;
for ($i = 0; $i < $totalPeople - 1; $i++) {
// 保证最少能拿到0.01元
$max = $restAmount / $restPeople * 2;
$money = mt_rand(1, $max * 100) / 100;
$restAmount -= $money;
$restPeople--;
$result[] = $money;
}
$result[] = round($restAmount, 2); // 最后一人拿剩余金额
shuffle($result); // 打乱顺序更公平
return $result;
}
关键点:使用mt_rand替代rand函数,随机性更好且性能更高3倍
2.2 并发控制方案对比
测试环境:4核8G服务器,PHP7.4 + Redis6.0
| 方案 | QPS | 实现复杂度 | 适用场景 |
|---|---|---|---|
| 文件锁 | 800 | 低 | 小型活动 |
| MySQL乐观锁 | 1500 | 中 | 已有MySQL的项目 |
| Redis+Lua | 3500+ | 高 | 高并发场景 |
我们最终选用Redis+Lua脚本方案,核心优势:
- 原子性执行无需额外锁机制
- 单线程模型避免竞争条件
- 执行速度是MySQL方案的20倍
3. 完整实现流程
3.1 数据库设计
红包主表结构:
sql复制CREATE TABLE `redpacket` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '创建用户',
`amount` decimal(10,2) NOT NULL COMMENT '总金额',
`size` int(11) NOT NULL COMMENT '红包个数',
`remain_amount` decimal(10,2) NOT NULL COMMENT '剩余金额',
`remain_size` int(11) NOT NULL COMMENT '剩余个数',
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
领取记录表:
sql复制CREATE TABLE `redpacket_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`redpacket_id` bigint(20) NOT NULL,
`user_id` int(11) NOT NULL,
`amount` decimal(10,2) NOT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_redpacket_user` (`redpacket_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 Redis抢红包Lua脚本
lua复制-- KEYS[1]: redpacket:{红包ID}
-- KEYS[2]: redpacket_record:{红包ID}
-- ARGV[1]: 用户ID
local redpacket = redis.call('HGETALL', KEYS[1])
if #redpacket == 0 then
return 0
end
local remain_amount = tonumber(redpacket[4])
local remain_size = tonumber(redpacket[6])
if remain_size <= 0 then
return 0
end
-- 检查是否已抢过
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
return -1
end
-- 计算分配金额
local amount = 0
if remain_size == 1 then
amount = remain_amount
else
local max = remain_amount / remain_size * 2
amount = math.random(1, max * 100) / 100
end
-- 更新红包信息
redis.call('HMSET', KEYS[1],
'remain_amount', remain_amount - amount,
'remain_size', remain_size - 1)
-- 记录领取信息
redis.call('SADD', KEYS[2], ARGV[1])
return amount
PHP调用示例:
php复制$script = <<<LUA
-- 上面完整的Lua脚本内容
LUA;
$sha = $redis->script('load', $script);
$result = $redis->evalSha($sha, [
'redpacket:'.$redpacketId,
'redpacket_record:'.$redpacketId,
$userId
], 2);
4. 防刷策略实战
4.1 七层防护体系
-
IP频率限制:使用Redis计数器
php复制$key = 'redpacket_ip:'.$_SERVER['REMOTE_ADDR']; if ($redis->incr($key) > 10) { throw new Exception('操作太频繁'); } $redis->expire($key, 60); -
设备指纹校验:前端生成唯一指纹
javascript复制// 使用canvas指纹+WebGL指纹+字体指纹 -
行为验证码:滑动拼图/点选验证
-
用户等级限制:新注册用户限额
-
业务规则校验:
- 同一活动每人限领1次
- 领取时间间隔控制
-
异步风控检查:
- 可疑操作二次确认
- 人工审核通道
-
数据监控报警:
- 实时监控领取曲线
- 异常模式自动阻断
4.2 性能优化技巧
-
连接池配置:
php复制$redis = new RedisCluster(null, [ 'redis-node1:6379', 'redis-node2:6379' ], 1.5, 1.5, false, 'password'); -
批量写入优化:
php复制// 使用pipeline批量写入领取记录 $redis->pipeline(function($pipe) use ($records) { foreach ($records as $record) { $pipe->hSet('redpacket_record', $record['id'], json_encode($record)); } }); -
热点数据分离:
- 红包元数据与领取记录分库
- 近期活跃数据单独缓存
5. 踩坑实录
5.1 金额精度问题
初期直接使用float类型导致金额计算出现误差:
php复制// 错误做法
$amount = 0.1 + 0.2; // 得到0.30000000000000004
// 正确做法
bcadd('0.1', '0.2', 2); // 得到0.30
必须使用BC Math函数处理金额计算
5.2 缓存雪崩预防
某次活动开始瞬间Redis连接数爆满,解决方案:
- 红包数据提前预热
- 采用二级缓存策略
- 第一层:本地缓存(APCu)
- 第二层:Redis集群
- 增加随机过期时间
php复制$redis->setex($key, mt_rand(300, 500), $value);
5.3 事务一致性
MySQL与Redis的数据同步方案:
- 先更新Redis
- 通过消息队列异步落库
- 定时对账补偿
php复制// 使用RabbitMQ确保最终一致
$channel->basic_publish(
new AMQPMessage(json_encode([
'redpacket_id' => $redpacketId,
'user_id' => $userId,
'amount' => $amount
])),
'',
'redpacket_record_queue'
);
这套系统经过三个春节的考验,峰值QPS达到5800,没有出现超发或少发的情况。核心经验就是:简单的事情重复做,重复的事情精细化做。特别是高并发场景下,每个0.1ms的优化累积起来就是巨大的性能提升。
