文章出處
原先的數據緩存都是放在jvm里的,所以機器多了每臺服務器都要自己去加載緩存,這樣一來命中就低。最近打算在系統里引入第三方緩存,當時在memcached和火的要死的redis里選擇。現在來看每種內存產品都各有優勢,如果硬生生的將現在這些老的緩存直接改成redis的如果以后需要用別的內存數據庫又得大改代碼。想到這就決定把緩存做一次設計,將現有的jvm緩存保留下來,然后做成策略以擴展新的緩存存儲。
文章列表
先從web session的共享說起
許多系統需要提供7*24小時服務,這類系統肯定需要考慮災備問題,單臺服務器如果宕機可能無法立馬恢復使用,這必定影響到服務。這個問題對于系統規模來說,從小到大可能面臨的難度會相差很大。但對于原理來說其實就是需要準備備份系統隨時可以替代正在服務的系統,也就是無論何時都有服務器可以提供服務。也就是災備系統或者負載均衡。
提供災備系統或者負載均衡系統都需要面臨一個問題,那就是如何解決共享數據的問題。對于web服務器而言首先要解決的就是web session共享問題,比如A服務器的session如何可以在B服務器上也能一樣使用呢?畢竟是物理隔離的兩臺服務器。
這方面的方案主要是兩類:cookies和session共享。
cookies
這種方案的思路就是將session的數據寫入到cookies里,每次請求的時候就可以帶上信息,這樣不管是哪臺服務器都能得到同樣的數據啦。這樣不管換多少服務器都好處理。只不過這種方案需要在服務端開發時需要注意session的數據管理,而且需要接管session的生命周期。如果有一些老的系統可能session用的比較多,就不大好使了。而且將一些敏感數據寫入cookies還要考慮安全問題,這對于一些數據敏感的系統也可能是個問題。
但如果能控制好session的數據這種方案個人覺得還是挺不錯的,畢竟session并不適合存過多的數據。所以在我們的系統中是支持這種方案的,只需要打開開關參數就行。
session池化
還有一種方法就是把session共享出來,所有的服務器都連接到這個共享。這種方案可能是許多系統會使用的方案吧。因為將session池化,對于系統而言就變成透明了。程序員終于開心的將數據寫入session咯。這種方案除了http服務器外,許多的tcp服務器也是類似的方案。
我們系統因為使用的java開發,使用tomcat時可以將session共享到memcached/redis中。而且這種操作完全不需要改動系統,直接在tomcat中配置即可。所以這種方案天然就支持啦。
做一個可擴展的緩存策略設計
原先的數據緩存都是放在jvm里的,所以機器多了每臺服務器都要自己去加載緩存,這樣一來命中就低。最近打算在系統里引入第三方緩存,當時在memcached和火的要死的redis里選擇。現在來看每種內存產品都各有優勢,如果硬生生的將現在這些老的緩存直接改成redis的如果以后需要用別的內存數據庫又得大改代碼。想到這就決定把緩存做一次設計,將現有的jvm緩存保留下來,然后做成策略以擴展新的緩存存儲。
以前的許多緩存用的HashMap/ConcurrentHashMap,反正是鍵-對值。如果我們直接使用Map結構來作為緩存接口就可以不改變現有的一些代碼,只需要改動緩存類內部的數據結構即可。這樣的改動量就比較少。
比如原來的一些緩存單元結構:
public class RoleMenuCache implements IClearCache { private static Map<String, RoleMenu> roleMenuCache = new HashMap<String, RoleMenu>(); ...業務代碼省略 }
這里主要是替換這個HashMap所以改動就比較小。
先來看看類圖


Cachemanager
這個就是緩存的管理類,用于創建、釋放緩存對象。這個類是各個所有緩存申請的入口。下面貼出來主要的代碼:
public class CacheManager { private final static Logger logger = LoggerFactory.getLogger(CacheManager.class); private static Map<String, ICache> caches = new ConcurrentHashMap<>(); private static ICacheStrategy cacheStrategy = new DefaultCacheStategy(); private static String cacheStrategyClass; @SuppressWarnings("unchecked") public static synchronized <T extends ICache> T getOrCreateCache(String cacheName, Class<?> keyClass, Class<?> valueCalss) { T cache = (T) caches.get(cacheName); if (cache != null) { return cache; } cache = (T) cacheStrategy.createCache(cacheName, keyClass, valueCalss); caches.put(cacheName, cache); return cache; } @SuppressWarnings("rawtypes") public static synchronized void destroyCache(String cacheName) { ICache cache = caches.remove(cacheName); if (cache != null) { cache.clear(); } } }
ICache<K,V>
這個接口是規范緩存類的接口,所有的緩存類都要實現這個接口,而且它是繼承java.util.Map接口的,這樣就支持了Map派生的類,兼容老程序就好多了。
ICacheStrategy
對于具體的緩存實現就有一套策略,有一個ICacheStrategy接口來規范。這么一來,不管是jvm還是redis都可以自己單獨擴展來實現。
public interface ICacheStrategy { ICache createCache(String name, Class<?> keyClass, Class<?> valueCalss); void destroyCache(ICache cache); }
看一下DefaultCache的實現(代碼只放了一部分主要的):
public class DefaultCache<K, V> implements ICache<K, V> { protected Map<K, V> map; private String name; private long maxCacheSize; private long maxLifetime; private int cacheSize = 0; public DefaultCache(String name, long maxSize, long maxLifetime) { this.name = name; this.maxCacheSize = maxSize; this.maxLifetime = maxLifetime; map = new ConcurrentHashMap<K, V>(103); } @Override public V get(Object key) { return map.get(key); } @Override public V put(K key, V value) { return map.put(key, value); } @Override public V remove(Object key) { return map.remove(key); } @Override public void putAll(Map<? extends K, ? extends V> m) { map.putAll(m); } @Override public void clear() { if (map != null) { map.clear(); } } }
對于調用方來說其實就很簡單,只需要調用CacheManager即可,還是前面舉的RoleMenuCache
例子,我們改造一下:
public class RoleMenuCache implements IClearCache { private static Map<String, RoleMenu> roleMenuCache; static { roleMenuCache = CacheManager.getOrCreateCache("permissionCache", String.class, RoleMenu.class); } ...業務代碼省略 }
對于老代碼的改造還是比較小的,而且這樣的好處是以后想換成redis的也很簡單,對于業務代碼就不需要再修改了。
遇到Redis與泛型的問題
在擴展redis緩存策略的時候遇到一個問題,就是使用的jedis時,對于key值都是使用的string類型,這就給我們使用泛型設計留下了難題。當然為了兼容現在的設計,最后用了JSON來解決。
但是新的問題來了,對于put時是這樣的:
/** * 根據key設置map的值 */ @Override public V put(K key, V value) { jedisTemp.hset(name, JSON.toJSONString(key), JSON.toJSONString(value)); return value; }
這并沒啥問題,因為對象轉換成json串是正常的。問題是get的時候,我們使用的
alibaba.fastjson提供的接口并不能轉回成具體類型的對象,因為get方法的的返回值是V類型,是泛型類型,沒法得到class的type。
像這樣的代碼就不行啦:JSON.parseObject(json, V.class)。最后沒辦法,我只好把K和V的類型在創建時由調用者傳入。看下面的代碼里,兩個紅色的參數,當然這也沒問題,畢竟調用者是知道類型的:
CacheManager.getOrCreateCache("permissionCache", String.class, RoleMenu.class);
最終get方法的實現就是這樣:
@Override public V get(Object key) { String json = jedisTemp.hget(name, JSON.toJSONString(key)); return (V) JSON.parseObject(json, valueClass); }
問題雖然是解決了,只不過總覺得怪怪的。
總結與反思
整套的設計受openfire的集群設計影響比較大,我基本是借鑒過來的,目前來看還是挺不錯,最近準備嘗試Ignite,非常容易就接入了系統。
只是openfire使用的是java實現的方案(Hazelcast/Coherence
),這些都是帶Map結構的,并不會有我遇到的Redis的問題。但我覺得這套設計還挺不錯,如果把map接口去掉,自己重新定義方法就可以解決這個問題,不使用泛型,當然這樣對老代碼的改動會比較大。
還有一種情況就是多種緩存產品并存,比如同時使用redis和memcached,現有的設計可能支持不了。但是因為入口限制在了CacheManager,我想加個泛型支持就可以解決。只是這種場景或許并不多見吧。
注:此文章為原創,歡迎轉載,請在文章頁面明顯位置給出此文鏈接!
若您覺得這篇文章還不錯請點擊下右下角的推薦,非常感謝!
http://www.cnblogs.com/5207
文章列表
全站熱搜