秒杀场景下的超售问题是电商系统中常见的挑战,下面我将详细介绍如何利用Redis事务特性在PHP中实现可靠的秒杀解决方案。
超售问题本质
当多个用户同时抢购同一商品时,如果没有正确的并发控制,会导致库存被多次扣减,出现实际售出数量超过库存数量的情况。
1. 使用WATCH/MULTI/EXEC事务
<?php
$redis=newRedis();
$redis->connect('127.0.0.1',6379);
// 商品ID和用户ID
$productId='product_123';
$userId='user_'.uniqid();
try{
// 监视库存key
$redis->watch($productId);
// 获取当前库存
$stock=$redis->get($productId);
// 库存不足直接返回
if($stock<=0){
$redis->unwatch();
return'秒杀失败,库存不足';
}
// 开始事务
$redis->multi();
// 减少库存
$redis->decr($productId);
// 将用户加入秒杀成功列表
$redis->lPush('seckill_success:'.$productId,$userId);
// 执行事务
$result=$redis->exec();
if($result===false){
return'秒杀失败,请重试';
}
return'秒杀成功';
}catch(Exception$e){
$redis->unwatch();
return'系统繁忙,请稍后再试';
}
2. 使用Lua脚本实现原子操作(推荐)
<?php
$redis=newRedis();
$redis->connect('127.0.0.1',6379);
$lua=<<<LUA
local productId = KEYS[1]
local userId = ARGV[1]
local stock = tonumber(redis.call('GET', productId))
if stock <= 0 then
return 0
end
redis.call('DECR', productId)
redis.call('LPUSH', 'seckill_success:'..productId, userId)
return 1
LUA;
$productId='product_123';
$userId='user_'.uniqid();
$result=$redis->eval($lua,[$productId,$userId],1);
if($result){
echo'秒杀成功';
}else{
echo'秒杀失败,库存不足';
}
完整解决方案设计
1. 系统架构
客户端 → 限流层 → Redis集群 → 数据库
2. 实现步骤
预热库存到Redis
$redis->set('product_123',100);// 初始化100个库存
秒杀接口实现
functionseckill($productId,$userId){
$redis=newRedis();
$redis->connect('127.0.0.1',6379);
// 1. 频率限制(防止用户频繁请求)
$key="user_limit:$userId:$productId";
if($redis->exists($key)){
return'操作太频繁';
}
$redis->setex($key,10,1);
// 2. 执行秒杀Lua脚本
$lua="...";// 同上文Lua脚本
$result=$redis->eval($lua,[$productId,$userId],1);
// 3. 处理结果
if($result){
// 异步处理订单
addToOrderQueue($productId,$userId);
return'秒杀成功';
}
return'秒杀失败';
}
异步订单处理
functionaddToOrderQueue($productId,$userId){
$data=[
'product_id'=>$productId,
'user_id'=>$userId,
'create_time'=>time()
];
$redis->lPush('order_queue',json_encode($data));
}
// 后台worker处理订单
functionorderWorker(){
while(true){
$data=$redis->brPop('order_queue',30);
if($data){
$order=json_decode($data[1],true);
// 写入数据库
$db->insert('orders',$order);
}
}
}
优化策略
库存分段:将库存分成多段,减少单个key的竞争
// 将100个库存分成10个key,每个10个库存
for($i=0;$i<10;$i++){
$redis->set("product_123:$i",10);
}
本地缓存:在应用层增加本地库存缓存,减少Redis访问
队列削峰:使用Redis List作为缓冲队列
库存预热:提前将库存加载到Redis
注意事项
Redis需要配置持久化,防止重启导致数据丢失 最终一致性:Redis与数据库之间可能存在短暂不一致 监控Redis性能,确保能承受高并发 考虑使用Redis集群提高可用性
性能测试建议
使用ab或JMeter工具模拟高并发场景:
ab -n10000-c1000"http://example.com/seckill?product_id=123"
通过以上方案,可以有效解决PHP秒杀系统中的超售问题,保证库存扣减的原子性和一致性。
发表评论 取消回复