[Redis] Redis最佳实践
🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (93平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(97平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(95平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
🎃Redis(97平均质量分)https://blog.csdn.net/2301_80050796/category_12777129.html?spm=1001.2014.3001.5482
🐰RabbitMQ(97平均质量分) https://blog.csdn.net/2301_80050796/category_12792900.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 1. Redis键值设计
- 1.1 优雅的key结构
- 1.2 BigKey问题
- 1.2.1 BigKey的危害
- 1.2.2 如何发现BigKey
- 1.2.3 如何删除bigKey
- 1.3 恰当的数据类型
- 1.4 总结
- 2. 批处理优化
- 2.1 Pipeline
- 2.1.1 客户端如何与Redis服务器交互?
- 2.1.2 MSet
- 2.1.3 Pipeline
- 2.2 集群下的批处理
- 3. 持久化配置
- 4. 慢查询优化
- 4.1 什么是慢查询
- 4.2 如何查看慢查询
- 5. Redis内存划分和内存配置
- 5.1 内存碎片与内存划分
- 5.3 缓冲区内存问题分析
- 6. 集群还是主从
- 6.1 问题1: 集群不可用
- 6.2 问题2: 集群带宽问题
- 6.3 问题3: 数据倾斜问题
- 6.4 问题4: 集群的批处理问题(单机命令与集群命令兼容性问题)
- 6.5 问题5: lua和事务的问题
1. Redis键值设计
1.1 优雅的key结构
Redis的key虽然可以自定义,但是最好遵循下面的几个最佳的实践约定:
- 遵循基本的格式: [业务名]:[数据名]:[id]
- 长度不超过44字节
- 不包含特殊字符
例如登录业务,保存用户信息,其key可以设计成如下的格式:
这样设计的好处: - 可读性强
- 避免不同的业务中产生的key发生冲突.
- 方便对不同业务的key进行管理.
1.2 BigKey问题
BigKey通常以Key的大小和Key中的成员的数量综合判定,例如:
- key本身的数据量过大: 一个String类型的key,他的值为5MB
- key中的成员数量过多: 一个Zset类型的key,他的成员数量为10000个.
- key中成员的数据量过大; 一个Hash类型的key,他的成员虽然只有1000个,但是这些成员的value(值)总大小为100MB.
如何判断元素的大小呢?Redis也给我们提供了命令
- 首先可以采用Memery usage命令来查看指定的key以及value占用的大小,但是我们一般不推荐使用Memery指令,因为这个指令对CPU的占用是比较高的.
- 在实际开发中,来衡量一个key是不是大key的时候,我们一般只衡量长度或者是集合中的元素个数就可以.
- 在实际开发中,推荐一个key的value要小于10KB,对于集合类型的key,建议元素数量小于1000.
1.2.1 BigKey的危害
- 网络阻塞
对于BigKey执行读请求时,少量的QPS就可以导致带宽使用率被占满,导致Redis实例,乃至所在的物理机变慢. - 数据倾斜
BigKey所在的Redis实例内存的使用率远超过其他的实例,无法使数据分片的内存资源达到均衡(集群的数据分片在这个实例上分配的插槽比较少). - 数据阻塞
对元素较多的hash,List,zset等做运算会比较好使,导致主线程被阻塞. - CPU压力
对BigKey的数据序列化和返序列化会导致CPU的使用率飙升,影响Redis实例和本机的其他使用.
1.2.2 如何发现BigKey
- redis-cli --bigkeys
利用redis-cli提供的–bigkeys参数,可以遍历分析所有的key,并返回key的整体统计信息与每个数据的Top1的bigkey.
命令:redis-cli -a 密码 --bigkeys
2. scan扫描
自己编程,利用scan(渐进式遍历)扫描redis中的所有key,利用strlen,hlen等命令判断key的长度(不建议使用Memery usage).
- 第三方工具
利用第三方工具,如redis-RDB-Tools分析RDB快照文件,全面分析内存的使用情况. - 网络监控
自定义工具,监控进出redis的网络数据,超过预警值的时候主动报警,一般阿里云搭建的云服务器就有相关的监控页面.
1.2.3 如何删除bigKey
如果BigKey占用的内存较多,即便删除了这样的key也需要耗费很长的时间,导致Redis主线程阻塞,引发一系列问题.
- Redis3.0以及一下版本
如果是集合类型,则遍历BigKey的元素,逐个删除子元素,最后删除BigKey - Redis4.0以后
提供了异步删除的命令unlink
.
1.3 恰当的数据类型
例1: 比如存储一个User对象,我们有三种存储的方式:
- 方式一: JSON字符串
优点: 实现简单粗暴
缺点: 数据耦合,不够灵活 - 方式二: 字段打散
优点: 可以灵活访问对象的任意字段
缺点: 占用空间大,没办法做统一控制 - 方式三: hash(推荐)
优点: 底层使用ziplist,占用空间小,可以灵活访问对象的任意字段.
缺点: 代码相对复杂
例2: 例如有hash类型的key,其中有100万对Field和value,Field是自增id,这个key存在什么问题,如何优化?
存在的问题:
- hash的Entry数量超过500的时候,会使用Hashtable而不是ziplist,内存占用的内存较多,是一个大key.
- 方案一:
拆分为String类型
存在的问题:
第一String结构底层没有太多的内存优化,内存占用比较多,第二想要批量获取这些数据比较麻烦
- 方案二
拆分为小的hash,将id/100作为key,将id%100作为Field,这样每100个元素为一个hash,这样让每个hash凑采用ziplist存储,存储空间会大大减小.
1.4 总结
- key的最佳实践
- 固定格式: [业务名]:[数据名]:[id]
- 足够简短,一般不超过44字节
- 不包含特殊字符
- value的最佳实践
- 合理的拆分数据,拒绝BigKey
- 选择合理的数据结构
- Hash结构的entry不要超过1000
- 设置合理的超时时间
2. 批处理优化
2.1 Pipeline
2.1.1 客户端如何与Redis服务器交互?
单个命令的执行流程,是一次往返网络传输的耗时+一次Redis执行命令的耗时.
N条命令执行的流程,N次往返的网络传输耗时+N次Redis执行命令的耗时.
Redis处理指令是很快的,其花费的时间主要在网络传输上,于是很容易想到将多条指令批量传输给Redis.批量传输的耗时等于1次往返的网络传输耗时+N次Redis执行命令的耗时.
2.1.2 MSet
Redis提供了很多的MXXX这样的命令,可以实现批量的数据插入,例如:
- mset
- hmset
比如利用mset批量插入10万条数据
@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));
}
我我们之前做的OJ刷题系统中,也用到了Redis的批量数据插入.在刷新历史竞赛,未完赛的竞赛,和用户竞赛中,我们使用了批量插入,使用了map把这些数据包装好,之后进行插入即可.
redisService.multiSet(historyExamMap);
redisService.multiSet(unfinishExamMap);
redisService.multiSet(userExamMap);
2.1.3 Pipeline
Mset虽然可以批量处理,但是却只能操作部分的数据类型,比如上面我们只能对set和Hash类型做批量插入,因此如果有对复杂的数据的批量处理需要,比如批量插入字符串类型,建议使用Pipeline.使用jedis.pipelined
方法创建一个Pipeline类.之后可以批量把命令放入Pipeline中,使用Pipeline.sync
批量执行.
@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必须落在同一个插槽中,但是如果我们在集群模式下进行批处理,这些数据很有可能因为计算出的哈希值不同而落在不同的节点上,如果我们就按照落在一个结点上来写命令的话,就会引起单机结点命令和集群命令不兼容的问题,引起报错.
这个时候,我们有以下的四种解决方案:
- 第一种方案: 串行执行,N次Redis处理+N次网络请求,但这样没有起到优化的效果,一般我们不建议这样干.
- 第二种方案: 串行slot,简单来说,就是执行前,客户端先计算一下对应的key的哈希槽==,一样槽位的key就放到一个组里面,不同的,就放到不同的组里面,然后对每个Pipeline中的数据依次进行批处理==,这种做饭比第一种方法耗时少,但是实现相对复杂一些.
- 第三种方案: 并行slot,相较于第二种方案,在分组完成后串行执行,每个Pipeline依次执行,第三种方案就变成了并行执行各个命令,好几个Pipeline一起执行,所以他的耗时比第二种更短,但是实现起来比第二种更加复杂.
- 第四种方案: hash_tag,Redis计算key的哈希槽的时候,其实是根据key的有效部分来计算的,我们设置key的时候为所有的key设置相同的有效部分,通过这种方式就能一次处理所有的key,这种方式就会导致所有的key都落在一个结点上,产生数据倾斜的问题,所以我们推荐使用第三种方式.
3. 持久化配置
Redis的持久化虽然可以保证数据安全,但是也会带来额外的性能开销,因此持久化请遵循一下原则;
- 单纯用来做缓存的Redis实例尽量不要开启持久化功能.
- 建议关闭RDB持久化功能,使用AOF持久化功能,保证数据的可靠性
- 虽然RDB不可以最大程度保证数据的可靠性,但是可以利用脚本定期在slave结点上做RDB数据备份.
- 合理设置rewrite阈值,避免频繁的bgrewrite.
- 配置
no-appendfsync-on-rewrite = yes
,禁止在重写期间做AOF,避免因为AOF引起的阻塞.
4. 慢查询优化
4.1 什么是慢查询
并不是很慢的查询才叫慢查询,而是Redis在执行耗时超过某个阈值的命令,称为慢查询.
慢查询的危害: 由于Redis值单线程的,所以当客户端发出指令之后,他们都会进入到Redis底层的Queue来执行,如果此时有一些慢查询的数据,就会导致大量请求阻塞,从而引起报错,所以我们需要解决慢查询的问题.
慢查询的阈值可以通过配置指定:
slowlog-log-slower-than
: 慢查询阈值,单位值微秒,默认是10000,建议是1000
慢查询会被放入到慢查询日志中,日志的长度有上限,可以通过配置指定:
slowlog-max-len
: 慢查询日志的长度(本质上是一个队列的长度),默认是128,建议1000.
修改这两个配置使用config set
命令.
4.2 如何查看慢查询
直到了以上内容之后,那么如何去查看慢查询日志列表呢?
slowlog len
: 查询慢查询日志的长度slowlog get[n]
: 读取n条慢查询日志slowlog reset
: 清空慢查询列表
5. Redis内存划分和内存配置
当Redis内存不足的时候,可能会导致key被频繁删除,响应时间变长,QPS不稳定等问题,当内存使用率达到90%以上的时候,就需要我们警惕,并快速定位到内存占用的原因.
5.1 内存碎片与内存划分
Redis底层分配并不是这个key有多大,他就会分配多大,而是有他自己的分配策略,比如有三块内存空间,8,16,20,假定当前的key只需要10个字节,此时分配8坑定不够,那么他就会分配16个字节,多出来的6个字节就不能被使用,这就是我们常说的碎片问题.
内存通常是按照功能去划分的:
一般通过info memory
来查看内存的分配情况
5.3 缓冲区内存问题分析
内存缓冲区常见的有三种:
- 复制缓冲区: 主从复制的repl_backlog_buf,在进行全量复制和部分复制的时候都会用到它,如果太小可能导致频繁的全量复制,影响性能,通过
replbacklog-size
来设置.默认1mb. - AOF缓冲区: AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区,无法设置容量上限.
- 客户端缓冲区: 分为输入缓冲区和输出缓冲区,输入缓冲区最大的1G且不能设置,输出缓冲区可以设置.
以上复制缓冲区和AOF缓冲区不会有问题,最关键的就是客户端缓冲区的问题
客户端缓冲区: 指的就是我们发送命令时,客户端用来缓存命令的一个缓冲区,也就是我们向Redis输入数据的输入端缓冲区和Redis向客户端返回数据的响应缓存区,输入缓冲区最大1G且不能设置,所以这一块我们不用担心,如果超过了这个空间,Redis会直接断开,因为本来此时此刻就代表着Redis处理不过来了,我们需要担心的就是输出端缓冲区.输出缓冲区的配置方法如下:
我们在使用Redis的过程中,处理大量的big value,那么会导致我们的输出结果过多,如果输入缓冲区过大,会导致Redis直接断开,而默认配置的情况下,其实他是没有大小的,这就比较坑了,内存可能一下子被占满,会直接导致咱们的Redis客户端断开,所以解决方案有两个.
- 通过我们上面提供的对客户端输出缓冲区的配置方案设置一个缓冲区大小.
- 增加Redis的网络贷款大小,避免我们出现大量的数据从而直接超过了Redis的承受能力.
6. 集群还是主从
先说结论:单体Redis(主从Redis)已经可以达到万级别的QPS,并且也具备很强的高可用性,如果主从能够满足整体业务需求的情况之下**,不是在万不得已的情况下,尽量不要搭建Redis集群**.
集群虽然具备很高的可用性,能实现自动故障回复,但是如果使用不当,会出现一下的问题:
- 可能会导致集群不可用
- 集群带宽问题
- 数据倾斜问题
- 客户端性能问题
- 集群的批处理问题
- lua和事务问题
6.1 问题1: 集群不可用
在Redis的默认配置中,如果发现了任意一个插槽不可用,整个集群都会停止对外提供服务.这样服务的可用性不可以保证,所以需要把如下的配置修改为no,即有slot不能用的时候,我们的Redis集群还是可以对外提供服务的.
cluster-require-full-coverage no
6.2 问题2: 集群带宽问题
集群结点之间会不断的互相ping,即发送心跳包来确定其他结点的状态,每次ping携带的信息至少包括:
- 插槽信息
- 集群状态信息
集群中的结点越多,集群状态信息的数据量也越大.这样哦会导致集群中的大量带宽被ping信息所占用,这是一个非常可怕的问题,所以我们需要解决这样的问题:
解决途径:
- 避免大集群,集群结点数最好少于1000,如果业务庞大,则建立多个集群.
- 避免在单个物理机中运行太多的Redis实例
- 配置合适的集群网络请求超时时间
6.3 问题3: 数据倾斜问题
这个问题我们在前面提到过,这里不再赘述
6.4 问题4: 集群的批处理问题(单机命令与集群命令兼容性问题)
有关这个问题咱们已经探讨过了,当我们使用批处理的命令时,redis要求我们的key必须落在相同的slot上,然后大量的key同时操作时,是无法完成的,所以客户端必须要对这样的数据进行处理,这些方案我们之前已经探讨过了,所以不再这个地方赘述了。
6.5 问题5: lua和事务的问题
lua和事务都是要保证原子性的问题,如果你的key不再一个结点上,那么无法保证lua的正常执行和事务的特性的.