https://www.cnblogs.com/wchxj/p/8159609.html 問題描述 場景:我們的應用系統是分散式集群的,可橫向擴展的。應用中某個介面操作滿足以下一個或多個條件: 1. 介面運行複雜代價大, 2. 介面返回數據量大, 3. 介面的數據基本不會更改, 4. 介面數據一致性 ...
https://www.cnblogs.com/wchxj/p/8159609.html
問題描述
場景:我們的應用系統是分散式集群的,可橫向擴展的。應用中某個介面操作滿足以下一個或多個條件:
1. 介面運行複雜代價大,
2. 介面返回數據量大,
3. 介面的數據基本不會更改,
4. 介面數據一致性要求不高(只需滿足最終一致)。
此時,我們會考慮將這個介面的返回值做緩存。考慮到上述條件,我們需要一套高可用分散式的緩存集群,並具備持久化功能,備選的有ehcache集群或redis主備(sentinel)。
- 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實現更加簡潔。
方案實施
1、 spring和ehcache集成
主要獲取ehcache作為操作ehcache的對象。
ehcache.xml 代碼如下:
<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。
<!-- 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>
2、 spring和redis集成
主要獲取redisTemplate作為操作redis的對象。
redis.properties配置信息
#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對象
<!-- 載入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>
3、 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" />
4、 模擬問題中提到的介面
此處假設該介面滿足上述條件。
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 + "]";