JedisPool 资源池优化及常见问题汇总

背景

合理的 JedisPool 资源池参数设置能为业务使用 Redis 保驾护航,本文将对 JedisPool 的使用、资源池的参数进行详细说明,最后给出 "最合理" 配置。

一、使用方法

以官方的 3.0.1 为例子 (Jedis Release),Maven 依赖如下:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.0.1</version>
    <scope>compile</scope>
</dependency>

Jedis 使用 apache commons-pool2 对 Jedis 资源池进行管理,所以在定义 JedisPool 时一个很重要的参数就是资源池 GenericObjectPoolConfig,使用方式如下,其中有很多资源管理和使用的参数 (具体看第二节)。注意:后面会提到建议用 JedisPoolConfig 代替 GenericObjectPoolConfig

GenericObjectPoolConfig jedisPoolConfig = new GenericObjectPoolConfig();
jedisPoolConfig.setMaxTotal(..); jedisPoolConfig.setMaxIdle(..); jedisPoolConfig.setMinIdle(..); jedisPoolConfig.setMaxWaitMillis(..); 

JedisPool 的初始化如下:

// redisHost 和 redisPort 是实例的 IP 和端口,redisPassword 是实例的密码
// timeout,这里既是连接超时又是读写超时,从 Jedis 2.8 开始有区分 connectionTimeout 和 soTimeout 的构造函数
JedisPool jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, timeout, redisPassword);

Jedis jedis = null;
try {
     jedis = jedisPool.getResource();
     // 具体的命令
     jedis.executeCommand()
 } catch (Exception e) {
     logger.error(e.getMessage(), e);
 } finally {
     // 注意这里不是关闭连接,在 JedisPool 模式下,Jedis 会被归还给资源池。
     if (jedis != null) 
         jedis.close();
 }

二、参数说明

JedisPool 保证资源在一个可控范围内,并且提供了线程安全,但是一个合理的 GenericObjectPoolConfig 配置能为应用使用 Redis 保驾护航,下面将对它的一些重要参数进行说明和建议: 在当前环境下,Jedis 连接就是资源,JedisPool 管理的就是 Jedis 连接。

1. 资源设置和使用

序号参数名含义默认值使用建议
1maxTotal资源池中最大连接数8设置建议见下节
2maxIdle资源池允许最大空闲的连接数8设置建议见下节
3minIdle资源池确保最少空闲的连接数0设置建议见下节
4blockWhenExhausted当资源池用尽后,调用者是否要等待。只有当为 true 时,下面的 maxWaitMillis 才会生效true建议使用默认值
5maxWaitMillis当资源池连接用尽后,调用者的最大等待时间 (单位为毫秒)-1:表示永不超时不建议使用默认值
6testOnBorrow向资源池借用连接时是否做连接有效性检测 (ping),无效连接会被移除false业务量很大时候建议设置为 false(多一次 ping 的开销)。
7testOnReturn向资源池归还连接时是否做连接有效性检测 (ping),无效连接会被移除false业务量很大时候建议设置为 false(多一次 ping 的开销)。
8jmxEnabled是否开启 jmx 监控,可用于监控true建议开启,但应用本身也要开启

2. 空闲资源监测

空闲 Jedis 对象检测,下面四个参数组合来完成,testWhileIdle 是该功能的开关。

序号参数名含义默认值使用建议
1testWhileIdle是否开启空闲资源监测falsetrue
2timeBetweenEvictionRunsMillis空闲资源的检测周期 (单位为毫秒)-1:不检测建议设置,周期自行选择,也可以默认也可以使用下面 JedisPoolConfig 中的配置
3minEvictableIdleTimeMillis资源池中资源最小空闲时间 (单位为毫秒),达到此值后空闲资源将被移除1000 * 60 * 30 = 30 分钟可根据自身业务决定,大部分默认值即可,也可以考虑使用下面 JeidsPoolConfig 中的配置
4numTestsPerEvictionRun做空闲资源检测时,每次的采样数3可根据自身应用连接数进行微调, 如果设置为 -1,就是对所有连接做空闲监测

为了方便使用,Jedis 提供了 JedisPoolConfig,它本身继承了 GenericObjectPoolConfig 设置了一些空闲监测设置

public class JedisPoolConfig extends GenericObjectPoolConfig {
  public JedisPoolConfig() {
    // defaults to make your life with connection pool easier :)
    setTestWhileIdle(true);
    //
    setMinEvictableIdleTimeMillis(60000);
    //
    setTimeBetweenEvictionRunsMillis(30000);
    setNumTestsPerEvictionRun(-1);
     }
 }

所有默认值可以从 org.apache.commons.pool2.impl.BaseObjectPoolConfig 中看到。

三、资源池大小 (maxTotal)、空闲 (maxIdle minIdle) 设置建议

1.maxTotal:最大连接数

实际上这个是一个很难回答的问题,考虑的因素比较多:

  • 业务希望 Redis 并发量
  • 客户端执行命令时间
  • Redis 资源:例如 nodes(例如应用个数) * maxTotal 是不能超过 redis 的最大连接数。
  • 资源开销:例如虽然希望控制空闲连接,但是不希望因为连接池的频繁释放创建连接造成不必靠开销。

以一个例子说明,假设:

  • 一次命令时间(borrow|return resource + Jedis 执行命令 (含网络) )的平均耗时约为 1ms,一个连接的 QPS 大约是 1000
  • 业务期望的 QPS 是 50000

那么理论上需要的资源池大小是 50000 / 1000 = 50 个。但事实上这是个理论值,还要考虑到要比理论值预留一些资源,通常来讲 maxTotal 可以比理论值大一些。

但这个值不是越大越好,一方面连接太多占用客户端和服务端资源,另一方面对于 Redis 这种高 QPS 的服务器,一个大命令的阻塞即使设置再大资源池仍然会无济于事。

2. maxIdle minIdle

maxIdle 实际上才是业务需要的最大连接数,maxTotal 是为了给出余量,所以 maxIdle 不要设置过小,否则会有 new Jedis(新连接) 开销,而 minIdle 是为了控制空闲资源监测。

连接池的最佳性能是 maxTotal = maxIdle , 这样就避免连接池伸缩带来的性能干扰。但是如果并发量不大或者 maxTotal 设置过高,会导致不必要的连接资源浪费。
可以根据实际总 OPS 和调用 redis 客户端的规模整体评估每个节点所使用的连接池。

3. 监控

实际上最靠谱的值是通过监控来得到 "最佳值" 的,可以考虑通过一些手段 (例如 jmx) 实现监控,找到合理值。

四、常见问题

1. 资源 "不足"

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) 

或者

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)

两种情况均属于无法从资源池获取到资源,但第一种是超时,第二种是因为 blockWhenExhausted 为 false 根本就不等。

遇到此类异常,不要盲目的认为资源池不够大,第三节已经进行了分析。具体原因可以排查:网络、资源池参数设置、资源池监控 (如果对 jmx 监控)、代码 (例如没执行 jedis.close())、慢查询、DNS 等问题。

2. 预热 JedisPool

由于一些原因 (例如超时时间设置较小原因),有的项目在启动成功后会出现超时。JedisPool 定义最大资源数、最小空闲资源数时,不会真的把 Jedis 连接放到池子里,第一次使用时,池子没有资源使用,会 new Jedis,使用后放到池子里,可能会有一定的时间开销,所以也可以考虑在 JedisPool 定义后,为 JedisPool 提前进行预热,例如以最小空闲数量为预热数量

List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
   Jedis jedis = null;
   try {
       jedis = pool.getResource();
       minIdleJedisList.add(jedis);
       jedis.ping();
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    } finally {
        if (jedis != null)
           jedis.close();
    }
}
    
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
    Jedis jedis = null;
    try {
        jedis = minIdleJedisList.get(i);
        jedis.close();
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    } finally {

        if (jedis != null)
           jedis.close(); 
    }
}

常见问题汇总

Jedis 虽然使用起来比较简单,但是如果不能根据使用场景设置合理的参数 (例如连接池参数),不合理的使用一些功能 (例如 Lua 和事务) 也会产生很多问题,本文对这些问题逐个说明:

详细目录:

  • 一、redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
  • 二、redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream
  • 三、redis.clients.jedis.exceptions.JedisDataException: ERR illegal address
  • 四、redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients reached
  • 五、redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
  • 六、redis.clients.jedis.exceptions.JedisDataException: NOAUTH Authentication required
  • 七、redis.clients.jedis.exceptions.JedisDataException: EXECABORT Transaction discarded because of previous errors
  • 八、java.lang.ClassCastException: java.lang.Long cannot be cast to java.util.List
  • 九、redis.clients.jedis.exceptions.JedisDataException: WRONGTYPE Operation against a key holding the wrong kind of value
  • 十、redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'
  • 十一、redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory
  • 十二、redis.clients.jedis.exceptions.JedisDataException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
  • 十三、redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
  • 十四、UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
  • 十五、java.lang.NoClassDefFoundError
  • 十六、redis.clients.jedis.exceptions.JedisDataException: ERR unknown command
  • 十七、redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method.
  • 十八、redis.clients.jedis.exceptions.JedisDataException: ERR command role not support for normal user.

一. 无法从连接池获取到 Jedis 连接

1. 异常堆栈

(1) 连接池参数 blockWhenExhausted = true(默认)

如果连接池没有可用 Jedis 连接,会等待 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)

(2) 连接池参数 blockWhenExhausted = false

设置如果连接池没有可用 Jedis 连接,立即抛出异常:

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. 异常描述

上述异常是客户端没有从连接池 (最大 maxTotal 个) 拿到可用 Jedis 连接造成的,具体可能有如下原因:

(1) 连接泄露 (较为常见)

JedisPool 默认的 maxTotal=8,下面的代码从 JedisPool 中借了 8 次 Jedis,但是没有归还,当第 9 次 (jedisPool.getResource().ping())

GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1;

// 向 JedisPool 借用 8 次连接,但是没有执行归还操作。
for (int i = 0; i < 8; i++) {
    Jedis jedis = null;
    try {
        jedis = jedisPool.getResource();
        jedis.ping();
    } catch (Exception e) {
         logger.error(e.getMessage(), e);
     }
 }
 jedisPool.getResource().ping();

所以推荐使用的代码规范是:

Jedis jedis = null;
try {
    jedis = jedisPool.getResource();
    // 具体的命令
    jedis.executeCommand()
} catch (Exception e) {
    // 如果命令有 key 最好把 key 也在错误日志打印出来,对于集群版来说通过 key 可以帮助定位到具体节点。
    logger.error(e.getMessage(), e);
 } finally {
     // 注意这里不是关闭连接,在 JedisPool 模式下,Jedis 会被归还给资源池。
     if (jedis != null) 
         jedis.close();
 }

(2) 业务并发量大,maxTotal 确实设置小了。

举个例子:

  • 一次命令时间(borrow|return resource + Jedis 执行命令 (含网络) )的平均耗时约为 1ms,一个连接的 QPS 大约是 1000
  • 业务期望的 QPS 是 50000

那么理论上需要的资源池大小是 50000 / 1000 = 50 个,实际 maxTotal 可以根据理论值进行微调。

(3) Jedis 连接还的太慢

例如 Redis 发生了阻塞 (例如慢查询等原因),所有连接在超时时间范围内等待,并发量较大时,会造成连接池资源不足。

(4) 其他问题

例如丢包、DNS、客户端 TCP 参数配置。

3. 解决方法:

可以看到这个问题稍微复杂一些,不要被异常的表象所迷惑,简单地认为连接池不够就盲目加大 maxTotal,要具体问题具体分析。

4. 处理人

客户先确认,如解决不了,需要借助工单解决

还有一种情况是:从池子里拿连接,由于没有空闲连接,需要重新生成一个 Jedis 连接,但是连接被拒绝:

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
    at redis.clients.util.Pool.getResource(Pool.java:50)
    at redis.clients.jedis.JedisPool.getResource(JedisPool.java:99)
    at TestAdmin.main(TestAdmin.java:14)
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: Connection refused
    at redis.clients.jedis.Connection.connect(Connection.java:164)
    at redis.clients.jedis.BinaryClient.connect(BinaryClient.java:80)
    at redis.clients.jedis.BinaryJedis.connect(BinaryJedis.java:1676)
    at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:87)
     at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:861)
     at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:435)
     at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363)
     at redis.clients.util.Pool.getResource(Pool.java:48)
     ... 2 more
 Caused by: java.net.ConnectException: Connection refused
     at java.net.PlainSocketImpl.socketConnect(Native Method)
     at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339)
     at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
     at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
     at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
     at java.net.Socket.connect(Socket.java:579)
     at redis.clients.jedis.Connection.connect(Connection.java:158)
     ... 9 more

可以从 at redis.clients.jedis.Connection.connect(Connection.java:158) 看到实际是一个 Socket 连接:

socket.setSoLinger(true, 0); // Control calls close () method,
      // the underlying socket is closed
      // immediately
      // <-@wjw_add
8:  socket.connect(new InetSocketAddress(host, port), connectionTimeout);

一般这种需要检查 Redis 的域名配置是否正确,排查该段时间网络是否正常

二、客户端缓冲区异常

1. 异常堆栈

redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
    at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
    at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
    at redis.clients.jedis.Protocol.process(Protocol.java:151)
......

2. 异常描述:

这个异常是客户端缓冲区异常,产生这个问题可能有三个原因:

(1) 常见原因:多个线程使用一个 Jedis 连接,正常的情况是一个线程使用一个 Jedis 连接,可以使用 JedisPool 管理 Jedis 连接,实现线程安全,防止出现这种情况,例如下面代码中两个线程用了一个 Jedis 连接:

new Thread(new Runnable() {
    public  void  run() {
        for (int i = 0; i < 100; i++) {
            jedis.get("hello");
        }
    }
}).start();
 new Thread(new Runnable() {
     public  void  run() {
         for (int i = 0; i < 100; i++) {
             jedis.hget("haskey", "f");
         }
     }
 }).start();

(2) 客户端缓冲区满了

Redis 有三种客户端缓冲区:

  • 普通客户端缓冲区 (normal):用于接受普通的命令,例如 get、set、mset、hgetall、zrange 等
  • slave 客户端缓冲区 (slave):用于同步 master 节点的写命令,完成复制。
  • 发布订阅缓冲区 (pubsub):pubsub 不是普通的命令,因此有单独的缓冲区。

Redis 的客户端缓冲区配置具体格式是:

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

(a) class: 客户端类型:(a) normal、(b) slave、(c) pubsub

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

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

例如下面是一份 Redis 缓冲区的配置,所以当条件满足时,客户端连接会被关闭,就会出现 Unexpected end of stream。

redis> config get client-output-buffer-limit
1) "client-output-buffer-limit"
2) "normal 524288000 0 0 slave 2147483648 536870912 480 pubsub 33554432 8388608 60"

(3) 长时间闲置连接被服务端主动断开,可以查询 timeout 配置的设置以及自身连接池配置是否需要做空闲检测。

3. 解决方法和处理人:

客户:排查自身代码是否使用 JedisPool 管理 Jedis 连接,是否存在并发操作 Jedis 的情况。

工单: 排查 (2)(3),阿里云 Redis 中 timeout=0,也就是不会主动关闭空闲连接,缓冲区设置为 0 0 0 也就是不会对客户端缓冲区进行限制,一般不会有问题

三、非法客户端地址 (阿里云 Redis 提供客户端白名单功能)

1. 异常堆栈

Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR illegal address
    at redis.clients.jedis.Protocol.processError(Protocol.java:117)
    at redis.clients.jedis.Protocol.process(Protocol.java:151)
    at redis.clients.jedis.Protocol.read(Protocol.java:205)
    ......

2. 异常描述:

Redis 实例配置了白名单,但当前访问 Redis 的客户端 (IP) 不在白名单中。

3. 解决方法:

添加该客户端 (IP) 的白名单

4. 处理人

客户或者工单都可以

四、客户端连接数达到最大值

1. 异常堆栈

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

2. 异常描述:

如果客户端连接数超过了 Redis 实例配置的最大 maxclients

3. 解决方法:

提工单帮助临时调大最大连接数,并让客户找到连接数暴涨的原因 (因为上述调整只是临时调整),

4. 处理人

  • 工单:临时调整最大连接数,协助定位问题
  • 客户:定位自身问题 (可以定位连接最多的客户端),找到问题原因(例如连接池配置等)

五、客户端读写超时

1. 异常堆栈

redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out

2. 异常描述:

该问题原因可能有如下几种:
(1) 读写超时设置的过短。
(2) 有慢查询或者 Redis 发生阻塞。
(3) 网络不稳定。

3. 解决方法:

客户提供读写超时时间,提交工单定位相关原因

4. 处理人:

工单。

六、密码相关的异常

1. 异常堆栈

Redis 设置了密码,客户端请求没传密码:

Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: NOAUTH Authentication required.
     at redis.clients.jedis.Protocol.processError(Protocol.java:127)
     at redis.clients.jedis.Protocol.process(Protocol.java:161)
     at redis.clients.jedis.Protocol.read(Protocol.java:215)

Redis 没有设置密码,客户端传了密码:

Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: ERR Client sent AUTH, but no password is set
     at redis.clients.jedis.Protocol.processError(Protocol.java:127)
     at redis.clients.jedis.Protocol.process(Protocol.java:161)
     at redis.clients.jedis.Protocol.read(Protocol.java:215)

客户端传了错误的密码:

redis.clients.jedis.exceptions.JedisDataException: ERR invalid password
    at redis.clients.jedis.Protocol.processError(Protocol.java:117)
    at redis.clients.jedis.Protocol.process(Protocol.java:151)
    at redis.clients.jedis.Protocol.read(Protocol.java:205)

2. 解决方法:弄清楚到底有没有密码,密码是否正确。

七、事务异常

1. 异常堆栈

redis.clients.jedis.exceptions.JedisDataException: EXECABORT Transaction discarded because of previous errors

2. 异常描述:

这个是 Redis 的事务异常:事务中包含了错误的命令,例如如下 sett 是个不存在的命令。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> sett key world
(error) ERR unknown command 'sett'
127.0.0.1:6379> incr counter
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

3. 解决方法和处理人:

客户修复自身代码错误。

八、类转换错误

1. 异常堆栈

java.lang.ClassCastException: java.lang.Long cannot be cast to java.util.List
         at redis.clients.jedis.Connection.getBinaryMultiBulkReply(Connection.java:199)
         at redis.clients.jedis.Jedis.hgetAll(Jedis.java:851)
         at redis.clients.jedis.ShardedJedis.hgetAll(ShardedJedis.java:198) 
java.lang.ClassCastException: java.util.ArrayList cannot be cast to [B
         at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java:182)
         at redis.clients.jedis.Connection.getBulkReply(Connection.java:171)
         at redis.clients.jedis.Jedis.rpop(Jedis.java:1109)
         at redis.clients.jedis.ShardedJedis.rpop(ShardedJedis.java:258)
.......

2. 异常描述:

Jedis 正确的使用方法是:一个线程操作一个 Jedis,通常来讲产生该错误是由于没有使用 JedisPool 造成的,例如如下代码在两个线程并发使用了一个 Jedis。(get、hgetAll 返回类型也是不一样的)

new Thread(new Runnable() {
   public void run() {
       for (int i = 0; i < 100; i++) {
           jedis.set("hello", "world");
           jedis.get("hello");
       }
   }
}).start();
new Thread(new Runnable() {
    public void run() {
        for (int i = 0; i < 100; i++) {
            jedis.hset("hashkey", "f", "v");
            jedis.hgetAll("hashkey");
        }
    }
}).start();

3. 解决方法和处理人:

客户排查自身代码是否存在上述问题

九、命令使用错误

1. 异常堆栈

Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: WRONGTYPE Operation against a key holding the wrong kind of value
    at redis.clients.jedis.Protocol.processError(Protocol.java:127)
    at redis.clients.jedis.Protocol.process(Protocol.java:161)
    at redis.clients.jedis.Protocol.read(Protocol.java:215)
.....

2. 异常描述:

例如 key="hello" 是字符串类型的键,而 hgetAll 是哈希类型的键,所以出现了错误。

jedis.set("hello","world");
jedis.hgetAll("hello");

3. 解决方法和处理人:

请客户修改自身代码错误。

十、Redis 使用的内存超过 maxmemory 配置

1. 异常堆栈

redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'.

2. 异常描述:

Redis 节点 (如果是集群,则是其中一个节点) 使用大于该实例的内存规格 (maxmemory 配置)。

3. 解决方法:

原因可能有以下几个:

  • 业务数据正常增加
  • 客户端缓冲区异常:例如使用了 monitor、pub/sub 使用不当等等
  • 纯缓存使用场景,但是 maxmemory-policy 配置有误 (例如没有过期键的业务配置 volatile-lru)

紧急处理,可以临时提工单帮助临时调整 maxmeory,后续咨询用户是否升配或者调整配置。

4. 处理人

  • 客户:找到内存增大的原因。
  • 工单:协助临时调整 maxmeomry,如果客户需要,可以协助解决

十一、Redis 正在加载持久化文件

1. 异常堆栈

redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory

2. 异常描述:

Jedis 调用 Redis 时,如果 Redis 正在加载持久化文件,无法进行正常的读写。

3. 解决方法:

正常情况下,阿里云 Redis 不会出现这种情况,如果出现,则提交工单处理。

4. 处理人:

工单。

十二、Lua 脚本超时

1. 异常堆栈

redis.clients.jedis.exceptions.JedisDataException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

2. 异常描述:

如果 Redis 当前正在执行 Lua 脚本,并且超过了 lua-time-limit,此时 Jedis 调用 Redis 时,会收到下面的异常

3. 解决方法:

按照异常提示:You can only call SCRIPT KILL or SHUTDOWN NOSAVE. (使用 script kill:kill 掉 Lua 脚本)

4. 处理人:

最好客户自己处理,如果解决不了,值班人员可以协助操作。

十三 连接超时

1. 异常堆栈

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

2. 异常描述:

可能产生的原因:

  • 连接超时设置的过短。
  • tcp-backlog 满,造成新的连接失败。
  • 客户端与服务端网络不正常。

3. 解决方法:

客户提供连接超时时间,提交工单定位相关原因。

4. 处理人:

工单。

十四 Lua 脚本写超时

1. 异常堆栈 {#toc_h4_74}

(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.

2. 异常描述:

如果 Redis 当前正在执行 Lua 脚本,并且超过了 lua-time-limit,并且已经执行过写命令,此时 Jedis 调用 Redis 时,会收到上面的异常

3. 解决方法:

提交工单做紧急处理,管理员要做重启或者切换 Redis 节点。

4. 处理人:

工单。

十五、类加载错误

1. 异常堆栈

例如找不到类和方法:

Exception in thread "commons-pool-EvictionTimer" java.lang.NoClassDefFoundError: redis/clients/util/IOUtils
    at redis.clients.jedis.Connection.disconnect(Connection.java:226)
    at redis.clients.jedis.BinaryClient.disconnect(BinaryClient.java:941)
    at redis.clients.jedis.BinaryJedis.disconnect(BinaryJedis.java:1771)
    at redis.clients.jedis.JedisFactory.destroyObject(JedisFactory.java:91)
    at         org.apache.commons.pool2.impl.GenericObjectPool.destroy(GenericObjectPool.java:897)
    at org.apache.commons.pool2.impl.GenericObjectPool.evict(GenericObjectPool.java:793)
    at org.apache.commons.pool2.impl.BaseGenericObjectPool$Evictor.run(BaseGenericObjectPool.java:1036)
    at java.util.TimerThread.mainLoop(Timer.java:555)
     at java.util.TimerThread.run(Timer.java:505)
 Caused by: java.lang.ClassNotFoundException: redis.clients.util.IOUtils
 ......

2. 异常描述:

运行时,Jedis 执行命令,抛出异常:某个类找不到。一般此类问题都是由于加载多个 jedis 版本 (例如 jedis 2.9.0 和 jedis 2.6),在编译期代码未出现问题,但类加载器在运行时加载了低版本的 Jedis,造成运行时找不到类。

3. 解决方法:

通常此类问题,可以将重复的 jedis 排除掉,例如利用 maven 的依赖树,把无用的依赖去掉或者 exclusion 掉。

4. 处理人

客户排查自身代码

十六、服务端命令不支持

1. 异常堆栈

例如客户端执行了 geoadd 命令,但是服务端返回不支持此命令

redis.clients.jedis.exceptions.JedisDataException: ERR unknown command 'GEOADD'

2. 异常描述:

该命令不能被 Redis 端识别,有可能有两个原因:

  • 社区版的一些命令,阿里云 Redis 的不支持,或者只在某些小版本上支持 (例如 geoadd 是 Redis 3.2 添加的地理信息 api)。
  • 命令本身是错误的 (不过对于 Jedis 来说还好,不支持直接组装命令,每个 API 都有固定的函数)。

3. 解决方法:

咨询是否有 Redis 版本支持该命令,如支持可以让客户做小版本升级。

4. 处理人

  • 管理员:确认版本是否支持该命令
  • 客户:确认后,做小版本升级

十七、pipeline 错误使用

1. 异常堆栈

redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method.

2. 异常描述:

在 pipeline.sync()执行之前,通过 response.get() 获取值,在 pipeline.sync()执行前,命令没有执行 ( 可以通过 monitor 做验证),下面代码就会引起上述异常

Jedis jedis = new Jedis("127.0.0.1", 6379);
Pipeline pipeline = jedis.pipelined();
pipeline.set("hello", "world"); 
pipeline.set("java", "jedis");
Response<String> pipeString = pipeline.get("java");
// 这个 get 必须在 sync 之后,如果是批量获取值建议直接用 List<Object> objectList = pipeline.syncAndReturnAll();
System.out.println(pipeString.get());
// 命令此时真正执行
 pipeline.sync();

Jedis 中 Reponse 中 get() 方法,有个判断: 如果 set=false 就会报错,而 response 中的 set 初始化为 false.

public T get() {
  //  if response has dependency response and dependency is  not built,
  // build it first and  no more!!
  if (dependency != null && dependency.set && !dependency.built) {
    dependency.build();
  }
  if (!set) {
    throw new JedisDataException(
        "Please close pipeline or multi block before calling this method.");
   }
   if (!built) {
     build();
   }
   if (exception != null) {
     throw exception;
   }
   return response;
 }

pipeline.sync() 会每个结果设置 set=true。

public  void  sync() {
  if (getPipelinedResponseLength() > 0) {
    List<Object> unformatted = client.getAll();
    for (Object o : unformatted) {
      generateResponse(o);
    }
  }
}

其中 generateResponse(o):

protected Response<?> generateResponse(Object data) {
  Response<?> response = pipelinedResponses.poll();
  if (response != null) {
    response.set(data);
  }
  return response;
}

其中 response.set(data);

public  void  set(Object data) {
    this.data = data;
    set = true;
}

3. 解决方法:

实际上对于批量结果的解析,建议使用 pipeline.syncAndReturnAll() 来实现,下面操作模拟了批量 hgetAll

/** * pipeline 模拟批量 hgetAll * @param keyList * @return */
public Map<String, Map<String, String>> mHgetAll(List<String> keyList) {
// 1. 生成 pipeline 对象
Pipeline pipeline = jedis.pipelined();
 // 2.pipeline 执行命令,注意此时命令并未真正执行
 for (String key : keyList) {
   pipeline.hgetAll(key);
 }
 
 // 3. 执行命令 syncAndReturnAll() 返回结果
 List<Object> objectList = pipeline.syncAndReturnAll();
 if (objectList == null || objectList.isEmpty()) {
   return Collections.emptyMap();
 }
 
// 4. 解析结果
Map<String,Map<String, String>> resultMap = new HashMap<String, Map<String,String>>();
for (int i = 0; i < objectList.size(); i++) {
  Object object = objectList.get(i);
  Map<String, String> map = (Map<String, String>) object;
  String key = keyList.get(i);
  resultMap.put(key, map);
}
return resultMap;
}

4. 处理人:

修改业务代码。

十八、管理员命令,普通用户不能执行

1. 异常堆栈

命令 role 不能被普通用户执行,可以参考暂未开放的 Redis 命令

redis.clients.jedis.exceptions.JedisDataException: ERR command role not support for normal user

2. 异常描述:

改命令尚未开放

3. 解决方法:

不能使用该命令,如果有需求或者疑问可以联系值班人员。

4. 处理人

从文档中确认该命令是否开放