php实现按调用次数计费的api,核心是在统一网关层用redis原子操作完成计数与扣减一体化:通过decrby检查返回值是否≥0来实时拦截超支请求,key设计为api:usage:{user_id}:{api_code}:{year_month}并设过期,配合mysql记录操作日志,且计费策略须配置化解耦路由。

PHP 实现按调用次数计费的 API,核心不是写接口,而是准确记录、原子扣减、防绕过——没加事务和幂等控制的计费逻辑,上线即出账务问题。
如何在请求入口处无感记录调用次数
不能依赖日志解析(延迟高、易丢)、也不能在业务逻辑里手动 $user->incApiCount()(漏调用、难审计)。最稳的方式是在统一网关层或中间件中拦截,用 Redis 原子操作完成计数 + 扣减一体化。
- 用
INCRBY记录总调用,但扣费必须走DECRBY并检查返回值是否 ≥ 0,否则拒绝请求 - Key 设计建议为
api:usage:{user_id}:{api_code}:{year_month},避免跨月统计混乱 - 务必设置过期时间(如
EXPIRE2592000 秒),否则 Redis 内存持续增长 - 不要用 MySQL 自增字段模拟计数——高并发下
UPDATE ... SET count = count - 1会锁行,秒级超时频发
为什么 redis->decrBy() 返回负数就代表额度超支
因为它是原子操作:Redis 先读当前值,减去扣减量,再写回,并返回新值。如果返回负数,说明这次扣减已使余额不足,且该次操作已被完整执行——你不能再“回滚”这个减法。
- 正确做法:立即返回 HTTP 402(Payment Required)或自定义错误码
ERR_QUOTA_EXHAUSTED - 错误做法:先
GET再判断再DECR,这中间存在竞态窗口,两个并发请求可能同时读到 1,都扣成 0,实际透支一次 - PHP 示例:
$key = "api:usage:123:search_v1:202406"; $remaining = $redis->decrBy($key, 1); if ($remaining incrBy($key, 1); // 补回,保证数值准确 throw new ApiException('Quota exceeded', 402); }
用户升级套餐后如何平滑续费不丢次数
直接覆盖 Redis key 会导致当月已用次数丢失;清空 key 又会让用户“白用”。真实场景要支持叠加与重置两种模式。
- 套餐含“每月 1000 次”,选重置型:用
SET+EXPIRE替换 key,但需先用GET读旧值,算出本次应补多少(如剩 300 次,新额度 1000 → 补 700) - 套餐含“额外赠送 500 次”,选叠加型:用
INCRBY直接加,key 过期时间保持不变 - 务必记录操作日志到 MySQL,字段至少含
user_id、before、after、action(reset/extend)、operator,财务对账全靠它
最常被忽略的一点:没有把「API 路径匹配」和「计费策略绑定」做成配置化。硬编码 if ($path === '/v1/search') 会让新增接口时反复改中间件,一漏就漏计费。应该用路由注解或独立配置表关联 api_code 与 cost_per_call,让计费逻辑彻底和业务路由解耦。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











