Problem Description
Recently we used spring cache + Redis to do caching. Under high concurrency the @cacheable annotation returns a null content. Look at the source code, in the use of annotations to get the cache, the Rediscache get method will first determine whether the key exists, and then to get the value. There's a copper leak, and when thread 1 judges that the key is there, then the key expires, and then the thread 1 returns NULL when it gets the value.
Rediscache's Get method source code:
Public rediscacheelement get (final Rediscachekey CacheKey) {assert.notnull (CacheKey, "CacheKey must is not being null!"); Determines whether a key exists Boolean exists = (Boolean) Redisoperations.execute (new rediscallback<boolean> () {@O Verride public Boolean Doinredis (redisconnection connection) throws DataAccessException {return Conne
Ction.exists (Cachekey.getkeybytes ());
}
});
if (!exists.booleanvalue ()) {return null;
//Get key corresponding value return new Rediscacheelement (CacheKey, Fromstorevalue (lookup (CacheKey))); }//Get Value Protected object lookup (object key) {Rediscachekey CacheKey = key instanceof Rediscachekey?
(Rediscachekey) Key:getrediscachekey (key); byte[] bytes = (byte[]) Redisoperations.execute (new abstractrediscachecallback<byte[]> Acheelement (New rediscacheelement (CacheKey, null), cachevalueaccessor), CacheMetadata) {@Override public Byte[] DoinreDis (binaryrediscacheelement element, redisconnection connection) throws DataAccessException {return connectio
N.get (Element.getkeybytes ());
}
}); return bytes = null?
Null:cacheValueAccessor.deserializeIfNecessary (bytes);
}
Solution
The problem with this process is that the solution is to turn the process upside down, get the value first, and then determine if the key exists. You cannot use the obtained value directly to determine whether there is a value based on whether it is null, because reids may cache null values.
To override the Rediscache get method:
Public rediscacheelement get (final Rediscachekey cachekey) {
assert.notnull (CacheKey, "CacheKey must is not being null!");
rediscacheelement rediscacheelement = new Rediscacheelement (CacheKey, Fromstorevalue (lookup (CacheKey)));
Boolean exists = (Boolean) Redisoperations.execute (new rediscallback<boolean> () {
@Override
public Boolean Doinredis (redisconnection connection) throws DataAccessException {return
connection.exists ( Cachekey.getkeybytes ());
if (!exists.booleanvalue ()) {return
null;
}
return rediscacheelement;
}
Full implementation:
to override the Rediscache get method
Package Com.xiaolyuh.redis.cache;
Import org.springframework.dao.DataAccessException;
Import Org.springframework.data.redis.cache.RedisCache;
Import org.springframework.data.redis.cache.RedisCacheElement;
Import Org.springframework.data.redis.cache.RedisCacheKey;
Import org.springframework.data.redis.connection.RedisConnection;
Import Org.springframework.data.redis.core.RedisCallback;
Import org.springframework.data.redis.core.RedisOperations;
Import Org.springframework.util.Assert; /** * Custom Redis Cache * * @author Yuhao.wang/public class Customizedrediscache extends Rediscache {private final
Redisoperations redisoperations;
Private final byte[] prefix; Public Customizedrediscache (String name, byte[] prefix, redisoperations<? extends Object,? extends Object> Redisop
Erations, long expiration) {super (name, prefix, redisoperations, expiration);
This.redisoperations = redisoperations;
This.prefix = prefix; } public CustomizedreDiscache (String name, byte[] prefix, redisoperations<? extends Object,? extends Object> redisoperations, long Expir
ation, Boolean allownullvalues) {super (name, prefix, redisoperations, expiration, allownullvalues);
This.redisoperations = redisoperations;
This.prefix = prefix;
/** * Overrides the Get function of the parent class. The Get method of the parent class is to use exists to determine whether a key exists, does not exist, returns NULL, and then takes the value in the Redis cache.
This can cause concurrency problems, * If a request calls the EXISTS function to determine that the key exists, the cache expires in the next moment, or is deleted.
* This time to go to the cache to get the value of the return is null.
* You can get the cached value first and then determine if the key exists. * * @param cachekey * @return/@Override public rediscacheelement get (final Rediscachekey Cacheke
Y) {assert.notnull (CacheKey, "CacheKey must not to be null!");
Rediscacheelement rediscacheelement = new Rediscacheelement (CacheKey, Fromstorevalue (lookup (CacheKey)));
Boolean exists = (Boolean) Redisoperations.execute (new rediscallback<boolean> () {@Override Public Boolean Doinredis (redisconnection connection) throws DataAccessException {return connection.exists (cachekey.getkeybytes)
);
}
});
if (!exists.booleanvalue ()) {return null;
return rediscacheelement; /** * Get Rediscachekey * * @param key * @return/private Rediscachekey Getrediscache Key (Object key) {return new Rediscachekey (key). Useprefix (This.prefix). Withkeyserializer (Redisope
Rations.getkeyserializer ());
}
}
Override Rediscachemanager
Package Com.xiaolyuh.redis.cache;
Import Org.slf4j.Logger;
Import Org.slf4j.LoggerFactory;
Import Org.springframework.cache.Cache;
Import Org.springframework.data.redis.cache.RedisCacheManager;
Import org.springframework.data.redis.core.RedisOperations;
Import Org.springframework.data.redis.core.StringRedisTemplate;
Import java.util.Collection;
/** * Custom Redis Cache Manager * @author Yuhao.wang/public class Customizedrediscachemanager extends Rediscachemanager {
private static final Logger Logger = Loggerfactory.getlogger (Customizedrediscachemanager.class);
Public Customizedrediscachemanager (Redisoperations redisoperations) {super (redisoperations);
Public Customizedrediscachemanager (Redisoperations redisoperations, collection<string> cacheNames) {
Super (Redisoperations, cachenames);
@Override protected Cache Getmissingcache (String name) {Long expiration = computeexpiration (name); return new CustomizedrediscachE (Name, (This.isuseprefix () this.getcacheprefix (). prefix (name): null),
This.getredisoperations (), expiration);
}
}
Configuring the Redis manager
@Configuration
the public class Redisconfig {
//Redis cache has a valid time unit of seconds
@Value ("${redis.default.expiration:3600} ")
private long redisdefaultexpiration;
@Bean public
Rediscachemanager CacheManager (redistemplate<object, object> redistemplate) {
Rediscachemanager Rediscachemanager = new Customizedrediscachemanager (redistemplate);
Rediscachemanager.setuseprefix (true);
Here you can set a default expiration time unit is seconds
rediscachemanager.setdefaultexpiration (redisdefaultexpiration);
return rediscachemanager;
}
/**
* Display Declaration cache key Generator
*
* @return
/@Bean public
keygenerator keygenerator () {
return new Simplekeygenerator ();
}
Source:
Https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
Spring-boot-student-cache-redis Engineering