先從web session的共用說起 許多系統需要提供7*24小時服務,這類系統肯定需要考慮災備問題,單台伺服器如果宕機可能無法立馬恢復使用,這必定影響到服務。這個問題對於系統規模來說,從小到大可能面臨的難度會相差很大。但對於原理來說其實就是需要準備備份系統隨時可以替代正在服務的系統,也就是無論何時 ...
先從web session的共用說起
許多系統需要提供7*24小時服務,這類系統肯定需要考慮災備問題,單台伺服器如果宕機可能無法立馬恢復使用,這必定影響到服務。這個問題對於系統規模來說,從小到大可能面臨的難度會相差很大。但對於原理來說其實就是需要準備備份系統隨時可以替代正在服務的系統,也就是無論何時都有伺服器可以提供服務。也就是災備系統或者負載均衡。 提供災備系統或者負載均衡系統都需要面臨一個問題,那就是如何解決共用數據的問題。對於web伺服器而言首先要解決的就是web session共用問題,比如A伺服器的session如何可以在B伺服器上也能一樣使用呢?畢竟是物理隔離的兩台伺服器。 這方面的方案主要是兩類:cookies和session共用。cookies
這種方案的思路就是將session的數據寫入到cookies里,每次請求的時候就可以帶上信息,這樣不管是哪台伺服器都能得到同樣的數據啦。這樣不管換多少伺服器都好處理。只不過這種方案需要在服務端開發時需要註意session的數據管理,而且需要接管session的生命周期。如果有一些老的系統可能session用的比較多,就不大好使了。而且將一些敏感數據寫入session還要考慮安全問題,這對於一些數據敏感的系統也可能是個問題。 但如果能控制好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