api接口分布在多台服务器, 如何同步用户的每日请求次数

分布式API接口用户请求次数同步技术方案,实现过程简单,小白也可以看懂

背景

易客云天气api提供接口服务, 项目初期, 天气API在分布式API服务架构中,每条接口线路对应不同的服务器,我们并没有做中间redis服务器,而是每台服务器独立统计,例如我们每个用户限制当日最多可以请求10万次,用户在线路A请求了接口10万次,超额报错之后再请求线路B,或者两条线路随机访问,当日一共可以请求20万次,为了防止用户薅羊毛,我们必须得把次数统一,降低服务器带宽。

目标

实现跨服务器节点的用户请求计数实时同步,确保每日请求限额的准确性和一致性,同时兼顾系统性能和可扩展性,方便后期扩展新的线路服务器。

实现思路流程图
代码思路

1. redis次数同步中间服务器

  • 设置每台服务器标识, key的规则为:uid+日期+服务器标识
  • 设置总的请求次数的key, 就是所有服务器加起来的次数
  • 同步线路服务器发来的次数, 更新总次数值, 并返回线路服务器需要增加的次数
  • 可以查询接口可用剩余次数

2. 线路服务器

  • 创建一个同步任务表, 配合crontab 一小时执行一次, 插入所有用户当前的请求次数, 并标记为待处理, 表的关键字段(处理状态, 用户ID, 当前请求次数)
  • 执行shell脚本, 1s一次请求任务表, 执行同步代码, 处理当前用户的同步次数
  • 线路服务器在请求接口的时候, 验证同步后的可用次数
实现示例
复制代码
redis中间服务器、线路次数同步代码、线上接口限制代码

1. redis中间服务器

php 复制代码
<?php
/**
 * 1. 同步接口
 * 接收单台线路服务器的本地计数并同步到全局, 返回可用次数等
 */
private $dailyLimit = 100000; // 单用户每日总限额
private $redisKeyPrefix = 'user_total_'; // 用户全局计数Key前缀
function syncLocalCount($userId, $serverId, $localCount, $date)
{
    if ($localCount <= 0) {
        return [
            'code' => 400,
            'msg' => '同步计数需大于0',
            'data' => []
        ];
    }
    $redisKey = $this->redisKeyPrefix . $userId . '_' . $date;
    $serverLocalKey = "server_local_count_{$userId}_{$date}_{$serverId}"; //记录该服务器已同步的数,避免重复同步
    $limitkey = 'limit_' . $userId;//每个用户的请求次数可能也不一样
    $this->dailyLimit = $this->redis->get($limitkey);
    $currentGlobalCount = $this->redis->get($redisKey) ?: 0;
    // 步骤1:检查该服务器本次同步的数是否已处理(防重复同步)
    $syncedCount = $this->redis->get($serverLocalKey) ?: 0;
    $needSyncCount = $localCount - $syncedCount; //仅同步新增的计数
    if ($needSyncCount <= 0) {
        //无需同步也返回一下当日可用次数
        return [
            'code' => 200,
            'msg' => '无新增计数需同步',
            'data' => [
                'server_id' => $serverId,
                'synced_count' => (int)$syncedCount,
                'local_count' => $localCount,
                'need_sync_count' => 0,
                'current_global_count' => (int)$currentGlobalCount,
                'dailyLimit' => (int)$this->dailyLimit,
                'available_count' => $this->dailyLimit - (int)$currentGlobalCount
            ]
        ];
    }

    // 步骤2:原子累加全局计数(核心,保证多服务器同步不冲突)
    $this->redis->incrBy($redisKey, $needSyncCount);
    // 步骤3:更新该服务器已同步的计数(标记已处理)
    $this->redis->set($serverLocalKey, $localCount);
    // 步骤4:设置全局Key过期时间(25小时,次日自动重置)
    if (!$this->redis->ttl($redisKey) || $this->redis->ttl($redisKey) < 0) {
        $this->redis->expire($redisKey, 86400 + 3600);
        $this->redis->expire($serverLocalKey, 86400 + 3600); // 同步Key同过期
    }
    // 步骤5:返回同步结果
    $currentGlobalCount = $this->redis->get($redisKey) ?: 0;
    return [
        'code' => 0,
        'msg' => '计数同步成功',
        'data' => [
            'user_id' => $userId,
            'server_id' => $serverId,
            'local_count' => $localCount,
            'synced_count' => (int)$syncedCount,
            'need_sync_count' => $needSyncCount,
            'current_global_count' => (int)$currentGlobalCount,
            'dailyLimit' => (int)$this->dailyLimit,
            'available_count' => $this->dailyLimit - (int)$currentGlobalCount
        ]
    ];

}

线路同步代码

php 复制代码
// 更新所有本地用户, 写入同步任务表
public function sync()
{
    $serverId = strtoupper(substr(hash('md5', gethostname() . file_get_contents('/sys/class/net/eth0/address')), 0, 8));
    $datefix = date('Ymd');
    // 场景:更新所有本地用户(遍历本地计数Key)
    $localCountPattern = $datefix . '_*';
    $keys = $this->redis->keys($localCountPattern);
    $uptime = time();
    $params = array();
    foreach ($keys as $key) {
        $appid = $key;
        $params[] = array(
            'zt' => 0,
            'uptime' => $uptime,
            'server' => $serverId,
            'appid' => $appid,
            'total' => (int)$this->redis->get($key)//本地总请求次数,
        );
    }
    $this->db->insert('account_sync', $params);
    echo $this->db->last();
}

// 执行同步任务
public function synctask()
{
    $serverId = strtoupper(substr(hash('md5', gethostname() . file_get_contents('/sys/class/net/eth0/address')), 0, 8));
    $row = $this->db->get('account_sync', '*', ['zt' => 0, 'server' => $serverId]);
    if (empty($row)) {
        exit('empty');
    }
    $this->db->update('account_sync', ['zt' => 1], ['id' => $row['id']]);
    $datefix = date('Ymd', $row['uptime']);
    $local_count = (int)$row['total'];
    $api_url = 'http://1.1.1.1:9001/?action=sync_local&date=' . $datefix . '&appid=' . $row['appid'] . '&server_id=' . $serverId . '&local_count=' . $local_count;
    $data = $this->getget($api_url);
    $json = json_decode($data, true);
    print_r($json);//输出请求结果
    if (intval($json['data']['dailyLimit'])) {
        echo 'server_limit_' . $datefix . '_' . $row['appid'] . ' > ' . intval($json['data']['dailyLimit']);
        $this->redis->set('server_limit_' . $datefix . '_' . $row['appid'], intval($json['data']['dailyLimit']));//总可以请求次数
        $this->redis->set('server_remain_' . $datefix . '_' . $row['appid'], intval($json['data']['available_count']));//服务器总的剩余可用请求次数,同步自Redis中间
        echo $this->redis->get('server_limit_' . $datefix . '_' . $row['appid']) . ' - ';
        echo $this->redis->get('server_remain_' . $datefix . '_' . $row['appid']) . '<br>';
        exit('已更新' . json_encode($json, JSON_UNESCAPED_UNICODE));
    }
}

接口验证代码

php 复制代码
$dailyLimit = (int)$this->redis->get('server_limit_' . $datefix . $this->appid);//同步后可以请求次数
$num = (int)$this->redis->get($datefix . $this->appid);//本地统计次数
if ($num < 1) {
    $this->redis->setex($datefix . $this->appid, 259200, 1);//3days
}
if ($dailyLimit > 0) {
    //服务器总的剩余可用请求次数,同步自Redis中间人
    $remainCount = (int)$this->redis->get('server_remain_' . $datefix . $this->appid);
    // 本地校验:剩余次数≤0 或 本地已用≥剩余 → 拒绝请求
    if ($remainCount < 1) {
        Help::print_json(100, '调用次数过高[' . $dailyLimit . ', ' . $remainCount . ', ' . $num . ']');
    }
}
方案总结

主要难点就是redis中间服务器的次数处理, 线路服务器能同步到次数, 然后增加可用验证就可以了

扩展考虑
  • 新增线路服务器部署一下同步代码, 配置下crontab和shell就可以了
  • 针对不同的接口进行限制
  • 增加每个用户的可请求次数不统一限制
相关推荐
小码吃趴菜2 小时前
服务器预约系统linux小项目-第一节课
运维·服务器
搬山境KL攻城狮3 小时前
ssh密钥对使用
运维·ssh
Zzj_tju3 小时前
Java 从入门到精通(六):抽象类与接口到底怎么选?
java·开发语言
道亦无名4 小时前
Linux下是STM32的编译修改配置文件tensorflow
linux·运维
Azure DevOps4 小时前
Azure DevOps Server:2026年3月份补丁
运维·microsoft·azure·devops
知识分享小能手8 小时前
Redis入门学习教程,从入门到精通,Redis 概述:知识点详解(1)
数据库·redis·学习
User_芊芊君子10 小时前
影音自由新玩法:Plex+cpolar 解锁异地访问,告别网盘限速烦恼
服务器·nginx·测评
wanhengidc10 小时前
云手机的运行环境如何
运维·服务器·游戏·智能手机·生活
cyber_两只龙宝10 小时前
【Haproxy】Haproxy的算法详解及配置
linux·运维·服务器·云原生·负载均衡·haproxy·调度算法