`
yychao
  • 浏览: 98392 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

spring redis源码分析 以及 代码漏洞

阅读更多

spring-data-redis提供了redis操作的封装和实现;RedisTemplate模板类封装了redis连接池管理的逻辑,业务代码无须关心获取,释放连接逻辑;spring redis同时支持了Jedis,Jredis,rjc 客户端操作;

 

spring redis 源码设计逻辑可以分为以下几个方面:

 

  1. Redis连接管理:封装了Jedis,Jredis,Rjc等不同redis 客户端连接
  2. Redis操作封装:value,list,set,sortset,hash划分为不同操作
  3. Redis序列化:能够以插件的形式配置想要的序列化实现
  4. Redis操作模板化: redis操作过程分为:获取连接,业务操作,释放连接;模板方法使得业务代码只需要关心业务操作
  5. Redis事务模块:在同一个回话中,采用同一个redis连接完成
spring redis设计类图:



 spring redis连接管理模块分析

spring redis封装了不同redis 客户端,对于底层redis客户端的抽象分装,使其能够支持不同的客户端;连接管理模块的类大概有以下:

 

 

类名 职责
RedisCommands 继承了Redis各种数据类型操作的整合接口;
RedisConnection

抽象了不同底层redis客户端类型:不同类型的redis客户端可以创建不同实现,例如:

JedisConnection

JredisConnection

RjcConnection

StringRedisConnection 代理接口,支持String类型key,value操作

RedisConnectionFactory

抽象Redis连接工厂,不同类型的redis客户端实现不同的工厂:

JedisConnectionFactory

JredisConnectionFactory

RjcConnectionFactory

JedisConnection 实现RedisConnection接口,将操作委托给Jedis
JedisConnectionFactory 实现RedisConnectionFactory接口,创建JedisConnection

 

 

基于工厂模式和代理模式设计的spring redis 连接管理模块,可以方面接入不同的redis客户端,而不影响上层代码;项目中为了支持ShardedJedis支持分布式redis集群,实现了自己的ShardedJedisConnection,ShardedJedisConnectionFactory,而不需要修改业务代码中;

redis 操作模板化,序列化,操作封装

RedisTemplate提供了 获取连接, 操作数据,释放连接的 模板化支持;采用RedisCallback来回调业务操作,使得业务代码无需关心 获取连接,归还连接,以及其他异常处理等过程,简化redis操作;

 

RedisTemplate继承RedisAccessor 类,配置管理RedisConnectionFactory实现;使得RedisTemplate无需关心底层redis客户端类型

 

RedisTemplate实现RedisOperations接口,提供value,list,set,sortset,hash以及其他redis操作方法;value,list,set,sortset,hash等操作划分为不同操作类:ValueOperations,ListOperations,SetOperations,ZSetOperations,HashOperations以及bound接口;这些操作都提供了默认实现,这些操作都采用RedisCallback回调实现相关操作

 

RedisTemplate组合了多个不同RedisSerializer示例,以实现对于key,value的序列化支持;可以方便地实现自己的序列化工具;

 

 

 

RedisTemplate 获取归还连接,事务

RedisTemplate 的execute方法作为执行redis操作的模板方法,封装了获取连接,回调业务操作,释放连接过程;

 

RedisTemplate 获取连接和释放连接的过程 借助于工具类RedisConnectionUtils 提供的连接获取,释放连接;

 

同时RedisTemplate 还提供了基于会话的事务支持,采用SessionCallback回调接口实现,保证同一个线程中,采用同一个连接执行一批redis操作;

 

RedisTemplate 支持事务的方法:

 

 

	public <T> T execute(SessionCallback<T> session) {
		RedisConnectionFactory factory = getConnectionFactory();
		// bind connection
		RedisConnectionUtils.bindConnection(factory);
		try {
			return session.execute(this);
		} finally {
			RedisConnectionUtils.unbindConnection(factory);
		}
	}

 

 

该方法通过RedisConnectionUtils.bindConnection操作将连接绑定到当前线程,批量方法执行时,获取ThreadLocal中的连接;

执行结束时,调用RedisConnectionUtils.unbindConnection释放当前线程的连接

 

SessionCallback接口方法:

 

 

public interface SessionCallback<T> {

	/**
	 * Executes all the given operations inside the same session.
	 * 
	 * @param operations Redis operations
	 * @return return value
	 */
	<K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException;
}
 

 

批量执行RedisOperation时,通过RedisTemplate的方法执行,代码如下:

 

 

	public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
		Assert.notNull(action, "Callback object must not be null");

		RedisConnectionFactory factory = getConnectionFactory();
		RedisConnection conn = RedisConnectionUtils.getConnection(factory);

		boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
		preProcessConnection(conn, existingConnection);

		boolean pipelineStatus = conn.isPipelined();
		if (pipeline && !pipelineStatus) {
			conn.openPipeline();
		}

		try {
			RedisConnection connToExpose = (exposeConnection ? conn : createRedisConnectionProxy(conn));
			T result = action.doInRedis(connToExpose);
			// TODO: any other connection processing?
			return postProcessResult(result, conn, existingConnection);
		} finally {
			try {
				if (pipeline && !pipelineStatus) {
					conn.closePipeline();
				}
			} finally {
				RedisConnectionUtils.releaseConnection(conn, factory);
			}
		}
	}
 

 

当前线程中绑定连接时,返回绑定的redis连接;保证同一回话中,采用同一个redis连接;

 

 

Spring redis 一些问题

连接未关闭问题

当数据反序列化存在问题时,redis服务器会返回一个Err报文:Protocol error,之后redis服务器会关闭该链接(redis protocol中未指明该协议);了解的jedis客户端为例,其仅仅将错误报文转化为JedisDataException抛出,也没有处理最后的关闭报文;  此时spring中 处理异常时,对于JedisDataException依旧认为连接有效,将其回收到jedispool中;当下个操作获取到该链接时,就会抛出“It seems like server has closed the connection.”异常

 

相关代码:

 

jedis Protocol读取返回信息:

 

 

    private Object process(final RedisInputStream is) {
        try {
            byte b = is.readByte();
            if (b == MINUS_BYTE) {
                processError(is);
            } else if (b == ASTERISK_BYTE) {
                return processMultiBulkReply(is);
            } else if (b == COLON_BYTE) {
                return processInteger(is);
            } else if (b == DOLLAR_BYTE) {
                return processBulkReply(is);
            } else if (b == PLUS_BYTE) {
                return processStatusCodeReply(is);
            } else {
                throw new JedisConnectionException("Unknown reply: " + (char) b);
            }
        } catch (IOException e) {
            throw new JedisConnectionException(e);
        }
        return null;
    }

 

 当redis 服务器返回错误报文时(以-ERR开头),就转换为JedisDataException异常;

 

 

    private void processError(final RedisInputStream is) {
        String message = is.readLine();
        throw new JedisDataException(message);
    }
 

 

Spring redis的各个RedisConnection实现中转换捕获异常,例如JedisConnection 一个操作:

 

 

	public Long dbSize() {
		try {
			if (isQueueing()) {
				throw new UnsupportedOperationException();
			}
			if (isPipelined()) {
				throw new UnsupportedOperationException();
			}
			return jedis.dbSize();
		} catch (Exception ex) {
			throw convertJedisAccessException(ex);
		}
	}

 

JedisConnection捕获到异常时,调用convertJedisAccessException方法转换异常;

 

 

	protected DataAccessException convertJedisAccessException(Exception ex) {
		if (ex instanceof JedisException) {
			// check connection flag
			if (ex instanceof JedisConnectionException) {
				broken = true;
			}
			return JedisUtils.convertJedisAccessException((JedisException) ex);
		}
		if (ex instanceof IOException) {
			return JedisUtils.convertJedisAccessException((IOException) ex);
		}

		return new RedisSystemException("Unknown jedis exception", ex);
	}
 

可以看到当捕获的异常为JedisConnectionException 时,才将broken设置为true(在关闭连接时,直接销毁Jedis示例); JedisDataException仅仅进行了转换;

 

JedisConnection释放连接逻辑:

 

 

	public void close() throws DataAccessException {
		// return the connection to the pool
		try {
			if (pool != null) {
				if (broken) {
					pool.returnBrokenResource(jedis);
				}
				else {
					// reset the connection 
					if (dbIndex > 0) {
						select(0);
					}

					pool.returnResource(jedis);
				}
			}
		} catch (Exception ex) {
			pool.returnBrokenResource(jedis);
		}

		if (pool != null) {
			return;
		}

		// else close the connection normally
		try {
			if (isQueueing()) {
				client.quit();
				client.disconnect();
				return;
			}
			jedis.quit();
			jedis.disconnect();
		} catch (Exception ex) {
			throw convertJedisAccessException(ex);
		}
	}
 

当JedisConnection实例的broken被设置为true时,就会销毁连接;

到此,可以发现当redis服务器返回Protocol error这个特殊类型的错误消息时,会抛出JedisDataException异常,这是spring不会销毁连接,当该链接再次被使用时,就会抛出“It seems like server has closed the connection.”异常。

 

该问题仅仅在发送不完整redis协议(可能是TCP报文错误,操作序列化错误等等)时,发生;PS:不是错误的redis操作,错误命令不一定回导致错误的报文;

 

并且该错误消息在redis协议中也没有指出,因此jedis也没有做处理;修复此问题,可以在spring redis 或者 jedis 中解决;

  1. spring jedis解决:可以在convertJedisAccessException方法中检查JedisDataException的消息内容是否包含"Protocol error",若包含设置broken = true,销毁连接
  2. jedis解决方案:Protocol.processError中检查 错误消息是否包含"Protocol error";如果包含,可以读取最后被忽略的关闭报文,并转换为JedisConnectionException异常抛出

Protocol error异常可以查看redis源码network.c;当redis接收到客户端的请求报文,都会经过检查,当报文不完整,超长等问题时,将抛出Protocol error异常,并关闭连接;该报文没有在redis protocol中明确指明;可参见:http://redis.io/topics/protocol

 

 

redis分库性能问题与连接池泄露

当采用redis分库方案时,spring redis 在每次获取连接时,都需要执行select 操作切换到指定库,性能开销大;

 

redis分库操作逻辑:

 

参见RedisTemplate execute模板方法,调用RedisConnectionUtils.getConnection(factory)获取连接;最终调用doGetConnection:

 

 

	public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind) {
		Assert.notNull(factory, "No RedisConnectionFactory specified");

		RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
		//TODO: investigate tx synchronization

		if (connHolder != null)
			return connHolder.getConnection();

		if (!allowCreate) {
			throw new IllegalArgumentException("No connection found and allowCreate = false");
		}

		if (log.isDebugEnabled())
			log.debug("Opening RedisConnection");

		RedisConnection conn = factory.getConnection();

		boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();

		if (bind || synchronizationActive) {
			connHolder = new RedisConnectionHolder(conn);
			if (synchronizationActive) {
				TransactionSynchronizationManager.registerSynchronization(new RedisConnectionSynchronization(
						connHolder, factory, true));
			}
			TransactionSynchronizationManager.bindResource(factory, connHolder);
			return connHolder.getConnection();
		}
		return conn;
	}
 

实际调用的是factory.getConnection方法,参见JedisConnectionFactory:

 

 

	public JedisConnection getConnection() {
		Jedis jedis = fetchJedisConnector();
		return postProcessConnection((usePool ? new JedisConnection(jedis, pool, dbIndex) : new JedisConnection(jedis,
				null, dbIndex)));
	}

 

fetchJedisConnector从JedisPool中获取Jedis连接,之后实例化JedisConnection对象:

 

 

	public JedisConnection(Jedis jedis, Pool<Jedis> pool, int dbIndex) {
		this.jedis = jedis;
		// extract underlying connection for batch operations
		client = (Client) ReflectionUtils.getField(CLIENT_FIELD, jedis);
		transaction = new Transaction(client);

		this.pool = pool;

		this.dbIndex = dbIndex;

		// select the db
		if (dbIndex > 0) {
			select(dbIndex);
		}
	}
 

可以看到每次都需要重复select操作,这回导致大量的redis 请求,严重影响性能;

 

此外,还存在连接池泄露的问题:

 

 

	public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
		Assert.notNull(action, "Callback object must not be null");

		RedisConnectionFactory factory = getConnectionFactory();
		RedisConnection conn = RedisConnectionUtils.getConnection(factory);

		.....

		try {
			......
		} finally {
			try {
				if (pipeline && !pipelineStatus) {
					conn.closePipeline();
				}
			} finally {
				RedisConnectionUtils.releaseConnection(conn, factory);
			}
		}
	}

 

当select操作发生异常时,RedisConnectionUtils.getConnection(factory)抛出异常,此时代码不在try catch块中,这是将无法回收连接,导致连接泄露

 

 

spring redis 设计的一些其他问题:http://ldd600.iteye.com/blog/1115196

 

 

  • 大小: 146 KB
分享到:
评论

相关推荐

    JAVA上百实例源码以及开源项目源代码

    Java编写的显示器显示模式检测程序 2个目标文件 内容索引:JAVA源码,系统相关,系统信息检测 用JAVA编写了一个小工具,用于检测当前显示器也就是显卡的显示模式,比如分辨率,色彩以及刷新频率等。 Java波浪文字制作...

    新闻发布系统源码(代码简单)

    在这个“新闻发布系统源码”中,我们可以深入理解如何构建这样一个系统,包括前端用户界面、后端服务器逻辑以及数据库交互。下面将详细探讨相关知识点。 一、系统架构与设计原则 1. MVC(Model-View-Controller)...

    手机话费充值系统源码.rar

    此外,系统需要防止恶意攻击,例如DDoS防护,以及对用户输入的合法性检查,防止SQL注入等安全漏洞。 为了实现高效运行,系统可能采用缓存技术,如Redis或Memcached,来加速频繁请求的处理。负载均衡器也可能被部署...

    大型在线购物商城系统源代码(紫红色风格)

    4. 安全编码:遵循OWASP(开放网络应用安全项目)的编码规范,避免代码层面的安全漏洞。 总结,这个大型在线购物商城系统源代码涵盖了前端与后端的完整开发流程,通过合理的架构设计和优化策略,确保了系统的高效...

    java开源包8

    J2C 将 Java 代码转成 C++ 代码,这是源码级别的转换,输出的 C++ 代码是有效的代码。 OSGi 分布式通讯组件 R-OSGi R-OSGi 是一套适用于任意满足 OSGi 架构的分布式通讯组件。它以 jar 的形式发布,部署容易,使用...

    Java企业进销存管理系统源码

    2. **Spring Boot**:Spring Boot简化了Spring应用程序的初始搭建以及配置工作,它集成了大量的常用组件,如数据访问、安全、缓存等,使得开发者可以快速构建可生产的应用。 3. **MyBatis**:MyBatis是一个优秀的...

    网上书店订货部分源码

    网上书店订货部分源码是实现在线图书订购功能的程序代码,主要涵盖了用户浏览、选择、添加到购物车以及下单支付等一系列流程。源码的编写语言可能是Java、Python、PHP等,具体取决于开发者的选择,而这里提到的...

    JAVA源码小区门户网站源码/文档

    通过以上分析可以看出,“JAVA源码小区门户网站源码/文档”不仅包含了一个完整的网站开发项目所需的所有技术要素,而且还提供了一系列实用的功能模块,为开发者们提供了一个很好的学习案例。无论是对于初学者还是有...

    架构狮之路

    此外,还需要关注数据架构和存储方案的选择,如关系型数据库(如MySQL、PostgreSQL)、NoSQL数据库(如MongoDB、Cassandra)以及缓存系统(如Redis、Memcached)。这些不同的存储解决方案各有优缺点,根据业务需求...

    Financial:基于Spring Boot 1.5.x构建的金融借贷项目,包含有用户系统与后台信息管理系统

    【Spring Boot 1.5.x 金融借贷项目详解】 在金融行业中,开发高效、安全的借贷系统至关重要。基于Spring Boot 1.5.x版本构建的...通过深入理解和实践,开发者能掌握Spring Boot的核心机制以及金融系统开发的关键要素。

    校园二手交易系统的设计

    本文将深入探讨"校园二手交易系统的设计",包括系统的主要功能、设计思路、技术选型以及实现过程,以帮助理解如何构建这样一个平台。 一、系统功能分析 1. 用户模块:用户注册、登录、个人信息管理,支持学生证或...

    java开源包10

    J2C 将 Java 代码转成 C++ 代码,这是源码级别的转换,输出的 C++ 代码是有效的代码。 OSGi 分布式通讯组件 R-OSGi R-OSGi 是一套适用于任意满足 OSGi 架构的分布式通讯组件。它以 jar 的形式发布,部署容易,使用...

    在线网络考试系统1.2源码.zip

    1. **Web应用框架**:源码可能基于某个流行的Web应用框架,如Spring Boot、Django或Laravel,这些框架简化了开发流程,提供了MVC(模型-视图-控制器)架构,便于模块化开发。 2. **前端技术**:考试系统的用户界面...

    java毕业设计&课设-供应链系统视频教程(视频+源码+资料).doc

    - **源代码**:提供完整源码,便于学习者理解和二次开发。 - **参考资料**:列出开发过程中参考的相关书籍、网站等资源。 综上所述,本教程不仅涵盖了供应链管理系统的理论知识,还提供了详细的实践指导,适合Java...

    基于ssm+mysql的大美新疆在线论坛交流系统源码数据库.doc

    - 集成JUnit进行单元测试,确保代码质量。 - 使用Git进行版本控制。 #### 功能模块 1. **用户管理**: - 用户注册与登录。 - 用户信息修改。 - 密码找回功能。 2. **帖子管理**: - 发布新帖子。 - 浏览...

    gulimall.zip

    1. 后端框架:可能基于Spring Boot或Django等后端开发框架,提供RESTful API接口。 2. 数据库:MySQL或PostgreSQL作为关系型数据库存储商品、订单等信息。 3. 持久层:MyBatis或Django ORM用于简化数据库操作。 4. ...

    java开源包1

    J2C 将 Java 代码转成 C++ 代码,这是源码级别的转换,输出的 C++ 代码是有效的代码。 OSGi 分布式通讯组件 R-OSGi R-OSGi 是一套适用于任意满足 OSGi 架构的分布式通讯组件。它以 jar 的形式发布,部署容易,使用...

    java开源包11

    J2C 将 Java 代码转成 C++ 代码,这是源码级别的转换,输出的 C++ 代码是有效的代码。 OSGi 分布式通讯组件 R-OSGi R-OSGi 是一套适用于任意满足 OSGi 架构的分布式通讯组件。它以 jar 的形式发布,部署容易,使用...

    java开源包2

    J2C 将 Java 代码转成 C++ 代码,这是源码级别的转换,输出的 C++ 代码是有效的代码。 OSGi 分布式通讯组件 R-OSGi R-OSGi 是一套适用于任意满足 OSGi 架构的分布式通讯组件。它以 jar 的形式发布,部署容易,使用...

    java开源包3

    J2C 将 Java 代码转成 C++ 代码,这是源码级别的转换,输出的 C++ 代码是有效的代码。 OSGi 分布式通讯组件 R-OSGi R-OSGi 是一套适用于任意满足 OSGi 架构的分布式通讯组件。它以 jar 的形式发布,部署容易,使用...

Global site tag (gtag.js) - Google Analytics