客户端
-
- 1、客户端通信协议
- 2、Java客户端Jedis
- 3、Python客户端redis-py
- 4、客户端管理
-
- 4.1、客户端API
-
- [4.1.1、client list](#4.1.1、client list)
- [4.1.2、client setName和client getName](#4.1.2、client setName和client getName)
- [4.1.3、client kill](#4.1.3、client kill)
- [4.1.4、client pause](#4.1.4、client pause)
- 4.1.5、monitor
- 4.2、客户端相关配置
- 4.3、客户端统计片段
- 5、客户端常见异常
- 6、客户端案例分析
Redis是用单线程来处理多个客户端的访问,因此作为Redis的开发和运维人员需要了解Redis服务端和客户端的通信协议,以及主流编程语言的Redis客户端使用方法,同时还需要了解客户端管理的相应API以及开发运维中可能遇到的问题。
1、客户端通信协议
几乎所有的主流编程语言都有Redis的客户端(http://redis.io/clients),不考虑Redis非常流行的原因,如果站在技术的角度看原因还有两个:第一,客户端与服务端之间的通信协议是在TCP协议之上构建的。第二,Redis制定了RESP(REdis SerializationProtocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。例如客户端发送一条set hello world命令给服务端,按照RESP的标准,客户端需要将其封装为如下格式(每行用\r\n分隔):
bash
*3
$3
SET
$5
hello
$5
world
这样Redis服务端能够按照RESP将其解析为set helloworld命令,执行后回复的格式如下:
bash
+OK
可以看到除了命令(set hello world)和返回结果(OK)本身还包含了一些特殊字符以及数字,下面将对这些格式进行说明。
1.发送命令格式
RESP的规定一条命令的格式如下,CRLF代表"\r\n"。
bash
*<参数数量> CRLF
$<参数1的字节数量> CRLF
<参数1> CRLF
...
$<参数N的字节数量> CRLF
<参数N> CRLF
依然以set hell world这条命令进行说明。
参数数量为3个,因此第一行为:
bash
*3
参数字节数分别是355,因此后面几行为:
bash
$3
SET
$5
hello
$5
world
有一点要注意的是,上面只是格式化显示的结果,实际传输格式为如下代码,整个过程如图所示:
bash
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
2.返回结果格式
Redis的返回结果类型分为以下五种,如图所示:


状态回复:在RESP中第一个字节为"+"。错误回复:在RESP中第一个字节为"-"。整数回复:在RESP中第一个字节为":"。字符串回复:在RESP中第一个字节为"$"。多条字符串回复:在RESP中第一个字节为"*"。
我们知道redis-cli只能看到最终的执行结果,那是因为redis-cli本身就是按照RESP进行结果解析的,所以看不到中间结果,redis-cli.c源码对命令结果的解析结构如下:
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;
}
例如执行set hello world,返回结果是OK,并不能看到加号:
bash
127.0.0.1:6379> set hello world
OK
为了看到Redis服务端返回的"真正"结果,可以使用nc命令、telnet命令、甚至写一个socket程序进行模拟。下面以nc命令进行演示,首先使用nc127.0.0.16379连接到Redis:
bash
nc 127.0.0.1 6379
状态回复:set hello world的返回结果为+OK:
bash
set hello world
+OK
错误回复:由于sethx这条命令不存在,那么返回结果就是"-"号加上错误消息:
bash
sethx
-ERR unknown command 'sethx'
整数回复:当命令的执行结果是整数时,返回结果就是整数回复,例如incr、exists、del、dbsize返回结果都是整数,例如执行incr counter返回结果就是":"加上整数:
bash
incr counter
:1
字符串回复:当命令的执行结果是字符串时,返回结果就是字符串回复。例如get、hget返回结果都是字符串,例如get hello的结果为"$5\r\nworld\r\n":
bash
get hello
$5
world
多条字符串回复:当命令的执行结果是多条字符串时,返回结果就是多条字符串回复。例如mget、hgetall、lrange等命令会返回多个结果,例如下面操作:
首先使用mset设置多个键值对:
bash
mset java jedis python redis-py
+OK
然后执行mget命令返回多个结果,第一个*2代表返回结果的个数,后面的格式是和字符串回复一致的:
bash
mget java python
*2
$5
jedis
$8
redis-py
有一点需要注意,无论是字符串回复还是多条字符串回复,如果有nil值,那么会返回$-1。
例如,对一个不存在的键执行get操作,返回结果为:
bash
get not_exist_key
$-1
如果批量操作中包含一条为nil值的结果,那么返回结果如下:
bash
mget hello not_exist_key java
*3
$5
world
$-1
$5
jedis
有了RESP提供的发送命令和返回结果的协议格式,各种编程语言就可以利用其来实现相应的Redis客户端,后面两节将介绍Java和Python两个编程语言的Redis客户端。
2、Java客户端Jedis
2.1、获取Jedis
Jedis属于Java的第三方开发包,在Java中获取第三方开发包通常有两种方式:
- 直接下载目标版本的Jedis-${version}.jar包加入到项目中。
- 使用集成构建工具,例如maven、gradle等将Jedis目标版本的配置加入到项目中。
通常在实际项目中使用第二种方式,但如果只是想测试一下Jedis,第一种方法也是可以的。在写本书时,Jedis最新发布的稳定版本2.8.2,以Maven为例子,在项目中加入下面的依赖即可:
xml
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.2</version>
</dependency>
对于第三方开发包,版本的选择也是至关重要的,因为Redis更新速度比较快,如果客户端跟不上服务端的速度,有些特性和bug不能及时更新,不利于日常开发。通常来讲选取第三方开发包有如下两个策略:
- 选择比较稳定的版本,也就是尽可能选择稳定的里程碑版本,这些版本已经经过多次alpha,beta的修复,基本算是稳定了。
- 选择更新活跃的第三方开发包,例如Redis3.0有了Redis Cluster新特性,但是如果使用的客户端一直不支持,并且维护的人也比较少,这种就谨慎选择。
2.2、Jedis的基本使用方法
Jedis的使用方法非常简单,只要下面三行代码就可以实现get功能:
java
# 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和端口,除了这两个参数外,还有一个包含了四个参数的构造函数是比较常用的:
java
Jedis(final String host, final int port, final int connectionTimeout, final int
soTimeout)
host:Redis实例的所在机器的IP。port:Redis实例的端口。connectionTimeout:客户端连接超时。soTimeout:客户端读写超时。
java
String setResult = jedis.set("hello", "world");
String getResult = jedis.get("hello");
System.out.println(setResult);
System.out.println(getResult);
输出结果为:
java
OK
world
可以看到jedis.set的返回结果是OK,和redis-cli的执行效果是一样的,只不过结果类型变为了Java的数据类型。上面的这种写法只是为了演示使用,在实际项目中比较推荐使用try catch finally的形式来进行代码的书写:一方面可以在Jedis出现异常的时候(本身是网络操作),将异常进行捕获或者抛出;另一个方面无论执行成功或者失败,将Jedis连接关闭掉,在开发中关闭不用的连接资源是一种好的习惯,代码类似如下:
java
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对于Redis五种数据结构的操作,为了节省篇幅,所有返回结果放在注释中。
java
// 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");
// 输出结果:[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还提供了字节数组的参数,例如:
java
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(finalbytekey)函数将字节数组取出,然后反序列化为Java对象即可。和很多NoSQL数据库(例如Memcache、Ehcache)的客户端不同,Jedis本身没有提供序列化的工具,也就是说开发者需要自己引入序列化的工具。序列化的工具有很多,例如XML、Json、谷歌的Protobuf、Facebook的Thrift等等,对于序列化工具的选择开发者可以根据自身需求决定,下面以protostuff (Protobuf的Java客户端)为例子进行说明。
1)protostuff的Maven依赖:
xml
<protostuff.version>1.0.11</protostuff.version>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>${protostuff.version}</version>
</dependency>
2)定义实体类:
java
// 俱乐部
public class Club implements Serializable {
private int id; // id
private String name; // 名称
private String info; // 描述
private Date createDate; // 创建日期
private int rank; // 排名
// 相应的getter setter不占用篇幅
}
3)序列化工具类ProtostuffSerializer提供了序列化和反序列化方法:
java
package com.sohu.tv.serializer;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import java.util.concurrent.ConcurrentHashMap;
//序列化工具
public class ProtostuffSerializer {
private Schema<Club> schema = RuntimeSchema.createFrom(Club.class);
public byte[] serialize(final Club club) {
final LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_
BUFFER_SIZE);
try {
return serializeInternal(club, schema, buffer);
} catch (final Exception e) {
throw new IllegalStateException(e.getMessage(), e);
} finally {
buffer.clear();
}
}
public Club deserialize(final byte[] bytes) {
try {
Club club = deserializeInternal(bytes, schema.newMessage(), schema);
if (club != null ) {
return club;
}
} catch (final Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
return null;
}
private <T> byte[] serializeInternal(final T source, final Schema<T>
schema, final LinkedBuffer buffer) {
return ProtostuffIOUtil.toByteArray(source, schema, buffer);
}
private <T> T deserializeInternal(final byte[] bytes, final T result, final
Schema<T> schema) {
ProtostuffIOUtil.mergeFrom(bytes, result, schema);
return result;
}
}
4)测试。
生成序列化工具类:
java
ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
生成Jedis对象:
java
Jedis jedis = new Jedis("127.0.0.1", 6379);
序列化:
java
String key = "club:1";
// 定义实体对象
Club club = new Club(1, "AC", "米兰", new Date(), 1);
// 序列化
byte[] clubBtyes = protostuffSerializer.serialize(club);
jedis.set(key.getBytes(), clubBtyes);
反序列化:
java
byte[] resultBtyes = jedis.get(key.getBytes());
// 反序列化[id=1, clubName=AC, clubInfo=米兰, createDate=Tue Sep 15 09:53:18 CST
// 2015, rank=1]
Club resultClub = protostuffSerializer.deserialize(resultBtyes);
2.3、Jedis连接池的使用方法
2.2节介绍的是Jedis的直连方式,所谓直连是指Jedis每次都会新建TCP连接,使用后再断开连接,对于频繁访问Redis的场景显然不是高效的使用方式,如图所示。

因此生产环境中一般使用连接池的方式对Jedis连接进行管理,如图所示,所有Jedis对象预先放在池子中(JedisPool),每次要连接Redis,只需要在池子中借,用完了在归还给池子。

客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连接,而连接池的方式是可以预先初始化好Jedis连接,所以每次只需要从Jedis连接池借用即可,而借用和归还操作是在本地进行的,只有少量的并发同步开销,远远小于新建TCP连接的开销。另外直连的方式无法限制Jedis对象的个数,在极端情况下可能会造成连接泄露,而连接池的形式可以有效的保护和控制资源的使用。但是直连的方式也并不是一无是处,下表给出两种方式各自的优劣势。

Jedis提供了JedisPool这个类作为对Jedis的连接池,同时使用了Apache的通用对象池工具common-pool作为资源的管理工具,下面是使用JedisPool操作Redis的代码示例:
1)Jedis连接池(通常JedisPool是单例的):
java
// common-pool连接池配置,这里使用默认配置,后面小节会介绍具体配置说明
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
2)获取Jedis对象不再是直接生成一个Jedis对象进行直连,而是从连接池直接获取,代码如下:
java
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()实现方式如下:
java
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()代表关闭连接。
前面GenericObjectPoolConfig使用的是默认配置,实际它提供有很多参数,例如池子中最大连接数、最大空闲连接数、最小空闲连接数、连接活性检测,等等,例如下面代码:
java
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几个比较常用的属性,下表给出了Generic-ObjectPoolConfig其他属性及其含义解释。

2.4、Redis中Pipeline的使用方法
Jedis支持Pipeline特性,我们知道Redis提供了mget、mset方法,但是并没有提供mdel方法,如果想实现这个功能,可以借助Pipeline来模拟批量删除,虽然不会像mget和mset那样是一个原子命令,但是在绝大数场景下可以使用。下面代码是mdel删除的实现过程。
这里为了节省篇幅,没有写try catch finally,没有关闭jedis。
java
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();
}
- 利用jedis对象生成一个pipeline对象,直接可以调用jedis.pipelined()。
- 将del命令封装到pipeline中,可以调用pipeline.del(String key),这个方法和jedis.del(Stringkey)的写法是完全一致的,只不过此时不会真正的执行命令。
- 使用pipeline.sync()完成此次pipeline对象的调用。
java
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);
}
输出结果为:
java
OK
1
2.5、Jedis的Lua脚本
Jedis中执行Lua脚本和redis-cli十分类似,Jedis提供了三个重要的函数实现Lua脚本的执行:
java
Object eval(String script, int keyCount, String... params)
Object evalsha(String sha1, int keyCount, String... params)
String scriptLoad(String script)
eval函数有三个参数,分别是:
script:Lua脚本内容。keyCount:键的个数。params:相关参数KEYS和ARGV。
以一个最简单的Lua脚本为例子进行说明:
java
return redis.call('get',KEYS[1])
在redis-cli中执行上面的Lua脚本,方法如下:
java
127.0.0.1:6379> eval "return redis.call('get',KEYS[1])" 1 hello
"world"
在Jedis中执行,方法如下:
java
String key = "hello";
String script = "return redis.call('get',KEYS[1])";
Object result = jedis.eval(script, 1, key);
// 打印结果为world
System.out.println(result)
scriptLoad和evalsha函数要一起使用,首先使用scriptLoad将脚本加载到Redis中,代码如下:
java
String scriptSha = jedis.scriptLoad(script);
evalsha函数用来执行脚本的SHA1校验和,它需要三个参数:
scriptSha:脚本的SHA1。keyCount:键的个数。params:相关参数KEYS和ARGV。
执行效果如下:
java
Stirng key = "hello";
Object result = jedis.evalsha(scriptSha, 1, key);
// 打印结果为world
System.out.println(result);
总体来说,Jedis的使用还是比较简单的,重点注意以下几点即可:
- Jedis操作放在try catch finally里更加合理。
- 区分直连和连接池两种实现方式优缺点。
- Jedis依赖了common-pool,有关common-pool的参数需要根据不同的使用场景,各不相同,需要具体问题具体分析。
- jedis.close()方法的两种实现方式。
- 如果key和value涉及了字节数组,需要自己选择适合的序列化方法。
3、Python客户端redis-py
4、客户端管理
4.1、客户端API
4.1.1、client list
client list命令能列出与Redis服务端相连的所有客户端连接信息,例如下面代码是在一个Redis实例上执行clientlist的结果:
bash
127.0.0.1:6379> client list
id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=300210 addr=10.2.xx.215:61972 fd=3342 name= age=8054103 idle=8054103 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=5448879 addr=10.16.xx.105:51157 fd=233 name= age=411281 idle=331077 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ttl
id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=7125108 addr=10.10.xx.103:33403 fd=139 name= age=241 idle=1 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del
id=7125109 addr=10.10.xx.101:58658 fd=140 name= age=241 idle=1 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del
...
输出结果的每一行代表一个客户端的信息,可以看到每行包含了十几个属性,它们是每个客户端的一些执行状态,理解这些属性对于Redis的开发和运维人员非常有帮助。下面将选择几个重要的属性进行说明,其余通过表格的形式进行展示。
(1)标识:id、addr、fd、name
这四个属性属于客户端的标识:
id:客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0。addr:客户端连接的ip和端口。fd:socket的文件描述符,与lsof命令结果中的fd是同一个,如果fd=-1代表当前客户端不是外部客户端,而是Redis内部的伪装客户端。name:客户端的名字,后面的client setName和clientgetName两个命令会对其进行说明。
** (2)输入缓冲区:qbuf、qbuf-free**
Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能,如图所示。

client list中qbuf和qbuf-free分别代表这个缓冲区的总容量和剩余容量,Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。下面是Redis源码中对于输入缓冲区的硬编码:
java
/* Protocol and I/O related defines */
#define REDIS_MAX_QUERYBUF_LEN (1024*1024*1024) /* 1GB max query buffer. */
输入缓冲使用不当会产生两个问题:
- 一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭。
- 输入缓冲区不受maxmemory控制,假设一个Redis实例设置了maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况(如图所示)。

执行效果如下:
java
127.0.0.1:6390> info memory
# Memory
used_memory_human:5.00G
...
maxmemory_human:4.00G
....
上面已经看到,输入缓冲区使用不当造成的危害非常大,那么造成输入缓冲区过大的原因有哪些?输入缓冲区过大主要是因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey,从而造成了输入缓冲区过大的情况。还有一种情况就是Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区,造成了输入缓冲区过大。
那么如何快速发现和监控呢?监控输入缓冲区异常的方法有两种:
- 通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。
- 通过info命令的info clients模块,找到最大的输入缓冲区,例如下面命令中的其中client_biggest_input_buf代表最大的输入缓冲区,例如可以设置超过10M就进行报警:
bash
127.0.0.1:6379> info clients
# Clients
connected_clients:1414
client_longest_output_list:0
client_biggest_input_buf:2097152
blocked_clients:0
这两种方法各有自己的优劣势,下表对两种方法进行了对比。

输入缓冲区问题出现概率比较低,但是也要做好防范,在开发中要减少bigkey、减少Redis阻塞、合理的监控报警。
(3)输出缓冲区:obl、oll、omem
Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲,如图所示。

与输入缓冲区不同的是,输出缓冲区的容量可以通过参数client-output-buffer-limit来进行设置,并且输出缓冲区做得更加细致,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端,如图所示。

对应的配置规则是:
java
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
<class>:客户端类型,分为三种。a)normal:普通客户端;b)slave:slave客户端,用于复制;c)pubsub:发布订阅客户端。<hard limit>:如果客户端使用的输出缓冲区大于<hard limit>,客户端会被立即关闭。<soft limit>和<soft seconds>:如果客户端使用的输出缓冲区超过了<soft limit>并且持续了<soft limit>秒,客户端会被立即关闭。
Redis的默认配置是:
java
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)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果,例如大的字符串、hgetall、smembers命令的结果等,通过Redis源码中redis.h的redisClient结构体(Redis3.2版本变为Client)可以看到两个缓冲区的实现细节:
java
typedef struct redisClient {
// 动态缓冲区列表
list *reply;
// 动态缓冲区列表的长度(对象个数)
unsigned long reply_bytes;
// 固定缓冲区已经使用的字节数
int bufpos;
// 字节数组作为固定缓冲区
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;
固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果,如图所示。

client list中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表使用的字节数。例如下面代表当前客户端的固定缓冲区的长度为0,动态缓冲区有4869个对象,两个部分共使用了133081288字节=126M内存:
bash
id=7 addr=127.0.0.1:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0 multi=-1
qbuf=0 qbuf-free=0 obl=0 oll=4869 omem=133081288 events=rw cmd=monitor
监控输出缓冲区的方法依然有两种:
- 通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。
- 通过info命令的info clients模块,找到输出缓冲区列表最大对象数,例如:
bash
127.0.0.1:6379> info clients
# Clients
connected_clients:502
client_longest_output_list:4869
client_biggest_input_buf:0
blocked_clients:0
其中,client_longest_output_list代表输出缓冲区列表最大对象数,这两种统计方法的优劣势和输入缓冲区是一样的,这里就不再赘述了。相比于输入缓冲区,输出缓冲区出现异常的概率相对会比较大,那么如何预防呢?方法如下:
- 进行上述监控,设置阀值,超过阀值及时处理。
- 限制普通客户端输出缓冲区的,把错误扼杀在摇篮中,例如可以进行如下设置:
bash
client-output-buffer-limit normal 20mb 10mb 120
- 适当增大slave的输出缓冲区的,如果master节点写入较大,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连。
- 限制容易让输出缓冲区增大的命令,例如,高并发下的monitor命令就是一个危险的命令。
- 及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。
(4)客户端的存活状态
client list中的age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间:
bash
id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
例如上面这条记录代表当期客户端连接Redis的时间为603382秒,其中空闲了331060秒:
bash
id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
例如上面这条记录代表当期客户端连接Redis的时间为8888581秒,其中空闲了8888581秒,实际上这种就属于不太正常的情况,当age等于idle时,说明连接一直处于空闲状态。
为了更加直观地描述age和idle,下面用一个例子进行说明:
java
String key = "hello";
// 1) 生成jedis,并执行get操作
Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println(jedis.get(key));
// 2) 休息10秒
TimeUnit.SECONDS.sleep(10);
// 3) 执行新的操作ping
System.out.println(jedis.ping());
// 4) 休息5秒
TimeUnit.SECONDS.sleep(5);
// 5) 关闭jedis连接
jedis.close();
下面对代码中的每一步进行分析,用client list命令来观察age和idle参数的相应变化。
为了与redis-cli的客户端区分,本次测试客户端IP地址:10.7.40.98。
1)在执行代码之前,client list只有一个客户端,也就是当前的redis-cli,下面为了节省篇幅忽略掉这个客户端。
java
127.0.0.1:6379> client list
id=45 addr=127.0.0.1:55171 fd=6 name= age=2 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
2)使用Jedis生成了一个新的连接,并执行get操作,可以看到IP地址为10.7.40.98的客户端,最后执行的命令是get,age和idle分别是1秒和0秒:
java
127.0.0.1:6379> client list
id=46 addr=10.7.40.98:62908 fd=7 name= age=1 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
3)休息10秒,此时Jedis客户端并没有关闭,所以age和idle一直在递增:
java
127.0.0.1:6379> client list
id=46 addr=10.7.40.98:62908 fd=7 name= age=9 idle=9 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
4)执行新的操作ping,发现执行后age依然在增加,而idle从0计算,也就是不再闲置:
java
127.0.0.1:6379> client list
id=46 addr=10.7.40.98:62908 fd=7 name= age=11 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
5)休息5秒,观察age和idle增加:
java
127.0.0.1:6379> client list
id=46 addr=10.7.40.98:62908 fd=7 name= age=15 idle=5 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
(5)客户端的限制maxclients和timeout
java
127.0.0.1:6379> info clients
# Clients
connected_clients:1414
...
可以通过config set maxclients对最大客户端连接数进行动态设置:
bash
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"
一般来说maxclients=10000在大部分场景下已经绝对够用,但是某些情况由于业务方使用不当(例如没有主动关闭连接)可能存在大量idle连接,无论是从网络连接的成本还是超过maxclients的后果来说都不是什么好事,因此Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一旦客户端连接的idle时间超过了timeout,连接将会被关闭,例如设置timeout为30秒:
java
#Redis默认的timeout是0,也就是不会检测客户端的空闲
127.0.0.1:6379> config set timeout 30
OK
下面继续使用Jedis进行模拟,整个代码和上面是一样的,只不过第2)步骤休息了31秒:
java
String key = "hello";
// 1) 生成jedis,并执行get操作
Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println(jedis.get(key));
// 2) 休息31秒
TimeUnit.SECONDS.sleep(31);
// 3) 执行get操作
System.out.println(jedis.get(key));
// 4) 休息5秒
TimeUnit.SECONDS.sleep(5);
// 5) 关闭jedis连接
jedis.close();
执行上述代码可以发现在执行完第2)步之后,client list中已经没有了Jedis的连接,也就是说timeout已经生效,将超过30秒空闲的连接关闭掉:
java
127.0.0.1:6379> client list
id=16 addr=10.7.40.98:63892 fd=6 name= age=19 idle=19 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
# 超过timeout后,Jedis连接被关闭
redis-cli client list | grep "10.7.40.98"为空
同时可以看到,在Jedis代码中的第3)步抛出了异常,因为此时客户端已经被关闭,所以抛出的异常是JedisConnectionException,并且提示Unexpected endof stream:
java
stream:
world
Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException:
Unexpected end of stream.
如果将Redis的loglevel设置成debug级别,可以看到如下日志,也就是客户端被Redis关闭的日志:
java
12885:M 26 Aug 08:46:40.085 - Closing idle client
Redis源码中redis.c文件中clientsCronHandleTimeout函数就是针对timeout参数进行检验的,只不过在源码中timeout被赋值给了server.maxidletime:
java
int clientsCronHandleTimeout(redisClient *c) {
// 当前时间
time_t now = server.unixtime;
// server.maxidletime就是参数timeout
if (server.maxidletime &&
// 很多客户端验证,这里就不占用篇幅,最重要的验证是下面空闲时间超过了maxidletime就会
// 被关闭掉客户端
(now - c->lastinteraction > server.maxidletime))
{
redisLog(REDIS_VERBOSE,"Closing idle client");
// 关闭客户端
freeClient(c);
}
}
Redis的默认配置给出的timeout=0,在这种情况下客户端基本不会出现上面的异常,这是基于对客户端开发的一种保护。例如很多开发人员在使用JedisPool时不会对连接池对象做空闲检测和验证,如果设置了timeout>0,可能就会出现上面的异常,对应用业务造成一定影响,但是如果Redis的客户端使用不当或者客户端本身的一些问题,造成没有及时释放客户端连接,可能会造成大量的idle连接占据着很多连接资源,一旦超过maxclients;后果也是不堪设想。所在在实际开发和运维中,需要将timeout设置成大于0,例如可以设置为300秒,同时在客户端使用上添加空闲检测和验证等等措施,例如JedisPool使用common-pool提供的三个属性:minEvictableIdleTimeMillis、testWhileIdle、timeBetweenEvictionRunsMillis,4.2节已经进行了说明,这里就不再赘述。
(6)客户端类型
client list中的flag是用于标识当前客户端的类型,例如flag=S代表当前客户端是slave客户端、flag=N代表当前是普通客户端,flag=O代表当前客户端正在执行monitor命令,下表列出了11种客户端类型。

(7)其他
上面已经将client list中重要的属性进行了说明下表列出之前介绍过以及一些比较简单或者不太重要的属性。


4.1.2、client setName和client getName
java
client setName xx
client getName
client setName用于给客户端设置名字,这样比较容易标识出客户端的来源,例如将当前客户端命名为test_client,可以执行如下操作:
bash
127.0.0.1:6379> client setName test_client
OK
此时再执行client list命令,就可以看到当前客户端的name属性为test_client:
bash
127.0.0.1:6379> client list
id=55 addr=127.0.0.1:55604 fd=7 name=test_client age=23 idle=0 flags=N db=0 sub=0
psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
如果想直接查看当前客户端的name,可以使用clientgetName命令,例如下面的操作:
java
127.0.0.1:6379> client getName
"test_client"
client getName和setName命令可以做为标识客户端来源的一种方式,但是通常来讲,在Redis只有一个应用方使用的情况下,IP和端口作为标识会更加清晰。当多个应用方共同使用一个Redis,那么此时client setName可以作为标识客户端的一个依据。
4.1.3、client kill
bash
client kill ip:port
此命令用于杀掉指定IP地址和端口的客户端,例如当前客户端列表为:
java
127.0.0.1:6379> client list
id=49 addr=127.0.0.1:55593 fd=6 name= age=9 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
id=50 addr=127.0.0.1:52343 fd=7 name= age=4 idle=4 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
如果想杀掉127.0.0.1:52343的客户端,可以执行:
java
127.0.0.1:6379> client kill 127.0.0.1:52343
OK
执行命令后,client list结果只剩下了127.0.0.1:55593这个客户端:
bash
127.0.0.1:6379> client list
id=49 addr=127.0.0.1:55593 fd=6 name= age=9 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
由于一些原因(例如设置timeout=0时产生的长时间idle的客户端),需要手动杀掉客户端连接时,可以使用client kill命令。
4.1.4、client pause
bash
client pause timeout(毫秒)
如图所示,client pause命令用于阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。

例如在一个客户端执行:
bash
127.0.0.1:6379> client pause 10000
OK
在另一个客户端执行ping命令,发现整个ping命令执行了9.72秒(手动执行redis-cli,只为了演示,不代表真实执行时间):
bash
127.0.0.1:6379> ping
PONG
(9.72s)
该命令可以在如下场景起到作用:
- client pause只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,也就是此期间主从复制是正常进行的,所以此命令可以用来让主从复制保持一致。
- client pause可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点。
需要注意的是在生产环境中,暂停客户端成本非常高。
4.1.5、monitor
monitor命令用于监控Redis正在执行的命令,如图所示,我们打开了两个redis-cli,一个执行set get ping命令,另一个执行monitor命令。可以看到monitor命令能够监听其他客户端正在执行的命令,并记录了详细的时间戳。

4.2、客户端相关配置
timeout:检测客户端空闲连接的超时时间,一旦idle时间达到了timeout,客户端将会被关闭,如果设置为0就不进行检测。maxclients:客户端最大连接数。tcp-keepalive:检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源。tcp-backlog:TCP三次握手后,会将接受的连接放入队列中,tcp-backlog就是队列的大小,它在Redis中的默认值是511。通常来讲这个参数不需要调整,但是这个参数会受到操作系统的影响,例如在Linux操作系统中,如果/proc/sys/net/core/somaxconn小于tcp-backlog,那么在Redis启动时会看到如下日志,并建议将/proc/sys/net/core/somaxconn设置更大。
bash
# WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/
sys/net/core/somaxconn is set to the lower value of 128.
修改方法也非常简单,只需要执行如下命令:
bash
echo 511 > /proc/sys/net/core/somaxconn
4.3、客户端统计片段
例如下面就是一次info clients的执行结果:
bash
127.0.0.1:6379> info clients
# Clients
connected_clients:1414
client_longest_output_list:0
client_biggest_input_buf:2097152
blocked_clients:0
说明如下:
connected_clients:代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。client_longest_output_list:当前所有输出缓冲区中队列对象个数的最大值。client_biggest_input_buf:当前所有输入缓冲区中占用的最大容量。blocked_clients:正在执行阻塞命令(例如blpop、brpop、brpoplpush)的客户端个数。
除此之外info stats中还包含了两个客户端相关的统计指标,如下:
bash
127.0.0.1:6379> info stats
# Stats
total_connections_received:80
...
rejected_connections:0
- total_connections_received:Redis自启动以来处理的客户端连接数总数。
- rejected_connections:Redis自启动以来拒绝的客户端连接数,需要重点监控。
5、客户端常见异常
5.1、无法从连接池获取到连接
JedisPool中的Jedis对象个数是有限的,默认是8个。这里假设使用的默认配置,如果有8个Jedis对象被占用,并且没有归还,此时调用者还要从JedisPool中借用Jedis,就需要进行等待(例如设置了maxWaitMillis>0),如果在maxWaitMillis时间内仍然无法获取到Jedis对象就会抛出如下异常:
java
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时的效果:
java
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的处理效率足够高。
- 客户端:没有正确使用连接池,比如没有进行释放,例如下面代码所示。
java
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
像JedisPool借用8次连接,但是没有执行归还操作:
java
jedisPool.getResource().ping();
- 客户端:存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢,造成池子满了。
- 服务端:客户端是正常的,但是Redis服务端由于一些原因造成了客户端命令执行过程的阻塞,也会使得客户端抛出这种异常。
可以看到造成这个异常的原因是多个方面的,不要被异常的表象所迷惑,而且并不存在万能钥匙解决所有问题,开发和运维只能不断加强对于Redis的理解,顺藤摸瓜逐渐找到问题所在。
5.2、客户端读写超时
Jedis在调用Redis时,如果出现了读写超时后,会出现下面的异常:
java
redis.clients.jedis.exceptions.JedisConnectionException:
java.net.SocketTimeoutException: Read timed out
造成该异常的原因也有以下几种:
- 读写超时间设置得过短。
- 命令本身就比较慢。
- 客户端与服务端网络不正常。
- Redis自身发生阻塞。
5.3、客户端连接超时
Jedis在调用Redis时,如果出现了连接超时后,会出现下面的异常:
java
redis.clients.jedis.exceptions.JedisConnectionException:
java.net.SocketTimeoutException: connect timed out
造成该异常的原因也有以下几种:
-
连接超时设置得过短,可以通过下面代码进行设置:
// 毫秒 jedis.getClient().setConnectionTimeout(time); -
Redis发生阻塞,造成tcp-backlog已满,造成新的连接失败。
-
客户端与服务端网络不正常。
5.4、客户端缓冲区异常
Jedis在调用Redis时,如果出现客户端数据流异常,会出现下面的异常:
java
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
造成这个异常的原因可能有如下几种:
1)输出缓冲区满。例如将普通客户端的输出缓冲区设置为1M1M60:
java
config set client-output-buffer-limit "normal 1048576 1048576 60 slave 268435456
67108864 60 pubsub 33554432 8388608 60"
如果使用get命令获取一个bigkey(例如3M),就会出现这个异常。
2)长时间闲置连接被服务端主动断开,上节已经详细分析了这个问题。
3)不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现上述异常。
5.5、Lua脚本正在执行
如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit,此时Jedis调用Redis时,会收到下面的异常。对于如何处理这类问题,在第3章Lua的小节已经进行了介绍,这里就不再赘述。
java
redis.clients.jedis.exceptions.JedisDataException: BUSY Redis is busy running a
script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
5.6、Redis正在加载持久化文件
Jedis调用Redis时,如果Redis正在加载持久化文件,那么会收到下面的异常:
java
redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the
dataset in memory
5.7、Redis使用的内存超过maxmemory配置
Jedis执行写操作时,如果Redis的使用内存大于maxmemory的设置,会收到下面的异常,此时应该调整maxmemory并找到造成内存增长的原因:
java
redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when
used memory > 'maxmemory'.
5.8、客户端连接数过大
如果客户端连接数超过了maxclients,新申请的连接就会出现如下异常:
java
redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients reached
此时新的客户端连接执行任何命令,返回结果都是如下:
java
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。
6、客户端案例分析
6.1、Redis内存陡增
6.1.1、现象
服务端现象:Redis主节点内存陡增,几乎用满maxmemory,而从节点内存并没有变化(第5章将介绍Redis复制的相关知识,这里只需要知道正常情况下主从节点内存使用量基本相同),如图所示。

客户端现象:客户端产生了OOM异常,也就是Redis主节点使用的内存已经超过了maxmemory的设置,无法写入新的数据:
java
redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when
used memory > 'maxmemory'
6.1.2、分析原因
从现象看,可能的原因有两个。
1)确实有大量写入,但是主从复制出现问题:查询了Redis复制的相关信息,复制是正常的,主从数据基本一致。
主节点的键个数:
bash
127.0.0.1:6379> dbsize
(integer) 2126870
从节点的键个数:
bash
127.0.0.1:6380> dbsize
(integer) 2126870
2)其他原因造成主节点内存使用过大:排查是否由客户端缓冲区造成主节点内存陡增,使用info clients命令查询相关信息如下:
bash
127.0.0.1:6379> info clients
# Clients
connected_clients:1891
client_longest_output_list:225698
client_biggest_input_buf:0
blocked_clients:0
很明显输出缓冲区不太正常,最大的客户端输出缓冲区队列已经超过了20万个对象,于是需要通过client list命令找到omem不正常的连接,一般来说大部分客户端的omem为0(因为处理速度会足够快),于是执行如下代码,找到omem非零的客户端连接:
bash
redis-cli client list | grep -v "omem=0"
找到了如下一条记录:
bash
id=7 addr=10.10.xx.78:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=224869 omem=2129300608 events=rw cmd=monitor
已经很明显是因为有客户端在执行monitor命令造成的。
6.1.3、处理方法和后期处理
对这个问题处理的方法相对简单,只要使用client kill命令杀掉这个连接,让其他客户端恢复正常写数据即可。但是更为重要的是在日后如何及时发现和避免这种问题的发生,基本有三点:
- 从运维层面禁止monitor命令,例如使用rename-command命令重置monitor命令为一个随机字符串,除此之外,如果monitor没有做rename-command,也可以对monitor命令进行相应的监控(例如client list)。
- 从开发层面进行培训,禁止在生产环境中使用monitor命令,因为有时候monitor命令在测试的时候还是比较有用的,完全禁止也不太现实。
- 限制输出缓冲区的大小。
- 使用专业的Redis运维工具,例如CacheCloud,上述问题在Cachecloud中会收到相应的报警,快速发现和定位问题。
6.2、客户端周期性的超时
6.2.1、现象
客户端现象:客户端出现大量超时,经过分析发现超时是周期性出现的,这为问题的查找提供了重要依据:
bash
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.
SocketTimeoutException: connect timed out
服务端现象:服务端并没有明显的异常,只是有一些慢查询操作。
6.2.2、分析
- 网络原因:服务端和客户端之间的网络出现周期性问题,经过观察网络是正常的。
- Redis本身:经过观察Redis日志统计,并没有发现异常。
- 客户端:由于是周期性出现问题,就和慢查询日志的历史记录对应了一下时间,发现只要慢查询出现,客户端就会产生大量连接超时,两个时间点基本一致(如表和图所示)。


最终找到问题是慢查询操作造成的,通过执行hlen发现有200万个元素,这种操作必然会造成Redis阻塞,通过与应用方沟通了解到他们有个定时任务,每5分钟执行一次hgetall操作。
bash
127.0.0.1:6399> hlen user_fan_hset_sort
(integer) 2883279
以上问题之所以能够快速定位,得益于使用客户端监控工具把一些统计数据收集上来,这样能更加直观地发现问题,如果Redis是黑盒运行,相信很难快速找到这个问题。处理线上问题的速度非常重要。
6.2.3、处理方法和后期处理
这个问题处理方法相对简单,只需要业务方及时处理自己的慢查询即可,但是更为重要的是在日后如何及时发现和避免这种问题的发生,基本有三点:
- 从运维层面,监控慢查询,一旦超过阀值,就发出报警。
- 从开发层面,加强对于Redis的理解,避免不正确的使用方式。
- 使用专业的Redis运维工具,例如13章会介绍CacheCloud,上述问题在CacheCloud中会收到相应的报警,快速发现和定位问题。