spring-bootazureredisslidingexpiration

getAndExpire , GETEX Error in Spring boot Data Redis package for Azure Cache for Redis Integration


I am working on Springboot service integration with Azure Cache for Redis for high performant application. I want to ensure that when Cache entry is stored with either just string data type or with hashmap data type , the cache entry has sliding expiration. So for that I want to use SetAndExpire or GetAndExpire command redis. Azure Cache redis version is 6.0 and rest of the package detials are in pom.xml. However I am getting error in below two command executed in below java applciation

LOGGER.info("Return the value from the cache: {}", ops.getAndExpire(key, duration)); String s = ops.getAndExpire(key, 15000, TimeUnit.MILLISECONDS);

Below there are 3 snippest. First is Error , 2nd is Springboot code and 3rd one is pom.xml.

2023-04-03 23:56:25.961  INFO 32460 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-04-03 23:56:25.976  INFO 32460 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 3.645 seconds (JVM running for 4.332)
2023-04-03 23:56:26.898  INFO 32460 --- [           main] com.example.demo.DemoApplication         : Return the value from the cache: Hello World
2023-04-03 23:56:27.136  INFO 32460 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-04-03 23:56:27.174 ERROR 32460 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:771) ~[spring-boot-2.7.10.jar:2.7.10]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:752) ~[spring-boot-2.7.10.jar:2.7.10]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) ~[spring-boot-2.7.10.jar:2.7.10]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-2.7.10.jar:2.7.10]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-2.7.10.jar:2.7.10]
at com.example.demo.DemoApplication.main(DemoApplication.java:39) ~[classes/:na]
Caused by: org.springframework.dao.InvalidDataAccessApiUsageException: ERR unknown command `GETEX`, with args beginning with: `testkey`, `ex`, `15`, ; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR unknown command `GETEX`, with args beginning with: `testkey`, `ex`, `15`, 
at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:69) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:42) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisConnection.convertJedisAccessException(JedisConnection.java:192) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisConnection.doWithJedis(JedisConnection.java:827) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisConnection.doInvoke(JedisConnection.java:165) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisConnection.lambda$new$0(JedisConnection.java:74) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisInvoker$Synchronizer.invoke(JedisInvoker.java:1018) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisInvoker.just(JedisInvoker.java:130) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisStringCommands.getEx(JedisStringCommands.java:84) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.DefaultedRedisConnection.getEx(DefaultedRedisConnection.java:286) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.DefaultStringRedisConnection.getEx(DefaultStringRedisConnection.java:468) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.core.DefaultValueOperations$4.inRedis(DefaultValueOperations.java:109) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:61) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:191) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:97) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.core.DefaultValueOperations.getAndExpire(DefaultValueOperations.java:105) ~[spring-data-redis-2.7.10.jar:2.7.10]
at com.example.demo.DemoApplication.run(DemoApplication.java:59) ~[classes/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768) ~[spring-boot-2.7.10.jar:2.7.10]
... 5 common frames omitted
Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR unknown command `GETEX`, with args beginning with: `testkey`, `ex`, `15`, 
at redis.clients.jedis.Protocol.processError(Protocol.java:142) ~[jedis-3.8.0.jar:na]
at redis.clients.jedis.Protocol.process(Protocol.java:176) ~[jedis-3.8.0.jar:na]
at redis.clients.jedis.Protocol.read(Protocol.java:230) ~[jedis-3.8.0.jar:na]
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:352) ~[jedis-3.8.0.jar:na]
at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java:289) ~[jedis-3.8.0.jar:na]
at redis.clients.jedis.BinaryJedis.getEx(BinaryJedis.java:465) ~[jedis-3.8.0.jar:na]
at org.springframework.data.redis.connection.jedis.JedisInvoker.lambda$just$5(JedisInvoker.java:130) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisConnection.lambda$doInvoke$2(JedisConnection.java:181) ~[spring-data-redis-2.7.10.jar:2.7.10]
at org.springframework.data.redis.connection.jedis.JedisConnection.doWithJedis(JedisConnection.java:824) ~[spring-data-redis-2.7.10.jar:2.7.10]
... 20 common frames omitted




package com.example.demo;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
//import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.redis.core.ValueOperations;

import ch.qos.logback.core.pattern.Converter;
import ch.qos.logback.core.util.Duration;


@SpringBootApplication
//@ConfigurationProperties
public class DemoApplication implements CommandLineRunner {

  private static final Logger LOGGER = LoggerFactory.getLogger(DemoApplication.class);

  private java.time.Duration duration = java.time.Duration.ofSeconds(15);

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

  @Autowired
  private StringRedisTemplate template;


  @Override
  //@ConfigurationProperties(prefix = "spring.redis")
  public void run(String...args) {
    ValueOperations < String, String > ops = this.template.opsForValue();



    String key = "testkey";
    if (!this.template.hasKey(key)) {
      ops.set(key, "Hello World", duration);
      LOGGER.info("Add a key is done");
    }
    LOGGER.info("Return the value from the cache: {}", ops.get(key));
    LOGGER.info("Return the value from the cache: {}", ops.getAndExpire(key, duration));
    String s = ops.getAndExpire(key, 15000, TimeUnit.MILLISECONDS);
    //hash functions
    HashRepository hashrepo = new HashRepository(template);

    String cacheKey = "user";

    if (!this.template.hasKey(cacheKey)) {

      HashMap < String, String > hashmap = new HashMap < String, String > ();
      hashmap.put("rimno", "1");
      hashmap.put("accountInfo", "<accounts><accountno>1</accountno></accounts>");

      hashrepo.addHashMap(cacheKey, hashmap);
      LOGGER.info("Add a hashmap is done");
    } else {
      hashrepo.updateHashValue(cacheKey, "name", "mynane1");
      hashrepo.updateHashValue(cacheKey, "accountInfo", "<accounts><accountno>2</accountno></accounts>");
      LOGGER.info(String.format("retrieved accountInfo with value %s", hashrepo.getHashValue(cacheKey, "accountInfo")));
    }

    LOGGER.info("Return the value from the cache: {}", hashrepo.getHashMap(cacheKey));



  }





}
class HashRepository {

  final Logger logger = LoggerFactory.getLogger(HashRepository.class);
  private HashOperations < String, String, String > hashOperations;
  private StringRedisTemplate template;

  public HashRepository(StringRedisTemplate template) {
    this.template = template;
    this.hashOperations = template.opsForHash();

  }

  public void addHashMap(String cacheKey, Map < String, String > data) {
    hashOperations.putAll(cacheKey, data);
    logger.info(String.format("Data with cachekey %s saved", cacheKey));
  }

  public void updateHashValue(String cacheKey, String hashKey, String data) {
    hashOperations.put(cacheKey, hashKey, data);
    logger.info(String.format("Data with hashkey %s update", hashKey));
  }

  public String getHashValue(String cacheKey, String hashKey) {
    String s = (String) hashOperations.get(cacheKey, hashKey);
    logger.info(String.format("Data retrieved with hashkey %s", s));
    return s;
  }

  public Map < String, String > getHashMap(String cacheKey) {
    return hashOperations.entries(cacheKey);
  }

  public void deleteHashMap(String cacheKey) {
    template.delete(cacheKey);
    logger.info(String.format("Map with cacheKey %s deleted", cacheKey));
  }
}


@Configuration
class MyConfig {
  @Autowired
  private Environment environment;

  @Bean
  JedisConnectionFactory redisConnectionFactory() {
    RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(
      "xyz.redis.cache.windows.net",
      6379
    );
    configuration.setPassword(environment.getProperty("spring.redis.password"));

    return new JedisConnectionFactory(configuration);
  }

  @Bean
  StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {

    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
  }
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.10</version>
    <relativePath/>
    <!-- lookup parent from repository -->
  </parent>
  <groupId>com.example</groupId>
  <artifactId>demo</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>demo</name>
  <description>Demo project for Spring Boot With Redis Cache</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!--<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency> -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>io.projectreactor</groupId>
      <artifactId>reactor-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>


Solution

  • GETEX is a new command only available from Redis 6.2 and above.

    The best approach is to upgrade, 2nd best would be to wrap a GET and an EXPIRE command in a single [transaction][1].

    MULTI
    GET <key>
    EXPIRE <key> <seconds>
    EXEC