Java tutorial
/** * Copyright (c) 2013-2019 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.redisson.cache; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import org.redisson.RedissonListMultimapCache; import org.redisson.RedissonObject; import org.redisson.RedissonScoredSortedSet; import org.redisson.RedissonSemaphore; import org.redisson.RedissonTopic; import org.redisson.api.LocalCachedMapOptions; import org.redisson.api.LocalCachedMapOptions.EvictionPolicy; import org.redisson.api.LocalCachedMapOptions.ReconnectionStrategy; import org.redisson.api.LocalCachedMapOptions.SyncStrategy; import org.redisson.api.RFuture; import org.redisson.api.RListMultimapCache; import org.redisson.api.RObject; import org.redisson.api.RScoredSortedSet; import org.redisson.api.RSemaphore; import org.redisson.api.RTopic; import org.redisson.api.listener.BaseStatusListener; import org.redisson.api.listener.MessageListener; import org.redisson.client.codec.ByteArrayCodec; import org.redisson.client.codec.Codec; import org.redisson.command.CommandAsyncExecutor; import org.redisson.misc.RPromise; import org.redisson.misc.RedissonPromise; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; /** * * @author Nikita Koksharov * */ public abstract class LocalCacheListener { public static final String TOPIC_SUFFIX = "topic"; public static final String DISABLED_KEYS_SUFFIX = "disabled-keys"; public static final String DISABLED_ACK_SUFFIX = ":topic"; private ConcurrentMap<CacheKey, String> disabledKeys = new ConcurrentHashMap<CacheKey, String>(); private static final Logger log = LoggerFactory.getLogger(LocalCacheListener.class); private String name; private CommandAsyncExecutor commandExecutor; private Cache<?, ?> cache; private RObject object; private byte[] instanceId = new byte[16]; private Codec codec; private LocalCachedMapOptions<?, ?> options; private long cacheUpdateLogTime; private volatile long lastInvalidate; private RTopic invalidationTopic; private int syncListenerId; private int reconnectionListenerId; public LocalCacheListener(String name, CommandAsyncExecutor commandExecutor, RObject object, Codec codec, LocalCachedMapOptions<?, ?> options, long cacheUpdateLogTime) { super(); this.name = name; this.commandExecutor = commandExecutor; this.object = object; this.codec = codec; this.options = options; this.cacheUpdateLogTime = cacheUpdateLogTime; ThreadLocalRandom.current().nextBytes(instanceId); } public byte[] generateId() { byte[] id = new byte[16]; ThreadLocalRandom.current().nextBytes(id); return id; } public byte[] getInstanceId() { return instanceId; } public Cache<CacheKey, CacheValue> createCache(LocalCachedMapOptions<?, ?> options) { if (options.getEvictionPolicy() == EvictionPolicy.NONE) { return new NoneCacheMap<CacheKey, CacheValue>(options.getTimeToLiveInMillis(), options.getMaxIdleInMillis()); } if (options.getEvictionPolicy() == EvictionPolicy.LRU) { return new LRUCacheMap<CacheKey, CacheValue>(options.getCacheSize(), options.getTimeToLiveInMillis(), options.getMaxIdleInMillis()); } if (options.getEvictionPolicy() == EvictionPolicy.LFU) { return new LFUCacheMap<CacheKey, CacheValue>(options.getCacheSize(), options.getTimeToLiveInMillis(), options.getMaxIdleInMillis()); } if (options.getEvictionPolicy() == EvictionPolicy.SOFT) { return ReferenceCacheMap.soft(options.getTimeToLiveInMillis(), options.getMaxIdleInMillis()); } if (options.getEvictionPolicy() == EvictionPolicy.WEAK) { return ReferenceCacheMap.weak(options.getTimeToLiveInMillis(), options.getMaxIdleInMillis()); } throw new IllegalArgumentException("Invalid eviction policy: " + options.getEvictionPolicy()); } public boolean isDisabled(Object key) { return disabledKeys.containsKey(key); } public void add(Cache<?, ?> cache) { this.cache = cache; invalidationTopic = new RedissonTopic(LocalCachedMessageCodec.INSTANCE, commandExecutor, getInvalidationTopicName()); if (options.getReconnectionStrategy() != ReconnectionStrategy.NONE) { reconnectionListenerId = invalidationTopic.addListener(new BaseStatusListener() { @Override public void onSubscribe(String channel) { if (options.getReconnectionStrategy() == ReconnectionStrategy.CLEAR) { cache.clear(); } if (options.getReconnectionStrategy() == ReconnectionStrategy.LOAD // check if instance has already been used && lastInvalidate > 0) { loadAfterReconnection(); } } }); } if (options.getSyncStrategy() != SyncStrategy.NONE) { syncListenerId = invalidationTopic.addListener(Object.class, new MessageListener<Object>() { @Override public void onMessage(CharSequence channel, Object msg) { if (msg instanceof LocalCachedMapDisable) { LocalCachedMapDisable m = (LocalCachedMapDisable) msg; String requestId = m.getRequestId(); Set<CacheKey> keysToDisable = new HashSet<CacheKey>(); for (byte[] keyHash : ((LocalCachedMapDisable) msg).getKeyHashes()) { CacheKey key = new CacheKey(keyHash); keysToDisable.add(key); } disableKeys(requestId, keysToDisable, m.getTimeout()); RedissonTopic topic = new RedissonTopic(LocalCachedMessageCodec.INSTANCE, commandExecutor, RedissonObject.suffixName(name, requestId + DISABLED_ACK_SUFFIX)); topic.publishAsync(new LocalCachedMapDisableAck()); } if (msg instanceof LocalCachedMapEnable) { LocalCachedMapEnable m = (LocalCachedMapEnable) msg; for (byte[] keyHash : m.getKeyHashes()) { CacheKey key = new CacheKey(keyHash); disabledKeys.remove(key, m.getRequestId()); } } if (msg instanceof LocalCachedMapClear) { LocalCachedMapClear clearMsg = (LocalCachedMapClear) msg; cache.clear(); RSemaphore semaphore = getClearSemaphore(clearMsg.getRequestId()); semaphore.releaseAsync(); } if (msg instanceof LocalCachedMapInvalidate) { LocalCachedMapInvalidate invalidateMsg = (LocalCachedMapInvalidate) msg; if (!Arrays.equals(invalidateMsg.getExcludedId(), instanceId)) { for (byte[] keyHash : invalidateMsg.getKeyHashes()) { CacheKey key = new CacheKey(keyHash); cache.remove(key); } } } if (msg instanceof LocalCachedMapUpdate) { LocalCachedMapUpdate updateMsg = (LocalCachedMapUpdate) msg; for (LocalCachedMapUpdate.Entry entry : updateMsg.getEntries()) { ByteBuf keyBuf = Unpooled.wrappedBuffer(entry.getKey()); ByteBuf valueBuf = Unpooled.wrappedBuffer(entry.getValue()); try { updateCache(keyBuf, valueBuf); } catch (IOException e) { log.error("Can't decode map entry", e); } finally { keyBuf.release(); valueBuf.release(); } } } if (options.getReconnectionStrategy() == ReconnectionStrategy.LOAD) { lastInvalidate = System.currentTimeMillis(); } } }); String disabledKeysName = RedissonObject.suffixName(name, DISABLED_KEYS_SUFFIX); RListMultimapCache<LocalCachedMapDisabledKey, String> multimap = new RedissonListMultimapCache<LocalCachedMapDisabledKey, String>( null, codec, commandExecutor, disabledKeysName); for (LocalCachedMapDisabledKey key : multimap.readAllKeySet()) { Set<CacheKey> keysToDisable = new HashSet<CacheKey>(); for (String hash : multimap.getAll(key)) { CacheKey cacheKey = new CacheKey(ByteBufUtil.decodeHexDump(hash)); keysToDisable.add(cacheKey); } disableKeys(key.getRequestId(), keysToDisable, key.getTimeout()); } } } public RFuture<Void> clearLocalCacheAsync() { RPromise<Void> result = new RedissonPromise<Void>(); byte[] id = generateId(); RFuture<Long> future = invalidationTopic.publishAsync(new LocalCachedMapClear(id)); future.onComplete((res, e) -> { if (e != null) { result.tryFailure(e); return; } RSemaphore semaphore = getClearSemaphore(id); semaphore.acquireAsync(res.intValue()).onComplete((r, ex) -> { if (ex != null) { result.tryFailure(ex); return; } semaphore.deleteAsync().onComplete((re, exc) -> { if (exc != null) { result.tryFailure(exc); return; } result.trySuccess(null); }); }); }); return result; } public String getInvalidationTopicName() { return RedissonObject.suffixName(name, TOPIC_SUFFIX); } protected abstract void updateCache(ByteBuf keyBuf, ByteBuf valueBuf) throws IOException; private void disableKeys(final String requestId, final Set<CacheKey> keys, long timeout) { for (CacheKey key : keys) { disabledKeys.put(key, requestId); cache.remove(key); } commandExecutor.getConnectionManager().getGroup().schedule(new Runnable() { @Override public void run() { for (CacheKey cacheKey : keys) { disabledKeys.remove(cacheKey, requestId); } } }, timeout, TimeUnit.MILLISECONDS); } public void remove() { List<Integer> ids = new ArrayList<Integer>(2); if (syncListenerId != 0) { ids.add(syncListenerId); } if (reconnectionListenerId != 0) { ids.add(reconnectionListenerId); } invalidationTopic.removeListenerAsync(ids.toArray(new Integer[ids.size()])); } public String getUpdatesLogName() { return RedissonObject.prefixName("redisson__cache_updates_log", name); } private void loadAfterReconnection() { if (System.currentTimeMillis() - lastInvalidate > cacheUpdateLogTime) { cache.clear(); return; } object.isExistsAsync().onComplete((res, e) -> { if (e != null) { log.error("Can't check existance", e); return; } if (!res) { cache.clear(); return; } RScoredSortedSet<byte[]> logs = new RedissonScoredSortedSet<byte[]>(ByteArrayCodec.INSTANCE, commandExecutor, getUpdatesLogName(), null); logs.valueRangeAsync(lastInvalidate, true, Double.POSITIVE_INFINITY, true).onComplete((r, ex) -> { if (ex != null) { log.error("Can't load update log", ex); return; } for (byte[] entry : r) { byte[] keyHash = Arrays.copyOf(entry, 16); CacheKey key = new CacheKey(keyHash); cache.remove(key); } }); }); } protected RSemaphore getClearSemaphore(byte[] requestId) { String id = ByteBufUtil.hexDump(requestId); RSemaphore semaphore = new RedissonSemaphore(commandExecutor, name + ":clear:" + id); return semaphore; } }