Jedis使用的常见问题及解决方法

我们在使用Jedis的时候,如果不考虑使用场景做出一些合理的设置是会产生不少问题的,另外合理的JedisPool资源池参数设置能为业务使用Redis保驾护航。

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

1.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)
异常描述及解决方法

连接过程如下图所示:

image.png

这种没有可用连接的异常是客户端没有从连接池(最大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", 6379);
//向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)nf_conntrack丢包问题
这种错误典型的异常如下:nf_conntrack: table full, dropping packet
通过dmesg检查客户端是否有异常,如果发生nf_conntract丢包可以通过修改设置sysctl -w net.netfilter.nf_conntrack_max=120000

5)TIME_WAIT问题
通过ss -s 查看time wait链接是否过多,如果TIME_WAIT过多可以修改以下参数:

sysctl -w net ipv4.tcp_max_tw_buckets=18000
sysctl -w net ipv4.tcp_tw_recycle=1

6)DNS问题
通过在/etc/hosts文件直接绑定host地址,绑定完成之后查看问题是否还存在,如果还存在则不是DNS解析问题


7)连接拒绝
还有一种情况,从池子里拿连接,由于没有空闲连接,需要重新生成一个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

这里可以看到实际是一个Socket连接,一般这种需要检查Redis的域名配置是否正确,排查该段时间网络是否正常。

2. 客户端缓冲异常

2.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.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();

这种问题需要排查自身代码是否使用JedisPool管理Jedis连接,是否存在并发操作Jedis的情况。

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

这种问题需要排查阿里云Redis中timeout=0,也就是不会主动关闭空闲连接,缓冲区设置为0 0 0 也就是不会对客户端缓冲区进行限制,一般不会有问题。

3)长时间闲置连接被服务端主动断开
这种问题的处理和问题2客户端缓冲区满是一样的,也要查下timeout配置,还有连接池的空闲检测。

3.非法客户端地址

3.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)
3.2异常描述及解决方法

这种异常就是Redis实例配置了白名单,但当前访问Redis的客户端(IP)不在白名单中。解决的话就是添加客户端IP到白名单中。

4.客户端连接数达到最大值

4.1异常堆栈信息
redis.clients.jedis.exceptions.JedisDataException:  ERR max number of clients reached
4.2异常描述及解决方法

客户端连接数超过了Redis实例配置的最大maxclients
提工单帮助临时调大最大连接数,并让客户找到连接数暴涨的原因(从连接最多的客户端开始看,看看连接池配置有什么问题)。

5.客户端读写超时

5.1异常堆栈信息
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException:
Read  timed out
5.2异常描述及解决方法

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

6.密码相关的异常

6.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)`
6.2解决方法

密码问题的解决方法及时检查密码是否正确,还是是否设置了密码

7.事务异常

7.1. 异常堆栈
redis.clients.jedis.exceptions.JedisDataException: EXECABORT Transaction discarded because of previous errors
7.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.

这种错误就是自己检查错误命令修改就可以了

8.类转换错误

8.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)
8.2异常描述及解决方法

检查代码中类涉及强制转换的部分。

9.命令使用错误

9.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)
9.2异常描述及解决方法

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

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

这种错误就是自己检查错误命令修改就可以了。

10.Redis使用的内存超过maxmemory配置

10.1异常堆栈
redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'.
10.2异常描述及解决方法

Redis节点(如果是集群,则是其中一个节点)使用大于该实例的内存规格(maxmemory配置)。
原因可能有以下几个:
1)业务数据正常增加
2)客户端缓冲区异常:例如使用了monitor、pub/sub使用不当等等
3)纯缓存使用场景,但是maxmemory-policy配置有误(例如没有过期键的业务配置volatile-lru)
紧急处理,可以临时提工单帮助临时调整maxmemory,后续咨询用户是否升配或者调整配置。最终的解决还是要确认内存增大原因。

11.Redis正在加载持久化文件

11.1.异常堆栈
redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory
11.2异常描述及解决方法

Jedis调用Redis时,如果Redis正在加载持久化文件,无法进行正常的读写,解决方法就是提交阿里云工单处理。

12.Lua脚本超时

12.1异常堆栈
redis.clients.jedis.exceptions.JedisDataException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
12.2异常描述及解决方法

如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit,此时Jedis调用Redis时,会收到这种异常。解决办法就是按照异常提示:You can only call SCRIPT KILL or SHUTDOWN NOSAVE. (使用script kill:kill掉Lua脚本)。

13.连接超时

13.1.异常堆栈
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
13.2异常描述及解决方法

可能产生的原因:
1)连接超时设置的过短。
2)tcp-backlog满,造成新的连接失败。
3)客户端与服务端网络不正常。
这种问题需要客户提供连接超时时间,提交工单定位相关原因。

14.Lua脚本写超时

14.1.异常堆栈
(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.
14.2.异常描述及解决方法

如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit,并且已经执行过写命令,此时Jedis调用Redis时,会收到上面的异常,这种错误需要提交工单做紧急处理,管理员要做重启或者切换Redis节点。

15.类加载错误

15.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
15.2.异常描述及解决方法

运行时,Jedis执行命令,抛出异常:某个类找不到。一般此类问题都是由于加载多个jedis版本(例如jedis 2.9.0和jedis 2.6),在编译期代码未出现问题,但类加载器在运行时加载了低版本的Jedis,造成运行时找不到类。
通常此类问题,可以将重复的jedis排除掉,例如利用maven的依赖树,把无用的依赖去掉或者exclusion掉。

16.服务端命令不支持

16.1.异常堆栈

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

redis.clients.jedis.exceptions.JedisDataException: ERR unknown command 'GEOADD'
16.2.异常描述及解决方法

该命令不能被Redis端识别,有可能有两个原因:
1)社区版的一些命令,阿里云Redis的不支持,或者只在某些小版本上支持(例如geoadd是Redis 3.2添加的地理信息api)。
2)命令本身是错误的(不过对于Jedis来说还好,不支持直接组装命令,每个API都有固定的函数)。
解决办法是看是否有Redis版本支持该命令,如支持可以让客户做小版本升级。

17.pipeline错误使用

17.1.异常堆栈
redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method.
17.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 pipeString = pipeline.get("java");
//这个get必须在sync之后,如果是批量获取值建议直接用List 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 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;
}

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

/**
* pipeline模拟批量hgetAll
* @param keyList
* @return
*/
public Map> mHgetAll(List keyList) {
// 1.生成pipeline对象
Pipeline pipeline = jedis.pipelined();
// 2.pipeline执行命令,注意此时命令并未真正执行
for (String key : keyList) {
  pipeline.hgetAll(key);
}
// 3.执行命令 syncAndReturnAll()返回结果
List objectList = pipeline.syncAndReturnAll();
if (objectList == null || objectList.isEmpty()) {
  return Collections.emptyMap();
}

// 4.解析结果
Map> resultMap = new HashMap>();
for (int i = 0; i < objectList.size(); i++) {
  Object object = objectList.get(i);
  Map map = (Map) object;
  String key = keyList.get(i);
  resultMap.put(key, map);
}
return resultMap;
}

18.普通用户没有权限执行

18.1.异常堆栈

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

redis.clients.jedis.exceptions.JedisDataException: ERR command role not support for normal user
18.2异常描述及解决方法

这个错误是因为命令没有对普通用户开放,不能使用,只能联系管理员。

参考资料:
https://mp.weixin.qq.com/s?__biz=MzAxODcyNjEzNQ==&mid=2247484932&idx=1&sn=0b7547deb4ba179ed077bdb5fc45935b&chksm=9bd0ab9caca7228a3d65154d843c58a78cb211dd70eb58df68886f9393681681ab8fc25f33ae&scene=21#wechat_redirect
https://mp.weixin.qq.com/s?__biz=MzAxODcyNjEzNQ==&mid=2247484938&idx=1&sn=bea2ef85795c5a2e812c3c1f66c5f1ac&chksm=9bd0ab92aca722841a72aa2e9bf43f167a68fc31752c0def1d21a0d7adfe455a6e3d9c30fc86&scene=21#wechat_redirect

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容