秒杀场景下的超售问题是电商系统中常见的挑战,下面我将详细介绍如何利用Redis事务特性在PHP中实现可靠的秒杀解决方案。

超售问题本质

当多个用户同时抢购同一商品时,如果没有正确的并发控制,会导致库存被多次扣减,出现实际售出数量超过库存数量的情况。

Redis事务解决方案

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

注意事项

  1. Redis需要配置持久化,防止重启导致数据丢失
  2. 最终一致性:Redis与数据库之间可能存在短暂不一致
  3. 监控Redis性能,确保能承受高并发
  4. 考虑使用Redis集群提高可用性

性能测试建议

使用ab或JMeter工具模拟高并发场景:

ab -n10000-c1000"http://example.com/seckill?product_id=123"

通过以上方案,可以有效解决PHP秒杀系统中的超售问题,保证库存扣减的原子性和一致性。