問題描述
場景:咱們的應用系統是分佈式集羣的,可橫向擴展的。應用中某個接口操做知足如下一個或多個條件:
1. 接口運行復雜代價大,
2. 接口返回數據量大,
3. 接口的數據基本不會更改,
4. 接口數據一致性要求不高(只需知足最終一致)。java
此時,咱們會考慮將這個接口的返回值作緩存。考慮到上述條件,咱們須要一套高可用分佈式的緩存集羣,並具有持久化功能,備選的有ehcache集羣或redis主備(sentinel)。web
- ehcache集羣由於節點之間數據同步經過組播的方式,可能帶來的問題:節點間大量的數據複製帶來額外的開銷,在節點多的狀況下此問題愈加嚴重,N個節點會出現N-1次網絡傳輸數據進行同步。(見下圖,緩存集羣中有三臺機器,其中一臺機器接收到數據,須要拷貝到其餘機器,一次input後須要copy兩次,兩次copy是須要網絡傳輸消耗的)
- redis主備因爲做爲中心節點提供緩存,其餘節點都向redis中心節點取數據,因此,一次網絡傳輸便可。(固然此處的一次網絡代價跟組播的代價是不同的)可是,隨着訪問量增大,大量的緩存數據訪問使得應用服務器和緩存服務器之間的網絡I/O消耗越大。(見下圖,一樣三臺應用服務器,redis sentinel做爲中心節點緩存。所謂中心,即全部應用服務器以redis爲緩存中心,再也不像ehcache集羣,緩存是分散存放在應用服務器中,須要互相同步的,任何一臺應用服務器的input,都會通過一次copy網絡傳輸到redis,因爲redis是中心共享的,那麼就能夠不用同步的步驟,其餘應用服務器須要只需去get取便可。可是,咱們會發現多了N臺服務器的get的網絡開銷。)
提出方案
那麼要怎麼處理呢?因此兩級緩存的思想誕生了,在redis的方案上作一步優化,在緩存到遠程redis的同時,緩存一份到本地進程ehcache(此處的ehcache不用作集羣,避免組播帶來的開銷),取緩存的時候會先取本地,沒有會向redis請求,這樣會減小應用服務器<–>緩存服務器redis之間的網絡開銷。(見下圖,爲了減小get這幾條網絡傳輸,咱們會在每一個應用服務器上增長本地的ehcache緩存做爲二級緩存,即第一次get到的數據存入ehcache,後面output輸出便可從本地ehcache中獲取,不用再訪問redis了,因此就減小了之後get的網絡開銷。get開銷只要一次,後續不須要了,除非本地緩存過時須要再get。)
若是用過j2cache的都應該知道,oschina用j2cache這種兩級緩存,實踐證實了該方案是可行的。該篇使用spring+ehcache+redis實現更加簡潔。redis
方案實施
一、 spring和ehcache集成
主要獲取ehcache做爲操做ehcache的對象。spring
ehcache.xml 代碼以下:shell
<ehcache updateCheck="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.sf.net/ehcache.xsd"> <diskStore path="java.io.tmpdir/ehcache"/> <!-- 默認的管理策略 maxElementsOnDisk: 在磁盤上緩存的element的最大數目,默認值爲0,表示不限制。 eternal:設定緩存的elements是否永遠不過時。若是爲true,則緩存的數據始終有效,若是爲false那麼還要根據timeToIdleSeconds,timeToLiveSeconds判斷。 diskPersistent: 是否在磁盤上持久化。指重啓jvm後,數據是否有效。默認爲false。 diskExpiryThreadIntervalSeconds:對象檢測線程運行時間間隔。標識對象狀態(過時/持久化)的線程多長時間運行一次。 --> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="3600" overflowToDisk="true" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"/> <!-- 對象無過時,一個1000長度的隊列,最近最少使用的對象被刪除 --> <cache name="userCache" maxElementsInMemory="1000" eternal="true" overflowToDisk="false" timeToIdleSeconds="0" timeToLiveSeconds="0" memoryStoreEvictionPolicy="LFU"> </cache> <!-- 組播方式:multicastGroupPort須要保證與其餘系統不重複,進行端口註冊 --> <!-- 若因未註冊,配置了重複端口,形成權限緩存數據異常,請自行解決 --> <cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" properties="peerDiscovery=automatic, multicastGroupAddress=230.0.0.1, multicastGroupPort=4546, timeToLive=1"/> <!-- replicatePuts=true | false – 當一個新元素增長到緩存中的時候是否要複製到其餘的peers. 默認是true。 --> <!-- replicateUpdates=true | false – 當一個已經在緩存中存在的元素被覆蓋時是否要進行復制。默認是true。 --> <!-- replicateRemovals= true | false – 當元素移除的時候是否進行復制。默認是true。 --> <!-- replicateAsynchronously=true | false – 複製方式是異步的(指定爲true時)仍是同步的(指定爲false時)。默認是true。 --> <!-- replicatePutsViaCopy=true | false – 當一個新增元素被拷貝到其餘的cache中時是否進行復制指定爲true時爲複製,默認是true。 --> <!-- replicateUpdatesViaCopy=true | false – 當一個元素被拷貝到其餘的cache中時是否進行復制(指定爲true時爲複製),默認是true。 --> <cache name="webCache_LT" maxElementsInMemory="10000" eternal="false" overflowToDisk="false" timeToIdleSeconds="3600" timeToLiveSeconds="3600" memoryStoreEvictionPolicy="LRU"> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" properties="replicateRemovals=true"/> <bootstrapCacheLoaderFactory class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/> </cache> <cache name="webCache_ST" maxElementsInMemory="1000" eternal="false" overflowToDisk="false" timeToIdleSeconds="300" timeToLiveSeconds="300" memoryStoreEvictionPolicy="LRU"> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" properties="replicateRemovals=true"/> <bootstrapCacheLoaderFactory class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/> </cache> </ehcache>
spring.xml中注入ehcacheManager和ehCache對象,ehcacheManager是須要加載ehcache.xml配置信息,建立ehcache.xml中配置不一樣策略的cache。bootstrap
<!-- ehCache 配置管理器 --> <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache.xml" /> <!--true:單例,一個cacheManager對象共享;false:多個對象獨立 --> <property name="shared" value="true" /> <property name="cacheManagerName" value="ehcacheManager" /> </bean> <!-- ehCache 操做對象 --> <bean id="ehCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <property name="cacheName" value="ehCache"/> <property name="cacheManager" ref="ehcacheManager"/> </bean>
二、 spring和redis集成
主要獲取redisTemplate做爲操做redis的對象。緩存
redis.properties配置信息ruby
#host 寫入redis服務器地址 redis.ip=127.0.0.1 #Port redis.port=6379 #Passord #redis.password=123456 #鏈接超時30000 redis.timeout=30 #最大分配的對象數 redis.pool.maxActive=100 #最大可以保持idel狀態的對象數 redis.pool.maxIdle=30 #當池內沒有返回對象時,最大等待時間 redis.pool.maxWait=1000 #當調用borrow Object方法時,是否進行有效性檢查 redis.pool.testOnBorrow=true #當調用return Object方法時,是否進行有效性檢查 redis.pool.testOnReturn=true
spring注入jedisPool、redisConnFactory、redisTemplate對象bash
<!-- 加載redis.propertis --> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations" value="classpath:redis.properties"/> </bean> <!-- Redis 鏈接池 --> <bean id="jedisPool" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxTotal" value="${redis.pool.maxActive}" /> <property name="maxIdle" value="${redis.pool.maxIdle}" /> <property name="testOnBorrow" value="${redis.pool.testOnBorrow}" /> <property name="testOnReturn" value="${redis.pool.testOnReturn}" /> <property name="maxWaitMillis" value="${redis.pool.maxWait}" /> </bean> <!-- Redis 鏈接工廠 --> <bean id="redisConnFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="${redis.ip}" /> <property name="port" value="${redis.port}" /> <!-- property name="password" value="${redis.password}" --> <property name="timeout" value="${redis.timeout}" /> <property name="poolConfig" ref="jedisPool" /> </bean> <!-- redis 操做對象 --> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="redisConnFactory" /> </bean>
三、 spring集成ehcache和redis
經過上面兩步注入的ehcache和redisTemplate咱們就能自定義一個方法將二者整合起來。詳見EhRedisCache類。
EhRedisCache.java
/** * 兩級緩存,一級:ehcache,二級爲redisCache * @author yulin * */ public class EhRedisCache implements Cache{ private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class); private String name; private net.sf.ehcache.Cache ehCache; private RedisTemplate<String, Object> redisTemplate; private long liveTime = 1*60*60; //默認1h=1*60*60 @Override public String getName() { return this.name; } @Override public Object getNativeCache() { return this; } @Override public ValueWrapper get(Object key) { Element value = ehCache.get(key); LOG.info("Cache L1 (ehcache) :{}={}",key,value); if (value!=null) { return (value != null ? new SimpleValueWrapper(value.getObjectValue()) : null); } //TODO 這樣會不會更好?訪問10次EhCache 強制訪問一次redis 使得數據不失效 final String keyStr = key.toString(); Object objectValue = redisTemplate.execute(new RedisCallback<Object>() { public Object doInRedis(RedisConnection connection) throws DataAccessException { byte[] key = keyStr.getBytes(); byte[] value = connection.get(key); if (value == null) { return null; } //每次得到,重置緩存過時時間 if (liveTime > 0) { connection.expire(key, liveTime); } return toObject(value); } },true); ehCache.put(new Element(key, objectValue));//取出來以後緩存到本地 LOG.info("Cache L2 (redis) :{}={}",key,objectValue); return (objectValue != null ? new SimpleValueWrapper(objectValue) : null); } @Override public void put(Object key, Object value) { ehCache.put(new Element(key, value)); final String keyStr = key.toString(); final Object valueStr = value; redisTemplate.execute(new RedisCallback<Long>() { public Long doInRedis(RedisConnection connection) throws DataAccessException { byte[] keyb = keyStr.getBytes(); byte[] valueb = toByteArray(valueStr); connection.set(keyb, valueb); if (liveTime > 0) { connection.expire(keyb, liveTime); } return 1L; } },true); } @Override public void evict(Object key) { ehCache.remove(key); final String keyStr = key.toString(); redisTemplate.execute(new RedisCallback<Long>() { public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.del(keyStr.getBytes()); } },true); } @Override public void clear() { ehCache.removeAll(); redisTemplate.execute(new RedisCallback<String>() { public String doInRedis(RedisConnection connection) throws DataAccessException { connection.flushDb(); return "clear done."; } },true); } public net.sf.ehcache.Cache getEhCache() { return ehCache; } public void setEhCache(net.sf.ehcache.Cache ehCache) { this.ehCache = ehCache; } public RedisTemplate<String, Object> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } public long getLiveTime() { return liveTime; } public void setLiveTime(long liveTime) { this.liveTime = liveTime; } public void setName(String name) { this.name = name; } /** * 描述 : Object轉byte[]. <br> * @param obj * @return */ private byte[] toByteArray(Object obj) { byte[] bytes = null; ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(obj); oos.flush(); bytes = bos.toByteArray(); oos.close(); bos.close(); } catch (IOException ex) { ex.printStackTrace(); } return bytes; } /** * 描述 : byte[]轉Object . <br> * @param bytes * @return */ private Object toObject(byte[] bytes) { Object obj = null; try { ByteArrayInputStream bis = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bis); obj = ois.readObject(); ois.close(); bis.close(); } catch (IOException ex) { ex.printStackTrace(); } catch (ClassNotFoundException ex) { ex.printStackTrace(); } return obj; } }
spring注入自定義緩存
<!-- 自定義ehcache+redis--> <bean id="ehRedisCacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property name="caches"> <set> <bean id="ehRedisCache" class="org.musicmaster.yulin.ercache.EhRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="ehCache" ref="ehCache"/> <property name="name" value="userCache"/> <!-- <property name="liveTime" value="3600"/> --> </bean> </set> </property> </bean> <!-- 註解聲明 --> <cache:annotation-driven cache-manager="ehRedisCacheManager" proxy-target-class="true" />
四、 模擬問題中提到的接口
此處假設該接口知足上述條件。
UserService.java
public interface UserService { User findById(long id); List<User> findByPage(int startIndex, int limit); List<User> findBySex(Sex sex); List<User> findByAge(int lessAge); List<User> findByUsers(List<User> users); boolean update(User user); boolean deleteById(long id); }
UserServiceImpl.java
@Service public class UserServiceImpl implements UserService{ private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class); @Cacheable("userCache") @Override public User findById(long id) { LOG.info("visit business service findById,id:{}",id); User user = new User(); user.setId(id); user.setUserName("tony"); user.setPassWord("******"); user.setSex(Sex.M); user.setAge(32); //耗時操做 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return user; } @Override public List<User> findByPage(int startIndex, int limit) { return null; } @Cacheable("userCache") @Override public List<User> findBySex(Sex sex) { LOG.info("visit business service findBySex,sex:{}",sex); List<User> users = new ArrayList<User>(); for (int i = 0; i < 5; i++) { User user = new User(); user.setId(i); user.setUserName("tony"+i); user.setPassWord("******"); user.setSex(sex); user.setAge(32+i); users.add(user); } return users; } @Override public List<User> findByAge(int lessAge) { // TODO Auto-generated method stub return null; } //FIXME 此處將list參數的地址做爲key存儲,是否有問題? @Cacheable("userCache") @Override public List<User> findByUsers(List<User> users) { LOG.info("visit business service findByUsers,users:{}",users); return users; } @CacheEvict("userCache") @Override public boolean update(User user) { return true; } @CacheEvict("userCache") @Override public boolean deleteById(long id) { return false; } }
User.java
public class User implements Serializable { private static final long serialVersionUID = 1L; public enum Sex{ M,FM } private long id; private String userName; private String passWord; private int age; private Sex sex; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassWord() { return passWord; } public void setPassWord(String passWord) { this.passWord = passWord; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Sex getSex() { return sex; } public void setSex(Sex sex) { this.sex = sex; } @Override public String toString() { return "User [id=" + id + ", userName=" + userName + ", passWord=" + passWord + ", age=" + age + ", sex=" + sex + "]"; } }
實施結果
咱們寫個測試類來模擬下
TestEhRedisCache.java
public class TestEhRedisCache{ public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("spring-ehRedisCache.xml"); UserService userService= (UserService) context.getBean("userServiceImpl"); System.out.println(userService.findById(5l)); System.out.println(userService.findById(5l)); System.out.println(userService.findById(5l)); System.out.println(userService.findById(5l)); System.out.println(userService.findById(5l)); } }
TEST1 輸出結果:
Cache L1 (ehcache) :UserServiceImpl/findById/5=null Cache L2 (redis) :UserServiceImpl/findById/5=null visit business service findById,id:5 User [id=5, userName=tony, passWord=******, age=32, sex=M] Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M] User [id=5, userName=tony, passWord=******, age=32, sex=M] Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M] User [id=5, userName=tony, passWord=******, age=32, sex=M] Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M] User [id=5, userName=tony, passWord=******, age=32, sex=M] Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M] User [id=5, userName=tony, passWord=******, age=32, sex=M]
上面第一次訪問,一級緩存ehcache和二級緩存redis都沒有數據,訪問接口耗時操做,打印日誌:
visit business service findById,id:5
第二次以後的訪問,都會訪問一級緩存ehcache,此時響應速度很快。
TEST2 在TEST1結束後,咱們在liveTime的時間內,也就是redis緩存還未過時再次執行,會出現如下結果
Cache L1 (ehcache) :UserServiceImpl/findById/5=null Cache L2 (redis) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M] User [id=5, userName=tony, passWord=******, age=32, sex=M] Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M] User [id=5, userName=tony, passWord=******, age=32, sex=M] Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M] User [id=5, userName=tony, passWord=******, age=32, sex=M] Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M] User [id=5, userName=tony, passWord=******, age=32, sex=M] Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M] User [id=5, userName=tony, passWord=******, age=32, sex=M]
因爲TEST1執行完結束後,ehcache爲進程間的緩存,天然隨着運行結束而釋放,因此TEST2出現:
Cache L1 (ehcache) :UserServiceImpl/findById/5=null
然而在第二次訪問二級緩存redis,還未到緩存過時時間,因此在redis中找到數據(同時數據入一級緩存ehcache):
Cache L2 (redis) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=**, age=32, sex=M]
此處不會visit….沒有通過接口的耗時操做,接下來數據均可以在本地緩存ehcache中獲取。