LoginSignup
0
3

More than 5 years have passed since last update.

Spring Data Redis で Null サポートキャッシュ

Posted at

Spring なプロジェクトで Spring Date Redis を使うと、Redis へのキャッシュが簡単にできるようになります。

ただ、キャッシュの難しい点の1つとして、null なものをキャッシュするかどうかという話があります。
時間を掛けて取得を試みたものが null だった場合(無かった場合)、null であったことを記憶しておきたいか、という話です。

今までは Spring Date Redis を使っていると、null はキャッシュ対象外でした。
(もちろん、シリアライザー/デシリアライザーをカスタマイズすることで対応はできます)
これは今まで結構困っていたのですが、バージョン 1.8 から公式にサポートされるようになっていました。

Support caching null values via RedisCache
https://jira.spring.io/browse/DATAREDIS-553

使い方としては、RedisCacheManager のコンストラクタ引数に null をサポートするかのフラグをセットするだけです。
以下、サンプルです。

Application.java
@EnableCaching
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Autowired
    private JedisConnectionFactory jedisConnectionFactory;

    @Bean
    public RedisTemplate<Object, Object> redisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);
        redisTemplate.setKeySerializer(new GenericJackson2JsonRedisSerializer(""));
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(""));
        return redisTemplate;
    }

    @Bean
    public CacheManager cacheManager() {
        List<String> cacheNames = Arrays.asList("person");
        RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate(), cacheNames, true); // 最後に引数で Null をキャッシュするようにする。

        // キャッシュキーのプレフィックスの設定
        redisCacheManager.setUsePrefix(true);
        redisCacheManager.setCachePrefix(new RedisCachePrefix() {
            @Override
            public byte[] prefix(String cacheName) {
                return ("SAMPLE:" + cacheName + ":").getBytes(StandardCharsets.UTF_8);
            }
        });

        return redisCacheManager;
    }
}
Person.java
@Data
@AllArgsConstructor
public class Person {
    private int id;
    private String name;
}
PersonService.java
@Slf4j
@Service
public class PersonService {

    @Cacheable("person")
    public Person findById(int id) {
        log.info("called findById. id = {}", id);

        try {
            TimeUnit.SECONDS.sleep(id);
        } catch (InterruptedException e) {
            // ignore
        }

        return id >= 10 ? null : new Person(id, "ほげ太郎" + id);
    }
}
PersonServiceTest.java
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonServiceTest {

    @Autowired
    PersonService target;
    @Autowired
    CacheManager cacheManager;

    @Before
    public void setup() {
        Cache cache = cacheManager.getCache("person");
        cache.clear();
    }

    @Test
    public void testFindByIdNull() {
        StopWatch stopWatch = new StopWatch();

        stopWatch.start();
        Person p1 = target.findById(10); // 10s かかる
        stopWatch.stop();
        log.info("result = {}, elapsed = {}", p1, stopWatch.getLastTaskTimeMillis());
        assertNull(p1);
        assertTrue(stopWatch.getLastTaskTimeMillis() > 10000);

        stopWatch.start();
        Person p2 = target.findById(10); // 2回目はキャッシュが効いて速い
        stopWatch.stop();
        log.info("result = {}, elapsed = {}", p2, stopWatch.getLastTaskTimeMillis());
        assertNull(p2);
        assertTrue(stopWatch.getLastTaskTimeMillis() < 1000);
    }
}
TestResults
2017-06-23 12:01:34.434  INFO 3246 --- [           main] n.s.cachesample.service.PersonService    : called findById. id = 10
2017-06-23 12:01:44.449  INFO 3246 --- [           main] n.s.c.service.PersonServiceTest          : result = null, elapsed = 10058
2017-06-23 12:01:44.498  INFO 3246 --- [           main] n.s.c.service.PersonServiceTest          : result = null, elapsed = 49

ちなみに Redis にどう保存されているかというと以下のようになっています。

$ redis-cli -h 192.168.99.100 get 'SAMPLE:hello:10'                                                                                                                                                                                12:06:23
"{\"@class\":\"org.springframework.cache.support.NullValue\"}"

これは、GenericJackson2JsonRedisSerializer で定義されてる以下のクラスによるものです。

NullValueSerializer
/**
 * {@link StdSerializer} adding class information required by default typing. This allows de-/serialization of
 * {@link NullValue}.
 *
 * @author Christoph Strobl
 * @since 1.8
 */
private class NullValueSerializer extends StdSerializer<NullValue> {

    private static final long serialVersionUID = 1999052150548658808L;
    private final String classIdentifier;

    /**
     * @param classIdentifier can be {@literal null} and will be defaulted to {@code @class}.
     */
    NullValueSerializer(String classIdentifier) {

        super(NullValue.class);
        this.classIdentifier = StringUtils.hasText(classIdentifier) ? classIdentifier : "@class";
    }

    /*
     * (non-Javadoc)
     * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
     */
    @Override
    public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider)
            throws IOException {

        jgen.writeStartObject();
        jgen.writeStringField(classIdentifier, NullValue.class.getName());
        jgen.writeEndObject();
    }
}

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3