Одной из основных структур данных, предоставляемых Hazelcast, является IMap<K, V>
расширяющий java.util.concurrent.ConcurrentMap
— который в основном является распределенной картой, часто используемой в качестве кэша. Вы можете настроить такую карту для использования пользовательского MapLoader<K, V>
— фрагмента кода Java, который будет запрашиваться каждый раз, когда вы пытаетесь .get()
что-то из этой карты (по ключу), которого еще нет. Это особенно полезно, когда вы используете IMap
в качестве распределенного кэша в памяти — если клиентский код запрашивает что-то, что еще не было кэшировано, Hazelcast прозрачно выполнит ваш MapLoader.load(key)
:
1
2
3
4
5
|
public interface MapLoader<K, V> { V load(K key); Map<K, V> loadAll(Collection<K> keys); Set<K> loadAllKeys(); } |
Оставшиеся два метода используются во время запуска для необязательного прогрева кэша путем загрузки предварительно определенного набора ключей. Ваш пользовательский MapLoader
может связаться с (Нет) базой данных SQL, веб-службой, файловой системой, вы называете это. Работать с таким кешем гораздо удобнее, потому что вам не нужно реализовывать утомительный цикл « если не в кеш загружать и помещать в кеш ». Более того, MapLoader
обладает фантастической функцией — если многие клиенты одновременно запрашивают один и тот же ключ (из разных потоков или даже из разных членов кластера — то есть с компьютеров), MapLoader
выполняется только один раз. Это значительно снижает нагрузку на внешние зависимости, не внося никакой сложности.
По сути IMap
с MapLoader
похож на LoadingCache
найденный в Guava — но распространяется. Однако с большой силой приходит большое разочарование, особенно когда вы не понимаете особенностей API и внутренней сложности распределенной системы.
Сначала давайте посмотрим, как настроить пользовательский MapLoader
. Для этого вы можете использовать hazelcast.xml
(элемент <map-store/>
), но тогда у вас нет контроля над жизненным циклом вашего загрузчика (например, вы не можете использовать bean-компонент Spring). Лучшая идея — настроить Hazelcast непосредственно из кода и передать экземпляр MapLoader
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
class HazelcastTest extends Specification { public static final int ANY_KEY = 42 public static final String ANY_VALUE = "Forty two" def 'should use custom loader' () { given: MapLoader loaderMock = Mock() loaderMock.load(ANY_KEY) >> ANY_VALUE def hz = build(loaderMock) IMap<Integer, String> emptyCache = hz.getMap( "cache" ) when: def value = emptyCache. get (ANY_KEY) then: value == ANY_VALUE cleanup: hz?.shutdown() } |
Обратите внимание, как мы получаем пустую карту, но когда мы спрашиваем ANY_KEY
, мы получаем ANY_VALUE
. Это не удивительно, это то, что ожидалось от нашего loaderMock
. Я оставил конфигурацию Hazelcast:
1
2
3
4
5
6
7
8
|
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 MapStoreFactory mapConfig.mapStoreConfig = mapStoreConfig return Hazelcast.getOrCreateHazelcastInstance(config) } |
Любой IMap
(идентифицируемый по имени) может иметь другую конфигурацию. Однако специальная карта "default"
определяет конфигурацию по умолчанию для всех карт. Давайте немного поиграем с пользовательскими загрузчиками и посмотрим, как они ведут себя, когда MapLoader
возвращает null
или выдает исключение:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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() } |
MapLoader
выполняется в отдельном потоке
Пока ничего удивительного. Первая ловушка, с которой вы можете столкнуться — это взаимодействие потоков. MapLoader
никогда не выполняется из клиентского потока, всегда из отдельного пула потоков:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
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"
. Это важное наблюдение, и на самом деле оно совершенно очевидно, если вспомнить, что когда многие потоки пытаются получить доступ к одному и тому же отсутствующему ключу, загрузчик вызывается только один раз. Но больше нужно объяснить здесь. Почти каждая операция в IMap
инкапсулируется в один из объектов операции (см. Также: Шаблон команды ). Позднее эта операция отправляется одному или всем членам кластера и выполняется удаленно в отдельном пуле потоков или даже на другом компьютере. Таким образом, не ожидайте, что загрузка произойдет в том же потоке или даже в той же JVM / сервере (!)
Это приводит к интересной ситуации, когда вы запрашиваете заданный ключ на одном компьютере, но фактическая загрузка происходит на другом. Или даже более эпично — машины A, B и C запрашивают заданный ключ, тогда как машина D физически загружает значение для этого ключа. Решение о том, какая машина отвечает за загрузку, принимается на основе согласованного алгоритма хеширования .
Последнее замечание — конечно, вы можете настроить размер пулов потоков, выполняющих эти операции, см. « Дополнительные параметры конфигурации» .
IMap.remove()
вызывает MapLoader
Это совершенно удивительно, и определенно стоит ожидать, если подумать:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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()
был выполнен. Это проблема, особенно когда ваш пользовательский загрузчик работает особенно медленно или дорого. Почему это было выполнено здесь? Найдите API `java.util.Map # remove () :
V remove(Object key)
[…]
Возвращает значение, с которым эта карта ранее ассоциировала ключ, или нуль, если карта не содержала сопоставления для ключа.
Может быть, это спорно, но можно было бы утверждать, что Hazelcast делает правильную вещь. Если вы рассматриваете нашу карту с подключенным MapLoader
как вид внешнего хранилища, это имеет смысл. При удалении отсутствующего ключа Hazelcast фактически спрашивает наш MapLoader
: что могло быть предыдущим значением? Он притворяется, будто карта содержит каждое значение, возвращенное из MapLoader
, но загружается лениво. Это не ошибка, поскольку существует специальный метод IMap.delete()
который работает так же, как и remove()
, но не загружает «предыдущее» значение:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
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()
не должен вызывать MapLoader.load()
, исправленный в 3.2.6 и 3.3. Если вы еще не обновились, даже IMap.delete()
перейдет в MapLoader
. Если вы думаете, что IMap.remove()
удивляет, посмотрите, как работает put()
!
IMap.put()
вызывает MapLoader
Если вы подумали, что remove()
значения remove()
вначале является подозрительным, как насчет явного метода put()
сначала загружающего значение для данного ключа? В конце концов, мы явно помещаем что-то в карту по ключу, почему Hazelcast сначала загружает это значение через MapLoader
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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() } |
Опять же, давайте восстановим JavaDoc для java.util.Map.put()
:
V положить (ключ K, значение V)
[…]
Возвращает:
предыдущее значение, связанное с ключом, или ноль, если не было сопоставления для ключа.
Hazelcast делает вид, что IMap
— это просто ленивый просмотр какого-либо внешнего источника, поэтому, когда мы put()
что-то в IMap
, которого не было раньше, он сначала загружает «предыдущее» значение, чтобы оно могло его вернуть. Опять же, это большая проблема, когда MapLoader
медленный или дорогой — если мы можем явно поместить что-то в карту, зачем сначала загружать это? К счастью, существует простой обходной путь putTransient()
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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
умолчанию IMap
. Но это также означает, что вы можете назначить произвольный TTL для каждой записи карты, не только глобально для всей карты — полезно.
IMap.containsKey()
включает MapLoader
, может быть медленным или блокировать
Вспомните нашу аналогию: IMap
с поддержкой MapLoader
ведет себя как просмотр внешнего источника данных. Вот почему не должно быть сюрпризом то, что containsKey()
на пустой карте вызовет MapLoader
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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
. Опять же, это не проблема, если ваш загрузчик работает быстро, без побочных эффектов и надежен. Если это не так, это убьет вас:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
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() } |
Несмотря на MapLoader
, что MapLoader
containsKey()
вызывает MapLoader
, он не « MapLoader
» загруженное значение для последующего использования. Вот почему MapLoader
containsKey()
за которой следует get()
MapLoader
вызывает MapLoader
, что весьма расточительно. К счастью, если вы вызываете функцию containsKey()
для существующего ключа, он запускается почти сразу, хотя, скорее всего, потребуется сетевой переход. Что не так удачно, так это поведение keySet()
, values()
, entrySet()
и нескольких других методов до версии 3.3 Hazelcast. Все они будут блокироваться в случае загрузки какого-либо ключа за раз. Поэтому, если у вас есть карта с тысячами ключей и вы запрашиваете keySet()
, один медленный MapLoader.load()
заблокирует весь кластер. К счастью, это было исправлено в 3.3, так что IMap.keySet()
, IMap.values()
и т. Д. Не блокируются, даже когда некоторые ключи вычисляются в данный момент.
Как видите, IMap
+ MapLoader
является мощной, но также заполнена ловушками. Некоторые из них продиктованы API, а в некоторых случаях — распределенной природой Hazelcast, наконец, некоторые зависят от конкретной реализации. Убедитесь, что вы понимаете их, прежде чем использовать функцию загрузки кеша
Ссылка: | Тестирование на основе свойств с помощью ScalaCheck — пользовательские генераторы от нашего партнера по JCG Томаша Нуркевича в блоге Java и окрестностях . |