1 Jedis 的基本使用

首先引入包

1
2
3
4
5
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.1.0</version>
    </dependency>

基本使用

1
2
3
4
5
6
7
8
    public static void main(String[] args) {
        Jedis jedis = new Jedis(IP, PORT);
        // 后面通过 jedis 中提供的各种方法就可以直接操作 redis 了
        String set = jedis.set("hello", "world");
        System.out.println(set);
        String hello = jedis.get("hello");
        System.out.println(hello);
    }

Jedis 的构造参数中还有两个参数

  • connectionTimeout: 客户端连接超时
  • soTimeout: 客户端读写超时

2 Jedis 连接池的使用方法

直接使用 Jedis 的问题在于开销比较大,每次 Jedis 都是新建 TCP 连接池访问 redis 的,形式如下图。

访问顺序

因为这个问题所以在生产中一般用连接池来管理,用的时候从池子里借,用完了再还回去。实现代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    public static void main(String[] args) {

        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        JedisPool jedisPool = new JedisPool(poolConfig, IP, PORT);
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.set("hello", "world");
            String hello = jedis.get("hello");
            System.out.println(hello);
        } finally {
            // 这里的 close 不是关闭连接,而是归还到连接池
            jedis.close();
        }
    }

注意,上文中的 jedis.close() 不是关闭连接,而是归还连接,下面是 close 的源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  public void close() {
    if (dataSource != null) {
      // 如果有池子则走这边
      JedisPoolAbstract pool = this.dataSource;
      this.dataSource = null;
      // 归还连接给池子
      if (client.isBroken()) {
        pool.returnBrokenResource(this);
      } else {
        pool.returnResource(this);
      }
    } else {
      // 没有池子走这边
      super.close();
    }
  }

连接池的常用参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        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);

3 使用 Jedis 中的 Pipeline 功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    public static void main(String[] args) {

        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        JedisPool jedisPool = new JedisPool(poolConfig, IP, PORT);
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            Pipeline pipelined = jedis.pipelined();
            for (int i = 0; i < 1000; i++) {
                pipelined.set("test" + i, i + "");
            }
            // 执行这个方法才是真正的执行了命令
            pipelined.sync();
        } finally {
            // 这里的 close 不是关闭连接,而是归还到连接池
            jedis.close();
        }
    }

    public static void main(String[] args) {

        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        JedisPool jedisPool = new JedisPool(poolConfig, IP, PORT);
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            Pipeline pipelined = jedis.pipelined();
            for (int i = 0; i < 1000; i++) {
                pipelined.set("test" + i, i + "");
            }
            // 这个方法会将执行结果进行返回
            List<Object> objects = pipelined.syncAndReturnAll();
            for (Object object : objects) {
                System.out.println(object);
            }

        } finally {
            // 这里的 close 不是关闭连接,而是归还到连接池
            jedis.close();
        }
    }

4 client list

client list 命令能列出与 Redis 服务端相连的所有客户端连接信息,下面便是一段执行 client list 后得到的结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$redis-cli> client list
id=18391604 addr=172.19.10.2:55009 fd=50 name= age=2092290 idle=2092275 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 read=0 write=0 type=user
id=18667591 addr=172.19.10.2:47210 fd=174 name= age=1311897 idle=1285349 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 read=0 write=0 type=user

# 横着看不方便,放倒了看
id=18667591
addr=172.19.10.2:47210
fd=174 
name= 
age=1311897 
idle=1285349 
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 
read=0 
write=0 
type=user

下面开始接上上述结果的各个参数

4.1 标识:id、addr、fd、name

这四个参数用于标识一个客户端

  • id: 客户端连接的唯一标识,这个 id 是随着 redis 的连接自增的,重启 redis 后会重制为 0。
  • addr: 客户端连接的 ip 和端口
  • fd: socket 的文件描述符,与 lsof 命令结果中的 fd 是同一个,如果 fd=-1 代表当前客户端不是外部客户端,而是 redis 内部的伪装客户端。
  • name: 客户端的名字,后面的 client setName 和 client getName 两个命令会对其进行说明。

4.2 输入缓冲区: qbuf、qbuf-free

Redis 为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时 Redis 会从输入缓冲区拉取命令执行。

qbuf 和 qbuf-free 分别代表了这个缓冲区的总容量和剩余容量, Redis 没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小不同来动态调整,只是要求每个客户端缓冲区的大小不能超过 1GB,超过后客户端会被关闭。

虽然单个客户端的大小被限制到最大 1GB,但是多个客户端的缓冲区加起来可能会超过 1GB。假如说输入缓冲区的使用达到了 3GB,而 redis 的最大内存只有 4G,同时 redis 本身使用了 2GB,那么就会导致使用的内存超过了最大内存,这个时候可能会产生数据丢失、键值淘汰等情况。

造成上面两种情况的可能原因有两种,一种是由于 redis 本身被某个命令阻塞了,这个时候别的命令就会在缓冲区滞留;第二种是由于输入了大量的 bigkey,造成了缓冲区过大。

防止这种情况有两种办法

  1. 一是通过定期执行 client list 命令,手机 qbuf 和 qbuf-free 找到一场的连续记录并分析,最终找出可能出问题的客户端。
  2. 二是通过 info 命令的 info clients,找到最大的输入缓冲区,例如下面命令中的其中 client_biggest_input_buf 代表最大的输入缓冲区,例如可以设置超过 10MB 就进行报警:

    1
    2
    3
    4
    5
    6
    
    127.0.0.1:6379> info clients 
    # Clients 
    connected_clients: 1414
    client_longest_output_list: 0 
    client_biggest_input_buf: 2097152 
    blocked_clients: 0

两种方式的优劣对比

命令 优点 缺点
client list 能精确分析每个客户端来定位问题 执行速度较慢(尤其在连接数较多的情况下,频繁执行存在阻塞 redis 的可能
info clients 执行速度比 client list 快,分析过程较为简单 不能精确定位到客户端,不能显示所有输入缓冲区的总量,只能显示最大值

4.3 输出缓冲区:obl、oll、omem

redis 为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果给客户端。与输入缓冲区不同的是,输出缓冲区的容量可以通过参数 client-output-buffer-limit 来设置。

1
$redis-cli> client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
  1. class: 客户端类型,分为三种:
    1. normal: 普通客户端
    2. slave: slave 客户端,用于复制
    3. pubsub: 发布订阅客户端
  2. hard limit: 如果客户端使用的输出缓冲区大于 hard limit,那么客户端会被立即关闭
  3. soft limit 和 soft seconds: 如果客户端使用的输出缓冲区超过了 soft limit 并且持续了 soft seconds 秒,那么客户端会被立刻关闭。

输出缓冲区与输入缓冲区一样也不会收到 maxmemory 的限制,如果使用不当也会造成数据丢失、键值淘汰等情况。

实际上输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果。固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将 Redis 新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果。

  • obl: 固定缓冲区的长度
  • oll: 动态缓冲区的长度
  • omem: 使用的字节数(obl 和 oll 加起来用的大小)

4.4 客户端的存活状态:age、idle

  • age: 当前客户端已经连接的时间
  • idle: 最近一次的空闲时间

    1
    2
    
    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
    # 这个信息中的 age 表示客户端连接 redis 的时间为 603382s,空闲时间为 331060s

redis 可以设置最大连接数和超时时间,如果空闲时间超过该时间会自动关闭该连接

1
2
3
4
5
# 最大客户端数
$redis-cli> config set maxclients 10000
# 连接超时时间,默认是 0,不检测超时
# 这里设置为 30s
$redis-cli> config set timeout 30

在实际生产使用时客户端需要加空闲检测和验证,防止大量空间连接导致连接占用过多最终导致严重故障。

4.5 客户端类型:flag

flag 表示客户端类型,各种状态说明见下表

客户端状态表

4.6 汇总

汇总

5 设置和获取客户端的名字

  • client setName <name>
  • client getName

    1
    2
    
    $redis-cli> client setName client
    $redis-cli> client getName

6 杀死客户端

client kill ip:port

此命令用于杀死指定 IP 和端口的客户端

7 阻塞客户端

client pause <timeout>

client pause 命令用于阻塞客户端 timeout 毫秒数,但是这个命令只对普通和发布订阅客户端有效,对于主从复制是无效的,也就是说此期间主从复制是正常进行的,所以此命令可以用来让主从复制保持一致。

8 监控

monitor

这个命令可用于监控别的客户端的状态

9 客户端统计片段

1
2
3
4
5
6
$redis-cli> info clients 
# Clients 
connected_clients:1414 
client_longest_output_list: 0 
client_biggest_input_buf: 2097152 
blocked_clients: 0
  1. connected_clients:代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。
  2. client_longest_output_list:当前所有输出缓冲区中队列对象个数的最大值。
  3. client_biggest_input_buf:当前所有输入缓冲区中占用的最大容量。
  4. blocked_clients:正在执行阻塞命令(例如blpop、brpop、 brpoplpush)的客户端个数。

    1
    2
    3
    4
    
    $redis-cli> info stats 
    total_connections_received: 80 
    ...
    rejected_connections: 0
  5. total_connections_received:Redis 自启动以来处理的客户端连接数总数。

  6. rejected_connections:Redis 自启动以来拒绝的客户端连接数,需要重点监控。

10 客户端常见异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 无法从连接池获取到连接
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
# 在等待一段时间后依然获取不到连接
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object

# 客户端读写超时
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
# 获取不到连接
Caused by: java.util.NoSuchElementException: Pool exhausted

# 客户端读写超时
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out

# 客户端连接超时
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out

# 客户端缓冲区异常
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stre

# Redis 正在加载持久化文件
redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory

# Redis 使用的内存超过 maxmemory 配置
redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'.

# 客户端连接数过大
redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients re

参考资料

1.Redis 开发与运维