秒杀场景下的超售问题是电商系统中常见的挑战,下面我将详细介绍如何利用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秒杀系统中的超售问题,保证库存扣减的原子性和一致性。
                                    
                                    
                                    
                                    

发表评论 取消回复