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

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

博客分类: 
redis
redis
spring
redisTemplate 

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连接管理模块分析

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 支持事务的方法:

 

 

Java代码  

  1. public <T> T execute(SessionCallback<T> session) {  
  2.     RedisConnectionFactory factory = getConnectionFactory();  
  3.     // bind connection  
  4.     RedisConnectionUtils.bindConnection(factory);  
  5.     try {  
  6.         return session.execute(this);  
  7.     } finally {  
  8.         RedisConnectionUtils.unbindConnection(factory);  
  9.     }  
  10. }  

 

 

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

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

 

SessionCallback接口方法:

 

 

Java代码  

  1. public interface SessionCallback<T> {  
  2.   
  3.     /** 
  4.      * Executes all the given operations inside the same session. 
  5.      *  
  6.      * @param operations Redis operations 
  7.      * @return return value 
  8.      */  
  9.     <K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException;  
  10. }  

 

 

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

 

 

Java代码  

  1. public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {  
  2.     Assert.notNull(action, “Callback object must not be null”);  
  3.   
  4.     RedisConnectionFactory factory = getConnectionFactory();  
  5.     RedisConnection conn = RedisConnectionUtils.getConnection(factory);  
  6.   
  7.     boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);  
  8.     preProcessConnection(conn, existingConnection);  
  9.   
  10.     boolean pipelineStatus = conn.isPipelined();  
  11.     if (pipeline && !pipelineStatus) {  
  12.         conn.openPipeline();  
  13.     }  
  14.   
  15.     try {  
  16.         RedisConnection connToExpose = (exposeConnection ? conn : createRedisConnectionProxy(conn));  
  17.         T result = action.doInRedis(connToExpose);  
  18.         // TODO: any other connection processing?  
  19.         return postProcessResult(result, conn, existingConnection);  
  20.     } finally {  
  21.         try {  
  22.             if (pipeline && !pipelineStatus) {  
  23.                 conn.closePipeline();  
  24.             }  
  25.         } finally {  
  26.             RedisConnectionUtils.releaseConnection(conn, factory);  
  27.         }  
  28.     }  
  29. }  

 

 

当前线程中绑定连接时,返回绑定的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读取返回信息:

 

 

Java代码  

  1. private Object process(final RedisInputStream is) {  
  2.     try {  
  3.         byte b = is.readByte();  
  4.         if (b == MINUS_BYTE) {  
  5.             processError(is);  
  6.         } else if (b == ASTERISK_BYTE) {  
  7.             return processMultiBulkReply(is);  
  8.         } else if (b == COLON_BYTE) {  
  9.             return processInteger(is);  
  10.         } else if (b == DOLLAR_BYTE) {  
  11.             return processBulkReply(is);  
  12.         } else if (b == PLUS_BYTE) {  
  13.             return processStatusCodeReply(is);  
  14.         } else {  
  15.             throw new JedisConnectionException(“Unknown reply: “ + (char) b);  
  16.         }  
  17.     } catch (IOException e) {  
  18.         throw new JedisConnectionException(e);  
  19.     }  
  20.     return null;  
  21. }  

 

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

 

 

Java代码  

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

 

 

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

 

 

Java代码  

  1. public Long dbSize() {  
  2.     try {  
  3.         if (isQueueing()) {  
  4.             throw new UnsupportedOperationException();  
  5.         }  
  6.         if (isPipelined()) {  
  7.             throw new UnsupportedOperationException();  
  8.         }  
  9.         return jedis.dbSize();  
  10.     } catch (Exception ex) {  
  11.         throw convertJedisAccessException(ex);  
  12.     }  
  13. }  

 

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

 

 

Java代码  

  1. protected DataAccessException convertJedisAccessException(Exception ex) {  
  2.     if (ex instanceof JedisException) {  
  3.         // check connection flag  
  4.         if (ex instanceof JedisConnectionException) {  
  5.             broken = true;  
  6.         }  
  7.         return JedisUtils.convertJedisAccessException((JedisException) ex);  
  8.     }  
  9.     if (ex instanceof IOException) {  
  10.         return JedisUtils.convertJedisAccessException((IOException) ex);  
  11.     }  
  12.   
  13.     return new RedisSystemException(“Unknown jedis exception”, ex);  
  14. }  

 

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

 

JedisConnection释放连接逻辑:

 

 

Java代码  

  1. public void close() throws DataAccessException {  
  2.     // return the connection to the pool  
  3.     try {  
  4.         if (pool != null) {  
  5.             if (broken) {  
  6.                 pool.returnBrokenResource(jedis);  
  7.             }  
  8.             else {  
  9.                 // reset the connection   
  10.                 if (dbIndex > 0) {  
  11.                     select(0);  
  12.                 }  
  13.   
  14.                 pool.returnResource(jedis);  
  15.             }  
  16.         }  
  17.     } catch (Exception ex) {  
  18.         pool.returnBrokenResource(jedis);  
  19.     }  
  20.   
  21.     if (pool != null) {  
  22.         return;  
  23.     }  
  24.   
  25.     // else close the connection normally  
  26.     try {  
  27.         if (isQueueing()) {  
  28.             client.quit();  
  29.             client.disconnect();  
  30.             return;  
  31.         }  
  32.         jedis.quit();  
  33.         jedis.disconnect();  
  34.     } catch (Exception ex) {  
  35.         throw convertJedisAccessException(ex);  
  36.     }  
  37. }  

 

当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:

 

 

Java代码  

  1. public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind) {  
  2.     Assert.notNull(factory, “No RedisConnectionFactory specified”);  
  3.   
  4.     RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);  
  5.     //TODO: investigate tx synchronization  
  6.   
  7.     if (connHolder != null)  
  8.         return connHolder.getConnection();  
  9.   
  10.     if (!allowCreate) {  
  11.         throw new IllegalArgumentException(“No connection found and allowCreate = false”);  
  12.     }  
  13.   
  14.     if (log.isDebugEnabled())  
  15.         log.debug(“Opening RedisConnection”);  
  16.   
  17.     RedisConnection conn = factory.getConnection();  
  18.   
  19.     boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();  
  20.   
  21.     if (bind || synchronizationActive) {  
  22.         connHolder = new RedisConnectionHolder(conn);  
  23.         if (synchronizationActive) {  
  24.             TransactionSynchronizationManager.registerSynchronization(new RedisConnectionSynchronization(  
  25.                     connHolder, factory, true));  
  26.         }  
  27.         TransactionSynchronizationManager.bindResource(factory, connHolder);  
  28.         return connHolder.getConnection();  
  29.     }  
  30.     return conn;  
  31. }  

 

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

 

 

Java代码  

  1. public JedisConnection getConnection() {  
  2.     Jedis jedis = fetchJedisConnector();  
  3.     return postProcessConnection((usePool ? new JedisConnection(jedis, pool, dbIndex) : new JedisConnection(jedis,  
  4.             null, dbIndex)));  
  5. }  

 

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

 

 

Java代码  

  1. public JedisConnection(Jedis jedis, Pool<Jedis> pool, int dbIndex) {  
  2.     this.jedis = jedis;  
  3.     // extract underlying connection for batch operations  
  4.     client = (Client) ReflectionUtils.getField(CLIENT_FIELD, jedis);  
  5.     transaction = new Transaction(client);  
  6.   
  7.     this.pool = pool;  
  8.   
  9.     this.dbIndex = dbIndex;  
  10.   
  11.     // select the db  
  12.     if (dbIndex > 0) {  
  13.         select(dbIndex);  
  14.     }  
  15. }  

 

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

 

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

 

 

Java代码  

  1. public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {  
  2.     Assert.notNull(action, “Callback object must not be null”);  
  3.   
  4.     RedisConnectionFactory factory = getConnectionFactory();  
  5.     RedisConnection conn = RedisConnectionUtils.getConnection(factory);  
  6.   
  7.     …..  
  8.   
  9.     try {  
  10.         ……  
  11.     } finally {  
  12.         try {  
  13.             if (pipeline && !pipelineStatus) {  
  14.                 conn.closePipeline();  
  15.             }  
  16.         } finally {  
  17.             RedisConnectionUtils.releaseConnection(conn, factory);  
  18.         }  
  19.     }  
  20. }  

 

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

 

 

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

 

 

    原文作者:Spring Cloud
    原文地址: https://blog.csdn.net/u014704496/article/details/40858701
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞