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 个广告,全部曝光,第一个广告点击,则有如下操作
仔细研究intset ,可以发现和我们的业务需求非常的匹配
当集合元素全部是整数,且数量少于
512(可通过set-max-intset-entries配置) 个,redis使用intset而不是hashtable来实现setintset保存的整数可以是 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,要判断该广告是否曝光过
总结
降低 redis 内存占用的方法
减少
key的数量提取出
key里的相同数据,使用hash来映射key里的不同数据要特别注意的是,如果
hash的字段很多,就要考虑负载均衡的问题:因为我们的redis环境是分布式的,如果个别hash的字段太多,会导致负载集中到承载该hash的服务器
降低
key本身的内存占用key的构造方式是否存在优化空间
降低
value的内存占用如果
value的值没有实际意义,建议存为 0合理的选择数据结构
Last updated