mhuelfen
mhuelfen

Reputation: 255

Setting key specific TTL with @TimeToLive for Redis Spring Caching triggers no invalidation

I have use case where single entries need to removed from the cache at a specific time. The TTL needs to be set on a key and not on a cache level

Following this spring redis documentation I tried to implement key specific TTL but it does not work. There is no event happing, I used a listener to check that and there is only an event happing when the cache ttl runs out.

The cached object has a field annotated with @TimeToLive from org.springframework.data.redis.core.TimeToLive looking at the documentation this should trigger an expire event after the time has run out

Cached object

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BrandResponse {

    @TimeToLive
    private Long ttl;

    @NotBlank
    private String id;
}

Used dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.6</version>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.6.3</version>
</dependency>

Enable Key Space Events

@SpringBootApplication
@ServletComponentScan
@EnableAsync
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
public class KikaRestApiApplication {

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

The default TTL for the cache is 5 minutes .entryTtl(Duration.ofMinutes(5))

Cache setup

@Configuration
@EnableCaching
public class RedisCachingConfiguration {

    private final KikaApiProperties kikaApiProperties;

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private Integer port;

    public RedisCachingConfiguration(KikaApiProperties kikaApiProperties) {
        this.kikaApiProperties = kikaApiProperties;
    }

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(5))
            .disableCachingNullValues()
            .serializeValuesWith(
                SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(host);
        configuration.setPort(port);
        return new JedisConnectionFactory(configuration);
    }

    @Bean
    public RedisTemplate<String, Idmap> redisTemplate() {
        RedisTemplate<String, Idmap> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }
}

Is there something I am missing of does @TimeToLive not work together with spring-redis caching.

Upvotes: 3

Views: 9971

Answers (1)

Sheel Prabhakar
Sheel Prabhakar

Reputation: 429

as per documentation

TimeToLive marks a single numeric property on aggregate root to be used for setting expirations in Redis. The annotated property supersedes any other timeout configuration.

RedisHash marks Objects as aggregate roots to be stored in a Redis hash.

Default ttl unit is second. @TimeToLive(unit = TimeUnit.SECONDS) you can use other values like MINUTES.

Use @RedisHash on BrandResponse. All the best

Below is my working code

@RedisHash
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BrandResponse {

    @TimeToLive(unit = TimeUnit.SECONDS )
    private Long ttl;

    @NotNull
    @Id
    private String id;
}

@Repository
public interface BrandRepository extends JpaRepository<BrandResponse, String> {
}

public interface CacheService {
    void add(BrandResponse response);

    boolean exists(String id);
}

@Service
public class RedisCacheServiceImpl implements CacheService {

    @Autowired
    private BrandRepository brandRepository;

    @Override
    public void add(BrandResponse response){
        this.brandRepository.save(response);
    }

    @Override
    public boolean exists(String id){
        return !this.brandRepository.findById(id).isEmpty();
    }
}

@Configuration
@EnableCaching
public class RedisCachingConfiguration {


    private String host ="192.168.1.59";

    private Integer port = 6379;

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(5))
                .disableCachingNullValues()
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }

    @Bean
    JedisConnectionFactory jedisConnectionFactory() {
        JedisConnectionFactory jedisConFactory
                = new JedisConnectionFactory();
        jedisConFactory.setHostName(host);
        jedisConFactory.setPort(port);
        return jedisConFactory;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        return template;
    }
}

I used two data sources one for Redis another for db

@SpringBootApplication
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP,
basePackages = {"com.c4c.authn.core.repository.redis"})
@EnableJpaRepositories(basePackages = {"com.c4c.authn.core.repository.db"})
public class AuthApplication {

    public static void main(String[] args) {

        SpringApplication.run(AuthApplication.class, args);
    }

}

Unit test

public class RedisCacheServiceImplTest extends BaseServiceTest {
    @Autowired
    private CacheService cacheService;

    @Test
    public void test_add_ok() throws InterruptedException {
        this.cacheService.add(new BrandResponse(5l, "ID1"));
        assertTrue(this.cacheService.exists("ID1"));

        Thread.sleep(6000);

        assertFalse(this.cacheService.exists("ID1"));
    }
}

Upvotes: 2

Related Questions