一、Redis键值设计
1.1、优雅的key结构
Redis的key,最佳实践约定:
- 遵循基本格式:【业务名称】:【数据名】:【id】
- 长度不超过44字节
- 不包含特殊字符
好处
- 可读性强
- 避免key冲突
- 方便管理
- 更节省内存
1.2、拒绝BigKey
BigKey通常以Key的大小和Key中成员的数量来综合判定,例如:
- Key本身的数据量过大:一个String类型的Key,它的值为5MB;
- Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个;
- Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100MB
推荐值
- 单个key的value小于10KB
- 对于集合类型的key,建议元素数量小于1000
1.2.1、BigKey的危害
- 网络阻塞
- 对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢;
- 数据倾斜
- BigKey所在的Redis实例内存使用率远超其他实例;无法使数据分片的内存资源达到均衡;
- Redis阻塞
- 对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞;
- CPU压力
- 对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用
1.2.2、如何删除BigKey
BigKey内存占用较多,即便是删除这样的Key也需要耗费很长时间,导致Redis主线程阻塞,引发一系列问题。
- redis3.0及以下版本
- 如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey
- redis4.0以后
- 异步删除:unlink
1.3、恰当的数据类型
例1:比如存储一个User对象,我们有三种存储方式
①方式一:json字符串
| user:1 | {“name”: “Jack”, “age”: 21} |
- 优点:实现简单粗暴
- 缺点:数据耦合,不够灵活
②方式二:字段打散
- 优点:可以灵活访问对象任意字段
- 缺点:占用空间大,没办法做统一控制
③方式三:hash(推荐)
优点:底层使用zipList,空间占用小,可以灵活访问对象的任意字段
缺点:代码相对复杂
1.4、总结
- Key的最佳实践
- 固定格式:【业务名】:【数据名】:【id】
- 足够简短:不超过44字节
- 不包含特殊字符
- Value的最佳实践
- 合理的拆分数据,拒绝BigKey
- 选择合适数据结构
- Hash结构的entry数量不要超过1000
- 设置合理的超时时间
2、批处理优化
2.1、Pipeline
我们的客户端与redis服务器是这样交互的
- 单个命令的执行流程
- N条命令的执行流程
redis处理指令是最快的,主要花费的时候在于网络传输。于是乎很容易想到将多条指令批量的传输给redis
2.1.1、Mset
Redis提供了很多Mxxx这样的命令,可以实现批量插入数据,例如:
- mset
- hmset
@Test
void testMxx() {
String[] arr = new String[2000];
int j;
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
j = (i % 1000) << 1;
arr[j] = "test:key_" + i;
arr[j + 1] = "value_" + i;
if (j == 0) {
jedis.mset(arr);
}
}
long e = System.currentTimeMillis();
System.out.println("time: " + (e - b));
}
2.1.3、Pipeline
MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline
@Test
void testPipeline() {
// 创建管道
Pipeline pipeline = jedis.pipelined();
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
// 放入命令到管道
pipeline.set("test:key_" + i, "value_" + i);
if (i % 1000 == 0) {
// 每放入1000条命令,批量执行
pipeline.sync();
}
}
long e = System.currentTimeMillis();
System.out.println("time: " + (e - b));
}
2.2、集群下的批处理
如MSET或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败。大家可以想一想这样的要求其实很难实现,因为我们在批处理时,可能一次要插入很多条数据,这些数据很有可能不会都落在相同的节点上,这就会导致报错了
四种解决方案:
3、服务器端优化-持久化配置
Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:
- 用来做缓存的Redis实例尽量不要开启持久化功能
- 建议关闭RDB持久化功能,使用AOF持久化
- 利用脚本定期在slave节点做RDB,实现数据备份
- 设置合理的rewrite阈值,避免频繁的bgrewrite
- 配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做AOF,避免因AOF引起的阻塞
- 部署有关建议:
- Redis实例的物理机要预留足够内存,应对fork和rewrite
- 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度,减少主从同步、数据迁移压力
- 不要与CPU密集型应用部署在一起
- 不要与高硬盘负载应用一起部署。比如:数据库、消息队列
4、服务器端优化-慢查询优化
4.1、什么是慢查询
在Redis执行时耗时超过某个阈值的命令,称为慢查询;
慢查询的危害:由于Redis是单线程的,所以当客户端发出指令后,他们都会进入到redis底层的queue来执行,如果此时有一些慢查询的数据,就会导致大量请求阻塞,从而引起报错,所以我们需要解决慢查询问题。
慢查询的阈值可以通过配置指定:
- lowlog-log-slower-than: 慢查询阈值,单位是微秒。默认是10000,建议1000
- slowlog-max-len: 慢查询日志(本质是一个队列)的长度。默认是128,建议1000
4.2、如何查看慢查询
- slowlog len: 查询慢查询日志长度
- slowlog get [n]: 读取n条慢查询日志
- slowlog reset: 清空慢查询列表
5、服务器端优化-Redis内存划分和内存配置
当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因
有关碎片问题分析
Redis底层分配并不是这个key有多大,他就会分配多大,而是有他自己的分配策略,比如8,16,20等等,假定当前key只需要10个字节,此时分配8肯定不够,那么他就会分配16个字节,多出来的6个节点就不能被使用,这就是我们常说的碎片问题
进程内存问题分析
这片内存,通常我们都可以忽略不计
缓冲区内存问题分析
一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,所以这片内存也是我们需要重点分析的内存问题。
我们可以通过一些命令,可以查看到Redis目前的内存分配状态
- info memory: 查看内存分配的情况
- memory xxx: 查看key的主要占用情况
接下来我们看到了这些配置,最关键的缓存区内存如何定位和解决呢?
内存缓冲区常见的有三种: - 复制缓冲区:主从复制的repl_backlog_buf,如果太小可能东芝频繁的全量复制,影响性能。通过replbacklog-size来设置,默认1mb
- AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区。无法设置容量上限
- 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设置
以上复制缓冲区和AOF缓冲区不会有问题,最关键就是客户但缓冲区的问题
客户端缓冲区:指的就是我们发送命令时,客户端用来缓存命令的一个缓冲区,也就是我们向redis输入数据的输入端缓冲区和redis向客户端返回数据的响应缓存区,输入缓冲区最大1G且不能设置,所以这一块我们根本不用担心,如果超过了这个空间,redis会直接断开,因为本来此时此刻就代表着redis处理不过来了,我们需要担心的就是输出端缓冲区