你的位置:首页 > Java教程

[Java教程]spring整合redis客户端及缓存接口设计


一、写在前面

缓存作为系统性能优化的一大杀手锏,几乎在每个系统或多或少的用到缓存。有的使用本地内存作为缓存,有的使用本地硬盘作为缓存,有的使用缓存服务器。但是无论使用哪种缓存,接口中的方法都是差不多。笔者最近的项目使用的是memcached作为缓存服务器,由于memcached的一些限制,现在想换redis作为缓存服务器。思路就是把memached的客户端换成redis客户端,接口依然是原来的接口,这样对系统可以无损替换,接口不变,功能不变,只是客户端变了。本文不介绍缓存的用法,不介绍redis使用方法,不介绍memcached与redis有何区别。只是实现一个redis客户端,用了jedis作为第三方连接工具。

二、一些想法

首先贴一下现项目中同事编写的缓存接口:

/** * @ClassName: DispersedCachClient * @Description: 分布式缓存接口,每个方法:key最大长度128字符,valueObject最大1Mb,默认超时时间30天 * @date 2015-4-14 上午11:51:18 * */public interface DispersedCachClient {			/**	 * add(要设置缓存中的对象(value),)	 *	 * @Title: add	 * @Description: 要设置缓存中的对象(value),如果没有则插入,有就不操作。	 * @param key	键	 * @param valueObject	缓存对象	 * @return Boolean true 成功,false 失败	 */	public Boolean add(String key, Object valueObject);		/**	 * add(要设置缓存中的对象(value),指定保存有效时长)	 *	 * @Title: add	 * @Description: 要设置缓存中的对象(value),指定有效时长,如果没有则插入,有就不操作。	 * @param key	键	 * @param valuObject	缓存对象	 * @param keepTimeInteger	有效时长(秒)	 * @return Boolean true 成功,false 失败	 */	public Boolean add(String key, Object valueObject, Integer keepTimeInteger);		/**	 * 	 * add(要设置缓存中的对象(value),指定有效时间点。)	 *	 * @Title: add	 * @Description: 要设置缓存中的对象(value),指定有效时间点,如果没有则插入,有就不操作。	 * @date 2015-4-14 上午11:58:12	 * @param key	键	 * @param valuObject	缓存对象	 * @param keepDate	时间点	 * @return Boolean true 成功,false 失败	 */	public Boolean add(String key, Object valueObject, Date keepDate);		/**	 * 	 * set(要设置缓存中的对象(value),)	 *	 * @Title: set	 * @Description: 如果没有则插入,如果有则修改	 * @date 2015-4-14 下午01:44:22	 * @param key	键	 * @param valueObject	缓存对象	 * @return Boolean true 成功,false 失败		 */	public Boolean set(String key,Object valueObject) ;		/**	 * 	 * set(要设置缓存中的对象(value),指定有效时长)	 *	 * @Title: set	 * @Description: 指定有效时长,如果没有则插入,如果有则修改	 * @date 2015-4-14 下午01:45:22	 * @param key	键	 * @param valueObject	缓存对象	 * @param keepTimeInteger	保存时长(秒)	 * @return Boolean true 成功,false 失败	 */	public Boolean set(String key, Object valueObject, Integer keepTimeInteger);		/**	 * 	 * set(要设置缓存中的对象(value),指定有效时间点)	 *	 * @Title: set	 * @Description: 指定有效时间点,如果没有则插入,如果有则修改	 * @date 2015-4-14 下午01:45:55	 * @param key	键	 * @param valueObject	缓存对象	 * @param keepDate	有效时间点	 * @return Boolean true 成功,false 失败	 */	public Boolean set(String key, Object valueObject, Date keepDate);		/**	 * 	 * replace(要设置缓存中的对象(value),有效)	 *	 * @Title: replace	 * @Description: 有效,如果没有则不操作,如果有则修改	 * @date 2015-4-14 下午01:47:04	 * @param key	键	 * @param valueObject	缓存对象	 * @return Boolean true 成功,false 失败 	 */	public Boolean replace(String key,Object valueObject) ;		/**	 * 	 * replace(要设置缓存中的对象(value),指定有效时长)	 *	 * @Title: replace	 * @Description: 指定有效时长,如果没有则不操作,如果有则修改	 * @date 2015-4-14 下午01:47:30	 * @param key	键	 * @param valueObject	缓存对象	 * @param keepTimeInteger	缓存时长(秒)	 * @return Boolean true 成功,false 失败 	 */	public Boolean replace(String key, Object valueObject, Integer keepTimeInteger);		/**	 * 	 * replace(要设置缓存中的对象(value),指定有效时间点)	 *	 * @Title: replace	 * @Description: 指定有效时间点,如果没有则不操作,如果有则修改	 * @date 2015-4-14 下午01:48:09	 * @param key	键值对	 * @param valueObject	缓存对象	 * @param keepDate	有效时间点	 * @return Boolean true 成功,false 失败 	 */	public Boolean replace(String key, Object valueObject, Date keepDate);		/**	 * 	 * get(获得一个缓存对象)	 *	 * @Title: get	 * @Description: 获得一个缓存对象,响应超时时间默认	 * @date 2015-4-14 下午04:18:16	 * @param key	键	 * @return Obeject 	 */	public Object get( String key );		/**	 * 	 * getMulti(获得Map形式的多个缓存对象)	 *	 * @Title: getMulti	 * @Description: 获得Map形式的多个缓存对象,响应超时时间默认	 * @date 2015-4-14 下午04:53:07	 * @param keys	键存入的string[]	 * return Map<String,Object> 	 */	public Map<String,Object> getMulti( List<String> keys );		/**	 * 	 * gets(获得一个带版本号的缓存对象)	 *	 * @Title: gets	 * @Description: 获得一个带版本号的缓存对象	 * @date 2015-4-16 上午09:15:57	 * @param key	键	 * @return Object	 */	public Object gets(String key);		/**	 * 	 * getMultiArray(获得数组形式的多个缓存对象)	 *	 * @Title: getMultiArray	 * @Description: 获得数组形式的多个缓存对象	 * @date 2015-4-16 上午09:27:29	 * @param keys	键存入的string[]	 * @return Object[]	 * @throws	 */	public Object[] getMultiArray( List<String> keys );		/**	 * 	 * cas(带版本号存缓存,与gets配合使用)	 *	 * @Title: cas	 * @Description: 带版本号存缓存,与gets配合使用,超时时间默认	 * @date 2015-4-16 上午09:53:39	 * @param key	键	 * @param valueObject	缓存对象	 * @param versionNo		版本号	 * @return Boolean true 成功,false 失败 	 */	public boolean cas(String key, Object valueObject, long versionNo);			/** cas(带版本号存缓存,与gets配合使用)	 *	 * @Title: cas	 * @Description: 带版本号存缓存,与gets配合使用,指定超时时长	 * @date 2015-4-16 上午09:58:06	 * @param key	键	 * @param valueObject	缓存对象	 * @param keepTimeInteger	超时时长	 * @param versionNo		版本号	 * @return Boolean true 成功,false 失败	 */	public boolean cas(String key, Object valueObject, Integer keepTimeInteger, long versionNo);		/**	 * 	 * cas(带版本号存缓存,与gets配合使用)	 *	 * @Title: cas	 * @Description: 带版本号存缓存,与gets配合使用,指定超时时间点	 * @date 2015-4-16 上午10:02:38	 * @param key	键	 * @param valueObject	缓存对象	 * @param keepTime	超时时间点	 * @param versionNo		版本号	 * @return Boolean true 成功,false 失败 	 */	public boolean cas(String key, Object valueObject, Date keepDate, long versionNo);	/**	 * 	 * delete(删除缓存)	 *	 * @Title: delete	 * @Description: 删除缓存	 * @date 2015-4-16 上午11:20:13	 * @param key	键	 */	public boolean delete(String key);	}

 

这个接口用起来总有一些别扭,我总结了一下:

1、接口名称命名为DispersedCachClient 这个命名含义是分布式缓存客户端(cache少了一个字母),其实这个接口跟分布式一点关系都没有,其实就是缓存接口;

2、接口方法太多了,实际在项目中并没有方法使用率只有20%左右,所有有精简的必要;

3、这个接口很多方法设计是套用memcached客户端设计的,也就是说换成redis后会不通用。

这里没有说这个接口写的不好,而是说还有优化的空间,其次也给自己提个醒,在设计一些使用公共的接口时有必要多花些心思,因为一旦设计后,后面改动的可能性比较小。 

三、代码实现

使用jedis客户端需要使用连接池,使用连接后需要将连接放回连接池,失效的连接要放到失效的连接池,类似jdbc需要进行连接的处理,为了避免写重复恶心的代码,参照了spring的JdbcTemple模板设计方式。废话没有,直接上代码吧。

1、重新设计的缓存客户端接口,这个接口就一个特点“简单”,目的是为了做到通用,故命名为SimpleCache。

/** * @ClassName: DistributedCacheClient * @Description: 缓存接口 * @author 徐飞 * @date 2016年1月26日 上午11:41:27 * */public interface SimpleCache {	/**	 * @Title: add	 * @Description: 添加一个缓冲数据	 * @param key 字符串的缓存key	 * @param value 缓冲的缓存数据	 * @return	 * @author 徐飞	 */	boolean add(String key, Object value);	/**	 * @Title: add	 * @Description: 缓存一个数据,并指定缓存过期时间	 * @param key	 * @param value	 * @param seconds	 * @return	 * @author 徐飞	 */	boolean add(String key, Object value, int seconds);	/**	 * @Title: get	 * @Description: 根据key获取到一直值	 * @param key 字符串的缓存key	 * @return	 * @author 徐飞	 */	Object get(String key);	/**	 * @Title: delete	 * @Description: 删除一个数据问题	 * @param key 字符串的缓存key	 * @return	 * @author 徐飞	 */	long delete(String key);	/**	 * @Title: exists	 * @Description: 判断指定key是否在缓存中已经存在	 * @param key 字符串的缓存key	 * @return	 * @author 徐飞	 */	boolean exists(String key);}

  

2、JedisTemple :Jedis 操作模板类,请参照Spring的JdbcTemple封装重复但又必要的操作

 1 /** 2  * @ClassName: JedisTemple 3  * @Description: Jedis 操作模板类,为啥要这个?请参照{@link JdbcTemple} 封装重复不必要的操作 4  * @author 徐飞 5  * @date 2016年1月26日 下午2:37:24 6  * 7 */ 8 public class JedisTemple { 9 10   /** 缓存客户端 **/11   private JedisPool jedisPool;// 非切片连接池12 13   public JedisTemple(JedisPool jedisPool) {14     this.jedisPool = jedisPool;15   }16 17   /**18    * @Title: execute19    * @Description: 执行{@link RedisPoolCallback#doInJedis(Jedis)}的方法20    * @param action21    * @return22    * @author 徐飞23   */24   public <T> T execute(RedisPoolCallback<T> action) {25     T value = null;26     Jedis jedis = null;27     try {28       jedis = jedisPool.getResource();29       return action.doInJedis(jedis);30     } catch (Exception e) {31       // 释放redis对象32       jedisPool.returnBrokenResource(jedis);33       e.printStackTrace();34     } finally {35       // 返还到连接池36       returnResource(jedisPool, jedis);37     }38 39     return value;40   }41 42   /** 43   * 返还到连接池 44   * @param pool 45   * @param redis 46   */47   private void returnResource(JedisPool pool, Jedis redis) {48     // 如果redis为空不返回49     if (redis != null) {50       pool.returnResource(redis);51     }52   }53 54   public JedisPool getJedisPool() {55     return jedisPool;56   }57 58   public void setJedisPool(JedisPool jedisPool) {59     this.jedisPool = jedisPool;60   }61 62 }

3、RedisPoolCallback:redis操作回调接口,此接口主要为JedisTemple模板使用

 1 import redis.clients.jedis.Jedis; 2  3 /** 4  * @ClassName: RedisPoolCallback 5  * @Description: redis操作回调接口,此接口主要为JedisTemple模板使用 6  * @author 徐飞 7  * @date 2016年1月26日 下午2:35:41 8  * 9  * @param <T>10 */11 public interface RedisPoolCallback<T> {12   /**13    * @Title: doInJedis14    * @Description: 回调执行方法,需要重新此方法,一般推荐使用匿名内部类15    * @param jedis16    * @return17    * @author 徐飞18   */19   T doInJedis(Jedis jedis);20 }

 

4、RedisCacheClient :redis客户端实现类

 1 import redis.clients.jedis.Jedis; 2 import redis.clients.jedis.JedisPool; 3 import redis.clients.jedis.JedisPoolConfig; 4 import redis.clients.util.SafeEncoder; 5  6 import com.cxypub.baseframework.sdk.util.ObjectUtils; 7  8 /** 9  * @ClassName: RedisCacheClient 10  * @Description: redis缓存客户端 11  * @author 徐飞 12  * @date 2015-4-16 上午10:42:32 13  * 14 */ 15 public class RedisCacheClient implements SimpleCache { 16  17   private JedisTemple jedisTemple; 18  19   public RedisCacheClient(JedisPoolConfig config, String host, Integer port) { 20     this.initialPool(config, host, port); 21   } 22  23   /** 24    * 初始化非切片池 25   */ 26   private void initialPool(JedisPoolConfig config, String host, Integer port) { 27     JedisPool jedisPool = new JedisPool(config, host, port); 28     this.jedisTemple = new JedisTemple(jedisPool); 29   } 30  31   @Override 32   public boolean add(final String key, final Object valueObject) { 33     try { 34       jedisTemple.execute(new RedisPoolCallback<Boolean>() { 35         @Override 36         public Boolean doInJedis(Jedis jedis) { 37           jedis.set(SafeEncoder.encode(key), ObjectUtils.object2Byte(valueObject)); 38           return true; 39         } 40  41       }); 42     } catch (Exception e) { 43       e.printStackTrace(); 44       return false; 45     } 46     return true; 47   } 48  49   @Override 50   public Object get(final String key) { 51  52     return jedisTemple.execute(new RedisPoolCallback<Object>() { 53       @Override 54       public Object doInJedis(Jedis jedis) { 55         byte[] cacheValue = jedis.get(SafeEncoder.encode(key)); 56         if (cacheValue != null) { 57           return ObjectUtils.byte2Object(cacheValue); 58         } 59         return null; 60       } 61  62     }); 63   } 64  65   @Override 66   public long delete(final String key) { 67     return jedisTemple.execute(new RedisPoolCallback<Long>() { 68       @Override 69       public Long doInJedis(Jedis jedis) { 70         return jedis.del(key); 71       } 72     }); 73   } 74  75   @Override 76   public boolean add(final String key, Object value, final int seconds) { 77     try { 78       this.add(key, value); 79       jedisTemple.execute(new RedisPoolCallback<Long>() { 80         @Override 81         public Long doInJedis(Jedis jedis) { 82           return jedis.expire(key, seconds); 83         } 84       }); 85     } catch (Exception e) { 86       e.printStackTrace(); 87       return false; 88     } 89     return true; 90   } 91  92   @Override 93   public boolean exists(final String key) { 94     return jedisTemple.execute(new RedisPoolCallback<Boolean>() { 95       @Override 96       public Boolean doInJedis(Jedis jedis) { 97         return jedis.exists(key); 98       } 99     });100   }101 102 }

5、实现了代码,下面就开始将客户端整合进spring就行了,上配置文件

redis.properties:

 1 # Redis settings 2 redis.host=192.168.1.215 3 redis.port=6379 4 redis.pass= 5  6 # 控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取; 7 # 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。 8 redis.maxTotal=600 9 # 控制一个pool最多有多少个状态为idle(空闲的)的jedis实例。10 redis.maxIdle=30011 # 表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException;12 redis.maxWaitMillis=100013 # 在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;14 redis.testOnBorrow=true

applicationContext-redis.

<?  ="http://www.w3.org/2001/  ="http://www.springframework.org/schema/tx"  xsi:schemaLocation="http://www.springframework.org/schema/beans      http://www.springframework.org/schema/beans/spring-beans-2.0.xsd      http://www.springframework.org/schema/aop       http://www.springframework.org/schema/aop/spring-aop-2.0.xsd      http://www.springframework.org/schema/tx       http://www.springframework.org/schema/tx/spring-tx-2.0.xsd"  default-autowire="autodetect" default-lazy-init="false">  <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">     <property name="maxIdle" value="${redis.maxIdle}" />     <property name="maxTotal" value="${redis.maxTotal}" />     <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />     <property name="testOnBorrow" value="${redis.testOnBorrow}" />   </bean>    <bean id="jedisClient" class="com.cxypub.baseframework.sdk.cache.RedisCacheClient">    <constructor-arg ref="jedisPoolConfig" />    <constructor-arg value="${redis.host}" />    <constructor-arg value="${redis.port}" type="java.lang.Integer" />  </bean>  </beans>

 

6、这样在项目中就可以将jedisClient 注入到任何类中了,我这里写了一个测试客户端,这个直接运行的,一同贴上。

 1 public class RedisTest { 2   public static void main(String[] args) { 3     JedisPoolConfig config = new JedisPoolConfig(); 4     config.setMaxTotal(500); 5     config.setMaxIdle(5); 6     config.setMaxWaitMillis(1000 * 100); 7     config.setTestOnBorrow(true); 8     RedisCacheClient client = new RedisCacheClient(config, "192.168.1.215", 6379); 9     Dictionary dict = new Dictionary();10     dict.setId("qwertryruyrtutyu");11     dict.setDictChineseName("上海");12     dict.setCreateTime(new Date());13     client.add("xufei", dict);14     Dictionary dict2 = (Dictionary) client.get("xufei");15     System.out.println(dict2);16     System.out.println(dict == dict2);17   }18 }