# 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: 

# 整数回复

整数回复:当命令的执行结果是整数时,返回结果就是整数回复,例如increxistsdeldbsize返回结果都是整数,例如执行incr counter返回结果就是“:”加上整数:

incr counter
:1

# 字符串回复

字符串回复:当命令的执行结果是字符串时,返回结果就是字符串回复。例如gethget返回结果都是字符串,例如get hello的结果。

get hello
$5
world

# 多条字符串回复

多条字符串回复:当命令的执行结果是多条字符串时,返回结果就是多条字符串回复。例如mgethgetalllrange等命令会返回多个结果,例如下面操作:

首先使用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提供了mgetmset方法,但是并没有提供mdel方法,如果想实现这个功能,可以借助Pipeline来模拟批量删除,虽然不会像mgetmset那样是一个原子命令,但是在绝大数场景下可以使用。下面,我们来实现一下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,我们进行了以下操作:

  1. 利用jedis对象调用jedis.pipelined()方法生成一个pipeline对象。

  2. 将del命令封装到pipeline中,可以调用pipeline.del(String key),这个方法和jedis.del(String key)的写法是完全一致的,只不过此时不会真正的执行命令。

  3. 使用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 setNameclient 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>:指的是客户端的类型,分为三种。

    1. normal:普通客户端
    2. slave:slave客户端,用于复制
    3. 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,一个执行setgetping命令,另一个执行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)

对于客户端无法从连接池获取到连接的原因,一般有以下几种可能:

客户端方面:

  1. 高并发下连接池设置过小,出现供不应求,所以会出现上面的错误,但是正常情况下只要比默认的最大连接数(8个)多一些即可,因为正常情况下JedisPool以及Jedis的处理效率足够高。
  2. 没有正确使用连接池,比如没有进行释放。
  3. 存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢。

服务端方面:

  1. 客户端是正常的,但是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。

LastUpdated: 3/11/2021, 7:31:36 PM