redis 内存占用优化实例
背景
adx 在处理曝光/点击上报时,使用 redis
的 setnx
命令去重,其逻辑如下
构造一个形如
s:track:%d:%s:%s
的key
,参数分别是上报类型(曝光 or 点击),请求 id,广告位 idsetnx(redisKey, timestamp)
若返回值不为 1,说明本次上报为重复上报
若返回值为 1,设置过期时间为 24 小时
现在的需求就是减少此项去重业务所占用的 redis
内存
技术方案
方案一
value
置为 0,这样每个key
节约了 8 字节 说明:虽然时间戳是 10 位数字,但redis
会以 8 字节整数形式进行存储简化
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合理的选择数据结构
Last updated