redis 内存占用优化实例

内容纲要

背景

adx 在处理曝光/点击上报时,使用 redis 的 setnx 命令去重,其逻辑如下

  1. 构造一个形如 s:track:%d:%s:%s 的 key,参数分别是上报类型(曝光 or 点击),请求 id,广告位 id
  2. setnx(redisKey, timestamp)
    1. 若返回值不为 1,说明本次上报为重复上报
    2. 若返回值为 1,设置过期时间为 24 小时

现在的需求就是减少此项去重业务所占用的 redis 内存

技术方案

方案一

  1. value 置为 0,这样每个 key 节约了 8 字节
    说明:虽然时间戳是 10 位数字,但 redis 会以 8 字节整数形式进行存储;而 0 在 redis 内部是共享的,可以理解为单例模式
  2. 简化 key,目前 key 的前缀太长,可以简化为 te/tc,表示曝光/点击上报
  3. 用广告序号来构造 key,而不是用广告位 id 构造 key,这样还能兼容一个广告位填充多个广告的情况
  4. 缩短过期时间

方案二

一次广告请求,实际上会导致多次广告上报

  1. 可能返回了多个广告
  2. 一个广告可能上报曝光和点击

如果一次下发了 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 结构,fielde/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,可以发现和我们的业务需求非常的匹配

  1. 当集合元素全部是整数,且数量少于 512(可通过 set-max-intset-entries 配置) 个,redis 使用 intset 而不是 hashtable 来实现 set
  2. intset 保存的整数可以是 2 字节,4 字节或者 8 字节,而我们的广告业务下发的广告数量肯定不会超过 65535 个,也就是说在 redis 里保存广告序号只需要用 2 字节的整数
  3. intset 判断元素是否存在使用二分查找法,时间复杂度是 O(logn)hashtable 的时间复杂度基本是 O(1),也就是说,intset 虽然内存消耗低,但是响应会慢一点,对于我们的业务来说,一个 set 里的元素平均只有 3-5 个,用二分查找的性能完全可以接受
  4. 即便是对于一些填充了几十个创意的广告请求,判断曝光/点击是否重复可能会慢点,但是我们这个操作本身就是异步的,慢一点可以接受

方案四

无论是使用 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
    • 合理的选择数据结构
redis 内存占用优化实例

One thought on “redis 内存占用优化实例

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Scroll to top
粤ICP备2020114259号 粤公网安备44030402004258