咱们但愿设计一套缓存API,适应不一样的缓存产品,而且基于Spring框架完美集成应用开发。java
本文旨在针对缓存产品定义一个轻量级的客户端访问框架,目标支持多种缓存产品,面向接口编程,目前支持简单的CRUD。redis
目前大多数NoSQL产品的Java客户端API都以彻底实现某个NoSQL产品的特性而实现,而缓存只是一个feature,若是缓存API只针对缓存这一个feature,那么它可否能够定义的更易于使用,API是否能定义的更合理呢?spring
即:站在抽象缓存产品设计的角度定义一个API,而不是完整封装NoSQL产品的客户端访问API数据库
以Memcached、Redis、MongoDB三类产品为例,后二者可不止缓存这样的一个feature:编程
也许有人会说,为何把MongoDB也做为缓存产品的一种选型呢?api
广义上讲,内存中的一个Map结构就能够成为一个缓存了,所以MongoDB这种文档型的NoSQL数据库更不用说了。数组
以百度百科对缓存的解释,适当补充缓存
仅仅以缓存定义来看,任何存取数据性能高于底层介质的存储结构均可以做为缓存。ruby
业务逻辑增长缓存处理的样例代码bash
// 从缓存中获取数据 Object result = cacheClient.get(key); // 结果为空 if(result == null) { // 执行业务处理 result = do(...); // 存入缓存 cacheClient.put(key, result); } // 返回结果 return result;
咱们的目标:尽量的抽象缓存读写定义,最大限度的兼容各类底层缓存产品的能力(没有蛀牙)
存储结构在接口方法维度上扩展
各种操做特性在Option对象上扩展
翻译成代码(代码过多、非完整版本):
缓存抽象接口
package org.wit.ff.cache; import java.util.List; import java.util.Map; /** * Created by F.Fang on 2015/9/23. * Version :2015/9/23 */ public interface IAppCache { /** * * @param key 键 * @param <K> * @return 目标缓存中是否存在键 */ <K> boolean contains(K key); /** * * @param key 键 * @param value 值 * @param <K> * @param <V> * @return 存储到目标缓存是否成功 */ <K,V> boolean put(K key, V value); /** * * @param key 键 * @param value 值 * @param option 超时,同步异步控制 * @param <K> * @param <V> * @return 存储到目标缓存是否成功 */ <K,V> boolean put(K key, V value, Option option); /** * * @param key 键 * @param type 值 * @param <K> * @param <V> * @return 返回缓存系统目标键对应的值 */ <K,V> V get(K key, Class<V> type); /** * * @param key 键 * @param <K> * @return 删除目标缓存键是否成功 */ <K> boolean remove(K key); }
缓存可选项
package org.wit.ff.cache; /** * Created by F.Fang on 2015/9/23. * Version :2015/9/23 */ public class Option { /** * 超时时间. */ private long expireTime; /** * 超时类型. */ private ExpireType expireType; /** * 调用模式. * 异步选项,默认同步(非异步) */ private boolean async; public Option(){ // 默认是秒设置. expireType = ExpireType.SECONDS; } public long getExpireTime() { return expireTime; } public void setExpireTime(long expireTime) { this.expireTime = expireTime; } public boolean isAsync() { return async; } public void setAsync(boolean async) { this.async = async; } public ExpireType getExpireType() { return expireType; } public void setExpireType(ExpireType expireType) { this.expireType = expireType; } }
过时时间枚举
package org.wit.ff.cache; /** * Created by F.Fang on 2015/9/18. * Version :2015/9/18 */ public enum ExpireType { SECONDS, DATETIME }
序列化接口
package org.wit.ff.cache; /** * Created by F.Fang on 2015/9/15. * Version :2015/9/15 */ public interface ISerializer<T> { byte[] serialize(T obj); T deserialize(byte[] bytes, Class<T> type); }
默认序列化实现
package org.wit.ff.cache.impl; import org.springframework.util.SerializationUtils; import org.wit.ff.cache.ISerializer; /** * Created by F.Fang on 2015/9/15. * Version :2015/9/15 */ public class DefaultSerializer<T> implements ISerializer<T>{ @Override public byte[] serialize(T obj) { return SerializationUtils.serialize(obj); } @Override public T deserialize(byte[] bytes, Class<T> type) { return (T)SerializationUtils.deserialize(bytes); } }
Jedis缓存API实现
package org.wit.ff.cache.impl; import org.wit.ff.cache.ExpireType; import org.wit.ff.cache.IAppCache; import org.wit.ff.cache.ISerializer; import org.wit.ff.cache.Option; import org.wit.ff.util.ByteUtil; import org.wit.ff.util.ClassUtil; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Created by F.Fang on 2015/9/16. * 目前的实现虽然不够严密,可是基本够用. * 由于对于put操做,对于目前的业务场景是容许失败的,由于下次执行正常业务逻辑处理时仍然能够重建缓存. * Version :2015/9/16 */ public class JedisAppCache implements IAppCache { /** * redis链接池. */ private JedisPool pool; /** * 序列化工具. */ private ISerializer serializer; /** * 全局超时选项. */ private Option option; public JedisAppCache() { serializer = new DefaultSerializer(); option = new Option(); } @Override public <K> boolean contains(K key) { if (key == null) { throw new IllegalArgumentException("key can't be null!"); } try (Jedis jedis = pool.getResource()) { byte[] kBytes = translateObjToBytes(key); return jedis.exists(kBytes); } } @Override public <K, V> boolean put(K key, V value) { return put(key, value, option); } @Override public <K, V> boolean put(K key, V value, Option option) { if (key == null || value == null) { throw new IllegalArgumentException("key,value can't be null!"); } try (Jedis jedis = pool.getResource()) { byte[] kBytes = translateObjToBytes(key); byte[] vBytes = translateObjToBytes(value); // 暂时不考虑状态码的问题, 成功状态码为OK. String code = jedis.set(kBytes, vBytes); // 若是设置了合法的过时时间才设置超时. setExpire(kBytes, option, jedis); return "OK".equals(code); } } @Override public <K, V> V get(K key, Class<V> type) { if (key == null || type == null) { throw new IllegalArgumentException("key or type can't be null!"); } try (Jedis jedis = pool.getResource()) { byte[] kBytes = translateObjToBytes(key); byte[] vBytes = jedis.get(kBytes); if (vBytes == null) { return null; } return translateBytesToObj(vBytes, type); } } @Override public <K> boolean remove(K key) { if (key == null) { throw new IllegalArgumentException("key can't be null!"); } try (Jedis jedis = pool.getResource()) { byte[] kBytes = translateObjToBytes(key); // 状态码为0或1(key数量)均可认为是正确的.0表示key本来就不存在. jedis.del(kBytes); // 暂时不考虑状态码的问题. return true; } } private <T> byte[] translateObjToBytes(T val) { byte[] valBytes; if (val instanceof String) { valBytes = ((String) val).getBytes(); } else { Class<?> classType = ClassUtil.getWrapperClassType(val.getClass().getSimpleName()); if (classType != null) { // 若是是基本类型. Boolean,Void不可能会出如今参数传值类型的位置. if (classType.equals(Integer.TYPE)) { valBytes = ByteUtil.intToByte4((Integer) val); } else if (classType.equals(Character.TYPE)) { valBytes = ByteUtil.charToByte2((Character) val); } else if (classType.equals(Long.TYPE)) { valBytes = ByteUtil.longToByte8((Long) val); } else if (classType.equals(Double.TYPE)) { valBytes = ByteUtil.doubleToByte8((Double) val); } else if (classType.equals(Float.TYPE)) { valBytes = ByteUtil.floatToByte4((Float) val); } else if(val instanceof byte[]) { valBytes = (byte[])val; } else { throw new IllegalArgumentException("unsupported value type, classType is:" + classType); } } else { // 其它均采用序列化 valBytes = serializer.serialize(val); } } return valBytes; } private <T> T translateBytesToObj(byte[] bytes, Class<T> type) { Object obj; if (type.equals(String.class)) { obj = new String(bytes); } else { Class<?> classType = ClassUtil.getWrapperClassType(type.getSimpleName()); if (classType != null) { // 若是是基本类型. Boolean,Void不可能会出如今参数传值类型的位置. if (classType.equals(Integer.TYPE)) { obj = ByteUtil.byte4ToInt(bytes); } else if (classType.equals(Character.TYPE)) { obj = ByteUtil.byte2ToChar(bytes); } else if (classType.equals(Long.TYPE)) { obj = ByteUtil.byte8ToLong(bytes); } else if (classType.equals(Double.TYPE)) { obj = ByteUtil.byte8ToDouble(bytes); } else if (classType.equals(Float.TYPE)) { obj = ByteUtil.byte4ToFloat(bytes); } else { throw new IllegalArgumentException("unsupported value type, classType is:" + classType); } } else { // 其它均采用序列化 obj = serializer.deserialize(bytes,type); } } return (T) obj; } private void setExpire(byte[] kBytes,Option option, Jedis jedis) { if (option.getExpireType().equals(ExpireType.SECONDS)) { int seconds = (int)option.getExpireTime()/1000; if(seconds > 0){ jedis.expire(kBytes, seconds); } } else { jedis.expireAt(kBytes, option.getExpireTime()); } } public void setPool(JedisPool pool) { this.pool = pool; } public void setSerializer(ISerializer serializer) { this.serializer = serializer; } public void setOption(Option option) { this.option = option; } }
Spring配置文件(spring-redis.xml)
<context:property-placeholder location="redis.properties"/> <!-- JedisPool --> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxTotal" value="4" /> <property name="maxIdle" value="2" /> <property name="maxWaitMillis" value="10000" /> <property name="testOnBorrow" value="true" /> </bean> <bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="destroy"> <constructor-arg index="0" ref="jedisPoolConfig" /> <constructor-arg index="1" value="${redis.host}" /> <constructor-arg index="2" value="${redis.port}" /> <constructor-arg index="3" value="10000" /> <constructor-arg index="4" value="${redis.password}" /> <constructor-arg index="5" value="0" /> </bean> <bean id="jedisAppCache" class="org.wit.ff.cache.impl.JedisAppCache" > <property name="pool" ref="jedisPool" /> </bean>
Redis配置文件
redis.host=192.168.21.125 redis.port=6379 redis.password=xxx
Xmemcached缓存API实现
package org.wit.ff.cache.impl; import net.rubyeye.xmemcached.MemcachedClient; import net.rubyeye.xmemcached.exception.MemcachedException; import org.wit.ff.cache.AppCacheException; import org.wit.ff.cache.ExpireType; import org.wit.ff.cache.IAppCache; import org.wit.ff.cache.Option; import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; /** * Created by F.Fang on 2015/9/24. * 基于xmemcached. * Version :2015/9/24 */ public class XMemAppCache implements IAppCache { /** * memcached客户端. */ private MemcachedClient client; /** * 选项. */ private Option option; public XMemAppCache(){ option = new Option(); } @Override public <K> boolean contains(K key) { String strKey = translateToStr(key); try { return client.get(strKey) != null; } catch (InterruptedException | MemcachedException |TimeoutException e){ throw new AppCacheException(e); } } @Override public <K, V> boolean put(K key, V value) { return put(key,value,option); } @Override public <K, V> boolean put(K key, V value, Option option) { if(option.getExpireType().equals(ExpireType.DATETIME)){ throw new UnsupportedOperationException("memcached no support ExpireType(DATETIME) !"); } // 目前考虑 set, add方法若是key已存在会发生异常. // 当前对缓存均不考虑更新操做. int seconds = (int)option.getExpireTime()/1000; String strKey = translateToStr(key); try { if(option.isAsync()){ // 异步操做. client.setWithNoReply(strKey, seconds, value); return true; } else { return client.set(strKey, seconds, value); } } catch (InterruptedException | MemcachedException |TimeoutException e){ throw new AppCacheException(e); } } @Override public <K, V> V get(K key, Class<V> type) { String strKey = translateToStr(key); try { return client.get(strKey); } catch (InterruptedException | MemcachedException |TimeoutException e){ throw new AppCacheException(e); } } @Override public <K> boolean remove(K key) { String strKey = translateToStr(key); try { return client.delete(strKey); } catch (InterruptedException | MemcachedException |TimeoutException e){ throw new AppCacheException(e); } } private <K> String translateToStr(K key) { if(key instanceof String){ return (String)key; } return key.toString(); } public void setClient(MemcachedClient client) { this.client = client; } public void setOption(Option option) { this.option = option; } }
Spring配置文件(spring-memcached.xml)
<context:property-placeholder location="memcached.properties"/> <bean id="memcachedClientBuilder" class="net.rubyeye.xmemcached.XMemcachedClientBuilder" p:connectionPoolSize="${memcached.connectionPoolSize}" p:failureMode="${memcached.failureMode}"> <!-- XMemcachedClientBuilder have two arguments.First is server list,and second is weights array. --> <constructor-arg> <list> <bean class="java.net.InetSocketAddress"> <constructor-arg> <value>${memcached.server1.host}</value> </constructor-arg> <constructor-arg> <value>${memcached.server1.port}</value> </constructor-arg> </bean> </list> </constructor-arg> <constructor-arg> <list> <value>${memcached.server1.weight}</value> </list> </constructor-arg> <property name="commandFactory"> <bean class="net.rubyeye.xmemcached.command.TextCommandFactory"/> </property> <property name="sessionLocator"> <bean class="net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator"/> </property> <property name="transcoder"> <bean class="net.rubyeye.xmemcached.transcoders.SerializingTranscoder"/> </property> </bean> <!-- Use factory bean to build memcached client --> <bean id="memcachedClient" factory-bean="memcachedClientBuilder" factory-method="build" destroy-method="shutdown"/> <bean id="xmemAppCache" class="org.wit.ff.cache.impl.XMemAppCache" > <property name="client" ref="memcachedClient" /> </bean>
memcached.properties
#链接池大小即客户端个数 memcached.connectionPoolSize=3 memcached.failureMode=true #server1 memcached.server1.host=xxxx memcached.server1.port=21212 memcached.server1.weight=1
示例测试代码:
package org.wit.ff.cache; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; import tmodel.User; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; /** * Created by F.Fang on 2015/10/19. * Version :2015/10/19 */ @ContextConfiguration("classpath:spring-redis.xml") public class AppCacheTest extends AbstractJUnit4SpringContextTests { @Autowired private IAppCache appCache; @Test public void demo() throws Exception{ User user = new User(1, "ff", "ff@adchina.com"); appCache.put("ff", user); TimeUnit.SECONDS.sleep(3); User result = appCache.get("ff",User.class); assertEquals(user, result); } }
注:Redis支持支持集合(list,map)存储结构,Memecached则不支持,所以能够考虑在基于Memcached缓存访问API实现中的putList(...)方法直接抛出UnsupportedOperationException异常
~未完待续