踩坑,spring-data-redis版本过低,导致并发获取数据为null问题一例,试用phind.comAI 搜索,快速定位到了问题原因。

背景

组内维护了一个商户服务,本文我们可以叫它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/

@Cacheable redis 获取到null

这里解释有点小问题,但是已经把核心问题说出来了:并发场景下,低版本的spring-data-redis获取数据时会获取到null,原因是RedisCacheget()方法中有非原子性操作。

这里并发是指我们的服务线程与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值。

解决

捋清脉络,有两种解决办法:

  1. 升级 spring-data-redis 版本到 1.8.11
  2. 重写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