背景
adx 在处理曝光/点击上报时,使用 redis 的 setnx
命令去重,其逻辑如下
- 构造一个形如
s:track:%d:%s:%s
的 key,参数分别是上报类型(曝光 or 点击),请求 id,广告位 id setnx(redisKey, timestamp)
- 若返回值不为 1,说明本次上报为重复上报
- 若返回值为 1,设置过期时间为 24 小时
现在的需求就是减少此项去重业务所占用的 redis 内存
技术方案
方案一
- value 置为 0,这样每个 key 节约了 8 字节
说明:虽然时间戳是 10 位数字,但 redis 会以 8 字节整数形式进行存储;而 0 在 redis 内部是共享的,可以理解为单例模式 - 简化 key,目前 key 的前缀太长,可以简化为
te/tc
,表示曝光/点击上报 - 用广告序号来构造 key,而不是用广告位 id 构造 key,这样还能兼容一个广告位填充多个广告的情况
- 缩短过期时间
方案二
一次广告请求,实际上会导致多次广告上报
- 可能返回了多个广告
- 一个广告可能上报曝光和点击
如果一次下发了 10 个广告,每个广告都上报了曝光和点击,则要在 redis 里创建 20 个 key,这 20 个 key 的请求 id 是完全一样的
这种情况可以用 hash
来节约内存
- 使用请求 id 做 key
- 上报类型+广告序号 做 field
hsetnx t:1ad72e9d0171ac12041c271000000000 e99 0
方案三
我们来考察一下 redis 的 set,通过 sadd key member
方法可以往一个集合里添加元素,如果添加成功,返回 1,添加不成功,说明元素已存在,返回 0。可见,set 也可以用来做去重
当 set 元素不多且元素值为整数时,redis 会使用 intset
来实现 set;否则使用 hashtable
方案二使用 hash
结构,field
是 e/c
拼接广告序号;改造成整数的话,曝光可以用 2*序号
,点击可以用 2*序号+1
,从性能考虑也可以用位操作
例如,广告请求 id = 0a80114e01717684281e000249ef,填充了 5 个广告,全部曝光,第一个广告点击,则有如下操作
String reqid = "0a80114e01717684281e000249ef";
int member;
// 第一个广告曝光
member = 0 * 2;
if (sadd(reqid, member) == 1) // 曝光未重复;
do something...
// 第一个广告点击
member = 0 * 2 + 1;
if (sadd(reqid, member) == 1) // 点击未重复;
do something...
// 第二个广告曝光
member = 1 * 2;
if (sadd(reqid, member) == 1) // 曝光未重复;
do something...
......
仔细研究 intset
,可以发现和我们的业务需求非常的匹配
- 当集合元素全部是整数,且数量少于
512
(可通过set-max-intset-entries
配置) 个,redis 使用intset
而不是hashtable
来实现 set intset
保存的整数可以是 2 字节,4 字节或者 8 字节,而我们的广告业务下发的广告数量肯定不会超过65535
个,也就是说在 redis 里保存广告序号只需要用 2 字节的整数intset
判断元素是否存在使用二分查找法,时间复杂度是O(logn)
,hashtable
的时间复杂度基本是O(1)
,也就是说,intset
虽然内存消耗低,但是响应会慢一点,对于我们的业务来说,一个 set 里的元素平均只有 3-5 个,用二分查找的性能完全可以接受- 即便是对于一些填充了几十个创意的广告请求,判断曝光/点击是否重复可能会慢点,但是我们这个操作本身就是异步的,慢一点可以接受
方案四
无论是使用 hash 还是 set ,都基于一个前提:认为一次广告请求会产生多次上报;但这个前提并非绝对,很多情况是一次请求只返回了一个广告,最终只触发了曝光,没有触发点击,这种情况使用 hash/set 是不是反而消耗了更多内存呢?
最简单的做法是,如果本次广告请求只返回了一个广告,我们使用 setnx
来设置一个 key——转了一圈回到原点了吗?如果又发生了点击该怎么办?
对于这种情况,有没有什么更好的方案呢?有没有一种方案,可以用一个 value 来表示多个广告是否曝光/点击过?
经过研究,位图似乎是个不错的方案
- 若广告序号=n,一共填充了 t 个广告,那么位图的第 n 位表示广告是否曝光,第 t + n 位表示广告是否点击
例如: 本次下发了 10 个广告,当前曝光的广告序号为 2,要判断该广告是否曝光过
// 第三个广告曝光,广告序号为 2
int offset = 2;
boolean result = setbit(key, offset, true);
if (result) {
// 重复曝光
} else {
// 首次曝光
}
总结
降低 redis 内存占用的方法
- 减少 key 的数量
- 提取出 key 里的相同数据,使用 hash 来映射 key 里的不同数据
- 要特别注意的是,如果 hash 的字段很多,就要考虑负载均衡的问题:因为我们的 redis 环境是分布式的,如果个别 hash 的字段太多,会导致负载集中到承载该 hash 的服务器
- 降低 key 本身的内存占用
- key 的构造方式是否存在优化空间
- 降低 value 的内存占用
- 如果 value 的值没有实际意义,建议存为 0
- 合理的选择数据结构
One thought on “redis 内存占用优化实例”