可行方案
站在前人的肩膀上,先看一些可行方案
缓存在内存中
在缓存中维护一个线程安全的map(如ConcurrentHashMap)用于缓存调用次数,每次调用请求过来,map中的value值自增。
对于区分调用成功、调用失败的需求,通过不同的key进行区分(比如增加前缀、后缀)。
本方案实现简单,但是有几个明显问题:
- 数据不能持久化保存
- 多实例场景不能使用,不同实例之间调用量缓存不能共享
- 只能统计到总调用量,不能根据时间段查询调用量
直接存数据库
每次调用请求过来,插入一条调用数据,同时存储调用时间 or 时间戳。 为了避免调用次数过多插入数据记录过多,可以做一层时间的优化,因为调用次数的最小统计单位是小时,可以将每个小时的调用次数存储到一行中。
本方案在缓存方案的基础上, 解决了调用次数多实例的可见性问题,不同实例都对同一个数据库进行读写; 解决了持久化保存的问题,数据库的记录会持久化保存在存储设备中。
但同时,也带来一个问题 修改数据时,会对修改数据加行锁,在并发量大时,会影响响应速度
Redis HyperLogLog统计接口url调用次数
只需要一个大概数量,不需要100%准确率的情况下,可以使用HyperLoglog blog.csdn.net/h2604396739…
日志打印,根据关键字匹配次数,确定调用次数
日志打印,根据关键字匹配次数,确定调用次数
AOP & Handler
需求
结合需求,来看具体实现:
- 不漏掉每一次的接口调用
- 可以区分接口调用成功、调用失败次数
- 可以通过时间筛选不同时间段的调用量,最小时间粒度为小时
- 可以区分不同用户的调用次数
- 哪些接口纳入统计,做到配置化
- 代码侵入小,尽量不在已有代码逻辑中做改动
实现方案
1. 低代码侵入 & 接口统计配置化 & 成功失败统计
想想我们的使用场景:尽量和代码解耦、公共能力(与业务无强关联),这和日志的某些使用方式上很接近,进而联想到Spring的特性之一--AOP。 实现方式:
-
定义自定义注解@Trend,注解中定义差异参数(可以唯一确定此次调用的一个或多个组合参数,比如api的id值)
-
以自定义注解作为切面,只对定义@Trend注解的方法进行统计
-
在AfterReturn方法中,根据状态码判断接口调用成功/调用失败
-
AfterThrowing在调用发生异常时回调,可视为调用失败
2. 不漏掉统计次数
不漏掉统计次数,其实包含了两部分:原子性统计、组件的高可用性
接口调用量,使用“区域id_账号id_接口标识”作为缓存的key,有新增请求调用时,使用incr进行键值的原子性自增
3. 按照时间维度调用量存入数据库
-
每个小时会执行定时任务,将redis缓存中的key值存入数据库的小时表中,同时清空缓存
-
每天(比如每天1点)汇总前一天的调用量,存到数据库的天表中
-
每个月(比如每月1号1点)汇总上个月的调用了,存到数据库的月表中
这样按照不同维度进行搜索时,只需要去对应表中取值即可,速度较快。
**使用什么方式实现定时任务? **
- @Schedule注解 & 分布式锁,实现单实例执行
- 问题:@Schedule为单线程串行调用,同一时刻 or 时间段有多个定时任务时,不能保证在设定的时间点一定执行设定的定时任务(可能会delay)
- Quartz实现分布式定时任务
5. 缓存入库过程中不遗漏新请求
每个小时将redis中缓存的<k,v>对取出来存入数据库,然后将redis中的缓存清空,这个动作可能会产生调用量丢失的问题。
步骤动作如下:
-
将缓存数据从Redis读入内存
-
将缓存数据插入数据库
-
清空Redis缓存
如果在将Redis中缓存数据读入内存 & 清空redis缓存中间,又有新的调用请求产生,此时会发生调用量丢失的情况。
为了解决这个问题,我们引入奇偶key的概念,奇小时时,将上个小时(偶小时)的调用量数据拿出来存到数据库中,用奇小时继续接进行调用次数的统计。比如此时为3:00,我们从redis中取出偶小时的key([2:00,3:00)之间的调用量缓存),并存入数据库,在3:00收到的新请求,会在奇小时的key中进行自增。
奇偶小时通过对redis中的key增加前缀odd_、even_来实现
思考
本方案应该不适用于高并发场景
- 每次调用,都需要和Redis交互使用incr自增,压缩了Redis可被正常业务调用的次数
- 依赖Redis的高可用,每小时将调用量入库,当未入库前Redis挂掉,会损失调用数据(和Redis的RDB、AOF的刷盘频率相关,1s/次的话,可能会损失1s的调用数据)