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