Redis的客户端

程序媛和她的猫

共 22259字,需浏览 45分钟

 · 2021-04-27

最近在看《Redis开发与运维》这本书,个人认为这是一本很好的 Redis 实战书籍,接下来几天将陆续更新这本书的读书笔记,仅供大家参考。

一、客户端通信协议

客户端与服务端之间的通信协议是在TCP协议之上构建的。

Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互。

// 客户端发送一条set hello world命令给服务端,按照 RESP 的标准,客户端需要将其序列化为如下格式(每行用\r\n分隔)
*3 
$3 
SET 
$5 
hello 
$5 
world

*3:set hello world 这个数组的长度
$3:表示下面的字符的长度是3,SET的长度是3
$5:hello的长度是5
$5:world的长度是5
// Redis服务端按照 RESP 将其反序列化为 set hello world,执行后
回复 OK
+OK

二、Java客户端Jedis

1、Jedis 基本用法

Java 有很多优秀的 Redis 客户端,最常用的是 Jedis 和 Redisson,Redis 官方推荐使用 Redisson,我们这里先简单介绍一下 Jedis,后面会详细用一篇文章来介绍 Redission。

Jedis jedis = null
try {
    // 生成一个 Jedis 对象,这个对象负责和指定Redis实例进行通信。
    jedis = new Jedis("127.0.0.1"6379);
    // jedis执行set操作
    jedis.set("hello""world");
    jedis.get("hello"); 
catch (Exception e) {    
    logger.error(e.getMessage(),e); 
finally {    
    if (jedis != null) {  
        // 关闭 Jedis 连接
        jedis.close();    
    } 
}

在实际项目中,Jedis操作放在try catch finally里更加合理,一方面可以在Jedis出现异常的时候(本身是网络操作),将异常进行捕获或者抛出;另一个方面无论执行成功或者失败,都会将Jedis连接关闭掉,在开发中关闭不用的连接资源是一种好的习惯

2、Jedis 提供了字节数组类型的参数,这样的话,当应用中涉及 Java 对象的时候,可以将 Java 对象序列化为二进制进行存储,当应用需要获取对象时,使用 get 函数将字节数组取出,然后反序列化为Java对象即可。
// key 和 value 都是字符串
public String set(final String key, String value)
public String get(final String key)

// key 和 value 都是字节数组
public String set(final byte[] key, final byte[] value) 
public byte[] get(final byte[] key) 

需要注意的是,和其他NoSQL数据库(例如 Memcache )的客户端不同,Jedis本身没有提供序列化的工具,也就是说开发者需要自己引入序列化的工具

什么是序列化和反序列化?
序列化:对象 -> 字节数组(二进制)
反序列化:字节数组(二进制) -> 对象

下面我们讲解一下 Jedis(Redis的客户端) 和 protostuff(Protobuf的客户端) 二者配合使用实现 Redis 的序列化操作。Protobuf是谷歌提供的一个具有高效的协议数据交换格式工具库(类似JSON),但是 Protobuf 相比于 JSON 有更高的转化效率,时间效率和空间效率都是 JSON 的3-5倍。

1)引入 protostuff 依赖。

<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)定义实体类。

// 俱乐部 
public class Club implements Serializable {      
private int id;    
private String name;// 名称    
private String info;// 描述    
private Date createDate;// 创建日期    
private int rank;// 排名    
// getter setter
}

3)序列化工具类 ProtostuffSerializer 封装了序列化和反序列化方法。

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> deserializeInternal(final byte[] bytes, final T result, final Schema<T> schema) {
        ProtostuffIOUtil.mergeFrom(bytes, result, schema);
        return result;
    }
}

4)测试。

ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
Jedis jedis = new Jedis("127.0.0.1"6379);
//序列化操作
String key = "club:1"
Club club = new Club(1"AC""米兰"new Date(), 1);// 定义实体对象  
byte[] clubBtyes = protostuffSerializer.serialize(club);// 序列化  
jedis.set(key.getBytes(), clubBtyes);
// 反序列化操作
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);
3、Jedis连接池

上面我们介绍的是 Jedis 的直连方式,所谓直连是指 Jedis 每次都会新建 TCP 连接,使用后再断开连接,对于频繁访问 Redis 的场景显然不是高效的使用方式。

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

直连方式和连接池方式的对比见下表:


优点缺点
直连简单方便,适用于少量长期连接的场景1)存在每次新建/关闭TCP连接的开销。
2)无法限制 Jedis 对象的个数,在极端情况下可能会造成连接泄露。
3)Jedis 对象线程不安全。
连接池1)无需每次连接都生成 Jedis 对象,降低开销。2)使用连接池的形式可以有效地保护和控制连接资源的使用。相对于直连,使用相对麻烦,尤其在资源的管理上需要很多参数来保证,一旦规划不合理就会出现问题。
// 使用 common-pool(Apache 的通用对象池工具) 作为资源的管理工具(连接池配置)
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); 
// 设置最大连接数为默认值的5倍
poolConfig.setMaxTotal(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL * 5); 
poolConfig.setMaxIdle(GenericObjectPoolConfig.DEFAULT_MAX_IDLE * 3);
// 设置连接池没有连接后客户端的最大等待时间(单位为毫秒) 
poolConfig.setMaxWaitMillis(3000);
// 初始化Jedis连接池 
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1"6379);

Jedis jedis = null
try {    
    // 1. 从连接池获取jedis对象    
    jedis = jedisPool.getResource();    
    // 2. 执行操作    
    jedis.set("hello""world"); 
    jedis.get("hello"); 
catch (Exception e) {    
    logger.error(e.getMessage(),e); 
finally {    
    if (jedis != null) {        
        // 如果使用JedisPool,close操作不是关闭连接,代表归还连接池         jedis.close();    
    } 
}

三、管理客户端的命令

1、client list 命令

在Redis实例上执行 client list 命令,就可以查看与该 Redis 实例相连的所有客户端的信息,返回的信息包括:

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:客户端唯一标识,随着Redis的连接自增,重启 Redis后会重置为0。
addr:客户端的ip和端口。
name:客户端的名字。

2)输入缓冲区和输出缓冲区的相关信息

a)什么是输入缓冲区和输出缓冲区?
Redis 为每个客户端分配了输入缓冲区和输出缓冲区,缓冲区的作用是,在客户端和 Redis 之间进行通信时,用来暂存客户端发送的命令,或者是 Redis 返回给客户端的命令执行结果,起到一个缓冲的功能。

Redis输入缓冲区和输出缓冲区

b)使用 client list命令,查看输入缓冲区的内存使用情况。
输入缓冲区:client list 命令的返回结果 qbuf 和 qbuf-free 分别代表输入缓冲区的总容量和剩余容量。

输出缓冲区:实际上输出缓冲区由两部分组成,分别是固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区用于暂存比较小的执行结果,而动态缓冲区用于暂存比较大的结果,例如大的字符串、hgetall、smembers等命令的结果。client list命令的返回结果中,obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表输出缓冲区已经使用的字节数。

c)输入缓冲区溢出。

c.1)、输入缓冲区溢出,有什么危害?

一旦输入缓冲区溢出,可能会产生数据丢失、键值淘汰、Redis OOM、客户端关闭的情况。

c.2)、输入缓冲区溢出的原因?

(1)、因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量 bigkey,从而造成了输入缓冲区过大的情况。
(2)、Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区,造成了输入缓冲区过大。

c.3)、如何避免输入缓冲区溢出?

(1)、输入缓冲区的上限阈值为1GB,输入缓冲区根据输入内容大小的不同动态调整,不能通过参数调整
(2)、为了防止输入缓冲区溢出,首先在代码里设置客户端输入缓冲区大小上限阈值为1GB,其次,避免客户端写入bigkey。
(3)、监控输入缓冲区,一旦超过某个值就进行报警提示。info clients 命令返回的 client_biggest_input_buf 表示当前Redis中最大的输入缓冲区,可以设置它超过10M就进行报警。

127.0.0.1:6379> info clients 
client_biggest_input_buf:2097152 
......

d)输出缓冲区溢出。

d.1)、输出缓冲区溢出,有什么危害?

和输入缓冲区溢出一样,一旦输出缓冲区溢出,也可能会产生数据丢失、键值淘汰、Redis OOM、客户端关闭的情况。

d.2)、输出缓冲区溢出的原因?

(1)、bigkey 操作,导致服务器端返回的数据太大。
(2)、在 Redis 高并发场景下,执行了 monitor 命令。
(3)、缓冲区大小设置得不合理。

d.3)、如何避免输出缓冲区溢出?

与输入缓冲区不同的是,输出缓冲区的容量可以通过参数 client-outputbuffer-limit 来进行设置,当输出缓冲区大小超过设置的值,连接就会自动断开。
(1)、避免 bigkey 操作一次性返回大量数据,可以使用 scan 命令,分批次返回小量数据。
(2)、monitor 命令主要用在调试环境中,不要在生产环境中使用 monitor。
monitor命令用于监听当前Redis正在执行的命令,一旦Redis的并发量过大,此时 Redis 中正在执行的命令数量巨大,此时如果执行 monitor 命令,会将 Redis 中正在执行的所有命令写入输出缓冲区,导致客户端的输出缓冲区暴涨甚至溢出。

(3)、使用 client-outputbuffer-limit 设置合理的缓冲区大小上限。

// class:客户端类型,分为三种,normal:普通客户端,slave:slave客户端,用于主从之间复制,pubsub:发布订阅客户端。
// hard limit:如果客户端使用的输出缓冲区大于hard limit,客户端会被立即关闭。
// soft limit和soft seconds:如果客户端使用的输出缓冲区超过了soft limit并且持续了soft limit秒,客户端会被立即关闭。
client-output-buffer-limit [class] [hard limit] [soft limit] [soft seconds]

(4)、主从复制的场景,如何避免 Redis 主节点输出缓冲区发生溢出?
如果在主从复制的过程中,Redis 主节点的输出缓冲区发生溢出,那么所有 slave 节点的连接都将被kill,而造成复制重连,之前同步的都白费了,效率降低。

如何避免 Redis 主节点输出缓冲区发生溢出,有三个出发点:
使用 client-outputbuffer-limit,将 Redis 主节点的输出缓冲区设置地大一点。
控制和主节点连接的从节点个数。
控制主节点保存的数据量大小,通常控制在2~4GB。

3)客户端存活状态:age、idle

age:表示当前客户端已经连接了多长时间,idle:表示当前客户端已经空闲多长时间了。

见 1 中 client list 返回信息的第一条记录,当前客户端连接Redis的时间为 8888581 秒,空闲了 8888581 秒,这属于不太正常的情况,因为当 age 等于 idle 时,说明连接一直处于空闲状态,Redis能连多少客户端是有限制的,这样的空闲连接属于对 Redis 资源的一种浪费

2、info clients 命令

使用 info clients 命令可以查看当前已经连接上 Redis 的客户端的数量。每个客户端都有一个输入缓存和输出缓存,使用 info clients 命令可以获得所有客户端中最大的输入缓存和输出缓存分别是多大。

127.0.0.1:6379> info clients 
// connected_clients : 已连接客户端的数量
connected_clients:1414 
// 当前连接的客户端当中,最长的输出列表,即最大的输出缓存
client_longest_output_list
// 当前连接的客户端当中,最大的输入缓存
client_longest_input_buf
3、monitor 命令

monitor 命令用于监听当前 Redis 正在执行的命令。

127.0.0.1:6379> monitor
OK
// 1378822105.089572 是时间戳,[0 127.0.0.1:56604] 中的 0 是数据库号码,127... 是 IP 地址和端口,"SET" "msg" "hello world" 是被执行的命令。
1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world" 
1378822109.036925 [0 127.0.0.1:56604] "SET" "number" "123"
1378822140.649496 [0 127.0.0.1:56604] "SADD" "fruits" "Apple" "Banana" "Cherry"
1378822154.117160 [0 127.0.0.1:56604] "EXPIRE" "msg" "10086"
1378822257.329412 [0 127.0.0.1:56604] "KEYS" "*"
4、client kill 命令

client kill 命令用于杀掉指定IP地址和端口的客户端。

// ip:客户端的IP,port:客户端的端口号。
client kill ip:port
// 客户端列表,127.0.0.1:55593 和 127.0.0.1:52343
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这个客户端
127.0.0.1:6379> client kill 127.0.0.1:52343 
OK

// 此时客户端列表,只剩下了127.0.0.1:55593这个客户端
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时产生的长时间空闲的客户端),需要手动杀掉客户端连接时,可以使用client kill命令。

四、管理客户端的参数配置

1、客户端相关配置参数

maxclients:最大客户端连接数,即 Redis 最多能连接多少个客户端,一旦连接数超过 maxclients,新的连接将被拒绝。maxclients默认值是10000。

timeout:连接的最大空闲时间,客户端与 Redis 建立连接,如果这个连接的空闲时间(idle)超过了timeout,连接就会断开,客户端就会被关闭。如果设置为0,那么不管连接空闲多久,都不会自动断开。

在开发和运维过程中,建议将timeout设置成大于0,防止由于客户端使用不当或
者客户端本身的一些问题,造成没有及时释放客户端连接,从而造成大量的空闲连
接占据着很多连接资源,一旦超过maxclients,后果将不堪设想。

tcp-keepalive:检测 Redis 与客户端之间 TCP 连接活性的周期,即每隔多久对 TCP 活性进行一次检测。默认值为0,也就是不进行检测,如果需要设置,建议设置为60,那么 Redis 会每隔60秒对它创建的 TCP 连接进行活性检测,防止大量死连接占用系统资源。

tcp-backlog:Redis 与客户端进行 TCP 三次握手后,会将接受的连接放入内部的队列中,tcpbacklog就是队列的大小,它在Redis中的默认值是511。

2、如何获取和修改客户端配置参数?

使用 config get 参数名,来获取已经配置好的参数值,使用 config set 参数名 参数值,对客户端参数进行动态配置。

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 客户端的使用过程中,无论是客户端使用不当还是 Redis 服务端出现 问题,客户端会反应出一些异常。下面我们分析一下 Jedis 使用过程中常见的异常情况。

1、无法从连接池获取到连接
(1)、两种异常情况

第一种情况:JedisPool 中的 Jedis 对象个数是有限的(默认是 8 个),如果 JedisPool 中所有的 Jedis 对象都已经被占用,此时来了一个客户端要从  JedisPool 中借用Jedis,就需要进行等待,等待时长为 maxWaitMillis (连接池设置参数),如果在 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:不阻塞直接报异常,ture:阻塞直到超时,默认是 true),那么客户端发现连接池中没有资源时,会立即抛出异常不进行等待。

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)
(2)、造成连接池没有资源的原因

a)客户端方面的原因:
1、高并发情况下连接池设置过小,供不应求。但是正常情况下只要比默认的最大连接数(8个)多一些即可,因 为正常情况下JedisPool以及Jedis的处理效率足够高。
2、客户端没有正确使用连接池,比如没有及时释放连接资源。
3、客户端存在慢查询操作,这些慢查询持有的 Jedis 对象归还速度会比较慢,造成连接池中没有连接可用。

b)服务端方面的原因:
客户端是正常的,但是 Redis 服务端由于一些原因,造成了客户端的命令在执行过程中发生阻塞,导致连接不能及时还回去。

比如 RDB 持久化时,fork 进程做内存快照,AOF 持久化时,AOF 文件重写,这些操作会占用大量的CPU资源,进而拖慢客户端命令。

2、客户端读写超时
(1)、读写超时异常情况

在使用 Jedis 调用 Redis 时,如果出现了读写超时,会报如下异常:

// 程序报下述的错误信息,就表示当前发生客户端读写超时异常了。
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
(2)、造成超时异常的原因

1、读写超时时间设置得过短。
2、命令本身就比较慢。
3、客户端与服务端之间网络不正常。
4、由于某些原因,Redis自身发生阻塞。

3、客户端连接超时
(1)、连接超时异常情况

在使用 Jedis 连接 Redis 的时候,如果出现连接超时,会报如下异常:

redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
(2)、造成连接超时的原因

1、连接超时时间设置得过短,可以通过下面代码进行设置:

// 毫秒 
jedis.getClient().setConnectionTimeout(time);

2、Redis发生阻塞,造成 tcp-backlog 已满,造成新的连接失败,tcp-backlog 的含义见 四.1 。
3、客户端与服务端之间网络不正常。

4、客户端数据流异常
(1)、客户端数据流异常
// 数据流意外结束。
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
(2)、造成客户端数据流异常的原因

1、输出缓冲区溢出。
2、长时间闲置的连接被服务端主动断开了。
3、不正常并发读写,Jedis 对象同时被多个线程并发操作,可能会出现上述异常。

5、Jedis调用Redis时,如果Redis正在加载持久化文件,此时客户端也会报错。
redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory
6、Redis使用的内存超过了 maxmemory 限制,客户端会报错。
redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'.

这个异常的解决方案是,调整 maxmemory 大小并找到造成内存增长的原因。

7、Redis 连接的客户端数量已经达到 maxclients 限制,此时新来的客户端尝试连接 Redis 会报错。
(1)、Redis 客户端连接数过大报异常的情况
// 已达到最大客户端数
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

六、客户端发生异常案例分析

1、Redis内存陡增
1)现象

服务端现象:Redis 主节点内存陡增,几乎用满 maxmemory ,而从节点内存并没有变化。

客户端现象:客户端产生了 OOM 异常,也就是说 Redis 主节点使用的内存 已经超过了 maxmemory 的设置,无法再写入新的数据。

2)分析原因

首先查看是否是主从复制出现问题导致主从不一致。

// 主节点的键个数
127.0.0.1:6379> dbsize 
(integer) 2126870

// 从节点的键个数
127.0.0.1:6380> dbsize 
(integer) 2126870

我们可以看到主从节点键个数一样,可以说明复制是正常的,主从数据基本一致。

其次排查是否是由客户端缓冲区(输入缓冲区和输出缓冲区)溢出,造成主节点内存陡增的。

127.0.0.1:6379> info clients 
connected_clients:1891 
client_longest_output_list:225698 
client_biggest_input_buf:0 blocked_clients:0

很明显输出缓冲区不太正常,最大的客户端输出缓冲区队列已经超过了 20 万个对象了。

因为 client list 命令返回的 omem 表示输出缓冲区使用的字节数(一般来说大部分客户端的 omem 为0,因为处理速度会足够快),所以我们通过 client list命令,可以找到输出缓冲区溢出的那个客户端。

redis-cli client list | grep -v "omem=0"

这样我们就找到了输出缓冲区溢出的客户端,客户端标识:id 为7,IP 和端口为10.10.xx.78:56358,通过 cmd=monitor ,可以知道输出缓冲区溢出是由于执行了 monitor 命令。

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 
3)处理方法和后期处理

当发生内存陡增问题时,在当下为了快速恢复业务,只要使用 client kill 命令杀掉这个连接,让其他客户端恢复正常写数据即可。

但是更为重要的是在日后如何及时发现和避免这种问题的发生,基本有三点:1、禁止monitor命令,例如使用rename-command命令重置 monitor命令为一个随机字符串。
2、使用 client-outputbuffer-limit 限制输出缓冲区的大小,使得当输出缓冲区大小超过设置的值时,连接就会自动断开。
3、尽量不要使用 smembers、hgetall、lrange 等返回大量数据结果的命令,避免输出缓冲区溢出。
4、使用专业的 Redis 运维工具,在发生内存陡增时能够及时接受到报警信息,快速发现和定位问题。

2、客户端周期性的超时
1)现象

客户端现象:客户端出现大量超时,经过分析发现超时是周期性出现的,这为问题的查找提供了重要依据

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out

服务端现象:服务端并没有明显的异常,只是有一些慢查询操作。

2)分析原因

首先排查是否是网络原因,经过排查网络没有问题。

再排查是否 Redis 自身出现问题,经过观察Redis日志统计,并没有发现异常。

最后排查是否是客户端的问题,由于超时是周期性出现的,所以将发生超时的时间点和慢查询日志的时间点对比了一下,发现只要慢查询出现,客户端就会产生大量连接超时,两个时间点基本一致。

慢查询记录
客户端耗时统计

所以可以断定,这个问题是慢查询操作造成的。查看代码,发现代码中有个定时任务,每 5 分钟对 user_fan_hset_sort 这个 key 执行一次 hgetall 操作,而使用 hlen 发现 user_fan_hset_sort 这个 key 对应的 value 中有 200 万个元素,执行 hgetall 必然导致阻塞。

127.0.0.1:6399> hlen user_fan_hset_sort 
(integer) 2883279 
3)处理方法和后期处理

从开发层面,尽量避免慢查询操作,类似 smembers、hgetall、lrange 这些 O(n) 的操作。

从运维层面,监控慢查询,一旦超过阀值,就发出报警。


浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报