客户端通信协议

客户端与服务端之间的通信协议是在TCP协议之上构建的。Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客 户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。

发送命令格式

  1. *<参数数量> CRLF # CRLF代表"\r\n"
  2. $<参数1的字节数量> CRLF
  3. <参数1> CRLF
  4. ...
  5. $<参数N的字节数量> CRLF
  6. <参数N> CRLF
  7. # 示例:set hello world的命令格式如下
  8. *3
  9. $3
  10. SET
  11. $5
  12. hello
  13. $5
  14. world

返回结果格式

Redis的返回结果类型分为以下五种:

  • 状态回复:在RESP中第一个字节为”+“。
  • 错误回复:在RESP中第一个字节为”-“。
  • 整数回复:在RESP中第一个字节为”:“。
  • 字符串回复:在RESP中第一个字节为”$“。
  • 多条字符串回复:在RESP中第一个字节为”*“。

redis-cli只能看到最终的执行结果,那是因为redis-cli本身就是 按照RESP进行结果解析的,所以看不到中间结果。

  1. mset java jedis python redis-py
  2. +OK
  3. mget java python
  4. *2
  5. $5
  6. jedis
  7. $8
  8. redis-py

客户端API

client list

client list命令能列出与Redis服务端相连的所有客户端连接信息。
image.png

标识: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-limit class:客户端类型,分为三种 (a)normal:普通客户端; (b)slave:slave客户端,用于复制; (c)pubsub:发布订阅客户端

hard limit:如果客户端使用的输出缓冲区大于,客户端会被立即关闭

soft limit和soft seconds:如果客户端使用的输出缓冲区超过了softlimit 并且持续了秒,客户端会被立即关闭

  1. 和输入缓冲区相同的是,输出缓冲区也不会受到maxmemory的限制,如果使用不当同样会造成maxmemory用满产生的数据丢失、键值淘汰、OOM 情况。<br />输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果。固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果。
  2. ```c
  3. typedef struct redisClient {
  4. // 动态缓冲区列表
  5. list *reply;
  6. // 动态缓冲区列表的长度(对象个数)
  7. unsigned long reply_bytes;
  8. // 固定缓冲区已经使用的字节数
  9. int bufpos;
  10. // 字节数组作为固定缓冲区
  11. char buf[REDIS_REPLY_CHUNK_BYTES];
  12. } 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中的ageidle分别代表当前客户端已经连接的时间和最近一次的空闲时间(以秒为单位)。当age等于idle时, 说明连接一直处于空闲状态。

    客户端限制:maxclients和timeout

    maxclients参数用来限制最大客户端连接数,一旦连接数超过 maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients来查询当前Redis的连接数。
    可以通过config set maxclients对最大客户端连接数进行动态设置。
    1. 127.0.0.1:6379> config get maxclients
    2. 1) "maxclients"
    3. 2) "10000"
    4. 127.0.0.1:6379> config set maxclients 50
    5. OK
    6. 127.0.0.1:6379> config get maxclients
    7. 1) "maxclients"
    8. 2) "50"
    默认情况下,maxclients=10000在大部分场景下已经绝对够用,但是某些情况由于业务方使用不当(例如没有主动关闭连接)可能存在大量idle连接, 无论是从网络连接的成本还是超过maxclients的后果来说都不是什么好事, 因此Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一 旦客户端连接的idle时间超过了timeout,连接将会被关闭。
    1. # Redis默认的timeout是0,也就是不会检测客户端的空闲
    2. 127.0.0.1:6379> config get timeout
    3. 1) "timeout"
    4. 2) "0"
    5. 127.0.0.1:6379> config set timeout 30
    6. OK
    7. 127.0.0.1:6379> config get timeout
    8. 1) "timeout"
    9. 2) "30"

    客户端类型

    client list中的flag是用于标识当前客户端的类型。
    image.png

    其他属性

    db:当前客户端正在使用的数据库索引下标;
    sub/psub:当前客户端订阅的频道或者模式数;
    muti:当前事务已执行命令的个数;
    events:文件描述符事件(r/w):r和w分别代表客户端套接字可读和可写;
    cmd:当前客户端最后一次执行的命令,不包含参数。

client setName和client getName

client setName用于给客户端设置名字,这样比较容易标识出客户端的来源。如果想直接查看当前客户端的name,可以使用client getName命令。

  1. 127.0.0.1:6379> client getName # 客户端默认无名字
  2. (nil)
  3. 127.0.0.1:6379> client setName cli-001
  4. OK
  5. 127.0.0.1:6379> client getName
  6. "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

  1. 127.0.0.1:6379> info clients
  2. # Clients
  3. connected_clients:1
  4. client_recent_max_input_buffer:2
  5. client_recent_max_output_buffer:0
  6. 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自启动以来拒绝的客户端连接数,需要重点监控。

客户端常见异常

  1. 无法从连接池获取到连接,可能原因如下:
  • 客户端:高并发下连接池设置过小,出现供不应求的情况;
  • 客户端:连接池使用完后没有进行释放;
  • 客户端:存在慢查询操作,慢查询持有的连接归还速度会比较慢;
  • 服务端:由于一些原因造成了客户端命令执行过程的阻塞也会产生该现象。
  1. 客户端读写超时,可能原因如下:
    1. redis.clients.jedis.exceptions.JedisConnectionException:
    2. java.net.SocketTimeoutException: Read timed out
  • 读写超时时间设置过短;
  • 命令本身执行的慢;
  • 客户端与服务端网络不正常;
  • Redis自身发生阻塞。
  1. 客户端连接超时,可能原因如下:
    1. redis.clients.jedis.exceptions.JedisConnectionException:
    2. java.net.SocketTimeoutException: connect timed out
  • 连接超时时间设置过短;
  • Redis发生阻塞,造成tcp-backlog已满,造成新的连接失败;
  • 客户端与服务端网络不正常。
  1. 客户端缓冲区异常,可能原因如下:
    1. redis.clients.jedis.exceptions.JedisConnectionException:
    2. Unexpected end of stream.
  • 输出缓冲区满,例如将普通客户端的输出缓冲区设置为1M 1M 60,此时使用get命令获取一个bigkey(例如3M)就会出现该异常。
  • 长时间闲置连接被服务端主动断开;
  • 不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现 上述异常。
  1. Lua脚本正在运行异常

    1. BUSY Redis is busy running a script.
    2. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

    如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit会出现该异常。

  2. Redis正在加载持久化文件异常

Jedis调用Redis时,如果Redis正在加载持久化文件,那么会收到下面的异常:

  1. redis.clients.jedis.exceptions.JedisDataException:
  2. LOADING Redis is loading the dataset in memory
  1. Redis使用的内存超过maxmemory配置

Jedis执行写操作时,如果Redis的使用内存大于maxmemory的设置,会 收到下面的异常,此时应该调整maxmemory并找到造成内存增长的原因

  1. redis.clients.jedis.exceptions.JedisDataException:
  2. OOM command not allowed when used memory > 'maxmemory'.
  1. 客户端连接数过大

如果客户端连接数超过了maxclients,新申请的连接会出现如下异常:

  1. redis.clients.jedis.exceptions.JedisDataException:
  2. ERR max number of clients reached