客户端通信协议
客户端与服务端之间的通信协议是在TCP协议之上构建的。Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客 户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。
发送命令格式
*<参数数量> CRLF # CRLF代表"\r\n"
$<参数1的字节数量> CRLF
<参数1> CRLF
...
$<参数N的字节数量> CRLF
<参数N> CRLF
# 示例:set hello world的命令格式如下
*3
$3
SET
$5
hello
$5
world
返回结果格式
Redis的返回结果类型分为以下五种:
- 状态回复:在RESP中第一个字节为”
+
“。 - 错误回复:在RESP中第一个字节为”
-
“。 - 整数回复:在RESP中第一个字节为”
:
“。 - 字符串回复:在RESP中第一个字节为”
$
“。 - 多条字符串回复:在RESP中第一个字节为”
*
“。
redis-cli只能看到最终的执行结果,那是因为redis-cli本身就是 按照RESP进行结果解析的,所以看不到中间结果。
mset java jedis python redis-py
+OK
mget java python
*2
$5
jedis
$8
redis-py
客户端API
client list
client list命令能列出与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执行命令提供了缓冲功能。
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模块,找到最大的输入缓冲区,可以设置超过某个值(例如10M)就进行报警。
输出缓冲区:obl、oll、omem
Redis为每个客户端分配了输出缓冲区,作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲。
输出缓冲区的容量可以通过参数client-output-buffer-limit
来进行设置,并且输出缓冲区做得更加细致,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端。 ```bash client-output-buffer-limitclass:客户端类型,分为三种 (a)normal:普通客户端; (b)slave:slave客户端,用于复制; (c)pubsub:发布订阅客户端
hard limit:如果客户端使用的输出缓冲区大于
soft limit和soft seconds:如果客户端使用的输出缓冲区超过了softlimit
并且持续了
和输入缓冲区相同的是,输出缓冲区也不会受到maxmemory的限制,如果使用不当同样会造成maxmemory用满产生的数据丢失、键值淘汰、OOM等 情况。<br />输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果。固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果。
```c
typedef struct redisClient {
// 动态缓冲区列表
list *reply;
// 动态缓冲区列表的长度(对象个数)
unsigned long reply_bytes;
// 固定缓冲区已经使用的字节数
int bufpos;
// 字节数组作为固定缓冲区
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;
client list中的obl
代表固定缓冲区的长度,oll
代表动态缓冲区列表的长度,omem
代表使用的字节数。
监控输出缓冲区的方法依然有两种:
- 通过定期执行
client list
命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。 - 通过info命令的
info clients
模块,找到输出缓冲区列表最大对象数。
预防输出缓冲异常的方法:
- 进行上述监控,设置阀值,超过阀值及时处理;
- 限制普通客户端输出缓冲区,防止客户端错误,例如进行如下设置:
client-output-buffer-limit normal 20mb 10mb 120
- 适当增大slave的输出缓冲区的,如果master节点写入较大,slave客户 端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出 被kill,会造成复制重连;
- 限制容易让输出缓冲区增大的命令,例如高并发下的monitor命令;
- 及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。
客户端的存活状态
client list中的age
和idle
分别代表当前客户端已经连接的时间和最近一次的空闲时间(以秒为单位)。当age等于idle时, 说明连接一直处于空闲状态。客户端限制:maxclients和timeout
maxclients参数用来限制最大客户端连接数,一旦连接数超过 maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients来查询当前Redis的连接数。
可以通过config set maxclients
对最大客户端连接数进行动态设置。
默认情况下,maxclients=10000在大部分场景下已经绝对够用,但是某些情况由于业务方使用不当(例如没有主动关闭连接)可能存在大量idle连接, 无论是从网络连接的成本还是超过maxclients的后果来说都不是什么好事, 因此Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一 旦客户端连接的idle时间超过了timeout,连接将会被关闭。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"
# Redis默认的timeout是0,也就是不会检测客户端的空闲
127.0.0.1:6379> config get timeout
1) "timeout"
2) "0"
127.0.0.1:6379> config set timeout 30
OK
127.0.0.1:6379> config get timeout
1) "timeout"
2) "30"
客户端类型
client list中的flag
是用于标识当前客户端的类型。其他属性
db
:当前客户端正在使用的数据库索引下标;sub/psub
:当前客户端订阅的频道或者模式数;muti
:当前事务已执行命令的个数;events
:文件描述符事件(r/w):r和w分别代表客户端套接字可读和可写;cmd
:当前客户端最后一次执行的命令,不包含参数。
client setName和client getName
client setName用于给客户端设置名字,这样比较容易标识出客户端的来源。如果想直接查看当前客户端的name,可以使用client getName命令。
127.0.0.1:6379> client getName # 客户端默认无名字
(nil)
127.0.0.1:6379> client setName cli-001
OK
127.0.0.1:6379> client getName
"cli-001"
在Redis只有一个应用方使用的情况下,IP和端口作为标识会更加清晰。当多个应用方共同使用一个Redis,那么此时client setName可以 作为标识客户端的一个依据。
client kill
client kill ip:port
命令用于杀掉指定IP地址和端口的客户端。
client pause
client pause _timeout_(ms)
命令用于阻塞客户端timeout毫秒,在此期间客户端连接将被阻塞。该命令的使用场景:
- client pause只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,也就是此期间主从复制是正常进行的, 所以此命令可以用来让主从复制保持一致。
- client pause可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点。
monitor命令
monitor命令用于监控Redis正在执行的命令。由于每个客户端都有自己的 输出缓冲区,既然monitor能监听到所有的命令,一旦Redis的并发量过大, monitor客户端的输出缓冲会暴涨,可能瞬间会占用大量内存。
客户端相关配置
tcp-keepalive
:检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连 接进行活性检测,防止大量死连接占用系统资源。tcp-backlog
:TCP三次握手后,会将接受的连接放入队列中,tcp-backlog就是队列的大小,它在Redis中的默认值是511。该参数受操作系统的影响。
客户端统计片段
info clients
127.0.0.1:6379> info clients
# Clients
connected_clients:1
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
该命令的输出包含了两个客户端相关的统计指标:total_connections_received
:Redis自启动以来处理的客户端连接数总数。rejected_connections
:Redis自启动以来拒绝的客户端连接数,需要重点监控。
客户端常见异常
- 无法从连接池获取到连接,可能原因如下:
- 客户端:高并发下连接池设置过小,出现供不应求的情况;
- 客户端:连接池使用完后没有进行释放;
- 客户端:存在慢查询操作,慢查询持有的连接归还速度会比较慢;
- 服务端:由于一些原因造成了客户端命令执行过程的阻塞也会产生该现象。
- 客户端读写超时,可能原因如下:
redis.clients.jedis.exceptions.JedisConnectionException:
java.net.SocketTimeoutException: Read timed out
- 读写超时时间设置过短;
- 命令本身执行的慢;
- 客户端与服务端网络不正常;
- Redis自身发生阻塞。
- 客户端连接超时,可能原因如下:
redis.clients.jedis.exceptions.JedisConnectionException:
java.net.SocketTimeoutException: connect timed out
- 连接超时时间设置过短;
- Redis发生阻塞,造成tcp-backlog已满,造成新的连接失败;
- 客户端与服务端网络不正常。
- 客户端缓冲区异常,可能原因如下:
redis.clients.jedis.exceptions.JedisConnectionException:
Unexpected end of stream.
- 输出缓冲区满,例如将普通客户端的输出缓冲区设置为
1M 1M 60
,此时使用get命令获取一个bigkey(例如3M)就会出现该异常。 - 长时间闲置连接被服务端主动断开;
- 不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现 上述异常。
Lua脚本正在运行异常
BUSY Redis is busy running a script.
You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit会出现该异常。
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