咱们在使用Jedis的时候,常常会出现类型转换异常,有以下状况:redis
多线程环境数组
Jedis是线程不安全的,若是存在多线程使用同一个Jedis,就会出现类型转换异常网上也流传着不少错误的解释,下面咱们以一个案例来复现下这个问题,这个很好理解。安全
单线程环境服务器
即便在单线程的状况下,也是会出现类型转换异常的,下面就针对此作一个案例分析微信
案例是从这里来的Jedis returnResource使用注意事项网络
代码以下:多线程
public static void main(String[] args) throws Exception{ Jedis jedis = new Jedis("192.168.126.131", 6379); System.out.println("get name=" + jedis.get("name")); System.out.println("Make SocketTimeoutException"); System.in.read(); //等待制造SocketTimeoutException try { System.out.println(jedis.get("name")); } catch (Exception e) { e.printStackTrace(); } System.out.println("Recover from SocketTimeoutException"); Thread.sleep(50000); // 继续休眠一段时间 等待网络彻底恢复 boolean isMember = jedis.sismember("urls", "baidu"); System.out.println("isMember " + isMember); jedis.close(); }
以及包含2个阻断和解除网络通讯的命令框架
阻断网络通讯异步
sudo iptables -A INPUT -p tcp --dport 6379 -j DROP
解除网络阻塞tcp
sudo iptables -F
案例运行过程描述:
这里再也不详细介绍。
Jedis内部有一个Socket与redis服务器创建链接。在建立Jedis对象的时候,并无去创建链接,而是在执行命令的时候才会先检查是否已链接,未链接的话,才创建链接。
Socket一旦链接创建,就会获取到Socket的OutputStream,并用RedisOutputStream进行包装,获取到Socket的InputStream,并用RedisInputStream进行包装。RedisOutputStream内部含有一个byte buf[]数组。
也就是说在jedis在向OutputStream写入命令的时候,会先写入到上述buf数组中,而后在读取的时候,才会flush上述数据,将数据写入到Socket的OutputStream中,并调用flush,以Jedis的get方法为例
public String get(final String key) { checkIsInMulti(); client.sendCommand(Protocol.Command.GET, key); return client.getBulkReply(); }
client.sendCommand方法会将数据写入到RedisOutputStream内部的buf中 client.getBulkReply方法会首先执行一次flush,即将buf中数据写入到Socket的OutputStream中,并调用Socket的OutputStream的flush。
网上不少人说形成上述场景的类型转换异常是由于:
出现SocketTimeoutException异常后,RedisOutputStream的buf中残留上次命令,没作清理处理,致使再执行其余命令时连同以前的命令一块儿发送过去了。
通过查看RedisOutputStream的源码,buf中确实不会去主动清除原有数据,而是每次都是直接覆盖,有count指针来标记,可是这也不会形成上述所说的影响,RedisOutputStream是OK的。
首先咱们要明白什么是SocketTimeoutException异常: 上述Jedis的Socket在发送完成数据后,就会去执行读取数据,即读取Socket的InputStream中的数据,而且又必定的阻塞时间,若是redis服务器迟迟不返回数据,一旦超过SO_TIMEOUT(即Socket的读取超时时间),客户端就会抛出一个SocketTimeoutException异常。
形成这种异常的缘由有不少:
上述缘由都会形成客户端读取超时。一旦超时,咱们的Jedis程序抛出异常,继续往下走,若是此时再次执行其余命令的话,仍然会读取服务器端响应,此时读到的响应就是上次请求的响应了,因此会致使类型转换异常。若是与上次请求的类型一致,那就更可怕了,错误就会被深深的掩盖过去了。
上述问题就是:咱们没有正确对待这个SocketTimeoutException异常,即一旦出现SocketTimeoutException异常,咱们是必需要废弃掉这个Jedis的。因此对于单线程环境下的Jedis来讲,一旦出现这种异常,咱们须要从新new一个新的Jedis来使用。
Jedis在内部执行出现异常,如SocketTimeoutException异常的时候,会标记一个boolean broken=true,即意味着该链接已经废弃了。
重要的大坑在这里,咱们一般使用JedisPool来应对多线程环境下Jedis的使用,通常使用方式以下:
Jedis jedis = null;//从pool中获取资源 try{ jedis = pool.getResource(); jedis.set("k1", "v1"); }catch(Exception e){ e.printStackTrace(); }finally{ if(jedis != null){ pool.returnResource(jedis);//向链接池“归还”资源,千万不要忘记。 } }
而对于JedisPool,咱们会使用returnResource方法来向pool中释放回Jedis,而这个returnResource却忽视了上述boolean broken属性,直接将一个标记废弃的链接放回到了pool中,下次别人取的时候,必然出问题。
因此针对JedisPool这种状况,解决办法以下:
1 在上述catch中捕获SocketTimeoutException异常,调用pool的returnBrokenResource方法来释放Jedis(该方法会将Jedis实例标记为下线,没法被他人获取到了),可是不推荐这种,还要考虑其余异常等等
2 另外一个就是直接调用Jedis的close方法,最新版2.9.0(其余版本没验证)中close方法对上述boolean broken标记进行了处理,而且将returnResource标记成废弃了,处理以下
public void close() { if (dataSource != null) { if (client.isBroken()) { this.dataSource.returnBrokenResource(this); } else { this.dataSource.returnResource(this); } } else { client.close(); } }
上述this.dataSource能够理解为JedisPool。 即一旦是broken,则调用pool的returnBrokenResource方法,不然调用pool的returnResource方法。
因此最终写法应该以下:
Jedis jedis = null;//从pool中获取资源 try{ jedis = pool.getResource(); jedis.set("k1", "v1"); }finally{ if(jedis != null){ jedis.close(); } }
能够想到2方面的问题:
问题1:jedis为何要暴漏这么个危险的API给用户使用(即要求用户自觉的close,不自觉后果自负)
若是是咱们在开发框架给被人使用,那就要尽可能避免这种API的设计,把close自动隐藏在框架内部,避免了使用人员的误使用,同时减小了代码的复杂度,即便是上述最终的写法也是很丑陋的,要完成一个set功能,要关注太多地方了,这部分彻底能够框架底层包装起来,只给用户一个set方法便可。
问题2:请求和响应的不匹配问题
这种不匹配的问题在同步和异步的时候分别怎么处理?
同步通讯:在设计的时候,必须发送一次请求就要读取一次响应,经过这种方式来匹配。然而在某些状况下,读取响应有必定的超时时间,一旦超时,就抛出SocketTimeoutException异常,从而结束本次读取,而响应可能后来又到达了,这种状况就会形成不匹配的现象。要避免这种状况,就必需要废弃掉这个Socket了,因此若是客户端设计成同步通讯的时候,一旦遇到这种异常,则就须要废弃了,从新创建链接了。
异步通讯:在设计的时候通常会为每一个请求分配一个请求id,服务器端在处理请求后,会把这个请求id返回给客户端,客户端根据返回的请求id来匹配是那一次的请求对应的响应,就不会出现上述那种匹配错乱的问题。异步通讯在读取数据的时候也一般是有数据可读才会去执行读操做,能够减小同步通讯中因网络拥堵或其余缘由形成的SocketTimeoutException问题。异步通讯好处的代价就是比同步通讯复杂。
因此若是咱们在设计的时候,就须要去考虑这样的问题,避免造出一个大坑来。
欢迎关注微信公众号:乒乓狂魔