hazelcast_Hazelcast的MapLoader陷阱
hazelcast
Hazelcast提供的核心數(shù)據(jù)結(jié)構(gòu)之一是IMap<K, V> ,它擴展了java.util.concurrent.ConcurrentMap ,它基本上是一個分布式地圖,通常用作緩存。 您可以將此類地圖配置為使用自定義MapLoader<K, V> -每次嘗試從該地圖(通過鍵)獲取.get()尚不存在的內(nèi)容時都會被詢問的一段Java代碼。 當您將IMap用作內(nèi)存中的分布式緩存時,這特別有用–如果客戶端代碼要求尚未緩存的內(nèi)容,Hazelcast將透明地執(zhí)行MapLoader.load(key) :
其余兩種方法在啟動期間用于通過加載預(yù)定義的鍵集來預(yù)熱緩存。 您的自定義MapLoader可以連接到(否)SQL數(shù)據(jù)庫,Web服務(wù),文件系統(tǒng)(您命名)。 使用這樣的緩存更加方便,因為您不必執(zhí)行繁瑣的“ 如果不在緩存負載中并放入緩存 ”循環(huán)。 此外, MapLoader具有一項出色的功能-如果許多客戶端在同一時間(從不同的線程,甚至從不同的集群成員,從而從機器)要求相同的密鑰,則MapLoader僅執(zhí)行一次。 這顯著減少了外部依賴項的負擔(dān),而沒有引入任何復(fù)雜性。
從本質(zhì)上說IMap與MapLoader類似于LoadingCache中發(fā)現(xiàn)的番石榴 -但分布。 但是,強大的功能會帶來極大的挫敗感,尤其是當您不了解API的特殊性和分布式系統(tǒng)固有的復(fù)雜性時。
首先,讓我們看看如何配置自定義MapLoader 。 您可以hazelcast.xml使用hazelcast.xml ( <map-store/>元素),但是您無法控制加載程序的生命周期(例如,不能使用Spring bean)。 一個更好的主意是直接從代碼配置Hazelcast并傳遞MapLoader的實例:
class HazelcastTest extends Specification {public static final int ANY_KEY = 42public static final String ANY_VALUE = "Forty two"def 'should use custom loader'() {given:MapLoader loaderMock = Mock()loaderMock.load(ANY_KEY) >> ANY_VALUEdef hz = build(loaderMock)IMap<Integer, String> emptyCache = hz.getMap("cache")when:def value = emptyCache.get(ANY_KEY)then:value == ANY_VALUEcleanup:hz?.shutdown()}請注意,我們?nèi)绾潍@得一個空的地圖,但是當要求輸入ANY_KEY ,我們得到ANY_VALUE作為回報。 這不足為奇,這正是我們的loaderMock所期望的。 我離開了Hazelcast配置:
def HazelcastInstance build(MapLoader<Integer, String> loader) {final Config config = new Config("Cluster")final MapConfig mapConfig = config.getMapConfig("default")final MapStoreConfig mapStoreConfig = new MapStoreConfig()mapStoreConfig.factoryImplementation = {name, props -> loader } as MapStoreFactorymapConfig.mapStoreConfig = mapStoreConfigreturn Hazelcast.getOrCreateHazelcastInstance(config) }任何IMap (按名稱標識)都可以具有不同的配置。 但是,特殊的"default"映射為所有映射指定默認配置。 讓我們玩一下自定義加載器,看看當MapLoader返回null或引發(fā)異常時它們的行為:
def 'should return null when custom loader returns it'() {given:MapLoader loaderMock = Mock()def hz = build(loaderMock)IMap<Integer, String> cache = hz.getMap("cache")when:def value = cache.get(ANY_KEY)then:value == null!cache.containsKey(ANY_KEY)cleanup:hz?.shutdown() }public static final String SOME_ERR_MSG = "Don't panic!"def 'should propagate exceptions from loader'() {given:MapLoader loaderMock = Mock()loaderMock.load(ANY_KEY) >> {throw new UnsupportedOperationException(SOME_ERR_MSG)}def hz = build(loaderMock)IMap<Integer, String> cache = hz.getMap("cache")when:cache.get(ANY_KEY)then:UnsupportedOperationException e = thrown()e.message.contains(SOME_ERR_MSG)cleanup:hz?.shutdown() }到目前為止,不足為奇。 您可能遇到的第一個陷阱是線程在這里如何交互。 永遠不會從客戶端線程執(zhí)行MapLoader ,而總是從單獨的線程池執(zhí)行:
def 'loader works in a different thread'() {given:MapLoader loader = Mock()loader.load(ANY_KEY) >> {key -> "$key: ${Thread.currentThread().name}"}def hz = build(loader)IMap<Integer, String> cache = hz.getMap("cache")when:def value = cache.get(ANY_KEY)then:value != "$ANY_KEY: ${Thread.currentThread().name}"cleanup:hz?.shutdown() }該測試通過是因為當前線程是"main"線程,而加載是從"hz.Cluster.partition-operation.thread-10" 。 這是一個重要的觀察結(jié)果,如果您記得當許多線程嘗試訪問相同的缺席密鑰時,加載程序僅被調(diào)用一次,則這實際上很明顯。 但是,這里需要進一步說明。 IMap上的幾乎每個操作都封裝到一個操作對象中 (另請參見: 命令模式 )。 此操作隨后分派給一個或所有群集成員,并在單獨的線程池中甚至在另一臺計算機上遠程執(zhí)行。 因此,不要期望加載發(fā)生在同一線程,甚至同一JVM /服務(wù)器(!)中。
這會導(dǎo)致一種有趣的情況,您在一臺計算機上請求給定密鑰,而另一臺計算機上實際加載。 甚至更史詩般的–機器A,B和C請求給定密鑰,而機器D實際加載該密鑰的值。 基于一致的哈希算法確定哪個機器負責(zé)加載。
最后一句話–當然,您可以自定義運行這些操作的線程池的大小,請參閱“ 高級配置屬性” 。
考慮到這一點,這是完全令人驚訝的,絕對可以預(yù)期的:
def 'IMap.remove() on non-existing key still calls loader (!)'() {given:MapLoader loaderMock = Mock()def hz = build(loaderMock)IMap<Integer, String> emptyCache = hz.getMap("cache")when:emptyCache.remove(ANY_KEY)then:1 * loaderMock.load(ANY_KEY)cleanup:hz?.shutdown() }仔細地看! 我們要做的就是從地圖上刪除缺少的密鑰。 沒有其他的。 但是, loaderMock.load()已執(zhí)行。 這是一個問題,特別是當您的自定義加載程序特別慢或昂貴時。 為什么在這里執(zhí)行? 查找`java.util.Map#remove()的API:
V remove(Object key)
[…]返回此映射先前與該鍵關(guān)聯(lián)的值;如果該映射不包含該鍵的映射,則返回null。
也許這是有爭議的,但有人可能會認為Hazelcast做得正確。 如果您認為附有MapLoader的地圖就像外部存儲的視圖一樣,那是有道理的。 當刪除缺少的密鑰時,Hazelcast實際上會詢問我們的MapLoader :以前的值是什么? 它假裝地圖好像包含從MapLoader返回的每個單個值,但延遲加載。 這不是錯誤,因為有一個特殊的方法IMap.delete()就像remove()一樣工作,但是不會加載“以前的”值:
@Issue("https://github.com/hazelcast/hazelcast/issues/3178") def "IMap.delete() doesn't call loader"() {given:MapLoader loaderMock = Mock()def hz = build(loaderMock)IMap<Integer, String> cache = hz.getMap("cache")when:cache.delete(ANY_KEY)then:0 * loaderMock.load(ANY_KEY)cleanup:hz?.shutdown() }實際上,存在一個錯誤: IMap.delete()不應(yīng)調(diào)用 3.2.6和3.3中修復(fù)的MapLoader.load() 。 如果尚未升級,則即使IMap.delete()也將轉(zhuǎn)到MapLoader 。 如果您認為IMap.remove()令人驚訝,請查看put()工作原理!
如果您認為remove()加載值是可疑的,那么顯式put()首先為給定鍵加載值怎么辦? 畢竟,我們明確地通過密鑰將某些內(nèi)容放入地圖中,為什么Hazelcast首先通過MapLoader加載此值?
def 'IMap.put() on non-existing key still calls loader (!)'() {given:MapLoader loaderMock = Mock()def hz = build(loaderMock)IMap<Integer, String> emptyCache = hz.getMap("cache")when:emptyCache.put(ANY_KEY, ANY_VALUE)then:1 * loaderMock.load(ANY_KEY)cleanup:hz?.shutdown() }再次,讓我們還原到j(luò)ava.util.Map.put() JavaDoc:
V put(K鍵,V值)
[…]返回值:
與key關(guān)聯(lián)的先前值;如果沒有key映射,則為null。
Hazelcast假設(shè)IMap只是對某些外部源的懶惰視圖,因此當我們put()某些內(nèi)容放到以前沒有的IMap中時,它會首先加載“ previous”值,以便它可以返回它。 同樣,當MapLoader速度慢或價格昂貴時,這又是一個大問題–如果我們可以明確地將某些內(nèi)容放入地圖中,為什么要先加載它? 幸運的是,有一個簡單的解決方法putTransient() :
def "IMap.putTransient() doesn't call loader"() {given:MapLoader loaderMock = Mock()def hz = build(loaderMock)IMap<Integer, String> cache = hz.getMap("cache")when:cache.putTransient(ANY_KEY, ANY_VALUE, 1, TimeUnit.HOURS)then:0 * loaderMock.load(ANY_KEY)cleanup:hz?.shutdown() }一個警告是,您必須顯式提供TTL,而不是依賴于已配置的IMap默認值。 但這也意味著您可以為每個映射條目分配任意TTL,不僅可以全局分配給整個映射-很有用。
記住我們的比喻: IMap與后盾MapLoader行為就像在數(shù)據(jù)的外部源視圖。 這就是為什么在空地圖上的containsKey()會調(diào)用MapLoader并不令人驚訝的原因:
def "IMap.containsKey() calls loader"() {given:MapLoader loaderMock = Mock()def hz = build(loaderMock)IMap<Integer, String> emptyMap = hz.getMap("cache")when:emptyMap.containsKey(ANY_KEY)then:1 * loaderMock.load(ANY_KEY)cleanup:hz?.shutdown() }每當我們請求地圖中不存在的鍵時,Hazelcast都會詢問MapLoader 。 同樣,只要裝載機速度快,無副作用且可靠,這不是問題。 如果不是這種情況,將會殺死您:
def "IMap.get() after IMap.containsKey() calls loader twice"() {given:MapLoader loaderMock = Mock()def hz = build(loaderMock)IMap<Integer, String> cache = hz.getMap("cache")when:cache.containsKey(ANY_KEY)cache.get(ANY_KEY)then:2 * loaderMock.load(ANY_KEY)cleanup:hz?.shutdown() }盡管containsKey()調(diào)用MapLoader ,它不會“緩存”加載的值以供以后使用。 這就是為什么containsKey()后跟get()兩次調(diào)用MapLoader ,這非常浪費。 幸運的是,如果您在現(xiàn)有密鑰上調(diào)用containsKey() ,則它幾乎立即運行,盡管很可能需要網(wǎng)絡(luò)跳轉(zhuǎn)。 不幸的是,Hazelcast 3.3版之前的keySet() , values() , entrySet()和其他一些方法的行為。 如果一次加載任何密鑰,這些將全部阻止。 因此,如果您有一個包含數(shù)千個鍵的映射,并且要求提供keySet() ,則緩慢的MapLoader.load()調(diào)用將阻塞整個群集。 幸運的是,此問題已在3.3中修復(fù),因此即使當前正在計算某些鍵,也不會阻塞IMap.keySet() , IMap.values()等。
如您所見, IMap + MapLoader組合功能強大,但也充滿陷阱。 其中一些由API規(guī)定,osme由Hazelcast的分布式特性決定,最后一些是特定于實現(xiàn)的。 在實施加載緩存功能之前,請確保您了解它們。
翻譯自: https://www.javacodegeeks.com/2014/09/hazelcasts-maploader-pitfalls.html
hazelcast
總結(jié)
以上是生活随笔為你收集整理的hazelcast_Hazelcast的MapLoader陷阱的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 海南房屋备案网查询(海南房屋备案网)
- 下一篇: Java 14:记录