文章目录
- 前言
- 7.2 批处理优化
- 7.2.1 命令执行流程
- 7.2.2 mset
- 7.2.3 Pipeline
- 7.2.4 集群下的批处理
- 7.2.4.1 问题与解决方案
- 7.2.4.2 基于Spring的串行化执行
- 7.3 服务器端优化
- 7.3.1 持久化配置
- 7.3.2 慢查询优化
- 7.3.2.1 什么是慢查询
- 7.3.2.2 如何查看慢查询
- 7.3.3 命令及安全配置
- 7.3.4 内存划分和内存配置
- 7.3.5 集群or主从?
- 7.3.5.1 集群完整性问题
- 7.3.5.2 集群带宽问题
- 7.3.5.3 其他问题
- 7.3.5.4 结论
前言
Redis最佳实现系列文章:
Redis从入门到精通(二十)Redis最佳实践(一)优雅的Key结构、拒绝BigKey
7.2 批处理优化
7.2.1 命令执行流程
客户端与Redis服务器交互时,单个命令的执行流程如下:
N条命令的执行流程:
Redis处理指令是很快的,主要花费的时候在于网络传输,于是很容易就想到可以将多条指令批量的传输给Redis:
7.2.2 mset
Redis提供了mset
、hmset
这样的命令,可以实现批量插入数据。 例如利用mset一次性批量插入1000条数据:
@Test
public void testMset() {
long b = System.currentTimeMillis();
String[] arr = new String[2000];
for (int i = 0; i < 2000; i++) {
arr[i] = "test:mset:key_" + i;
arr[i+1] = "value_" + i;
i++;
}
jedis.mset(arr);
long e = System.currentTimeMillis();
System.out.println("time: " + (e - b));
}
控制台打印执行时间:
time: 7
此时Redis中的数据:
7.2.3 Pipeline
mset虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline,例如:
@Test
public void testPipeline() {
long b = System.currentTimeMillis();
// 创建管道
Pipeline pipeline = jedis.pipelined();
for (int i = 1; i <= 1000; i++) {
// 放入命令到管道
pipeline.set("test:pipeline:key_" + i, "value_" + i);
}
pipeline.sync();
long e = System.currentTimeMillis();
System.out.println("time: " + (e - b));
}
控制台打印执行时间:
time: 119
此时Redis中的数据:
7.2.4 集群下的批处理
7.2.4.1 问题与解决方案
mset或Pipeline批处理命令在一次请求中一般会携带多条命令,而如果此时Redis是一个集群,那批处理命令的多个Key必须落在一个插槽中,否则就会导致执行失败。
这样的要求其实很难实现,因为在批处理时可能一次要插入很多条数据,这些数据很有可能落在不相同的节点上,这就会导致报错了。
解决这个问题,有4种方案:
7.2.4.2 基于Spring的串行化执行
@Test
public void testMset() {
// MSET写数据
Map<String, String> map = new HashMap<>(3);
map.put("name", "Rose");
map.put("age", "21");
map.put("sex", "Female");
stringRedisTemplate.opsForValue().multiSet(map);
// MGET取数据
List<String> strings = stringRedisTemplate.opsForValue().multiGet(Arrays.asList("name", "age", "sex"));
strings.forEach(System.out::println);
}
运行单元测试结果如下:
Rose
21
Female
此时Redis种的数据:
7.3 服务器端优化
7.3.1 持久化配置
Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化一般要遵循下列建议:
-
用来做缓存的Redis实例尽量不要开启持久化功能
-
建议关闭RDB持久化功能,使用AOF持久化
-
利用脚本定期在slave节点做RDB,实现数据备份
-
设置合理的rewrite阈值,避免频繁的bgrewrite
-
配置
no-appendfsync-on-rewrite = yes
,禁止在rewrite期间做AOF,避免因AOF引起的阻塞 -
部署有关建议:
- Redis实例的物理机要预留足够内存,应对fork和rewrite
- 单个Redis实例内存上限不要太大,例如4G或8G
- 不要与CPU密集型应用部署在一起
- 不要与高硬盘负载应用一起部署。例如:数据库、消息队列。
7.3.2 慢查询优化
7.3.2.1 什么是慢查询
并不是说很慢的查询才是慢查询,而是在Redis执行时,耗时超过某个阈值的命令,称为慢查询。
慢查询的危害:由于Redis是单线程的,所以当客户端发出指令后,它们都会进入到Rdis底层的queue来执行,如果此时有一些慢查询的数据,就会导致大量请求阻塞,从而引起报错。
- 慢查询的阈值可以通过
slowlog-log-slower-than
配置指定,单位是微秒。默认是10000,建议1000。
127.0.0.1:6379> CONFIG GET slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "10000"
- 慢查询会被放入慢查询日志中,日志的长度有上限,可以通过
slowlog-max-len
配置指定,本质是一个队列。默认是128,建议1000。
127.0.0.1:6379> CONFIG GET slowlog-max-len
1) "slowlog-max-len"
2) "128"
- 要修改这两个配置可以使用
config set
命令:
127.0.0.1:6379> CONFIG set slowlog-log-slower-than 1000
OK
127.0.0.1:6379> CONFIG set slowlog-max-len 1000
OK
7.3.2.2 如何查看慢查询
查看慢查询日志列表可以使用以下名:
slowlog len
:查询慢查询日志长度slowlog get [n]
:读取n条慢查询日志slowlog reset
:清空慢查询列表
7.3.3 命令及安全配置
Redis默认情况下,会绑定在0.0.0.0:6379,这样将会使Redis服务暴露到公网上,如果Redis没有开启身份认证,则可以导致任意用户在可以访问目标服务器的情况下未授权访问Redis以及读取Redis的数据。
攻击者在未授权访问Redis的情况下,还可以利用Redis的相关方法,在Redis服务器上写入公钥,进而可以使用对应私钥直接登录目标服务器。
更详细的漏洞描述和重现方式见:https://cloud.tencent.com/developer/article/1039000
漏洞出现的核心的原因有以下几点:
- Redis未设置密码
- 利用了Redis的
config set
命令动态修改Redis配置 - 使用了root账号权限启动Redis
为了避免这样的漏洞,可以参照以下建议:
- Redis一定要设置密码
- 禁止使用下面命令:keys、flushall、flushdb、config set等命令,可以利用rename-command禁用。
- bind:限制网卡,禁止外网网卡访问
- 开启防火墙
- 不要使用root账户启动Redis
- 尽量不使用默认的端口
7.3.4 内存划分和内存配置
当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。Redis的内存占用一般包括三个方面:
-
1)数据内存:Redis最主要的部分,存储Redis的键值信息,主要问题是BigKey问题、内存碎片问题。
- 碎片问题:Redis底层分配有自己的分配策略。例如当前Key只需要10个字节,此时分配8字节肯定不够,那么底层就会分配16个字节,多出来的6个字节就不能被使用,也就时产生了碎片。
-
2)进程内存:Redis主进程本身运⾏需要占⽤的内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与Redis数据占⽤的内存相⽐可以忽略。
-
3)缓冲区内存:一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出。
Redis提供了一些命令,用于查询Redis目前的内存分配状态:
info memory
:查看内存分配状态
memory usage key
:查看某个key的内存
127.0.0.1:6379> memory usage test:pipeline:key_195
(integer) 80
内存缓冲区常见的有三种:
- 复制缓冲区:主从复制的repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。可以通过
repl-backlog-size
配置来设置,默认1mb。 - AOF缓冲区:AOF执行
rewrite
之前的缓冲区。无法设置容量上限。 - 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置,输出缓冲区可以设置。
一般复制缓冲区和AOF缓冲区不会有问题,可能有问题的是客户端缓冲区。
客户端缓冲区指的就是客户端向Redis发送命令时,用来缓存命令的一个输入端缓冲区,以及Redis向客户端返回数据的输出缓存区。输入缓冲区最大1G且不能设置,一般不会有问题,如果超过了这个空间,Redis会直接断开,因为此时此刻就代表着Redis处理不过来了。
因此输入端缓冲区并不需要担心,要担心的时输出端缓冲区。 客户端缓冲区的配置如下:
如上图所示,client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
配置的几个参数含义如下:
<class>
:客户端类型,包括normal(普通客户端)、replica(主从复制客户端)、pubsub(PubSub客户端)<hard limit>
:缓冲区上限,超过hard limit后断开客户端<soft limit> <soft seconds>
:持续时间限制,当内存大小达到soft limit,并持续soft seconds秒后,断开客户端
7.3.5 集群or主从?
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:
7.3.5.1 集群完整性问题
在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务。
在实际开发中,最重要的是可用性,即使有slot不能使用,Redis集群也应该可以对外提供服务。为此,需要把如下配置修改成no:
# 默认为yes
cluster-require-full-coverage no
7.3.5.2 集群带宽问题
集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:插槽信息、集群状态信息等。
集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高,这样会导致集群中大量的带宽都会被Ping信息所占用。
解决方案主要有:
- 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
- 避免在单个物理机中运行太多Redis实例。
- 配置合适的cluster-node-timeout值。
7.3.5.3 其他问题
- 数据倾斜问题
- 客户端性能问题
- 命令的集群兼容性问题
- lua和事务问题
7.3.5.4 结论
单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,所以如果不是在万不得已的情况下,尽量不搭建Redis集群。
…
本节完。本节所涉及的代码和资源可从git仓库下载:https://gitee.com/weidag/redis_learning.git
更多内容请查阅分类专栏:Redis从入门到精通
感兴趣的读者还可以查阅我的另外几个专栏:
- SpringBoot源码解读与原理分析(已完结)
- MyBatis3源码深度解析(已完结)
- 再探Java为面试赋能(持续更新中…)