1.优雅的Redis的Key结构
Redish中的key虽然可以自定义,但是我们在命名的时候最好遵守以下规范
- 遵循基本的格式 [业务名称]:[数据名称]:[id]。
- 优点
- 可读性强
- 避免key的重读
- 方便管理
- 优点
比如保存用户登录信息的key可以这样命名
login:user:32
- 不要包含特殊的字符
- 长度不要超过44字节
要求key的长度不小于44是因为在key的类型都是String,在redis的底层存储字符串有三种形式
- 如果字符串是数值类型,则直接转换成int
- 如果字符串长度小于44且不是数字,则使用embstr方式存储,否则使用raw方式存储
- embStr方式在连续的空间中存储字符串,raw使用离散方式存储字符串,类似数组和链表的区别。因此raw占用的空间要更大一些。
2.BigKey问题
2.1 什么是BigKey
BigKey通常是值一些Key的大小以及Key对应的value的大小很大的键值对
- Key本身的数据量过大,一个String类型的Key,value值约为5MB
- Key中的成员数量过多,一个set集合可能包含100000个元素
- Key中成员所占用的空间较大,虽然一个set中元素只有100个,可是每一个元素占用的空间可能很大。
推荐值
- 单个Key的value小于10KB
- 对于集合类型的Key建议数量小于1000
2.2 BigKey的危害
- 导致网络阻塞,对于一些Bigkey,可能少量的网络请求就可以将服务器的带宽占满。
- 数据倾斜:在集群环境下BigKey所在实例的内存利用率远远高于其他实例,导致内存分配不均衡
- Redis阻塞:在元素较多的list set做运算的时候耗时较长,导致主线程被阻塞。
- CPU压力:对BigKey数据的序列化和反序列化会导致CPU的利用率飙升。
2.3 搜索BigKey
- 使用
redis -cli --bigKeys
命令:只能找到每一种类型占用空间最大的key。 - 使用Scan扫描,这种方式一次扫描一部分
- 第三方工具 如Redis-Rdb-Tools
- 网络监控
3. 选择合适的数据类型
在Redis中不同的数据类型占用的内存是不同的,我们应该结合我们数据本身的特点去选择合适的数据类型。
3.1 例子
比如我们要存储一个User对象到Redis中,有3种数据类型可以选择。
- 使用字符串存储User对象的JSON
- 将字符串打散,每一个key存储一个用户的一种信息
- 利用哈希存储
3.2 举例2
前面提到哈希存储会将数据压缩成ZipList进一步节省空间。但是这个压缩是有条件的。当哈希结构中的entry数目超过500的时候,就不在使用ziplist压缩存储了。
这里可以通过hash-max-ziplist-entries
来指定entry的上限。
3.2.1 方案一 利用String存储
将hash的每一条记录全部都取出来,然后使用String类型存储。
存在的问题
- String类型底层没有很多的优化,导致占用内存非常多
- 想要批量获取这些数据变得非常困难
我们可以看到,在使用了String类型存储以后导致占用比之前使用哈希存储的空间还要大。
3.2.2 将哈希打散
这种的好处
- 采用哈希存储,相比使用String来说占用的空间更小
- 每一个哈希键里面只存储100条数据,可以利用ziplist进一步压缩大小
- 相比String,这种方式在批量获取元素的时候比较方便。
可以看到使用这种方式内存占用仅为24M
4. Key Value最佳实践总结
4.1 Key的最佳实践
- 使用固定格式的key [业务名] : [数据名] : [id]
- key的长度不超过44个字节
- 不要包含特殊字符
4.2 Value的最佳实践
- 合理拆分数据,不要使用BigKey
- 选择合理的数据结构,减少内存的占用
- 哈希存储保证entry不要超过500
- 设置合理的超时时间
5. Redis中的批处理
5.1 传统单条命令执行的问题
传统的Redis执行命令的方式是一条命令一条命令的执行,但是在需要插入海量数据的时候,这种单条命令执行的方式效率很低。
这里我们来测试一下向Redis中插入10w条数据的耗时
@Test
void test() {
for (int i = 1; i <= 100000; i++) {
jedis.set("test:key" + i, "value_" + i);
}
}
耗时长达44秒。
实际上,Redis执行N条命令的耗时应该是N次网络传输的时间+N次Redis执行命令的时间
。事实上,Redis执行命令的时间是非常短的,几乎可以忽略不计。因此大部分的时间都是花费在了网络传输阶段。
5.2 使用批处理方式解决
正如上面分析的,如果我们将我们要执行的命令打包,然后一次性发送给redis,那么实际上花费的时间就变成1次网络传输的时间+N次Redis执行命令的时间
,时间会大大缩短。
这里我们使用Redis中的mset
命令测试插入10w条数据的耗时。
可以看到耗时仅为182毫秒。对比之前的44秒,提升明显。
5.3 使用Pipeline批处理
虽然存在mset和hmset命令,但是大部分的命令都是设置一个key的多个值,而不能设置多个key多个值。为了解决上述问题,可以使用Pipeline解决。
Pipeline和Redis内置批处理的对比
Pipeline的执行时间要比Redis内置的批处理慢一些,原因是Redis的批处理是原子性的,执行过程中不会有其他的操作打断,而Pipeline方式只是将多个命令同时发送给Redis,命令与命令之间不一定是原子性,可能在执行过程中又有别的命令进来,导致执行的速度降低。