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