php如何 用Redis事务 解决秒杀超售 现象

云游道人 2025-04-15 524 阅读 0评论

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

发表评论

快捷回复: 表情:
aoman baiyan bishi bizui cahan ciya dabing daku deyi doge fadai fanu fendou ganga guzhang haixiu hanxiao zuohengheng zhuakuang zhouma zhemo zhayanjian zaijian yun youhengheng yiwen yinxian xu xieyanxiao xiaoku xiaojiujie xia wunai wozuimei weixiao weiqu tuosai tu touxiao tiaopi shui se saorao qiudale qinqin qiaoda piezui penxue nanguo liulei liuhan lenghan leiben kun kuaikule ku koubi kelian keai jingya jingxi jingkong jie huaixiao haqian aini OK qiang quantou shengli woshou gouyin baoquan aixin bangbangtang xiaoyanger xigua hexie pijiu lanqiu juhua hecai haobang caidao baojin chi dan kulou shuai shouqiang yangtuo youling
提交
评论列表 (有 0 条评论, 524人围观)

最近发表

热门文章

最新留言

热门推荐

标签列表