Spring Boot 2.x基礎教程:使用EhCache快取叢集

程式猿DD發表於2020-07-17

上一篇我們介紹了在Spring Boot中整合EhCache的方法。既然用了ehcache,我們自然要說說它的一些高階功能,不然我們用預設的ConcurrentHashMap就好了。本篇不具體介紹EhCache快取如何落檔案、如何配置各種過期引數等常規細節配置,這部分內容留給讀者自己學習,如果您不知道如何搞,可以看看這裡的官方文件

那麼我們今天具體講什麼呢?先思考一個場景,當我們使用了EhCache,在快取過期之前可以有效的減少對資料庫的訪問,但是通常我們將應用部署在生產環境的時候,為了實現應用的高可用(有一臺機器掛了,應用還需要可用),肯定是會部署多個不同的程式去執行的,那麼這種情況下,當有資料更新的時候,每個程式中的快取都是獨立維護的,如果這些程式快取同步機制,那麼就存在因快取沒有更新,而一直都用已經失效的快取返回給使用者,這樣的邏輯顯然是會有問題的。所以,本文就來說說當使用EhCache的時候,如果來組建程式內快取EnCache的叢集以及配置配置他們的同步策略。

由於下面是組建叢集的過程,務必採用多機的方式除錯,避免不必要的錯誤發生。

動手試試

本篇的實現將基於上一篇的基礎工程來進行。先來回顧下上一篇中的程式要素:

User實體的定義

@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實體的資料訪問實現(涵蓋了快取註解)

@CacheConfig(cacheNames = "users")
public interface UserRepository extends JpaRepository<User, Long> {

    @Cacheable
    User findByName(String name);

}

下面開始改造這個專案:

第一步:為需要同步的快取物件實現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;
    }
}
注意:如果沒有做這一步,後續快取叢集通過過程中,因為要傳輸User物件,會導致序列化與反序列化相關的異常

第二步:重新組織ehcache的配置檔案。我們嘗試手工組建叢集的方式,不同例項在網路相關配置上會產生不同的配置資訊,所以我們建立不同的配置檔案給不同的例項使用。比如下面這樣:

例項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>

例項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>

配置說明:

  • cache標籤中定義名為users的快取,這裡我們增加了一個子標籤定義cacheEventListenerFactory,這個標籤主要用來定義快取事件監聽的處理策略,它有以下這些引數用來設定快取的同步策略:

    • replicatePuts:當一個新元素增加到快取中的時候是否要複製到其他的peers。預設是true。
    • replicateUpdates:當一個已經在快取中存在的元素被覆蓋時是否要進行復制。預設是true。
    • replicateRemovals:當元素移除的時候是否進行復制。預設是true。
    • replicateAsynchronously:複製方式是非同步的指定為true時,還是同步的,指定為false時。預設是true。
    • replicatePutsViaCopy:當一個新增元素被拷貝到其他的cache中時是否進行復制指定為true時為複製,預設是true。
    • replicateUpdatesViaCopy:當一個元素被拷貝到其他的cache中時是否進行復制指定為true時為複製,預設是true。
  • 新增了一個cacheManagerPeerProviderFactory標籤的配置,用來指定組建的叢集資訊和要同步的快取資訊,其中:

    • hostName:是當前例項的主機名
    • port:當前例項用來同步快取的埠號
    • socketTimeoutMillis:同步快取的Socket超時時間
    • peerDiscovery:叢集節點的發現模式,有手工與自動兩種,這裡採用了手工指定的方式
    • rmiUrls:當peerDiscovery設定為manual的時候,用來指定需要同步的快取節點,如果存在多個用|連線

第三步:打包部署與啟動。打包沒啥大問題,主要快取配置內容存在一定差異,所以在指定節點的模式下,需要單獨拿出來,然後使用啟動引數來控制讀取不同的配置檔案。比如這樣:

-Dspring.cache.ehcache.config=classpath:ehcache-1.xml
-Dspring.cache.ehcache.config=classpath:ehcache-2.xml

第四步:實現幾個介面用來驗證快取的同步效果

@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;
    }

}

驗證邏輯:

  1. 啟動通過第三步說的命令引數,啟動兩個例項
  2. 呼叫例項1的/create介面,建立一條資料
  3. 呼叫例項1的/find介面,例項1快取User,同時同步快取資訊給例項2,在例項1中會存在SQL查詢語句
  4. 呼叫例項2的/find介面,由於快取叢集同步了User的資訊,所以在例項2中的這次查詢也不會出現SQL語句

進一步思考

上一篇釋出的時候,公眾號上有網友留言問,資料更新之後怎麼辦?

其實當構建了快取叢集之後,就比較好辦了。比如這裡的例子,需要做兩件事:

  1. save操作增加@CachePut註解,讓更新操作完成之後將結果再put到快取中
  2. 保證快取事件監聽的replicateUpdates=true,這樣資料在更新之後可以保證複製到其他節點

這樣就可以防止快取的髒資料了,但是這種方法還並不是很好,因為快取叢集的同步依然需要時間,會存在短暫的不一致。同時程式內的快取要在每個例項上都佔用,如果大量儲存的話始終不那麼經濟。所以,很多時候程式內快取不會作為主要的快取手段。下一篇將具體說說,另一個更重要的快取使用!

歡迎關注本系列教程:《Spring Boot 2.x基礎教程》

參考資料

本文首發:Spring Boot 2.x基礎教程:使用EhCache快取叢集,轉載請註明出處。
歡迎關注我的公眾號:程式猿DD,獲得獨家整理的學習資源和日常乾貨推送。點選直達本系列教程目錄

相關文章