# Redis客户端的意义
Redis是用单线程来处理多个客户端的访问,因此,作为Redis的开发和运维人员需要了解Redis服务端和客户端的通信协议,以及主流编程语言的Redis客户端使用方法,同时还需要了解客户端管理的相应API以及开发运维中可能遇到的问题。
# 客户端通信协议
几乎所有的主流编程语言都有Redis的客户端(http://redis.io/clients),站在技术的角度看原因有两个:
- 客户端与服务端之间的通信协议是在TCP协议之上构建的。
- Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。
例如客户端发送一条set hello world
命令给服务端,按照RESP的标准,客户端需要将其封装为如下格式(每行用\r\n分隔)。
*3
$3
SET
$5
hello
$5
world
这样Redis服务端能够按照RESP将其解析为set hello world
命令,执行后回复的格式如下:
+OK
可以看到除了命令(set hello world)和返回结果(OK)本身还包含了一些特殊字符以及数字,下面将对这些格式进行说明。
# 客户端协议定义的发送命令格式
RESP的规定一条命令的格式如下,CRLF代表"\r\n"。
*<参数数量> CRLF
248
$<参数1的字节数量> CRLF
<参数1> CRLF
...
$<参数N的字节数量> CRLF
<参数N> CRLF
以set hello world
命令进行说明,参数数量为3,所以,第一行显示:
*3
后面参数的字节数分别是355,因此后面几行为:
$3
SET
$5
hello
$5
world
需要注意的是,上面只是格式化显示的结果,实际传输格式为如下代码:
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
# 客户端协议定义的返回结果格式
Redis的返回结果类型分为以下五种:
- 状态回复:在RESP中第一个字节为"+"
- 错误回复:在RESP中第一个字节为"-"
- 整数回复:在RESP中第一个字节为":"
- 字符串回复:在RESP中第一个字节为"$"
- 多条字符串回复:在RESP中第一个字节为"*"
我们知道redis-cli只能看到最终的执行结果,那是因为redis-cli本身就是按照RESP进行结果解析的,所以看不到中间结果,redis-cli.c源码对命令结果的解析结构如下:
static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
sds out = sdsempty();
switch (r->type) {
case REDIS_REPLY_ERROR:
// 处理错误回复
case REDIS_REPLY_STATUS:
// 处理状态回复
case REDIS_REPLY_INTEGER:
// 处理整数回复
case REDIS_REPLY_STRING:
// 处理字符串回复
case REDIS_REPLY_NIL:
// 处理空
case REDIS_REPLY_ARRAY:
// 处理多条字符串回复
return out;
}
为了看到Redis服务端返回的“真正”结果,可以使用nc
命令、telnet
命令、甚至写一个socket程序进行模拟。下面以nc
命令进行演示,首先使用nc127.0.0.16379连接到Redis:
nc 127.0.0.1 6379
若提示命令不存在,则需要安装nc
命令。
yum install -y nc
# 状态回复
状态回复::set hello world的返回结果为+OK:
set hello world
+OK
# 错误回复
错误回复:由于sethx这条命令不存在,那么返回结果就是"-"号加上错误消息:
sethx
-ERR unknown command `sethx`, with args beginning with:
# 整数回复
整数回复:当命令的执行结果是整数时,返回结果就是整数回复,例如incr
、exists
、del
、dbsize
返回结果都是整数,例如执行incr counter返回结果就是“:”加上整数:
incr counter
:1
# 字符串回复
字符串回复:当命令的执行结果是字符串时,返回结果就是字符串回复。例如get
、hget
返回结果都是字符串,例如get hello
的结果。
get hello
$5
world
# 多条字符串回复
多条字符串回复:当命令的执行结果是多条字符串时,返回结果就是多条字符串回复。例如mget
、hgetall
、lrange
等命令会返回多个结果,例如下面操作:
首先使用mset
命令设置多个键值对:
mset java jedis python redis-py
+OK
然后执行mget命令返回多个结果,第一个*2代表返回结果的个数,后面的格式是和字符串回复一致的。
mget java python
*2
$5
jedis
$8
redis-py
有一点需要注意,无论是字符串回复还是多条字符串回复,如果有nil值,那么会返回$-1。 例如,对一个不存在的键执行get操作,返回结果为:
get not_exist_key
$-1
如果批量操作中包含一条为nil值的结果,那么返回结果如下:
mget hello not_exist_key java
*3
$5
world
$-1
$5
jedis
# Java客户端-Jedis
Java有很多优秀的Redis客户端(详见:http://redis.io/clients#java),这里介绍使用较为广泛的客户端Jedis。
# 获取Jedis
Jedis属于Java的第三方开发包,在Java中获取第三方开发包通常有两种方式:
- 直接下载目标版本的Jedis-${version}.jar包加入到项目中。
- 使用集成构建工具,例如maven、gradle等将Jedis目标版本的配置加入到项目中。
通常在实际项目中使用第二种方式,但如果只是想测试一下Jedis,第一种方法也是可以的。以Maven为例子,在项目中加入下面的依赖即可。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.2</version>
</dependency>
TIP
对于第三方开发包,版本的选择也是至关重要的,因为Redis更新速度比较快,如果客户端跟不上服务端的速度,有些特性和bug不能及时更新,不利于日常开发。通常来讲选取第三方开发包有如下两个策略:
- 选择比较稳定的版本,也就是尽可能选择稳定的里程碑版本,这些版本已经经过多次alpha,beta的修复,基本算是稳定了。
- 选择更新活跃的第三方开发包,例如Redis3.0有了Redis Cluster新特性,但是如果使用的客户端一直不支持,并且维护的人也比较少,这种就谨慎选择。
# Jedis的基本使用方法
Jedis的使用方法非常简单,只要下面三行代码就可以实现get功能:
# 1. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信
Jedis jedis = new Jedis("127.0.0.1", 6379);
# 2. jedis执行set操作
jedis.set("hello", "world");
# 3. jedis执行get操作, value="world"
String value = jedis.get("hello");
可以看到初始化Jedis需要两个参数:Redis实例的IP和端口,除了这两个参数外,还有一个包含了四个参数的构造函数是比较常用的。
Jedis(final String host, final int port, final int connectionTimeout, final int
soTimeout)
各个参数的含义如下:
- host:Redis实例的所在机器的IP
- port:Redis实例的端口
- connectionTimeout:客户端连接超时
- soTimeout:客户端读写超时
下面我们尝试使用一下Jedis字符串命令
String setResult = jedis.set("hello", "world");
String getResult = jedis.get("hello");
System.out.println(setResult);
System.out.println(getResult);
输出结果为:
OK
world
可以看到jedis.set的返回结果是OK,和redis-cli的执行效果是一样的,只不过结果类型变为了Java的数据类型。上面的这种写法只是为了演示使用,在实际项目中比较推荐使用try catch finally的形式来进行代码的书写,主要有以下两点理由:
- 一方面可以在Jedis出现异常的时候(本身是网络操作),将异常进行捕获或者抛出。
- 另一个方面无论执行成功或者失败,将Jedis连接关闭掉,在开发中关闭不用的连接资源是一种好的习惯。
Jedis jedis = null;
try {
jedis = new Jedis("127.0.0.1", 6379);
jedis.get("hello");
} catch (Exception e) {
logger.error(e.getMessage(),e);
} finally {
if (jedis != null) {
jedis.close();
}
}
下面是Jedis对5种数据结构的基本使用方法:
// 1.string
// 输出结果:OK
jedis.set("hello", "world");
// 输出结果:world
jedis.get("hello");
// 输出结果:1
jedis.incr("counter");
// 2.hash
jedis.hset("myhash", "f1", "v1");
jedis.hset("myhash", "f2", "v2");
// 输出结果:{f1=v1, f2=v2}
jedis.hgetAll("myhash");
// 3.list
jedis.rpush("mylist", "1");
jedis.rpush("mylist", "2");
jedis.rpush("mylist", "3");
258
// 输出结果:[1, 2, 3]
jedis.lrange("mylist", 0, -1);
// 4.set
jedis.sadd("myset", "a");
jedis.sadd("myset", "b");
jedis.sadd("myset", "a");
// 输出结果:[b, a]
jedis.smembers("myset");
// 5.zset
jedis.zadd("myzset", 99, "tom");
jedis.zadd("myzset", 66, "peter");
jedis.zadd("myzset", 33, "james");
// 输出结果:[[["james"],33.0], [["peter"],66.0], [["tom"],99.0]]
jedis.zrangeWithScores("myzset", 0, -1);
参数除了可以是字符串,Jedis还提供了字节数组的参数,例如:
public String set(final String key, String value)
public String set(final byte[] key, final byte[] value)
public byte[] get(final byte[] key)
public String get(final String key)
有了这些API的支持,就可以将Java对象序列化为二进制,当应用需要获取Java对象时,使用get(final byte[]key)函数将字节数组取出,然后反序列化为Java对象即可。
TIP
和很多NoSQL数据库(例如Memcache、Ehcache)的客户端不同,Jedis本身没有提供序列化的工具,开发者需要自己引入序列化的工具,一般我们将其序列化为Json字符串,通过Gson等Json相关依赖提供序列化和反序列化功能。
# Jedis连接池的使用方法
在上一节中,我们使用的是Jedis的直连方式,所谓直连是指Jedis每次都会新建TCP连接,使用后再断开连接,对于频繁访问Redis的场景显然不是高效的使用方式。
因此生产环境中一般使用连接池的方式对Jedis连接进行管理,所有Jedis对象预先放在池子中(JedisPool),每次要连接Redis,只需要在池子中借用即可,用完后再将Jedis对象归还给池子。
# 直连和连接池方式的优缺点
客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连接,而连接池的方式是可以预先初始化Jedis连接,所以每次只需要从Jedis连接池借用即可,而借用和归还操作是在本地进行的,只有少量的并发同步开销,远远小于新建TCP连接的开销。另外直连的方式无法限制Jedis对象的个数,在极端情况下可能会造成连接泄露,而连接池的形式可以有效的保护和控制资源的使用。
下表我们将列出直连和使用连接池方式的优缺点。
优点 | 缺点 | |
---|---|---|
直连 | 简单方便,适用于少量长期连接的情况 | 存在每次需要连接/关闭TCP连接开销的情况 资源无法控制,极端情况会出现连接泄漏 Jedis对象线程不安全 |
连接池 | 无需每次都生成redis对象,降低开销 使用连接池的形式保护和控制资源的使用 |
# 连接池方式使用Jedis
Jedis提供了JedisPool这个类作为对Jedis的连接池,同时使用了Apache的通用对象池工具common-pool作为资源的管理工具,下面是使用JedisPool操作Redis的代码示例:
# 配置Jedis连接池
// common-pool连接池配置,这里使用默认配置,后面小节会介绍具体配置说明
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
# 获取Jedis对象并使用
Jedis jedis = null;
try {
// 1. 从连接池获取jedis对象
jedis = jedisPool.getResource();
// 2. 执行操作
jedis.get("hello");
} catch (Exception e) {
logger.error(e.getMessage(),e);
} finally {
if (jedis != null) {
// 如果使用JedisPool,close操作不是关闭连接,代表归还连接池
jedis.close();
}
}
这里可以看到在finally中依然是jedis.close()
操作,为什么会把连接关闭呢,这不和连接池的原则违背了吗?但实际上Jedis的close()
方法实现方式如下:
public void close() {
// 使用Jedis连接池
if (dataSource != null) {
if (client.isBroken()) {
this.dataSource.returnBrokenResource(this);
} else {
this.dataSource.returnResource(this);
}
// 直连
} else {
client.close();
}
}
针对这段代码,我们进行参数说明:
dataSource!=null代表使用的是连接池,所以jedis.close()代表归还连接给连接池,而且Jedis会判断当前连接是否已经断开。
dataSource=null代表直连,jedis.close()代表关闭连接。
# Jedis连接池常用的配置
前面我们使用的是GenericObjectPoolConfig的默认配置,实际它提供了很多参数:
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 设置最大连接数为默认值的5倍
poolConfig.setMaxTotal(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL * 5);
// 设置最大空闲连接数为默认值的3倍
poolConfig.setMaxIdle(GenericObjectPoolConfig.DEFAULT_MAX_IDLE * 3);
// 设置最小空闲连接数为默认值的2倍
poolConfig.setMinIdle(GenericObjectPoolConfig.DEFAULT_MIN_IDLE * 2);
// 设置开启jmx功能
poolConfig.setJmxEnabled(true);
// 设置连接池没有连接后客户端的最大等待时间(单位为毫秒)
poolConfig.setMaxWaitMillis(3000);
下表罗列出GenericObjectPoolConfig的一些重要属性
参数名 | 含义 | 默认值 |
---|---|---|
maxActive | 连接池中的最大连接数 | 8 |
maxIdle | 连接池中最大的空闲连接数 | 8 |
minIdle | 连接池中最小的空闲连接数 | 0 |
maxWaitMillis | 当连接池资源用尽后,调用者的最大等待时间,单位为毫秒,一般不建议用默认值 | -1,表示永远不超时,一直等 |
testOnBorrow | 向连接池借用连接时是否做连接有效性检测,无效连接将被移除,每次连接多执行一次ping命令 | false |
testOnReturn | 向连接池借用归还时是否做连接有效性检测,无效连接将被移除,每次连接多执行一次ping 命令 | false |
testWhileIdle | 向连接池借用连接时是否做连接空闲检测,空闲超时的连接将被移除,每次连接多执行一次ping 命令 | false |
blockWhenExhausted | 当连接池资源耗尽时,是否要等待。这个参数和maxWaitMillis参数配合使用,只有当此参数为true时,maxWaitMillis才会生效 | true |
# Jedis使用Pipeline
在前面,我们已经介绍了Pipeline,本节我们将通过Jedis使用Redis的Pipeline特性。
我们知道Redis提供了mget
、mset
方法,但是并没有提供mdel
方法,如果想实现这个功能,可以借助Pipeline来模拟批量删除,虽然不会像mget
和mset
那样是一个原子命令,但是在绝大数场景下可以使用。下面,我们来实现一下mdel
命令。
这里我们没有写try catch finally
,没有关闭jedis。
public void mdel(List<String> keys) {
Jedis jedis = new Jedis("127.0.0.1");
// 1)生成pipeline对象
Pipeline pipeline = jedis.pipelined();
// 2)pipeline执行命令,注意此时命令并未真正执行
for (String key : keys) {
pipeline.del(key);
}
// 3)执行命令
pipeline.sync();
}
可以看到,为了使用Pipeline,我们进行了以下操作:
利用jedis对象调用
jedis.pipelined()
方法生成一个pipeline对象。将del命令封装到pipeline中,可以调用
pipeline.del(String key)
,这个方法和jedis.del(String key)
的写法是完全一致的,只不过此时不会真正的执行命令。使用
pipeline.sync()
方法完成此次pipeline对象的调用。
除了pipeline.sync()
,还可以使用pipeline.syncAndReturnAll()
将pipeline的命令进行返回,例如下面代码将set和incr做了一次pipeline操作,并顺序打印了两个命令的结果:
Jedis jedis = new Jedis("127.0.0.1");
Pipeline pipeline = jedis.pipelined();
pipeline.set("hello", "world");
pipeline.incr("counter");
List<Object> resultList = pipeline.syncAndReturnAll();
for (Object object : resultList) {
System.out.println(object);
}
执行程序,输出结果:
OK
1
# Redis客户端管理
Redis提供了客户端相关API对其状态进行监控和管理,本节将深入介绍各个API的使用方法以及在开发运维中可能遇到的问题。
client list
命令能列出与Redis服务端相连的所有客户端连接信息。
127.0.0.1:6379> client list
id=331 addr=127.0.0.1:54304 fd=8 name= age=42 idle=34 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=client
id=332 addr=127.0.0.1:54344 fd=9 name= age=5 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
输出结果的每一行代表一个客户端的信息,可以看到每行包含了十几个属性,它们是每个客户端的一些执行状态,理解这些属性对于Redis的开发和运维人员非常有帮助。下面将选择几个重要的属性进行说明。
# 客户端标识
Redis客户端标识总共有四个:id、addr、fd、name。下面解释各种标识的含义。
- id :客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0。
- addr :客户端连接的ip和端口。
- fd:socket的文件描述符,与
lsof
命令结果中的fd是同一个,如果fd=-1代表当前客户端不是外部客户端,而是Redis内部的伪装客户端。 - name:客户端的名字,后面的
client setName
和client getName
两个命令会对其进行说明。
# 输入缓冲区
输入缓冲区需要查看两个指标:qbuf、qbuf-free。
Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能。
client list
中qbuf和qbuf-free分别代表这个缓冲区的总容量和剩余容量,Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。
# 输入缓冲区使用不当产生的问题
输入缓冲使用不当会产生两个问题:
- 一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭。
- 输入缓冲区不受maxmemory控制,假设一个Redis实例设置了maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况。
# 输入缓冲区过大的原因
输入缓冲区过大主要有以下两大原因:
- 主要是因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey,从而造成了输入缓冲区过大的情况。
- 还有一种情况就是Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区,造成了输入缓冲区过大。
# 快速发现和监控缓冲区的方法
监控输入缓冲区异常的方法有以下两种:
通过定期执行
client list
命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。通过info命令的
info clients
模块,找到最大的输入缓冲区,例如下面命令中的client_biggest_input_buf
代表最大的输入缓冲区,例如可以设置超过10M就进行报警。127.0.0.1:6379> info clients # Clients connected_clients:2 client_recent_max_input_buffer:2 client_recent_max_output_buffer:0 blocked_clients:0
这两种方式各有利弊:
命令 | 优点 | 缺点 |
---|---|---|
client list | 能精确分析每个客户端来定位问题 | 执行速度较慢,频繁执行存在阻塞Redis的可能 |
info clients | 执行速度块,分析过程比较简单 | 不能精确定位到客户端 不能显示所有输入缓冲区的总量,只能显示最大的缓冲区大小 |
# 输出缓冲区
Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲。
与输入缓冲区不同的是,输出缓冲区的容量可以通过参数client-outputbuffer-limit
来进行设置,并且输出缓冲区做得更加细致,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端。我们可以根据不同的客户端类型进行相应的设置。
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
参数说明:
<class>
:指的是客户端的类型,分为三种。- normal:普通客户端
- slave:slave客户端,用于复制
- pubsub:发布订阅客户端
<hard limit>
:如果客户端使用的输出缓冲区大于,客户端会被立即关闭。<soft limit>
和<soft seconds>
:如果客户端使用的输出缓冲区超过了并且持续了秒,客户端会被立即关闭。
Redis的默认配置是:
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
和输入缓冲区相同的是,输出缓冲区也不会受到maxmemory的限制,如果使用不当同样会造成maxmemory用满产生的数据丢失、键值淘汰、OOM等情况。
实际上,输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果,固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲 区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果。
client list
中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表使用的字节数。
# 监控输出缓冲区的方法
监控输出缓冲区的方法依然有两种:
通过定期执行
client list
命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。通过info命令的
info clients
模块,找到输出缓冲区列表最大对象数。127.0.0.1:6379> info clients # Clients connected_clients:2 client_recent_max_input_buffer:2 client_recent_max_output_buffer:0 blocked_clients:0
相比于输入缓冲区,输出缓冲区出现异常的概率相对会比较大,可以采取以下措施预防。
# 预防输出缓冲区异常的方法
预防输出缓冲区异常需要从以下几个方面着手:
·进行监控,设置阀值,超过阀值及时处理。
·限制普通客户端输出缓冲区的,把错误扼杀在摇篮中。
适当增大slave的输出缓冲区的,如果master节点写入较大,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连。
限制容易让输出缓冲区增大的命令,例如,高并发下的monitor命令就是一个危险的命令。
及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。
# 客户端的存活状态
client list
中的age和idle分别代表当前客户端已经连接的时间(单位:秒)和最近一次的空闲时间。当age等于idle时,说明连接一直处于空闲状态。
# 客户端的限制maxclients和timeout
Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients
来查询当前Redis的连接数。
可以通过config set maxclients
对最大客户端连接数进行动态设置:
127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "10000"
127.0.0.1:6379> config set maxclients 50
OK
127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "50"
127.0.0.1:6379> config set maxclients 10000
OK
一般来说maxclients=10000在大部分场景下已经绝对够用,但是某些情况由于业务方使用不当(例如没有主动关闭连接)可能存在大量idle连接,无论是从网络连接的成本还是超过maxclients的后果来说都不是什么好事,因此,Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一旦客户端连接的idle时间超过了timeout,连接将会被关闭。
#Redis默认的timeout是0,也就是不会检测客户端的空闲
127.0.0.1:6379> config set timeout 30
OK
Redis的默认配置给出的timeout=0,在这种情况下客户端基本不会出现JedisConnectionException(并提示Unexpected end of stream)异常,这是基于对客户端开发的一种保护。例如很多开发人员在使用JedisPool时不会对连接池对象做空闲检测和验证,如果设置了timeout>0,可能就会出现上面的异常,对应用业务造成一定影响,但是如果Redis的客户端使用不当或者客户端本身的一些问题,造成没有及时释放客户端连接,可能会造成大量的idle连接占据着很多连接资源,一旦超过maxclients;后果也是不堪设想。所在在实际开发和运维中,需要将timeout设置成大于0。
# 客户端类型
client list
中的flag是用于标识当前客户端的类型,例如flag=S代表当前客户端是slave客户端、flag=N代表当前是普通客户端,flag=O代表当前客户端正在执行monitor命令。
下表列出常见的客户端类型:
序号 | 客户端类型 | 说明 |
---|---|---|
1 | N | 普通客户端 |
2 | M | 当前客户端是master节点 |
3 | S | 当前客户端是slave节点 |
4 | O | 当前客户端正在执行Monitor命令 |
5 | x | 当前客户端正在执行事务 |
6 | b | 当前客户端正在等待阻塞事件 |
7 | i | 当前客户端正在等待VM I/O,此状态已经废弃不用 |
8 | d | 一个受监视的键已经被修改,exec命令将失败 |
9 | u | 客户端未被阻塞 |
10 | c | 回复完整输出后,关闭连接 |
11 | A | 尽可能块地关闭连接 |
# 设置和获取客户端名称
client setName xx
client getName
client setName
用于给客户端设置名字,这样比较容易标识出客户端的来源,我们可以先查看一下客户端信息:
127.0.0.1:6379> client list
id=341 addr=127.0.0.1:40098 fd=8 name= age=19 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
例如将当前客户端命名test_client,可以执行如下操作:
127.0.0.1:6379> client setname test_client
OK
再次执行上一条命令,查看客户端信息:
127.0.0.1:6379> client list
id=341 addr=127.0.0.1:40098 fd=8 name=test_client age=227 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
可以看到,此时name字段的值为test_client。
如果想直接查看当前客户端的name,可以使用client getName
命令,例如下面的操作:
127.0.0.1:6379> client getName
"test_client"
TIP
client getName和setName命令可以做为标识客户端来源的一种方式,但是通常来讲,在Redis只有一个应用方使用的情况下,IP和端口作为标识会更加清晰。当多个应用方共同使用一个Redis,那么此时client setName可以 作为标识客户端的一个依据。
# 杀掉指定IP地址和端口的客户端
client kill ip:port
此命令用于杀掉指定IP地址和端口的客户端,例如由于一些原因(例如设置timeout=0时产生的长时间idle的客户端),需要手动杀掉客户端连接时,可以使用client kill
命令。
# 阻塞客户端连接
若我们想要阻塞客户端连接,可以使用client pause
命令。
client pause timeout(毫秒)
一般情况下,client pause
命令在以下场景起作用:
client pause
只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,也就是此期间主从复制是正常进行的,所以此命令可以用来让主从复制保持一致。client pause
可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点。
需要注意的是在生产环境中,暂停客户端成本非常高。
# 监控Redis正在执行的命令
monitor
命令可以用于监控Redis正在执行的命令,为了演示,我们打开两个redis-cli,一个执行set
、get
、 ping
命令,另一个执行monitor
命令。可以看到monitor命令能够监听其他客户端正在执行的命令,并记录了详细的时间戳。
第一个Redis客户端:
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> ping
PONG
第二个Redis客户端:
127.0.0.1:6379> monitor
OK
1570589510.267950 [0 127.0.0.1:40098] "set" "hello" "world"
1570589514.813916 [0 127.0.0.1:40098] "get" "hello"
1570589516.514599 [0 127.0.0.1:40098] "ping"
monitor
的作用很明显,如果开发和运维人员想监听Redis正在执行的命令,就可以用monitor
命令。但是每个客户端都有自己的输出缓冲区,既然monitor
能监听到所有的命令,一旦Redis的并发量过大,monitor
客户端的输出缓冲会暴涨,可能瞬间会占用大量内存。
# 客户端相关配置
前面已经介绍了一些客户端配置,接下来继续介绍剩余配置。
timeout:检测客户端空闲连接的超时时间,一旦idle时间达到了timeout,客户端将会被关闭,如果设置为0就不进行检测。
maxclients:客户端最大连接数,但是这个参数会受到操作系统设置的限制。
tcp-keepalive:检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源。
tcp-backlog:TCP三次握手后,会将接受的连接放入队列中,tcpbacklog就是队列的大小,它在Redis中的默认值是511。通常来讲这个参数不需要调整,但是这个参数会受到操作系统的影响,例如在Linux操作系统 中,如果/proc/sys/net/core/somaxconn小于tcp-backlog的默认值,那么在Redis启动时会看到如下日志,并建议将/proc/sys/net/core/somaxconn设置更大。
WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/ sys/net/core/somaxconn is set to the lower value of 128.
修改方法也非常简单,只需要执行如下命令:
echo 511 > /proc/sys/net/core/somaxconn
# 客户端统计片段
下面,我们先执行以下info clients
命令。
127.0.0.1:6379> info clients
# Clients
connected_clients:2
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
connected_clients
:代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。client_longest_output_list
:当前所有输出缓冲区中队列对象个数的最大值。client_biggest_input_buf
:当前所有输入缓冲区中占用的最大容量。blocked_clients
:正在执行阻塞命令(例如blpop、brpop、brpoplpush)的客户端个数。
除此之外info stats
中还包含了两个客户端相关的统计指标:
# Stats
total_connections_received:135
total_commands_processed:200185
instantaneous_ops_per_sec:0
total_net_input_bytes:8103762
total_net_output_bytes:1884895
instantaneous_input_kbps:0.00
instantaneous_output_kbps:0.00
rejected_connections:205
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
expired_stale_perc:0.00
expired_time_cap_reached_count:0
evicted_keys:0
keyspace_hits:100011
keyspace_misses:19
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:232
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0
在这些统计中,最重要的两个指标:
total_connections_received:Redis
自启动以来处理的客户端连接数总数。rejected_connections:Redis
自启动以来拒绝的客户端连接数,需要重点监控。
# 客户端常见异常
在客户端的使用过程中,无论是客户端使用不当还是Redis服务端出现问题,客户端会反应出一些异常。本小节将分析一下Jedis使用过程中常见的异常情况。
# 无法从连接池获取到连接
JedisPool中的Jedis对象个数是有限的,默认是8个。这里假设使用的默认配置,如果有8个Jedis对象被占用,并且没有归还,此时调用者还要从JedisPool中借用Jedis,就需要进行等待(例如设置了maxWaitMillis>0),如果在maxWaitMillis时间内仍然无法获取到Jedis对象就会抛出如下异常:
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource
from the pool
…
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.
java:449)
还有一种情况,就是设置了blockWhenExhausted=false,那么调用者发现池子中没有资源时,会立即抛出异常不进行等待,下面的异常就是blockWhenExhausted=false时的效果:
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource
from the pool
…
Caused by: java.util.NoSuchElementException: Pool exhausted
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.
java:464)
对于客户端无法从连接池获取到连接的原因,一般有以下几种可能:
客户端方面:
- 高并发下连接池设置过小,出现供不应求,所以会出现上面的错误,但是正常情况下只要比默认的最大连接数(8个)多一些即可,因为正常情况下JedisPool以及Jedis的处理效率足够高。
- 没有正确使用连接池,比如没有进行释放。
- 存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢。
服务端方面:
- 客户端是正常的,但是Redis服务端由于一些原因造成了客户端命令执行过程的阻塞,也会使得客户端抛出这种异常。
# 客户端读写超时
Jedis在调用Redis时,如果出现了读写超时后,会出现下面的异常:
redis.clients.jedis.exceptions.JedisConnectionException:
java.net.SocketTimeoutException: Read timed out
造成该异常的原因也有以下几种:
- 读写超时间设置得过短。
- 命令本身就比较慢。
- 客户端与服务端网络不正常。
- Redis自身发生阻塞。
# 客户端连接超时
Jedis在调用Redis时,如果出现了连接超时后,会出现下面的异常:
redis.clients.jedis.exceptions.JedisConnectionException:
java.net.SocketTimeoutException: connect timed out
造成该异常的原因也有以下几种:
连接超时设置得过短,可以通过下面代码进行设置:
// 毫秒 jedis.getClient().setConnectionTimeout(time);
Redis发生阻塞,造成tcp-backlog已满,造成新的连接失败。
客户端与服务端网络不正常。
# 客户端缓冲区异常
Jedis在调用Redis时,如果出现客户端数据流异常,会出现下面的异常:
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
造成这个异常的原因可能有如下几种:
- 输出缓冲区满。例如将普通客户端的输出缓冲区设置为1M60KB,如果使用get命令获取一个bigkey(例如3M),就会出现这个异常。
- 长时间闲置连接被服务端主动断开。
- 不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现上述异常。
# Redis正在加载持久化文件
Jedis调用Redis时,如果Redis正在加载持久化文件,那么会收到下面的异常:
redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the
dataset in memory
# Redis使用的内存超过maxmemory配置
Jedis执行写操作时,如果Redis的使用内存大于maxmemory的设置,会收到下面的异常,此时应该调整maxmemory并找到造成内存增长的原因:
redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when
used memory > 'maxmemory'.
# 客户端连接数过大
如果客户端连接数超过了maxclients,新申请的连接就会出现如下异常:
redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients reached
此时新的客户端连接执行任何命令,返回结果都是如下:
127.0.0.1:6379> get hello
(error) ERR max number of clients reached
这个问题可能会比较棘手,因为此时无法执行Redis命令进行问题修复,一般来说可以从两个方面进行着手解决:
客户端方面:如果maxclients参数不是很小的话,应用方的客户端连接数基本不会超过maxclients,通常来看是由于应用方对于Redis客户端使用不当造成的。此时如果应用方是分布式结构的话,可以通过下线部分应用节点(例 如占用连接较多的节点),使得Redis的连接数先降下来。从而让绝大部分节点可以正常运行,此时再通过查找程序bug或者调整maxclients进行问题的修复。
服务端方面:如果此时客户端无法处理,而当前Redis为高可用模式(例如Redis Sentinel和Redis Cluster),可以考虑将当前Redis做故障转移。
此问题不存在确定的解决方式,但是无论从哪个方面进行处理,故障的快速恢复极为重要,当然更为重要的是找到问题的所在,否则一段时间后客户端连接数依然会超过maxclients。