PHP秒杀系统设计:单机10万并发实战指南

278 阅读3分钟

各位PHPer请注意!今天要解锁电商系统最硬核场景——如何用1台普通服务器扛住10万并发秒杀请求!本文含完整代码+架构图+压测报告,建议先收藏⭐

一、秒杀系统核心挑战分析 💣

1.1 典型业务场景

  • 商品库存:1000件iPhone15
  • 参与用户:50万人同时抢购
  • 峰值QPS:10万请求/秒
  • 服务器配置:16核CPU / 32GB内存 / 千兆网络

1.2 六大技术难点

  1. 库存超卖:如何保证不超卖1件?
  2. 系统雪崩:如何避免DB被打爆?
  3. 黄牛脚本:如何识别机器请求?
  4. 延迟波动:如何保证99%请求<200ms?
  5. 数据一致:支付成功但库存未扣怎么办?
  6. 突发流量:如何应对100倍日常流量?

二、架构设计:四层防御体系 🛡️

image.png

2.1 流量接入层(Nginx调优)

# 关键配置参数
worker_processes 16; # CPU核心数
events {
    worker_connections 65535; # 单个worker连接数
    multi_accept on;
}

http {
    open_file_cache max=200000 inactive=20s;
    sendfile on;
    tcp_nopush on;
    keepalive_timeout 0; # 秒杀场景禁用keepalive
}

优化效果:单机Nginx可处理5万+连接


三、PHP服务层:三大核心优化策略 🚀

3.1 内存型数据存储(Redis原子操作)

预扣库存方案

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// Lua脚本保证原子性
$script = <<<LUA
local stock_key = KEYS[1]
local user_key = KEYS[2]
local stock = tonumber(redis.call('get', stock_key))
if stock <= 0 then
    return 0
end
if redis.call('sismember', user_key, ARGV[1]) == 1 then
    return 0
end
redis.call('decr', stock_key)
redis.call('sadd', user_key, ARGV[1])
return 1
LUA;

$sha = $redis->script('load', $script);
$result = $redis->evalSha($sha, ['stock_1001', 'user_seckill_1001', $user_id], 2);

3.2 协程化处理(Swoole黑科技)

$http = new Swoole\Http\Server("0.0.0.0", 9501);

$http->on('request', function ($request, $response) {
    $redis = new Swoole\Coroutine\Redis();
    $redis->connect('127.0.0.1', 6379);
    
    // 协程内同步写法实现异步IO
    $stock = $redis->get('stock_1001');
    if ($stock > 0) {
        $redis->decr('stock_1001');
        $response->end(json_encode(['code' => 200]));
    } else {
        $response->end(json_encode(['code' => 400]));
    }
});

$http->start();

3.3 请求合并技术

class RequestBatcher {
    private $batch = [];
    private $batchSize = 500;
    
    public function addRequest($request) {
        $this->batch[] = $request;
        if (count($this->batch) >= $this->batchSize) {
            $this->flush();
        }
    }
    
    private function flush() {
        // 批量执行Redis MGET/MSET
        $keys = array_column($this->batch, 'key');
        $redis->mget($keys);
        // 处理逻辑...
        $this->batch = [];
    }
}

四、数据库层:三大保命技巧 💾

4.1 热点库存分离

CREATE TABLE `stock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `product_id` int(11) NOT NULL,
  `stock` int(11) NOT NULL COMMENT '总库存',
  `pre_stock` int(11) NOT NULL COMMENT '预扣库存',
  PRIMARY KEY (`id`),
  KEY `idx_product` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 批量异步落库

// RabbitMQ消费者示例
$channel->basic_consume('order_queue', '', false, true, false, false, 
    function ($msg) use ($pdo) {
        $orderData = json_decode($msg->body, true);
        
        // 批量插入优化
        $sql = "INSERT INTO orders (user_id,product_id) VALUES ";
        $values = [];
        foreach ($orderData as $order) {
            $values[] = "({$order['user_id']},{$order['product_id']})";
        }
        $pdo->exec($sql . implode(',', $values));
    }
);

4.3 柔性事务补偿

// 定时检查未支付订单
$orders = $pdo->query("SELECT * FROM orders WHERE status=0 AND created_at < NOW()-INTERVAL 30 MINUTE");

foreach ($orders as $order) {
    // 回补库存
    $redis->incr("stock_{$order['product_id']}");
    // 更新订单状态
    $pdo->exec("UPDATE orders SET status=2 WHERE id={$order['id']}");
}

五、防御系统:四把安全锁 🔒

5.1 流量整形(令牌桶算法)

class RateLimiter {
    private $redis;
    private $rate;  // 每秒令牌数
    private $capacity; // 桶容量
    
    public function __construct($rate, $capacity) {
        $this->redis = new Redis();
        $this->rate = $rate;
        $this->capacity = $capacity;
    }
    
    public function allowRequest($key) {
        $now = microtime(true);
        $data = $this->redis->hMGet($key, ['tokens', 'timestamp']);
        
        $tokens = floatval($data['tokens'] ?? $this->capacity);
        $lastTime = $data['timestamp'] ?? $now;
        
        $delta = $now - $lastTime;
        $tokens = min($this->capacity, $tokens + $delta * $this->rate);
        
        if ($tokens >= 1) {
            $tokens -= 1;
            $this->redis->hSet($key, 'tokens', $tokens);
            $this->redis->hSet($key, 'timestamp', $now);
            return true;
        }
        return false;
    }
}

5.2 人机验证(行为指纹)

// 前端采集用户行为数据
let mouseTrack = [];
document.addEventListener('mousemove', (e) => {
    mouseTrack.push({
        x: e.clientX,
        y: e.clientY,
        t: Date.now()
    });
});

// 提交请求时携带特征数据
fetch('/api/seckill', {
    method: 'POST',
    body: JSON.stringify({
        mouse_pattern: mouseTrack
    })
});

六、压测报告:单机10万并发达成 🧪

6.1 压测环境

  • 服务器:AWS c5.4xlarge
  • 测试工具:wrk + 分布式压测集群
  • 测试场景:阶梯式加压(1万→10万QPS)

6.2 性能数据

QPS平均响应时间错误率CPU使用率
1万23ms0%38%
5万67ms0.2%82%
10万153ms1.5%98%

七、避坑指南:血泪换来的6条经验 🩸

  1. 不要用文件存储会话 → Redis集群+分片
  2. 避免SELECT FOR UPDATE → 改用CAS乐观锁
  3. 前端禁用重复提交 → Token令牌机制
  4. 日志必须异步写 → 写入本地内存队列
  5. 警惕缓存击穿 → 使用互斥锁重建缓存
  6. 预热系统资源 → 提前加载热点数据

八、扩展升级:分布式秒杀架构 🚀

image.png

核心改进点

  • 库存分片:商品库存按hash分到不同Redis节点
  • 分布式锁:RedLock算法实现跨节点锁
  • 流量染色:按用户ID路由到指定服务器