spring整合redis客户端及缓存接口设计

GregRhea 8年前

来自: http://www.cnblogs.com/xumanbu/p/5160765.html

一、写在前面

缓存作为系统性能优化的一大杀手锏,几乎在每个系统或多或少的用到缓存。有的使用本地内存作为缓存,有的使用本地硬盘作为缓存,有的使用缓存服务器。但是无论使用哪种缓存,接口中的方法都是差不多。笔者最近的项目使用的是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: execute  19      * @Description: 执行{@link RedisPoolCallback#doInJedis(Jedis)}的方法  20      * @param action  21      * @return  22      * @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: doInJedis  14      * @Description: 回调执行方法,需要重新此方法,一般推荐使用匿名内部类  15      * @param jedis  16      * @return  17      * @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=300  11 # 表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException;  12 redis.maxWaitMillis=1000  13 # 在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;  14 redis.testOnBorrow=true

applicationContext-redis.xml

<?xml version="1.0" encoding="UTF-8"?>  <beans xmlns="http://www.springframework.org/schema/beans"      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"      xmlns:tx="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 }