Jedis虽然使用起来比较简单,但是不能根据使用场景设置合理的参数(例如连接池参数),或者不合理地使用了一些功能(例如Lua和事务)时,也会产生很多问题,本文对这些常见问题进行逐一说明。
1 | redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool<br> …<br>Caused by: java.util.NoSuchElementException: Timeout waiting for idle object<br> at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java: 449 ) |
1 | redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool<br> …<br>Caused by: java.util.NoSuchElementException: Pool exhausted<br> at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java: 464 ) |
上述异常是客户端没有从连接池获得可用的Jedis连接造成,Jedis资源最大数量由maxTotal值决定,可能有下列几种原因。
JedisPool默认的maxTotal值为8,从下列代码得知,从JedisPool中获取了8个Jedis资源,但是没有归还资源。因此,当第9次尝试获取Jedis资源的时候,则无法调用jedisPool.getResource().ping()。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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(); |
推荐使用下列规范代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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(); } |
业务并发量大导致出现异常的示例:一次命令运行时间(borrow|return resource + Jedis执行命令 + 网络时间)的平均耗时约为1ms,一个连接的QPS大约是1000。业务期望的QPS是50000。那么理论上需要的资源池大小为50000除以1000等于50个。因此用户需要根据实际情况对maxTotal值进行微调。
例如Redis发生了阻塞(例如慢查询等原因),所有连接会在超时时间范围内等待,当并发量较大时,会造成连接池资源不足。
从连接池中获取连接时,由于没有空闲连接,需要重新生成一个Jedis连接,但是连接被拒绝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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连接。
1 2 3 4 5 | socket.setSoLinger( true , 0 ); // Control calls close () method, // the underlying socket is closed // immediately // <-@wjw_add 158 : socket.connect( new InetSocketAddress(host, port), connectionTimeout); |
提示:一般这类报错需要检查Redis的域名配置是否正确,排查该段时间网络是否正常。
丢包、DNS、客户端TCP参数配置等,可以提交工单获取帮助。
从上述分析,可以看出这个问题的原因比较复杂,不能简单地认为连接池不够就盲目加大maxTotal值,需要具体问题具体分析。
异常堆栈如下。
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 ) ...... |
这个异常描述是客户端缓冲区异常,产生这个问题可能有下列三个原因。
正常的情况是一个线程使用一个Jedis连接,可以使用JedisPool管理Jedis连接,实现线程安全,避免出现这种情况。例如,下面代码就是两个线程共用了一个Jedis连接。
1 | new Thread( new Runnable() {<br> public void run() {<br> for ( int i = 0 ; i < 100 ; i++) {<br> jedis.get( "hello" );<br> }<br> }<br>}).start();<br> new Thread( new Runnable() {<br> public void run() {<br> for ( int i = 0 ; i < 100 ; i++) {<br> jedis.hget( "haskey" , "f" );<br> }<br> }<br>}).start(); |
Redis有下列三种客户端缓冲区。
Redis客户端缓冲区配置的格式如下。
1 | client-output-buffer-limit [$Class] [$Hard_Limit] [$Soft_Limit] [$Soft_Seconds] |
例如下面是一份Redis缓冲区的配置,所以当条件满足时,客户端连接会被关闭,就会出现Unexpected end of stream
报错。
1 | redis> config get client-output-buffer-limit<br> 1 ) "client-output-buffer-limit" <br> 2 ) "normal 524288000 0 0 slave 2147483648 536870912 480 pubsub 33554432 8388608 60" |
长时间闲置连接会被服务端主动断开,可以查询timeout配置的设置以及自身连接池配置确定是否需要做空闲检测。
提示:阿里云Redis提供客户端白名单功能。
异常堆栈如下。
1 | Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR illegal address<br> at redis.clients.jedis.Protocol.processError(Protocol.java: 117 )<br> at redis.clients.jedis.Protocol.process(Protocol.java: 151 )<br> at redis.clients.jedis.Protocol.read(Protocol.java: 205 )<br> ...... |
向集合添加一个成员时,例如使用SET TEST "Helloworld"
命令,出现下列报错,也可能是白名单问题。
1 | Error: Insert the diskette for drive % 1 . |
Redis实例配置了白名单,但当前访问Redis的客户端(IP)不在白名单中。
添加该客户端(IP)的白名单,关于如何添加白名单,请参考设置IP白名单。
异常堆栈如下。
1 | redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients reached |
客户端连接数超过了Redis实例配置的最大maxclients。
1 | redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out |
问题原因可能有下列几种。
用户提供读写超时时间,提交工单定位相关原因并解决。
1 | Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: NOAUTH Authentication required.<br> at redis.clients.jedis.Protocol.processError(Protocol.java: 127 )<br> at redis.clients.jedis.Protocol.process(Protocol.java: 161 )<br> at redis.clients.jedis.Protocol.read(Protocol.java: 215 ) |
1 | Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: ERR Client sent AUTH, but no password is set<br> at redis.clients.jedis.Protocol.processError(Protocol.java: 127 )<br> at redis.clients.jedis.Protocol.process(Protocol.java: 161 )<br> at redis.clients.jedis.Protocol.read(Protocol.java: 215 ) |
1 | redis.clients.jedis.exceptions.JedisDataException: ERR invalid password<br> at redis.clients.jedis.Protocol.processError(Protocol.java: 117 )<br> at redis.clients.jedis.Protocol.process(Protocol.java: 151 )<br> at redis.clients.jedis.Protocol.read(Protocol.java: 205 ) |
确认有没有设置密码鉴权,是否提供了正确的密码。
异常堆栈如下。
1 | redis.clients.jedis.exceptions.JedisDataException: EXECABORT Transaction discarded because of previous errors |
这个是Redis的事务异常,事务中包含了错误的命令,例如,下列sett是个不存在的命令。
1 | 127.0 . 0.1 : 6379 > multi<br>OK<br> 127.0 . 0.1 : 6379 > sett key world<br>(error) ERR unknown command 'sett' <br> 127.0 . 0.1 : 6379 > incr counter<br>QUEUED<br> 127.0 . 0.1 : 6379 > exec<br>(error) EXECABORT Transaction discarded because of previous errors. |
查看自身代码逻辑,修复代码错误。
异常堆栈如下。
1 | java.lang.ClassCastException: java.lang.Long cannot be cast to java.util.List<br> at redis.clients.jedis.Connection.getBinaryMultiBulkReply(Connection.java: 199 )<br> at redis.clients.jedis.Jedis.hgetAll(Jedis.java: 851 )<br> at redis.clients.jedis.ShardedJedis.hgetAll(ShardedJedis.java: 198 ) |
1 | java.lang.ClassCastException: java.util.ArrayList cannot be cast to [B<br> at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java: 182 )<br> at redis.clients.jedis.Connection.getBulkReply(Connection.java: 171 )<br> at redis.clients.jedis.Jedis.rpop(Jedis.java: 1109 )<br> at redis.clients.jedis.ShardedJedis.rpop(ShardedJedis.java: 258 )<br>....... |
Jedis正确的使用方法是,一个线程操作一个Jedis,如果多个线程操作同一个Jedis连接就会发生此类错误。使用JedisPool可避免此类问题。例如下列代码在两个线程并发使用了一个Jedis(get、hgetAll返回不同的类型)。
1 | new Thread( new Runnable() {<br> public void run() {<br> for ( int i = 0 ; i < 100 ; i++) {<br> jedis.set( "hello" , "world" );<br> jedis.get( "hello" );<br> }<br> }<br>}).start();<br> new Thread( new Runnable() {<br> public void run() {<br> for ( int i = 0 ; i < 100 ; i++) {<br> jedis.hset( "hashkey" , "f" , "v" );<br> jedis.hgetAll( "hashkey" );<br> }<br> }<br>}).start(); |
用户排查自身代码是否存在问题。
异常堆栈如下。
1 | Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: WRONGTYPE Operation against a key holding the wrong kind of value<br> at redis.clients.jedis.Protocol.processError(Protocol.java: 127 )<br> at redis.clients.jedis.Protocol.process(Protocol.java: 161 )<br> at redis.clients.jedis.Protocol.read(Protocol.java: 215 )<br>..... |
例如key=”hello”
是字符串类型的键,而hgetAll返回哈希类型的键,所以出现了错误。
1 | jedis.set( "hello" , "world" );<br>jedis.hgetAll( "hello" ); |
请用户修改自身代码错误。
Redis节点(如果是集群,则是其中一个节点)使用内存大于该实例的内存规格(maxmemory配置)。异常堆栈如下。
1 | redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory' . |
原因可能有下列几种。
异常堆栈如下。
1 | redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory |
Jedis调用Redis时,如果Redis正在加载持久化文件,无法进行正常的读写。
正常情况下,阿里云Redis不会出现这种情况,如果出现,则提交工单。
异常堆栈如下。
1 | redis.clients.jedis.exceptions.JedisDataException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. |
如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit,此时Jedis调用Redis时,会收到上述异常。
按照异常提示You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
,使用kill命令终止Lua脚本。
1 | redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out |
可能原因有下列几种。
用户提供连接超时时间,提交工单定位相关原因。
异常堆栈如下。
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. |
如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit,并且已经执行过写命令,此时Jedis调用Redis时,会收到上述异常。
提交工单紧急处理,管理员需要重启或者切换Redis节点。
找不到类和方法的异常堆栈如下。
1 | Exception in thread "commons-pool-EvictionTimer" java.lang.NoClassDefFoundError: redis/clients/util/IOUtils<br> at redis.clients.jedis.Connection.disconnect(Connection.java: 226 )<br> at redis.clients.jedis.BinaryClient.disconnect(BinaryClient.java: 941 )<br> at redis.clients.jedis.BinaryJedis.disconnect(BinaryJedis.java: 1771 )<br> at redis.clients.jedis.JedisFactory.destroyObject(JedisFactory.java: 91 )<br> at org.apache.commons.pool2.impl.GenericObjectPool.destroy(GenericObjectPool.java: 897 )<br> at org.apache.commons.pool2.impl.GenericObjectPool.evict(GenericObjectPool.java: 793 )<br> at org.apache.commons.pool2.impl.BaseGenericObjectPool$Evictor.run(BaseGenericObjectPool.java: 1036 )<br> at java.util.TimerThread.mainLoop(Timer.java: 555 )<br> at java.util.TimerThread.run(Timer.java: 505 )<br>Caused by: java.lang.ClassNotFoundException: redis.clients.util.IOUtils<br>...... |
运行时,Jedis执行命令,抛出异常,提示某个类找不到。此类问题一般都是由于加载多个jedis版本(例如jedis 2.9.0和jedis 2.6),在编译期间代码未出现问题,但类加载器在运行时加载了低版本的Jedis,造成运行时找不到类。
通常此类问题,可以将重复的Jedis排除掉,例如利用maven的依赖树,把无用的依赖去掉或者exclusion掉。
例如客户端执行了geoadd命令,但是服务端返回不支持此命令。
1 | redis.clients.jedis.exceptions.JedisDataException: ERR unknown command 'GEOADD' |
该命令不能被Redis端识别,可能有下列两个原因。
咨询是否有Redis版本支持该命令,如支持可以让客户做小版本升级。
异常堆栈如下。
1 | redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method. |
pipeline.sync()
执行之前,通过response.get()
获取值,在pipeline.sync()
执行前,命令没有执行(可以通过monitor做验证),下面代码就会引起上述异常。 1 | Jedis jedis = new Jedis( "127.0.0.1" , 6379 );<br>Pipeline pipeline = jedis.pipelined();<br>pipeline.set( "hello" , "world" ); <br>pipeline.set( "java" , "jedis" );<br>Response<String> pipeString = pipeline.get( "java" );<br> //这个get必须在sync之后,如果是批量获取值建议直接用List<Object> objectList = pipeline.syncAndReturnAll();<br>System.out.println(pipeString.get());<br>//命令此时真正执行<br>pipeline.sync(); |
set=false
就会报错,而response中的set初始化为false。 1 | public T get() {<br> // if response has dependency response and dependency is not built,<br> // build it first and no more!!<br> if (dependency != null && dependency.set && !dependency.built) {<br> dependency.build();<br> }<br> if (!set) {<br> throw new JedisDataException(<br> "Please close pipeline or multi block before calling this method.");<br> }<br> if (!built) {<br> build();<br> }<br> if (exception != null) {<br> throw exception;<br> }<br> return response;<br>} |
pipeline.sync()
代码会将每个运行结果设置set=true
,如下所示。 1 | public void sync() {<br> if (getPipelinedResponseLength() > 0 ) {<br> List<Object> unformatted = client.getAll();<br> for (Object o : unformatted) {<br> generateResponse(o);<br> }<br> }<br>} |
generateResponse(o)
代码如下。 1 | protected Response<?> generateResponse(Object data) {<br> Response<?> response = pipelinedResponses.poll();<br> if (response != null ) {<br> response.set(data);<br> }<br> return response;<br>} |
response.set(data)
代码如下。 1 | public void set(Object data) {<br> this .data = data;<br> set = true ;<br>} |
对于批量结果的解析,建议使用pipeline.syncAndReturnAll()
来实现,下面操作模拟了批量hgetAll。
1 | /**<br>* pipeline模拟批量hgetAll<br>* @param keyList<br>* @return<br>*/ <br> public Map<String, Map<String, String>> mHgetAll(List<String> keyList) {<br> // 1.生成pipeline对象<br>Pipeline pipeline = jedis.pipelined();<br>// 2.pipeline执行命令,注意此时命令并未真正执行<br>for (String key : keyList) {<br> pipeline.hgetAll(key);<br>}<br>// 3.执行命令 syncAndReturnAll()返回结果<br>List<Object> objectList = pipeline.syncAndReturnAll();<br>if (objectList == null || objectList.isEmpty()) {<br> return Collections.emptyMap();<br>}<br>// 4.解析结果<br>Map<String,Map<String, String>> resultMap = new HashMap<String, Map<String,String>>();<br>for (int i = 0; i < objectList.size(); i++) {<br> Object object = objectList.get(i);<br> Map<String, String> map = (Map<String, String>) object;<br> String key = keyList.get(i);<br> resultMap.put(key, map);<br>}<br>return resultMap;<br>} |
检查并修改业务代码。
命令role不能被普通用户执行,详情可参考暂未开放的Redis命令。
1 | redis.clients.jedis.exceptions.JedisDataException: ERR command role not support for normal user |
该命令尚未开放。
不能使用该命令,如果有需求或者疑问可以提交工单。
原则上选择最新的release版本,但最好选择发行一段时间后的版本,因为jedis历史上出现过一次问题较大的release版本,目前来说2.9.0比较稳定。
1 | <dependency><br> <groupId>redis.clients</groupId><br> <artifactId>jedis</artifactId><br> <version> 2.9 . 0 </version><br> <type>jar</type><br> <scope>compile</scope><br></dependency> |
不是阿里云Redis集群版的客户端,请使用阿里云集群版的客户端,直接使用Jedis和JedisPool即可。因为官方集群和阿里云Redis集群是不同的架构,具体参考Redis 4.0、codis、云数据库 Redis 版集群对比分析。
资源设置和使用如下。
序号 | 参数名 | 含义 | 默认值 | 使用建议 |
---|---|---|---|---|
1 | maxTotal | 资源池中最大连接数 | 8 | - |
2 | maxIdle | 资源池允许最大空闲的连接数 | 8 | - |
3 | minIdle | 资源池确保最少空闲的连接数 | 0 | - |
4 | blockWhenExhausted | 当资源池用尽后,调用者是否要等待。只有当为true时,下面的maxWaitMillis才会生效 | true | 建议使用默认值 |
5 | maxWaitMillis | 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒) | -1:表示永不超时 | 不建议使用默认值 |
6 | testOnBorrow | 向资源池借用连接时是否做连接有效性检测(ping),无效连接会被移除 | false | 业务量很大时候建议设置为false(设置为true会多一次ping的开销) |
7 | testOnReturn | 向资源池归还连接时是否做连接有效性检测(ping),无效连接会被移除 | false | 业务量很大时候建议设置为false(设置为true会多一次ping的开销) |
8 | jmxEnabled | 是否开启jmx监控,可用于监控 | true | 建议开启,但应用本身也要开启 |
空闲Jedis对象检测,下面四个参数组合来完成,testWhileIdle是该功能的开关。
序号 | 参数名 | 含义 | 默认值 | 使用建议 |
---|---|---|---|---|
1 | testWhileIdle | 是否开启空闲资源监测 | false | 设置为true |
2 | timeBetweenEvictionRunsMillis | 空闲资源的检测周期(单位为毫秒) | -1:不检测 | 建议设置,周期自行选择,也可以默认也可以使用下面JedisPoolConfig中的配置 |
3 | minEvictableIdleTimeMillis | 资源池中资源最小空闲时间(单位为毫秒),达到此值后空闲资源将被移除 | 1000 * 60 * 30 = 30分钟 | 可根据自身业务决定,大部分默认值即可,也可以考虑使用下面JeidsPoolConfig中的配置 |
4 | numTestsPerEvictionRun | 做空闲资源检测时,每次的采样数 | 3 | 可根据自身应用连接数进行微调,如果设置为-1,就是对所有连接做空闲监测 |