踩坑,spring-data-redis
版本过低,导致并发获取数据为null
问题一例,试用phind.com
AI 搜索,快速定位到了问题原因。
组内维护了一个商户服务,本文我们可以叫它user-service
,年前上的代码中加了一个 @Cacheable("key#30")
的读缓存。
从上周开始,以极低频率,偶发报查询NPE
空指针。一周大概报错3-4次。
伪代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
|
@Cacheable(cacheNames = "businessSimpleInfo#30", key = "#businessCode")
public BusinessSimpleVo queryBusinessSimpleInfo(String businessCode) {
BusinessSimpleVo result = new BusinessSimpleVo();
Business business = this.queryByBusinessCode(businessCode);
if (business == null) {
return result;
}
result.setBusinessTerminateStatus(business.getTerminalStatus());
result.setBusinessCode(business.getBusinessCode());
result.setSystemType(business.getSystemType());
return result;
}
|
日志报错指向,调用queryBusinessSimpleInfo
时,报NPE
。
user-service
使用的spring-data-redis
版本为:
1
|
org.springframework.data:spring-data-redis:jar:1.8.8.RELEASE:compile
|
分析代码#
可以看到,我们的result
无论何时,都不可能为null
。
所以问题只可能出现在@Cacheable
上。
搜索答案#
直接搜索 https://phind.com/ 。
这里解释有点小问题,但是已经把核心问题说出来了:并发场景下,低版本的spring-data-redis
获取数据时会获取到null
,原因是RedisCache
类get()
方法中有非原子性操作。
这里并发是指我们的服务线程与Redis
过期线程同时操作数据。
分析spring-data-redis源码#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/**
* Return the value to which this cache maps the specified key.
*
* @param cacheKey the key whose associated value is to be returned via its binary representation.
* @return the {@link RedisCacheElement} stored at given key or {@literal null} if no value found for key.
* @since 1.5
*/
public RedisCacheElement get(final RedisCacheKey cacheKey) {
Assert.notNull(cacheKey, "CacheKey must not be null!");
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 new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
}
|
可以看到上述24行,当请求线程(单个或多个)同时走到24行时,此时数据过期,此时fromStoreValue
返回的就是null
值。
捋清脉络,有两种解决办法:
- 升级
spring-data-redis
版本到 1.8.11
。
- 重写
RedisCache.get
方法,示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
public static class CustomizedRedisCache extends RedisCache {
private RedisOperations redisOperations;
public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<?, ?> redisOperations, long expiration) {
super(name, prefix, redisOperations, expiration);
this.redisOperations = redisOperations;
}
// https://github.com/spring-projects/spring-data-redis/issues/1312
public RedisCacheElement get(final RedisCacheKey cacheKey) {
Assert.notNull(cacheKey, "CacheKey must not be null!");
// 前置查数据,保证null都会fail-fast
RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
Boolean exists = (Boolean) this.redisOperations.execute((RedisCallback<Boolean>) connection -> connection.exists(cacheKey.getKeyBytes()));
// fail-fast
if (!exists.booleanValue()) {
return null;
}
// 要么为未过期的有效数据;要么为刚过期的数据;
return redisCacheElement;
}
}
public static class RedisCustomizedCacheManager extends RedisCacheManager {
private RedisOperations redisOperations;
private CacheableRedisProperties cacheableRedisProperties;
public RedisCustomizedCacheManager(RedisOperations redisOperations, CacheableRedisProperties cacheableRedisProperties) {
super(redisOperations);
this.redisOperations = redisOperations;
this.cacheableRedisProperties = cacheableRedisProperties;
}
public RedisCustomizedCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
super(redisOperations, cacheNames);
this.redisOperations = redisOperations;
}
public RedisCustomizedCacheManager(RedisOperations redisOperations, Collection<String> cacheNames, boolean cacheNullValues) {
super(redisOperations, cacheNames, cacheNullValues);
this.redisOperations = redisOperations;
}
protected RedisCache createCache(String cacheName) {
long expiration = computeExpiration(cacheName);
return new CustomizedRedisCache(cacheName, (isUsePrefix() ? new CustomizedRedisCachePrefix(cacheableRedisProperties.getPrefix()).prefix(cacheName) : null), redisOperations, expiration);
}
protected boolean isUsePrefix() {
return true;
}
}
|
Ref#