后端简单优化之redis

又去研究了一下redis,想到一些值得优化的地方。

编码/解码

如果缓存数据是字符串类型,将数据存储到 Redis 之前,需要进行编码操作,常规的做法是编码为 JSON 字符串,这样从 Redis 读取到缓存的字符串数据后, 如果数据不需要被读取并且不需要被修改,那么就可以直接将数据输出接口,这样可以节省 2 次 CPU 开销:

  • 将缓存的字符串数据解码为具体对象
  • 将具体对象再编码为字符串后输出

key 的命名

在保证辨识度的前提下,key 的长度越短越好,不仅可以节省存储,还可以提升查询速度。

作为用户数据的缓存 key,user:xxxx:profile 明显由于 user:profile:xxxx,因为前者的辨识度更高,查询速度更快, 在这个基础上可以对 key 的长度再次优化,例如优化为 u:xxxx:pf

读写分离

这类应用场景的典型特征是只有一端固定的数据生产者,例如:

  • 运营角色在管理后台完成整个 CMS 网站的内容
  • 定时任务从第三方同步数据,完成后展示给所有用户
  • 定时发送通知给特定用户

这种场景最容易优化,启动一个后台任务,定时刷新数据到缓存即可,这里不再赘述了。

HyperLogLog

数据量很大的情况下,

  • 统计一个 APP 的日活、月活数。
  • 统计一个页面的每天被多少个不同账户访问量(Unique Visitor,UV)。
  • 统计用户每天搜索不同词条的个数。
  • 统计注册/登录 IP 数。

使用 HyperLogLog 实现的唯一计数器可以大大降低内存使用量,比较适合使用在一些应用级别,接口级别的非精确唯一性统计上,比如统计当前某个页面的 uv, 某个接口的请求 uv 等等,而不是特别适合为每个用户进行统计,可以计算一下,每个用户需要 12k 字节的时候,如果你有 1 亿用户,那么内存使用量也比较夸张了。

时间区间

这类应用场景的典型特征是不同时间段内的数据组合优化,项目中类似场景之前的做法是使用筛选条件中的 (开始时间 + 结束时间 + 业务 key) 进行拼接作为缓存数据 key, 稍微思考后会发现这其中有很大的潜在问题: 不同的两个日期组合结果集合是一个庞大的数字,除了重复数据导致的巨量内存浪费外,还会造成很大的安全隐患。

下面举个浪费内存的例子,不同用户查询的时间区间是重叠的:

1
2
3
- 用户A 2024-01-01 ~ 2024-01-10
- 用户B 2024-01-02 ~ 2024-01-05
- 用户C 2024-01-05 ~ 2024-01-08

通过示例可以看到,虽然有三个用户在查询,但是用户 B 和 用户 C 查询的数据都在 用户 A 的结果集内,也就造成了数据重估存储,白白浪费了内存。

可以想到的优化方案为:

  • 根据更小的粒度来缓存 (项目中以天为单位),这样单个业务场景一年最多 365 个 key
  • 控制时间范围的上限,不能超过 31 天
  • 根据请求参数批量从 Redis 读取缓存数据
  • 将读取到的缓存数据组装完成后输出接口

注意: 如果项目的数据量很大,就需要调整时间粒度,并且进行数据异步批处理优化,但是整体的思路是不变的。

使用Lua脚本

对于复杂的操作,使用Lua脚本可以在Redis内部执行,减少网络往返。

1
2
3
4
5
6
7
8
9
10
const script = `
if redis.call("EXISTS", KEYS[1]) == 1 then
return redis.call("INCRBY", KEYS[1], ARGV[1])
else
return 0
end
`;

const result = await redis.eval(script, 1, 'myCounter', 1);
console.log(result); // 递增计数器的结果

总结

看似写了很多,其实就是几个要点作为主要手段:

  • 找对数据结构
  • 减少数据粒度
  • 异步处理