咱們但願設計一套緩存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異常
~未完待續