<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: P Myls</title>
    <description>The latest articles on DEV Community by P Myls (@prav_myls_3b497e599d01fde).</description>
    <link>https://dev.to/prav_myls_3b497e599d01fde</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2948439%2F847fab56-3bd1-4d1f-92d1-869193fd4e48.png</url>
      <title>DEV Community: P Myls</title>
      <link>https://dev.to/prav_myls_3b497e599d01fde</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prav_myls_3b497e599d01fde"/>
    <language>en</language>
    <item>
      <title>Cache mutex locks using Spring boot</title>
      <dc:creator>P Myls</dc:creator>
      <pubDate>Mon, 17 Mar 2025 00:09:37 +0000</pubDate>
      <link>https://dev.to/prav_myls_3b497e599d01fde/cache-mutex-locks-using-spring-boot-3fk9</link>
      <guid>https://dev.to/prav_myls_3b497e599d01fde/cache-mutex-locks-using-spring-boot-3fk9</guid>
      <description>&lt;h2&gt;
  
  
  What's a cache stampede?
&lt;/h2&gt;

&lt;p&gt;A Cache stampede occurs when multiple requests flood the database after cache invalidation/expiration. A mutex lock ensures only one call fetches data from the database at a time and other requests wait and get the cached result, preventing database overload.&lt;/p&gt;

&lt;p&gt;Spring Boot’s &lt;code&gt;@Cacheable&lt;/code&gt; works out of the box, but you have to override it to support mutex locks to prevent cache stampede.&lt;br&gt;
To override it, we need to extend &lt;code&gt;CacheInterceptor&lt;/code&gt; and implement custom locking logic.&lt;/p&gt;

&lt;p&gt;Customize your cache manager to use mutex&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package com.myapp.config;

import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.CacheInterceptor;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class MutexCacheInterceptor extends CacheInterceptor {
    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    public MutexCacheInterceptor(CacheManager cacheManager, RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate) {
        super();
        setCacheManager(cacheManager);
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected Object invokeOperation(CacheOperationInvocationContext&amp;lt;?&amp;gt; context) {
        String cacheName = context.getOperation().getCacheNames().iterator().next();
        String key = context.getKeyGenerator().generate(context.getTarget(), context.getMethod(), context.getArgs()).toString();

        Cache cache = getCacheManager().getCache(cacheName);
        if (cache == null) {
            return super.invokeOperation(context);
        }


        Cache.ValueWrapper valueWrapper = cache.get(key);
        if (valueWrapper != null) {
            return valueWrapper.get();
        }

        String lockKey = "lock:" + key;


        Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);

        if (Boolean.TRUE.equals(lockAcquired)) {
            try {         
                Object result = super.invokeOperation(context);
               cache.put(key, result);
                return result;
            } finally {

                redisTemplate.delete(lockKey);
            }
        } else {
            while (cache.get(key) == null) {
                try {
                    Thread.sleep(100); 
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }

            return cache.get(key).get();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring config&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package com.myapp.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheInterceptor;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheInterceptor cacheInterceptor(CacheManager cacheManager, RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate) {
        return new MutexCacheInterceptor(cacheManager, redisTemplate);
    }

@Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1)) // Set cache expiration time
            .disableCachingNullValues();

        return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(cacheConfig)
            .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache invalidation&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package com.myapp.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class CacheService {

    @CacheEvict(value = "users", key = "#userId") 
    public void invalidateCache(String userId) {
        log.info("Cache invalidated for userId: " + userId);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>springboot</category>
      <category>cache</category>
      <category>mutex</category>
      <category>redis</category>
    </item>
  </channel>
</rss>
