# 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 字节整数形式进行存储
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` 结构，`field` 是 `e/c` 拼接广告序号；改造成整数的话，曝光可以用 `2*序号`，点击可以用 `2*序号+1`，从性能考虑也可以用位操作

例如，广告请求 id = 0a80114e01717684281e000249ef，填充了 5 个广告，全部曝光，第一个广告点击，则有如下操作

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

### 方案四

无论是使用 `hash` 还是 `set` ，都基于一个前提：认为一次广告请求会产生多次上报；但这个前提并非绝对，很多情况是一次请求只返回了一个广告，最终只触发了曝光，没有触发点击，这种情况使用 `hash/set` 是不是反而消耗了更多内存呢？

最简单的做法是，如果本次广告请求只返回了一个广告，我们使用 `setnx` 来设置一个 `key`——转了一圈回到原点了吗？如果又发生了点击该怎么办？

对于这种情况，有没有什么更好的方案呢？有没有一种方案，可以用一个 `value` 来表示多个广告是否曝光/点击过？

经过研究，位图似乎是个不错的方案

* 若广告序号=n，一共填充了 t 个广告，那么位图的第 n 位表示广告是否曝光，第 t + n 位表示广告是否点击

例如: 本次下发了 10 个广告，当前曝光的广告序号为 2，要判断该广告是否曝光过

```java
// 第三个广告曝光，广告序号为 2
int offset = 2;
boolean result = setbit(key, offset, true);
if (result) {
  // 重复曝光
} else {
  // 首次曝光
}
```

## 总结

降低 `redis` 内存占用的方法

* 减少 `key` 的数量
  * 提取出 `key` 里的相同数据，使用 `hash` 来映射 `key` 里的不同数据&#x20;
  * 要特别注意的是，如果 `hash` 的字段很多，就要考虑负载均衡的问题：因为我们的 `redis` 环境是分布式的，如果个别 `hash` 的字段太多，会导致负载集中到承载该 `hash` 的服务器&#x20;
* 降低 `key` 本身的内存占用&#x20;
  * `key` 的构造方式是否存在优化空间&#x20;
* 降低 `value` 的内存占用&#x20;
  * 如果 `value` 的值没有实际意义，建议存为 0&#x20;
  * 合理的选择数据结构
