先思考一個(gè)場(chǎng)景,當(dāng)我們使用了EhCache,在緩存過(guò)期之前可以有效的減少對(duì)數(shù)據(jù)庫(kù)的訪問(wèn),但是通常我們將應(yīng)用部署在生產(chǎn)環(huán)境的時(shí)候,為了實(shí)現(xiàn)應(yīng)用的高可用(有一臺(tái)機(jī)器掛了,應(yīng)用還需要可用),肯定是會(huì)部署多個(gè)不同的進(jìn)程去運(yùn)行的,那么這種情況下,當(dāng)有數(shù)據(jù)更新的時(shí)候,每個(gè)進(jìn)程中的緩存都是獨(dú)立維護(hù)的,如果這些進(jìn)程緩存同步機(jī)制,那么就存在因緩存沒(méi)有更新,而一直都用已經(jīng)失效的緩存返回給用戶,這樣的邏輯顯然是會(huì)有問(wèn)題的。所以,本文就來(lái)說(shuō)說(shuō)當(dāng)使用EhCache的時(shí)候,如果來(lái)組建進(jìn)程內(nèi)緩存EnCache的集群以及配置配置他們的同步策略。
由于下面是組建集群的過(guò)程,務(wù)必采用多機(jī)的方式調(diào)試,避免不必要的錯(cuò)誤發(fā)生。
#動(dòng)手試試
本篇的實(shí)現(xiàn)將基于上一篇open in new window的基礎(chǔ)工程來(lái)進(jìn)行。先來(lái)回顧下上一篇中的程序要素:
User實(shí)體的定義
@Entity
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
}
User實(shí)體的數(shù)據(jù)訪問(wèn)實(shí)現(xiàn)(涵蓋了緩存注解)
@CacheConfig(cacheNames = "users")
public interface UserRepository extends JpaRepository<User, Long> {
@Cacheable
User findByName(String name);
}
下面開(kāi)始改造這個(gè)項(xiàng)目:
第一步:為需要同步的緩存對(duì)象實(shí)現(xiàn)Serializable
接口
@Entity
@Data
@NoArgsConstructor
public class User implements Serializable {
@Id
@GeneratedValue
private Long id;
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
}
注意:如果沒(méi)有做這一步,后續(xù)緩存集群通過(guò)過(guò)程中,因?yàn)橐獋鬏擴(kuò)ser對(duì)象,會(huì)導(dǎo)致序列化與反序列化相關(guān)的異常
第二步:重新組織ehcache的配置文件。我們嘗試手工組建集群的方式,不同實(shí)例在網(wǎng)絡(luò)相關(guān)配置上會(huì)產(chǎn)生不同的配置信息,所以我們建立不同的配置文件給不同的實(shí)例使用。比如下面這樣:
實(shí)例1,使用ehcache-1.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<cache name="users"
maxEntriesLocalHeap="200"
timeToLiveSeconds="600">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
properties="replicateAsynchronously=true,
replicatePuts=true,
replicateUpdates=true,
replicateUpdatesViaCopy=false,
replicateRemovals=true "/>
</cache>
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="hostName=10.10.0.100,
port=40001,
socketTimeoutMillis=2000,
peerDiscovery=manual,
rmiUrls=//10.10.0.101:40001/users" />
</ehcache>
實(shí)例2,使用ehcache-2.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<cache name="users"
maxEntriesLocalHeap="200"
timeToLiveSeconds="600">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
properties="replicateAsynchronously=true,
replicatePuts=true,
replicateUpdates=true,
replicateUpdatesViaCopy=false,
replicateRemovals=true "/>
</cache>
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="hostName=10.10.0.101,
port=40001,
socketTimeoutMillis=2000,
peerDiscovery=manual,
rmiUrls=//10.10.0.100:40001/users" />
</ehcache>
配置說(shuō)明:
cache
標(biāo)簽中定義名為users的緩存,這里我們?cè)黾恿艘粋€(gè)子標(biāo)簽定義cacheEventListenerFactory
,這個(gè)標(biāo)簽主要用來(lái)定義緩存事件監(jiān)聽(tīng)的處理策略,它有以下這些參數(shù)用來(lái)設(shè)置緩存的同步策略:- 新增了一個(gè)
cacheManagerPeerProviderFactory
標(biāo)簽的配置,用來(lái)指定組建的集群信息和要同步的緩存信息,其中:
第三步:打包部署與啟動(dòng)。打包沒(méi)啥大問(wèn)題,主要緩存配置內(nèi)容存在一定差異,所以在指定節(jié)點(diǎn)的模式下,需要單獨(dú)拿出來(lái),然后使用啟動(dòng)參數(shù)來(lái)控制讀取不同的配置文件。比如這樣:
-Dspring.cache.ehcache.config=classpath:ehcache-1.xml
-Dspring.cache.ehcache.config=classpath:ehcache-2.xml
第四步:實(shí)現(xiàn)幾個(gè)接口用來(lái)驗(yàn)證緩存的同步效果
@RestController
static class HelloController {
@Autowired
private UserRepository userRepository;
@GetMapping("/create")
public void create() {
userRepository.save(new User("AAA", 10));
}
@GetMapping("/find")
public User find() {
User u1 = userRepository.findByName("AAA");
System.out.println("查詢AAA用戶:" + u1.getAge());
return u1;
}
}
驗(yàn)證邏輯:
- 啟動(dòng)通過(guò)第三步說(shuō)的命令參數(shù),啟動(dòng)兩個(gè)實(shí)例
- 調(diào)用實(shí)例1的
/create
接口,創(chuàng)建一條數(shù)據(jù) - 調(diào)用實(shí)例1的
/find
接口,實(shí)例1緩存User,同時(shí)同步緩存信息給實(shí)例2,在實(shí)例1中會(huì)存在SQL查詢語(yǔ)句 - 調(diào)用實(shí)例2的
/find
接口,由于緩存集群同步了User的信息,所以在實(shí)例2中的這次查詢也不會(huì)出現(xiàn)SQL語(yǔ)句
#進(jìn)一步思考
上一篇open in new window發(fā)布的時(shí)候,公眾號(hào)上有網(wǎng)友留言問(wèn),數(shù)據(jù)更新之后怎么辦?
其實(shí)當(dāng)構(gòu)建了緩存集群之后,就比較好辦了。比如這里的例子,需要做兩件事:
save
操作增加@CachePut
注解,讓更新操作完成之后將結(jié)果再put到緩存中- 保證緩存事件監(jiān)聽(tīng)的replicateUpdates=true,這樣數(shù)據(jù)在更新之后可以保證復(fù)制到其他節(jié)點(diǎn)
這樣就可以防止緩存的臟數(shù)據(jù)了,但是這種方法還并不是很好,因?yàn)榫彺婕旱耐揭廊恍枰獣r(shí)間,會(huì)存在短暫的不一致。同時(shí)進(jìn)程內(nèi)的緩存要在每個(gè)實(shí)例上都占用,如果大量存儲(chǔ)的話始終不那么經(jīng)濟(jì)。所以,很多時(shí)候進(jìn)程內(nèi)緩存不會(huì)作為主要的緩存手段。下一篇將具體說(shuō)說(shuō),另一個(gè)更重要的緩存使用!
歡迎關(guān)注本系列教程:《Spring Boot 2.x基礎(chǔ)教程》
#參考資料
- EhCache 分布式緩存/緩存集群open in new window
- Java RMI:rmi Connection refused to host: 127.0.0.1異常解決open in new window
#代碼示例
本文的相關(guān)例子可以查看下面?zhèn)}庫(kù)中的chapter5-3
目錄:
- Github:https://github.com/dyc87112/SpringBoot-Learning/open in new window
- Gitee:https://gitee.com/didispace/SpringBoot-Learning/