Skip to content

Valkey Cache

Spring Data Valkey provides an implementation of Spring Framework’s Cache Abstraction in the io.valkey.springframework.data.cache package. To use Valkey as a backing implementation, add io.valkey.springframework.data.cache.ValkeyCacheManager to your configuration, as follows:

@Bean
public ValkeyCacheManager cacheManager(ValkeyConnectionFactory connectionFactory) {
return ValkeyCacheManager.create(connectionFactory);
}

ValkeyCacheManager behavior can be configured with io.valkey.springframework.data.cache.ValkeyCacheManager$ValkeyCacheManagerBuilder, letting you set the default io.valkey.springframework.data.cache.ValkeyCacheManager, transaction behavior, and predefined caches.

ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(connectionFactory)
.cacheDefaults(ValkeyCacheConfiguration.defaultCacheConfig())
.transactionAware()
.withInitialCacheConfigurations(Collections.singletonMap("predefined",
ValkeyCacheConfiguration.defaultCacheConfig().disableCachingNullValues()))
.build();

As shown in the preceding example, ValkeyCacheManager allows custom configuration on a per-cache basis.

The behavior of io.valkey.springframework.data.cache.ValkeyCache created by io.valkey.springframework.data.cache.ValkeyCacheManager is defined with ValkeyCacheConfiguration. The configuration lets you set key expiration times, prefixes, and ValkeySerializer implementations for converting to and from the binary storage format, as shown in the following example:

ValkeyCacheConfiguration cacheConfiguration = ValkeyCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(1))
.disableCachingNullValues();

io.valkey.springframework.data.cache.ValkeyCacheManager defaults to a lock-free io.valkey.springframework.data.cache.ValkeyCacheWriter for reading and writing binary values. Lock-free caching improves throughput. The lack of entry locking can lead to overlapping, non-atomic commands for the Cache putIfAbsent and clean operations, as those require multiple commands to be sent to Valkey. The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key, which leads to additional requests and potential command wait times.

Locking applies on the cache level, not per cache entry.

It is possible to opt in to the locking behavior as follows:

ValkeyCacheManager cacheManager = ValkeyCacheManager
.builder(ValkeyCacheWriter.lockingValkeyCacheWriter(connectionFactory))
.cacheDefaults(ValkeyCacheConfiguration.defaultCacheConfig())
...

By default, any key for a cache entry gets prefixed with the actual cache name followed by two colons. This behavior can be changed to a static as well as a computed prefix.

The following example shows how to set a static prefix:

// static key prefix
ValkeyCacheConfiguration.defaultCacheConfig().prefixCacheNameWith("(͡° ᴥ ͡°)");

The following example shows how to set a computed prefix:

// computed key prefix
ValkeyCacheConfiguration.defaultCacheConfig()
.computePrefixWith(cacheName -> "¯\_(ツ)_/¯" + cacheName);

The cache implementation defaults to use KEYS and DEL to clear the cache. KEYS can cause performance issues with large keyspaces. Therefore, the default ValkeyCacheWriter can be created with a BatchStrategy to switch to a SCAN-based batch strategy. The SCAN strategy requires a batch size to avoid excessive Valkey command round trips:

ValkeyCacheManager cacheManager = ValkeyCacheManager
.builder(ValkeyCacheWriter.nonLockingValkeyCacheWriter(connectionFactory, BatchStrategies.scan(1000)))
.cacheDefaults(ValkeyCacheConfiguration.defaultCacheConfig())
...

SCAN is fully supported when using the Lettuce driver. Jedis supports SCAN only in non-clustered modes. Valkey GLIDE supports SCAN in standalone mode, with cluster mode support planned for a future release.

The following table lists the default settings for ValkeyCacheManager:

Table 1. ValkeyCacheManager defaults

SettingValue
Cache WriterNon-locking, KEYS batch strategy
Cache ConfigurationValkeyCacheConfiguration#defaultConfiguration
Initial CachesNone
Transaction AwareNo

The following table lists the default settings for ValkeyCacheConfiguration:

Table 2. ValkeyCacheConfiguration defaults

SettingValue
Key ExpirationNone
Cache nullYes
Prefix KeysYes
Default PrefixThe actual cache name
Key SerializerStringValkeySerializer
Value SerializerJdkSerializationValkeySerializer
Conversion ServiceDefaultFormattingConversionService with default cache key converters

Use ValkeyCacheManagerBuilder.enableStatistics() to collect local hits and misses through ValkeyCache#getStatistics(), returning a snapshot of the collected data.

The implementation of time-to-idle (TTI) as well as time-to-live (TTL) varies in definition and behavior even across different data stores.

In general:

  • time-to-live (TTL) expiration - TTL is only set and reset by a create or update data access operation. As long as the entry is written before the TTL expiration timeout, including on creation, an entry’s timeout will reset to the configured duration of the TTL expiration timeout. For example, if the TTL expiration timeout is set to 5 minutes, then the timeout will be set to 5 minutes on entry creation and reset to 5 minutes anytime the entry is updated thereafter and before the 5-minute interval expires. If no update occurs within 5 minutes, even if the entry was read several times, or even just read once during the 5-minute interval, the entry will still expire. The entry must be written to prevent the entry from expiring when declaring a TTL expiration policy.

  • time-to-idle (TTI) expiration - TTI is reset anytime the entry is also read as well as for entry updates, and is effectively and extension to the TTL expiration policy.

Spring Data Valkey’s Cache implementation supports time-to-live (TTL) expiration on cache entries. Users can either configure the TTL expiration timeout with a fixed Duration or a dynamically computed Duration per cache entry by supplying an implementation of the new ValkeyCacheWriter.TtlFunction interface.

If all cache entries should expire after a set duration of time, then simply configure a TTL expiration timeout with a fixed Duration, as follows:

ValkeyCacheConfiguration fiveMinuteTtlExpirationDefaults =
ValkeyCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5));

However, if the TTL expiration timeout should vary by cache entry, then you must provide a custom implementation of the ValkeyCacheWriter.TtlFunction interface:

enum MyCustomTtlFunction implements TtlFunction {
INSTANCE;
@Override
public Duration getTimeToLive(Object key, @Nullable Object value) {
// compute a TTL expiration timeout (Duration) based on the cache entry key and/or value
}
}

Then, you can either configure the fixed Duration or the dynamic, per-cache entry Duration TTL expiration on a global basis using:

Global fixed Duration TTL expiration timeout

ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(valkeyConnectionFactory)
.cacheDefaults(fiveMinuteTtlExpirationDefaults)
.build();

Or, alternatively:

Global, dynamically computed per-cache entry Duration TTL expiration timeout

ValkeyCacheConfiguration defaults = ValkeyCacheConfiguration.defaultCacheConfig()
.entryTtl(MyCustomTtlFunction.INSTANCE);
ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(valkeyConnectionFactory)
.cacheDefaults(defaults)
.build();

Of course, you can combine both global and per-cache configuration using:

Global fixed Duration TTL expiration timeout

ValkeyCacheConfiguration predefined = ValkeyCacheConfiguration.defaultCacheConfig()
.entryTtl(MyCustomTtlFunction.INSTANCE);
Map<String, ValkeyCacheConfiguration> initialCaches = Collections.singletonMap("predefined", predefined);
ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(valkeyConnectionFactory)
.cacheDefaults(fiveMinuteTtlExpirationDefaults)
.withInitialCacheConfigurations(initialCaches)
.build();

Valkey itself does not support the concept of true, time-to-idle (TTI) expiration. Still, using Spring Data Valkey’s Cache implementation, it is possible to achieve time-to-idle (TTI) expiration-like behavior.

The configuration of TTI in Spring Data Valkey’s Cache implementation must be explicitly enabled, that is, is opt-in. Additionally, you must also provide TTL configuration using either a fixed Duration or a custom implementation of the TtlFunction interface as described above in Valkey Cache Expiration.

For example:

@Configuration
@EnableCaching
class ValkeyConfiguration {
@Bean
ValkeyConnectionFactory valkeyConnectionFactory() {
// ...
}
@Bean
ValkeyCacheManager cacheManager(ValkeyConnectionFactory connectionFactory) {
ValkeyCacheConfiguration defaults = ValkeyCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.enableTimeToIdle();
return ValkeyCacheManager.builder(connectionFactory)
.cacheDefaults(defaults)
.build();
}
}

Because Valkey servers do not implement a proper notion of TTI, then TTI can only be achieved with Valkey commands accepting expiration options. In Valkey, the “expiration” is technically a time-to-live (TTL) policy. However, TTL expiration can be passed when reading the value of a key thereby effectively resetting the TTL expiration timeout, as is now the case in Spring Data Valkey’s Cache.get(key) operation.

ValkeyCache.get(key) is implemented by calling the Valkey GETEX command.