springboot-redis spring cache mid-story

1.spring cache parsing 1.1.RedisCache and RedisCache Manager 1.1.1. Structure 1.1.2. Resolution RedisCache uses ...
1.1.RedisCache and RedisCache Manager
1.2.CompositeCacheManager
1.spring cache parsing

1.1.RedisCache and RedisCache Manager

1.1.1. Structure

1.1.2. Resolution

  • RedisCache uses the RedisCacheWriter interface for reading and writing redis;
  • RedisCacheWriter
    • Differences between RedisCacheWriter and Cache interfaces:
      • All methods need to specify a name, which is the key of the lock when the redis command is executed.
      • RedisCacheWriter clears the cache regularly;
public interface RedisCacheWriter { //**Static method to create an unlocked EdsCacheWriter instance static RedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) { Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); return new DefaultRedisCacheWriter(connectionFactory); } //**Static method, creating a locked EdsCacheWriter instance static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) { Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); return new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50)); } //** Save key-value pairs to specify expiration time void put(String name, byte[] key, byte[] value, @Nullable Duration ttl); //**Get value from key byte[] get(String name, byte[] key); //**Save if a key-value pair does not exist, returning the current value in the cache byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl); //** Delete key-value pairs void remove(String name, byte[] key); //** Clear regular cache void clean(String name, byte[] pattern); }
  • DefaultRedisCacheWriter
    • DefaultRedisCacheWriter is the only implementation class for the DefaultRedisCacheWriter interface;
    • DefaultRedisCacheWriter is a static method provided by the DefaultRedisCacheWriter interface whose scope is visible within the package and cannot be created by the user.
    • If sleepTime is Zero or a negative number, it will not be locked, otherwise it will be locked when the redis command is executed, defaulting to Zero;
class DefaultRedisCacheWriter implements RedisCacheWriter { //**redis Connection Factory private final RedisConnectionFactory connectionFactory; //** Wait for the lock, sleep the sleepTime milliseconds when there is a lock, and retrieve the lock.If sleepTime is Zero or negative, it will not be locked, otherwise it will be locked when the redis command is executed private final Duration sleepTime; //** Set key-value pairs public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { ... //** Execute after lock is acquired execute(name, connection -> { //**If ttl is valid, save key-value pairs and set validity period if (shouldExpireWithin(ttl)) { connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert()); } else { //**Otherwise, save key-value pairs connection.set(key, value); } return "OK"; }); } //**Save if a key-value pair does not exist, returning the current value in the cache public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) { ... //** Execute after lock is acquired return execute(name, connection -> { //** Lock if lock is required if (isLockingCacheWriter()) { doLock(name, connection); } try { //**Set key-value pairs when they do not exist if (connection.setNX(key, value)) { //** Set validity period if ttl is valid if (shouldExpireWithin(ttl)) { connection.pExpire(key, ttl.toMillis()); } return null; } //** Return the currently saved value return connection.get(key); } finally { //**Unlock if (isLockingCacheWriter()) { doUnlock(name, connection); } } }); } //** Clear regular cache public void clean(String name, byte[] pattern) { ... //** Execute after lock is acquired execute(name, connection -> { //**Did lock succeed boolean wasLocked = false; try { //** Lock if necessary and set lock successfully if (isLockingCacheWriter()) { doLock(name, connection); wasLocked = true; } //** Get key from regular byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet()) .toArray(new byte[0][]); //** If there are configured key s, delete them all if (keys.length > 0) { connection.del(keys); } } finally { //**Unlock if lock succeeds and locks are required if (wasLocked && isLockingCacheWriter()) { doUnlock(name, connection); } } return "OK"; }); } //**Locking void lock(String name) { execute(name, connection -> doLock(name, connection)); } //**Unlock void unlock(String name) { executeLockFree(connection -> doUnlock(name, connection)); } //**Locking private Boolean doLock(String name, RedisConnection connection) { return connection.setNX(createCacheLockKey(name), new byte[0]); } //**Unlock private Long doUnlock(String name, RedisConnection connection) { return connection.del(createCacheLockKey(name)); } //** Check for locks boolean doCheckLock(String name, RedisConnection connection) { return connection.exists(createCacheLockKey(name)); } //** Lock or not private boolean isLockingCacheWriter() { return !sleepTime.isZero() && !sleepTime.isNegative(); } //** Execute the redis command (wait until a lock is acquired, then execute the redis command if no lock is acquired) private <T> T execute(String name, Function<RedisConnection, T> callback) { RedisConnection connection = connectionFactory.getConnection(); try { checkAndPotentiallyWaitUntilUnlocked(name, connection); return callback.apply(connection); } finally { connection.close(); } } //** Execute the redis command private void executeLockFree(Consumer<RedisConnection> callback) { RedisConnection connection = connectionFactory.getConnection(); try { callback.accept(connection); } finally { connection.close(); } } //** Wait for locks (threads sleep sleepTime s for milliseconds when locks are present, then retrieve locks) private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) { if (!isLockingCacheWriter()) { return; } try { while (doCheckLock(name, connection)) { Thread.sleep(sleepTime.toMillis()); } } catch (InterruptedException ex) { // Re-interrupt current thread, to allow other participants to react. Thread.currentThread().interrupt(); throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name), ex); } } //**Is ttl valid private static boolean shouldExpireWithin(@Nullable Duration ttl) { return ttl != null && !ttl.isZero() && !ttl.isNegative(); } //**key to create lock from name private static byte[] createCacheLockKey(String name) { return (name + "~lock").getBytes(StandardCharsets.UTF_8); } }
  • RedisCacheConfiguration
    • RedisCache configures common options through RedisCacheConfiguration;
    • RedisCacheConfiguration uses builder mode. Since the properties of RedisCacheConfiguration are final, all properties are initialized to create a default object, and then an object is re-created in each method of initializing the properties.
      • Create an example: RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(60));
public class RedisCacheConfiguration { //** Cache validity period private final Duration ttl; //** Cache null values private final boolean cacheNullValues; //**key prefix calculator private final CacheKeyPrefix keyPrefix; //**Whether to use prefix private final boolean usePrefix; //**Serializer for key, holding reference to RedisSerializer, key can only be string private final SerializationPair<String> keySerializationPair; //**Serializer of value, holding a reference to RedisSerializer, value can be of any type private final SerializationPair<Object> valueSerializationPair; //**spring ConversionService type conversion private final ConversionService conversionService; //**Static method, returns the default EdsCacheConfiguration instance public static RedisCacheConfiguration defaultCacheConfig() { return defaultCacheConfig(null); } //**Static method, returns the default EdsCacheConfiguration instance //** Create a no-expiration time to allow caching of null values, key needs to be prefixed with the format "name::", key serializer StringRedisSerializer, //**value Serializer This jdk serializer, conversionService is the ReedisCacheConfiguration of DefaultFormattingConversionService public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); registerDefaultConverters(conversionService); return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(), SerializationPair.fromSerializer(RedisSerializer.string()), SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService); } //** Create a RedisCacheConfiguration and set the cache expiration time public RedisCacheConfiguration entryTtl(Duration ttl) { Assert.notNull(ttl, "TTL duration must not be null!"); return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair, conversionService); } ...A series of builder mode initialization methods for attributes //**Generate prefix for key from cacheName public String getKeyPrefixFor(String cacheName) { Assert.notNull(cacheName, "Cache name must not be null!"); return keyPrefix.compute(cacheName); } //**Add Converter to conversionService public void addCacheKeyConverter(Converter<?, String> cacheKeyConverter) { configureKeyConverters(it -> it.addConverter(cacheKeyConverter)); } //**conversionService configuration public void configureKeyConverters(Consumer<ConverterRegistry> registryConsumer) { ... registryConsumer.accept((ConverterRegistry) getConversionService()); } //** Register two Converter s public static void registerDefaultConverters(ConverterRegistry registry) { Assert.notNull(registry, "ConverterRegistry must not be null!"); registry.addConverter(String.class, byte[].class, source -> source.getBytes(StandardCharsets.UTF_8)); registry.addConverter(SimpleKey.class, String.class, SimpleKey::toString); } }
  • RedisCache
    • Use RedisCacheWriter to operate redis read and write;
    • key and value are serialized and deserialized using cacheConfig's keySerializationPair and valueSerializationPair;
public class RedisCache extends AbstractValueAdaptingCache { //** Serialize NullValue using jdk serializer private static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); //**name of cache private final String name; //**RedisCacheWriter instance private final RedisCacheWriter cacheWriter; //**redis configuration private final RedisCacheConfiguration cacheConfig; //**spring ConversionService type conversion private final ConversionService conversionService; //**Get value, return object object protected Object lookup(Object key) { //** Get value using cacheWriter byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key)); if (value == null) { return null; } //**Value serializer deserialized value for cacheConfig return deserializeCacheValue(value); } //**Get value, return the object object object, if value does not exist, use valueLoader to generate value public synchronized <T> T get(Object key, Callable<T> valueLoader) { //** Call the lookup method, get the value, and convert it to a ValueWrapper object ValueWrapper result = get(key); //**Returns value if value is not empty and cast to the specified type if (result != null) { return (T) result.get(); } //**If value is empty, save the result of valueLoader as value and return T value = valueFromLoader(key, valueLoader); put(key, value); return value; } //**Save key-value pairs public void put(Object key, @Nullable Object value) { //** Convert value to NullValue if value is null and null values are allowed Object cacheValue = preProcessCacheValue(value); //** Throw an exception if value is null and null values are not allowed if (!isAllowNullValues() && cacheValue == null) { throw new IllegalArgumentException(String.format( "Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.", name)); } //** Serialize values using cacheConfig's value serializer, then use cacheWriter to save key-value pairs and set expiration time cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl()); } //** Delete key-value pairs public void evict(Object key) { cacheWriter.remove(name, createAndConvertCacheKey(key)); } //** Clear all key-value pairs public void clear() { byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class); cacheWriter.clean(name, pattern); } //** Convert value to NullValue if value is null and null values are allowed protected Object preProcessCacheValue(@Nullable Object value) { if (value != null) { return value; } return isAllowNullValues() ? NullValue.INSTANCE : null; } //** Serialize key using the key serializer of cacheConfig protected byte[] serializeCacheKey(String cacheKey) { return ByteUtils.getBytes(cacheConfig.getKeySerializationPair().write(cacheKey)); } //** Serialize value using cacheConfig's value serializer protected byte[] serializeCacheValue(Object value) { if (isAllowNullValues() && value instanceof NullValue) { return BINARY_NULL_VALUE; } return ByteUtils.getBytes(cacheConfig.getValueSerializationPair().write(value)); } //** Deserialize value using cacheConfig's value serializer protected Object deserializeCacheValue(byte[] value) { if (isAllowNullValues() && ObjectUtils.nullSafeEquals(value, BINARY_NULL_VALUE)) { return NullValue.INSTANCE; } return cacheConfig.getValueSerializationPair().read(ByteBuffer.wrap(value)); } //** Generate a key saved in redis based on the specified key protected String createCacheKey(Object key) { //** Convert key to string String convertedKey = convertKey(key); if (!cacheConfig.usePrefix()) { return convertedKey; } //**If the key requires a prefix, use cacheConfig's keyPrefix calculator to calculate the key value, defaulting to "name::key" return prefixCacheKey(convertedKey); } //** Convert key to string protected String convertKey(Object key) { //** If the key is of type string, return it directly if (key instanceof String) { return (String) key; } TypeDescriptor source = TypeDescriptor.forObject(key); //** If key can be converted to string, it is converted to string and returns if (conversionService.canConvert(source, TypeDescriptor.valueOf(String.class))) { ... return conversionService.convert(key, String.class); ... } //**Call the toString method of key to convert to string and return Method toString = ReflectionUtils.findMethod(key.getClass(), "toString"); if (toString != null && !Object.class.equals(toString.getDeclaringClass())) { return key.toString(); } ... } }
  • RedisCacheManager
public class RedisCacheManager extends AbstractTransactionSupportingCacheManager { //**redis read-write private final RedisCacheWriter cacheWriter; //**redis configuration private final RedisCacheConfiguration defaultCacheConfig; //**Initialize cache configuration private final Map<String, RedisCacheConfiguration> initialCacheConfiguration; //**Is it allowed to create a new cache when the cache corresponding to name does not exist private final boolean allowInFlightCacheCreation; //**Static method to create a default ReedisCacheManager based on connectionFactory public static RedisCacheManager create(RedisConnectionFactory connectionFactory) { Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); return new RedisCacheManager(new DefaultRedisCacheWriter(connectionFactory), RedisCacheConfiguration.defaultCacheConfig()); } //** Builder mode, create RedisCacheManagerBuilder from connectionFactory public static RedisCacheManagerBuilder builder(RedisConnectionFactory connectionFactory) { Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); return RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory); } //** Builder mode, create RedisCacheManagerBuilder from cacheWriter public static RedisCacheManagerBuilder builder(RedisCacheWriter cacheWriter) { Assert.notNull(cacheWriter, "CacheWriter must not be null!"); return RedisCacheManagerBuilder.fromCacheWriter(cacheWriter); } //** Load cache based on initialization cache configuration protected Collection<RedisCache> loadCaches() { List<RedisCache> caches = new LinkedList<>(); for (Map.Entry<String, RedisCacheConfiguration> entry : initialCacheConfiguration.entrySet()) { caches.add(createRedisCache(entry.getKey(), entry.getValue())); } return caches; } //** Create a new cache based on defaultCacheConfig if the cache corresponding to name does not exist and allows caching protected RedisCache getMissingCache(String name) { return allowInFlightCacheCreation ? createRedisCache(name, defaultCacheConfig) : null; } //**Create RedisCache from name protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) { return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig); } //**Builder public static class RedisCacheManagerBuilder { private final RedisCacheWriter cacheWriter; //**Create default ReedisCacheConfiguration private RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); //** cache is not initialized by default private final Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>(); //**Is Transaction Enabled private boolean enableTransactions; //**When the cache corresponding to name does not exist, new cache creation is allowed by default boolean allowInFlightCacheCreation = true; } }

1.1.3.RedisCacheManager configuration example

  • RedisCacheConfiguration
    • The key serializer is StringRedisSerializer;
    • The value serializer is GenericJackson2JsonRedisSerializer;
    • The cache expiration time is 60 seconds;
  • DefaultRedisCacheWriter
    • The redis command is executed without locking;
  • RedisCache
    • Allow null values;
  • RedisCacheManager
    • cache was not initialized;
    • Get cache by name, create cache when cache does not exist;
    • Transactions are not supported;
@Bean @ConditionalOnMissingBean(name = "cacheManager") public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { //**redis default profile and set expiration time to 60 seconds RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(60)); //** Set up serializer redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json())); //**Create RedisCacheManager Generator RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfiguration); return builder.build(); }

1.2.CompositeCacheManager

  • Combine other cacheManager s to use multiple caches at the same time.
  • Problem: By default, the getCache method creates a cache and returns when the cache corresponding to the name does not exist.The first cacheManager that causes traversal always returns the cache, and subsequent caches will never be invoked, so the cacheManager managed by Composite CacheManager must initialize the cache and prohibit dynamic cache creation?
public class CompositeCacheManager implements CacheManager, InitializingBean { //**Other Cache Managers Managed private final List<CacheManager> cacheManagers = new ArrayList<>(); //**Whether to add a NoOpCacheManager at the end private boolean fallbackToNoOpCache = false; //**Add a NoOpCacheManager at the end, and any calls to getCache that do not match the CacheManager will return NoOpCacheManager public void afterPropertiesSet() { if (this.fallbackToNoOpCache) { this.cacheManagers.add(new NoOpCacheManager()); } } //**Get cache from name (Problem: By default, the getCache method creates a cache and returns it when the cache corresponding to name does not exist //**Cache Manager that causes traversal will always return the cache, and subsequent caches will never be called) public Cache getCache(String name) { for (CacheManager cacheManager : this.cacheManagers) { Cache cache = cacheManager.getCache(name); if (cache != null) { return cache; } } return null; } //** Get all name s public Collection<String> getCacheNames() { Set<String> names = new LinkedHashSet<>(); for (CacheManager manager : this.cacheManagers) { names.addAll(manager.getCacheNames()); } return Collections.unmodifiableSet(names); } }

11 June 2020, 21:08 | Views: 7571

Add new comment

For adding a comment, please log in
or create account

0 comments