又去研究了一下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 亿用户,那么内存使用量也比较夸张了。此时可以考虑通过HyperLogLog牺牲一定的精度来换取更低的内存占用。它非常适合需要统计大量数据但只关注近似值的场景,可以有效降低 Redis 的内存使用量。
时间区间
这类应用场景的典型特征是不同时间段内的数据组合优化,项目中类似场景之前的做法是使用筛选条件中的 (开始时间 + 结束时间 + 业务 key) 进行拼接作为缓存数据 key, 稍微思考后会发现这其中有很大的潜在问题: 不同的两个日期组合结果集合是一个庞大的数字,除了重复数据导致的巨量内存浪费外,还会造成很大的安全隐患。
下面举个浪费内存的例子,不同用户查询的时间区间是重叠的:
1 | - 用户A 2024-01-01 ~ 2024-01-10 |
通过示例可以看到,虽然有三个用户在查询,但是用户 B 和 用户 C 查询的数据都在 用户 A 的结果集内,也就造成了数据重估存储,白白浪费了内存。
可以想到的优化方案为:
- 根据更小的粒度来缓存 (项目中以天为单位),这样单个业务场景一年最多 365 个 key
- 控制时间范围的上限,不能超过 31 天
- 根据请求参数批量从 Redis 读取缓存数据
- 将读取到的缓存数据组装完成后输出接口
注意: 如果项目的数据量很大,就需要调整时间粒度,并且进行数据异步批处理优化,但是整体的思路是不变的。
使用Lua脚本
对于复杂的操作,使用Lua脚本可以在Redis内部执行,减少网络往返。
1 | const script = ` |
总结
看似写了很多,其实就是几个要点作为主要手段:
- 找对数据结构
- 减少数据粒度
- 异步处理