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 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);
	}
}

Tags: Redis Spring calculator JDK

Posted on Thu, 11 Jun 2020 21:08:01 -0400 by mebar3