redis分布式Jedis类型转换的异常深入研究

目录
  • 1 类型转换异常场景
    • 多线程环境
    • 单线程环境
  • 2 Jedis类型转换异常案例
    • 2.1 案例介绍
    • 2.2 Jedis原理介绍
    • 2.3 类型转换异常的原因
  • 3 Jedis类型转换异常的解决办法
  • 4 问题深思

1 类型转换异常场景

我们在使用Jedis的时候,经常会出现类型转换异常,有如下情况:

多线程环境

Jedis是线程不安全的,如果存在多线程使用同一个Jedis,就会出现类型转换异常网上也流传着很多错误的解释,下面我们以一个案例来复现下这个问题,这个很好理解。

单线程环境

即使在单线程的情况下,也是会出现类型转换异常的,下面就针对此做一个案例分析

2 Jedis类型转换异常案例

2.1 案例介绍

案例是从这里来的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

解除网络阻塞

sudo iptables -F

案例运行过程描述:

1 创建Jedis,发送get命令,启动与redis的连接,连接成功后获取到响应数据

2 程序阻塞在System.in.read(),等待输入,此时我们需要将网络连接阻塞,执行上述阻断网络命令

3 输入任意数据,让程序不再阻塞,继续走下去,执行get命令,此时由于网络不通,导致出现SocketTimeoutException异常

4 打印出异常,继续往下走,sleep 50s,此时我们需要解除网络阻塞,执行上述对应命令

5 50s过完,就会执行jedis的sismember方法,此时就会出现类型转换异常

2.2 Jedis原理介绍

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。

2.3 类型转换异常的原因

网上很多人说造成上述场景的类型转换异常是因为:

出现SocketTimeoutException异常后,RedisOutputStream的buf中残留上次命令,没做清理处理,导致再执行其他命令时连同之前的命令一起发送过去了。

经过查看RedisOutputStream的源码,buf中确实不会去主动清除原有数据,而是每次都是直接覆盖,有count指针来标记,但是这也不会造成上述所说的影响,RedisOutputStream是OK的。

首先我们要明白什么是SocketTimeoutException异常: 上述Jedis的Socket在发送完成数据后,就会去执行读取数据,即读取Socket的InputStream中的数据,并且又一定的阻塞时间,如果redis服务器迟迟不返回数据,一旦超过SO_TIMEOUT(即Socket的读取超时时间),客户端就会抛出一个SocketTimeoutException异常。

造成这种异常的原因有很多:

  • 网络闪断(会TCP重传):上述案例情景就是网络断开,数据包发送失败,会TCP重传
  • 网络没有断,但是传输比较慢,或者redis服务器处理很慢

上述原因都会造成客户端读取超时。一旦超时,我们的Jedis程序抛出异常,继续往下走,如果此时再次执行其他命令的话,仍然会读取服务器端响应,此时读到的响应就是上次请求的响应了,所以会导致类型转换异常。如果与上次请求的类型一致,那就更可怕了,错误就会被深深的掩盖过去了。

3 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();
	}
}

4 问题深思

可以想到2方面的问题:

问题1:jedis为什么要暴漏这么个危险的API给用户使用(即要求用户自觉的close,不自觉后果自负)

如果是我们在开发框架给被人使用,那就要尽量避免这种API的设计,把close自动隐藏在框架内部,避免了使用人员的误使用,同时减少了代码的复杂度,即使是上述最终的写法也是很丑陋的,要完成一个set功能,要关注太多地方了,这部分完全可以框架底层包装起来,只给用户一个set方法即可。

问题2:请求和响应的不匹配问题

这种不匹配的问题在同步和异步的时候分别怎么处理?

同步通信:在设计的时候,必须发送一次请求就要读取一次响应,通过这种方式来匹配。然而在某些情况下,读取响应有一定的超时时间,一旦超时,就抛出SocketTimeoutException异常,从而结束本次读取,而响应可能后来又到达了,这种情况就会造成不匹配的现象。要避免这种情况,就必须要废弃掉这个Socket了,所以如果客户端设计成同步通信的时候,一旦遇到这种异常,则就需要废弃了,重新建立连接了。

异步通信:在设计的时候一般会为每个请求分配一个请求id,服务器端在处理请求后,会把这个请求id返回给客户端,客户端根据返回的请求id来匹配是那一次的请求对应的响应,就不会出现上述那种匹配错乱的问题。异步通信在读取数据的时候也通常是有数据可读才会去执行读操作,可以减少同步通信中因网络拥堵或其他原因造成的SocketTimeoutException问题。异步通信好处的代价就是比同步通信复杂。

所以如果我们在设计的时候,就需要去考虑这样的问题,避免造出一个大坑来。

以上就是redis分布式Jedis类型转换的异常深入研究的详细内容,更多关于redis分布式Jedis类型转换异常的资料请关注我们其它相关文章!

(0)

相关推荐

  • redis分布式锁及会出现的问题解决

    一.redis实现分布式锁的主要原理: 1.加锁 最简单的方法是使用setnx命令.key是锁的唯一标识,按业务来决定命名.比如想要给一种商品的秒杀活动加锁,可以给key命名为 "lock_sale_商品ID" .而value设置成什么呢?我们可以姑且设置成1.加锁的伪代码如下: setnx(key,1) 当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁:当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败. 2.解锁 有加锁就得有解锁.当得到锁的

  • 详解Redis 分布式锁遇到的序列化问题

    场景描述 最近使用 Redis 遇到了一个类似分布式锁的场景,跟 Redis 实现分布式锁类比一下,就是释放锁失败,也就是缓存删不掉.又踩了一个 Redis 的坑-- 这是什么个情况.又是怎样排查的呢? 本文主要对此做个复盘. 问题排查 既然是释放锁有问题,那就先看看释放锁的代码吧. 释放锁 释放锁使用了 Lua 脚本,代码逻辑和 Lua 脚本如下: 释放锁示例代码 public Object release(String key, String value) { Object existedV

  • 详解RedisTemplate下Redis分布式锁引发的系列问题

    自己的项目因为会一直抓取某些信息,但是本地会和线上经常一起跑,造成冲突.这其实就是我们常说的分布式集群的问题了,本地和线上的服务器构成了集群以及QPS为2的小并发(其实也不叫并发,不知道拿什么词形容?). 首先,分布式集群的问题大家都知道,会造成数据库的插入重复问题,会造成一系列的并发性问题. 解决的方式呢也大概如下几点,百度以及谷歌上都能搜到的解决方式: 1:数据库添加唯一索引 2:设计接口幂等性 3:依靠中间件使用分布式锁,而分布式锁又分为Redis和Zookeeper 由于Zookeepe

  • Redis连接超时异常的处理方法

    0.问题描述 使用Jedis连接redis进行数据查询操作,正常的代码运行没有问题,但是时不时会报出如下错误: Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out  at redis.clients.util.RedisInputStream.ensureFill(RedisI

  • 详解redis分布式锁的这些坑

    一.白话分布式 什么是分布式,用最简单的话来说,就是为了较低单个服务器的压力,将功能分布在不同的机器上面,本来一个程序员可以完成一个项目:需求->设计->编码->测试 但是项目多的时候,一个人也扛不住,这就需要不同的人进行分工合作了 这就是一个简单的分布式协同工作了: 二.分布式锁 首先看一个问题,如果说某个环节被终止或者别侵占,就会发生不可知的事情 这就会出现,设计好的或者设计的半成品会被破坏,导致后面环节出错: 这时候,我们就需要引入分布式锁的概念: 何为分布式锁 当在分布式模型下,

  • redis分布式Jedis类型转换的异常深入研究

    目录 1 类型转换异常场景 多线程环境 单线程环境 2 Jedis类型转换异常案例 2.1 案例介绍 2.2 Jedis原理介绍 2.3 类型转换异常的原因 3 Jedis类型转换异常的解决办法 4 问题深思 1 类型转换异常场景 我们在使用Jedis的时候,经常会出现类型转换异常,有如下情况: 多线程环境 Jedis是线程不安全的,如果存在多线程使用同一个Jedis,就会出现类型转换异常网上也流传着很多错误的解释,下面我们以一个案例来复现下这个问题,这个很好理解. 单线程环境 即使在单线程的情

  • redis分布式锁解决表单重复提交的问题

    假如用户的网速慢,用户点击提交按钮,却因为网速慢,而没有跳转到新的页面,这时的用户会再次点击提交按钮,举个例子:用户点击订单页面,当点击提交按钮的时候,也许因为网速的原因,没有跳转到新的页面,这时的用户会再次点击提交按钮,如果没有经过处理的话,这时用户就会生成两份订单,类似于这种场景都叫重复提交. 使用redis的setnx和getset命令解决表单重复提交的问题. 1.引入redis依赖和aop依赖 <dependency> <groupId>org.springframewor

  • 详解Redis分布式锁的原理与实现

    目录 前言 使用场景 为什么要使用分布式锁 如何使用分布式锁 流程图 分布式锁的状态 分布式锁的特点 分布式锁的实现方式(以redis分布式锁实现为例) 总结 前言 在单体应用中,如果我们对共享数据不进行加锁操作,会出现数据一致性问题,我们的解决办法通常是加锁.在分布式架构中,我们同样会遇到数据共享操作问题,此时,我们就需要分布式锁来解决问题,下面我们一起聊聊使用redis来实现分布式锁. 使用场景 库存超卖 比如 5个笔记本 A 看 准备买3个 B 买2个 C 4个 一下单 3+2+4 =9

  • 深入理解redis分布式锁和消息队列

    最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP等,不能在内存中使用锁,或者如Java这样的,需要一下更为简单的锁校验的时候,redis分布式锁的使用就足够满足了. redis的分布式锁其实就是基于setnx方法和redis对key可设置有效时间的功能来实现的.基本用法比较简单. public boolean tryLock(String loc

  • Redis分布式锁实现方式及超时问题解决

    一 前言 redis在分布式应用十分广泛,本篇文章也是互联网面试的重点内容,读者至少需要知道为什么需要分布式锁,分布式锁的实现原理,分布式锁的应用场景,在使用分布式锁时遇到哪些问题?你是如何解决的,如果读者能掌握以上问题,那么关于这道面试题,你也就基本过关了: 二 分布式锁的产生背景 分布式锁对应的是多个应用,每个应用中都可能会处理相同的数据,如果多个应用对用一个操作进行了重复操作,就会出现数据不一致,数据重复问题,于是分布式锁应用而生,通常你可以理解为多线程中的synchronized 三 分

  • Redis分布式锁升级版RedLock及SpringBoot实现方法

    分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问,Java中我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的方式.但是现在公司都是流行分布式架构,在分布式环境下,如何保证不同节点的线程同步执行呢?因此就引出了分布式锁,它是控制分布式系统之间互斥访问共享资源的一种方式. 在一个分布式系统中,多台机器上部署了多个服务,当客户端一个用户发起一个数据插入请求时,如果没有分布式锁机制保证,那么那多台机器上的多个服务可能进行并发插

  • 关于Springboot2.x集成lettuce连接redis集群报超时异常Command timed out after 6 second(s)

    背景:最近在对一新开发Springboot系统做压测,发现刚开始压测时,可以正常对redis集群进行数据存取,但是暂停几分钟后,接着继续用jmeter进行压测时,发现redis就开始突然疯狂爆出异常提示:Command timed out after 6 second(s)...... Caused by: io.lettuce.core.RedisCommandTimeoutException: Command timed out after 6 second(s) at io.lettuce

  • Redis分布式限流组件设计与使用实例

    目录 1.背景 2.Redis计数器限流设计 2.1Lua脚本 2.2自定义注解 2.3限流组件 2.4限流切面实现 3.测试一下 3.1方法限流示例 3.2动态入参限流示例 4.其它扩展 5.源码地址 本文主要讲解基于 自定义注解+Aop+反射+Redis+Lua表达式 实现的限流设计方案.实现的限流设计与实际使用. 1.背景 在互联网开发中经常遇到需要限流的场景一般分为两种 业务场景需要(比如:5分钟内发送验证码不超过xxx次); 对流量大的功能流量削峰; 一般我们衡量系统处理能力的指标是每

  • Redis分布式锁如何实现续期

    目录 Redis分布式锁如何续期 Redis分布式锁的正确姿势 如何回答 源码分析 真相大白 Redis分布式锁的5个坑 一.锁未被释放 二.B的锁被A给释放了 三.数据库事务超时 四.锁过期了,业务还没执行完 五.redis主从复制的坑 Redis分布式锁如何续期 Redis分布式锁的正确姿势 据肥朝了解,很多同学在用分布式锁时,都是直接百度搜索找一个Redis分布式锁工具类就直接用了.关键是该工具类中还充斥着很多System.out.println();等语句.其实Redis分布式锁比较正确

随机推荐