PHPAPI计数器怎么设计

wen PHP项目 14

本文目录导读:

PHPAPI计数器怎么设计

  1. 方案一:简单文件/数据库计数器(适合低并发、内部小工具)
  2. 方案二:基于 Redis 的高速计数器(适合高并发、实时统计)
  3. 方案三:高并发 + 持久化(Redis + 异步入库)
  4. 方案四:基于文件记录(最简单,适合极低并发)
  5. 总结 & 选择建议

设计一个PHP API计数器,主要需要考虑存储方式、性能、以及对不同维度的统计需求,以下是几种常见的设计方案,从简单到复杂,你可以根据项目实际情况选择。

核心通常是:谁调用了 + 哪个端点 + 调用时间 + 调用结果


简单文件/数据库计数器(适合低并发、内部小工具)

如果API流量不大(日均几百到几千次),直接用数据库或文件记录是最简单的。

数据库表设计 (MySQL/PostgreSQL)

-- 1. 总调用次数表
CREATE TABLE api_call_counter (
    id INT AUTO_INCREMENT PRIMARY KEY,
    endpoint VARCHAR(255) NOT NULL,        -- API路径,如 /api/user/login
    request_method VARCHAR(10) NOT NULL,   -- GET, POST, PUT, DELETE
    call_date DATE NOT NULL,              -- 统计日期
    call_count INT UNSIGNED DEFAULT 1,    -- 当日调用次数
    success_count INT UNSIGNED DEFAULT 0, -- 成功次数
    failure_count INT UNSIGNED DEFAULT 0, -- 失败次数
    total_duration_ms INT UNSIGNED DEFAULT 0, -- 总耗时(毫秒)
    -- 为了快速查询某日的某个端点,建立唯一索引
    UNIQUE KEY uk_endpoint_date (endpoint, request_method, call_date)
);
-- 2. 详细日志表 (可选,用于分析调用来源、IP、用户)
CREATE TABLE api_call_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    endpoint VARCHAR(255) NOT NULL,
    request_method VARCHAR(10) NOT NULL,
    caller_ip VARCHAR(45),
    user_id INT UNSIGNED DEFAULT NULL,       -- 如果是登录用户
    status_code INT NOT NULL,                -- HTTP状态码 200, 500等
    duration_ms INT UNSIGNED DEFAULT 0,      -- 本次请求耗时
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    -- 按天分表或定期清理,避免日志表无限增长
    INDEX idx_created_at (created_at),
    INDEX idx_endpoint (endpoint)
);

PHP 实现代码 (中间件模式)

<?php
class ApiCounterMiddleware
{
    private $pdo;
    private $logTable;
    public function __construct(PDO $pdo, string $logTable = 'api_call_log')
    {
        $this->pdo = $pdo;
        $this->logTable = $logTable;
    }
    /**
     * 在API请求处理结束后调用
     *
     * @param string $endpoint    当前API路径
     * @param string $method      请求方法
     * @param int    $statusCode  HTTP状态码
     * @param int    $durationMs  耗时(毫秒)
     * @param array  $extra       额外信息,如user_id, ip
     */
    public function record(string $endpoint, string $method, int $statusCode, int $durationMs, array $extra = []): void
    {
        $date = date('Y-m-d');
        $success = ($statusCode >= 200 && $statusCode < 300) ? 1 : 0;
        $failure = $success ? 0 : 1;
        // 1. 更新每日汇总表 (使用 ON DUPLICATE KEY)
        $sql = "INSERT INTO api_call_counter (endpoint, request_method, call_date, call_count, success_count, failure_count, total_duration_ms)
                VALUES (:endpoint, :method, :date, 1, :success, :failure, :duration)
                ON DUPLICATE KEY UPDATE
                    call_count = call_count + 1,
                    success_count = success_count + VALUES(success_count),
                    failure_count = failure_count + VALUES(failure_count),
                    total_duration_ms = total_duration_ms + VALUES(total_duration_ms)";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            ':endpoint' => $endpoint,
            ':method'   => $method,
            ':date'     => $date,
            ':success'  => $success,
            ':failure'  => $failure,
            ':duration' => $durationMs,
        ]);
        // 2. 记录详细日志 (异步/批量会更好,但简单实现直接插入)
        $logSql = "INSERT INTO {$this->logTable} (endpoint, request_method, caller_ip, user_id, status_code, duration_ms)
                   VALUES (:endpoint, :method, :ip, :userId, :statusCode, :duration)";
        $logStmt = $this->pdo->prepare($logSql);
        $logStmt->execute([
            ':endpoint'   => $endpoint,
            ':method'     => $method,
            ':ip'         => $extra['ip'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            ':userId'     => $extra['user_id'] ?? null,
            ':statusCode' => $statusCode,
            ':duration'   => $durationMs,
        ]);
    }
}

优点:结构清晰,数据持久化,能直接做各种SQL查询统计。
缺点:高并发下数据库写压力大,日志表增长快。


基于 Redis 的高速计数器(适合高并发、实时统计)

对于高并发场景(每秒几百次以上),数据库插入会成为瓶颈,利用 Redis 的内存操作和原子特性,可以扛住很高的 QPS。

Redis 数据结构设计

利用 Redis 的 Sorted Set(排序集合) 和 Hash(哈希)。

  • Hash: 存储每日或每小时的各 API 调用次数。
  • Sorted Set: 按时间排序的详细日志(便于最近查看,同时可以利用过期时间自动清理)。
# 1. 当日总调用次数(Hash)
# Key: api:counter:{date}
# Field: {method}:{endpoint}
# Value: 调用次数
HSET api:counter:20251028 "GET:/api/users" 100
HSET api:counter:20251028 "POST:/api/orders" 50
# 2. 最近N条调用日志(Sorted Set,用于监控和调试)
# Key: api:log:recent
# Member: 序列化的JSON或ID
# Score: 时间戳(微秒)
ZADD api:log:recent 1698472800000 "{'endpoint':'/api/users','method':'GET','status':200}"

PHP 实现代码 (使用 Predis/PhpRedis)

<?php
class RedisApiCounter
{
    private $redis;
    private $recentLogLimit = 10000; // 只保留最近1万条日志
    public function __construct(\Redis $redis)
    {
        $this->redis = $redis;
    }
    /**
     * 记录一次API调用(原子操作,高性能)
     */
    public function record(string $endpoint, string $method, int $statusCode, int $durationMs, array $extra = []): void
    {
        $date = date('Y-m-d');
        $timestamp = (int)(microtime(true) * 1000); // 毫秒时间戳
        // 1. 累加当日Counter (Hash)
        $hashKey = "api:counter:{$date}";
        $field = "{$method}:{$endpoint}";
        $this->redis->hIncrBy($hashKey, $field, 1);
        // 设置过期时间,避免历史数据一直占用内存 (保留30天)
        $this->redis->expire($hashKey, 30 * 86400);
        // 2. 记录到最近日志 (Sorted Set)
        $logKey = "api:log:recent";
        $logData = json_encode([
            'endpoint'   => $endpoint,
            'method'     => $method,
            'status'     => $statusCode,
            'duration'   => $durationMs,
            'ip'         => $extra['ip'] ?? $_SERVER['REMOTE_ADDR'] ?? '',
            'user_id'    => $extra['user_id'] ?? 0,
            'time'       => date('Y-m-d H:i:s'),
        ]);
        // 使用毫秒时间戳作为score,确保唯一性(如果并发极高,可以加随机后缀)
        $this->redis->zAdd($logKey, $timestamp, $logData);
        // 3. 控制日志集合大小,只保留最近N条
        $this->redis->zRemRangeByRank($logKey, 0, -($this->recentLogLimit + 1));
        // 4. (可选) 记录成功/失败总数 (另开一个Hash分别统计成功/失败)
        // 成功后累加: $this->redis->hIncrBy("api:success:{$date}", $field, 1);
        // 失败后累加: $this->redis->hIncrBy("api:failure:{$date}", $field, 1);
    }
    /**
     * 获取某个API今日的调用次数
     */
    public function getTodayCount(string $endpoint, string $method = 'GET'): int
    {
        $date = date('Y-m-d');
        $field = "{$method}:{$endpoint}";
        return (int)$this->redis->hGet("api:counter:{$date}", $field);
    }
    /**
     * 获取今日所有API的TopN排行
     */
    public function getTopEndpoints(string $date, int $topN = 10): array
    {
        $hashKey = "api:counter:{$date}";
        $allFields = $this->redis->hGetAll($hashKey);
        arsort($allFields);
        return array_slice($allFields, 0, $topN);
    }
}

优点:性能极高(单机10万+ QPS),支持实时排行榜,数据自动过期。
缺点:数据存储在内存中(成本高),断电或宕机可能丢失近期数据(Redis持久化配置可缓解),不支持复杂的跨天关联查询(需要定期同步到数据库)。


高并发 + 持久化(Redis + 异步入库)

这是生产环境最推荐的方案,结合方案二的高性能写入和方案一的持久化查询需求。

核心流程:

请求到达 -> [内存/Redis计数] -> 返回响应给客户端
                           -> 异步进程/定时任务 -> 批量写入MySQL

写入端 (PHP 继续使用 Redis + 将日志推入消息队列)

// 修改 record 方法,不直接操作日志表,而是将日志发送到消息队列
public function recordToQueue(string $endpoint, $method, $statusCode, $durationMs, $extra = []): void
{
    // --- 仍然先更新Redis的实时Counter (保证毫秒级响应) ---
    $date = date('Y-m-d');
    $hashKey = "api:counter:{$date}";
    $field = "{$method}:{$endpoint}";
    $this->redis->hIncrBy($hashKey, $field, 1);
    // --- 将详细日志推入 Redis List 或 Kafka/RabbitMQ ---
    $logEntry = [
        'endpoint'   => $endpoint,
        'method'     => $method,
        'status'     => $statusCode,
        'duration'   => $durationMs,
        'ip'         => $extra['ip'] ?? '',
        'user_id'    => $extra['user_id'] ?? 0,
        'created_at' => date('Y-m-d H:i:s'),
    ];
    // 使用 Redis List 作为简易队列 (注意:Redis List数据量大时注意内存)
    $this->redis->rPush('api:log:queue', json_encode($logEntry));
    // 或者使用高吞吐的消息队列中间件
    // $this->kafkaProducer->send('api-logs', $logEntry);
}

消费端 (另一个PHP脚本/定时任务,或使用队列工作进程)

<?php
// consumer.php (例如每分钟跑一次,或者作为守护进程)
while (true) {
    // 从队列中取出最多100条日志
    $batch = $this->redis->lRange('api:log:queue', 0, 99); // 注意防止阻塞
    if (empty($batch)) {
        sleep(1);
        continue;
    }
    $this->redis->lTrim('api:log:queue', count($batch), -1); // 移除已取出的数据
    // 批量插入数据库
    $pdo->beginTransaction();
    foreach ($batch as $logJson) {
        $log = json_decode($logJson, true);
        // 插入到 api_call_log 表
        // 更新 api_call_counter 表的汇总数据
    }
    $pdo->commit();
}

优点

  • 写入极快:API响应时间只受Redis的短操作(微秒级)影响。
  • 数据不丢:Redis持久化 + 队列机制确保数据最终能写入MySQL。
  • 查询灵活:MySQL存放全量历史数据,支持复杂统计分析。

缺点

  • 架构复杂:需要维护消息队列和消费进程。
  • 数据一致性问题:一旦Redis或消费者崩溃,可能会有短暂数据丢失窗口。

基于文件记录(最简单,适合极低并发)

如果项目极小,甚至不需要数据库,可以直接写日志文件。

function countApiCall($endpoint, $statusCode) {
    $logFile = __DIR__ . '/api_counter.log';
    $data = date('Y-m-d H:i:s') . "|$endpoint|$statusCode\n";
    file_put_contents($logFile, $data, FILE_APPEND | LOCK_EX);
}

分析:并发高时LOCK_EX会导致大量进程排队,性能极差,只适合内部调试或日均千次以下。


& 选择建议

方案 适用场景 优点 缺点
方案一 (MySQL) 低并发 (< 100 QPS),需要历史分析和报表 结构清晰,查询方便,持久化稳定 高并发写入慢,日志表会非常大
方案二 (Redis only) 高并发 (>1000 QPS),实时排行榜/大屏 极高性能,实时性好,自动过期 数据量大时内存成本高,断电可能丢部分数据
方案三 (Redis + 异步入库) 生产环境推荐,高并发+需要持久化 兼顾性能与持久性,可扩展性好 架构复杂,需要维护队列和消费者
方案四 (文件日志) 极低并发,开发调试或单机小项目 最简单,零依赖 性能差,不能直接统计分析

额外建议

  1. 一定要加缓存:如果前端需要频繁调用查询统计接口(如实时PV),使用Redis缓存统计结果,设置5-10秒过期。
  2. 降级策略:如果Redis挂了,可以降级为直接写MySQL(或就放弃记录此次调用,以保证API正常),不要因为计数而影响主要业务逻辑。
  3. 限流联动:计数器数据可以作为限流(Rate Limiting)的数据源,使用滑动窗口或令牌桶(Redis实现)限制恶意请求。

在设计时,先把记录和查询分离(CQRS思想),写入路径尽量轻量(Redis/队列),查询路径可以批量从数据库计算,这样比较优雅。

抱歉,评论功能暂时关闭!