Master Two‑Level Java Cache: From Guava Local Cache to Redis Distributed Cache

This article walks through building a two‑level caching system in Java, starting with a generic cache interface, implementing a local Guava cache, then adding a Redis distributed cache, and finally combining them with versioned keys to achieve timely expiration and seamless cache switching.

Programmer DD
Programmer DD
Programmer DD
Master Two‑Level Java Cache: From Guava Local Cache to Redis Distributed Cache

Cache is one of the most effective ways to improve system performance, and mastering cache usage is a basic skill for excellent programmers.

1. Generic Cache Interface

1.1 Cache Basic Algorithms

FIFO (First In First Out) – remove the earliest entry when the cache is full.

LFU (Least Frequently Used) – remove entries that are accessed least often.

LRU (Least Recently Used) – remove the least recently accessed entry.

1.2 Interface Definition

Define a generic cache provider interface as follows:

package com.power.demo.cache.contract;

import java.util.function.Function;

/**
 * Cache provider interface
 */
public interface CacheProviderService {

    <T extends Object> T get(String key);

    <T extends Object> T get(String key, Function<String, T> function);

    <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm);

    <T extends Object> T get(String key, Function<String, T> function, Long expireTime);

    <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime);

    <T extends Object> void set(String key, T obj);

    <T extends Object> void set(String key, T obj, Long expireTime);

    void remove(String key);

    boolean contains(String key);
}

Note: this list excludes statistics, distributed lock, and increment functions.

2. Local Cache

Local cache (JVM level) can be implemented with Guava Cache.

2.1 What is Guava

Guava is a powerful Java library providing utilities, including caching.

2.2 Add Dependency

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
</dependency>

2.3 Implement Interface

/* Local cache provider (Guava Cache) */
@Configuration
@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
@Qualifier("localCacheService")
public class LocalCacheProviderImpl implements CacheProviderService {

    private static Map<String, Cache<String, Object>> _cacheMap = Maps.newConcurrentMap();

    static {
        Cache<String, Object> cacheContainer = CacheBuilder.newBuilder()
                .maximumSize(AppConst.CACHE_MAXIMUM_SIZE)
                .expireAfterWrite(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)
                .recordStats()
                .build();
        _cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE), cacheContainer);
    }

    @Override
    public <T extends Object> T get(String key) {
        T obj = get(key, null, null, AppConst.CACHE_MINUTE);
        return obj;
    }

    @Override
    public <T extends Object> T get(String key, Function<String, T> function) {
        T obj = get(key, function, key, AppConst.CACHE_MINUTE);
        return obj;
    }

    @Override
    public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
        T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
        return obj;
    }

    @Override
    public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
        T obj = get(key, function, key, expireTime);
        return obj;
    }

    @Override
    public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {
        if (StringUtils.isEmpty(key)) {
            return null;
        }
        expireTime = getExpireTime(expireTime);
        Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
        try {
            if (function == null) {
                obj = (T) cacheContainer.getIfPresent(key);
            } else {
                final Long cachedTime = expireTime;
                obj = (T) cacheContainer.get(key, () -> function.apply(funcParm));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }

    @Override
    public <T extends Object> void set(String key, T obj) {
        set(key, obj, AppConst.CACHE_MINUTE);
    }

    @Override
    public <T extends Object> void set(String key, T obj, Long expireTime) {
        if (StringUtils.isEmpty(key) || obj == null) {
            return;
        }
        expireTime = getExpireTime(expireTime);
        Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
        cacheContainer.put(key, obj);
    }

    @Override
    public void remove(String key) {
        if (StringUtils.isEmpty(key)) {
            return;
        }
        long expireTime = getExpireTime(AppConst.CACHE_MINUTE);
        Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
        cacheContainer.invalidate(key);
    }

    @Override
    public boolean contains(String key) {
        if (StringUtils.isEmpty(key)) {
            return false;
        }
        Object obj = get(key);
        return obj != null;
    }

    private static final Lock lock = new ReentrantLock();

    private Cache<String, Object> getCacheContainer(Long expireTime) {
        if (expireTime == null) {
            return null;
        }
        String mapKey = String.valueOf(expireTime);
        if (_cacheMap.containsKey(mapKey)) {
            return _cacheMap.get(mapKey);
        }
        lock.lock();
        try {
            Cache<String, Object> cacheContainer = CacheBuilder.newBuilder()
                    .maximumSize(AppConst.CACHE_MAXIMUM_SIZE)
                    .expireAfterWrite(expireTime, TimeUnit.MILLISECONDS)
                    .recordStats()
                    .build();
            _cacheMap.put(mapKey, cacheContainer);
            return cacheContainer;
        } finally {
            lock.unlock();
        }
    }

    private Long getExpireTime(Long expireTime) {
        if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {
            return AppConst.CACHE_MINUTE;
        }
        return expireTime;
    }
}

Guava Cache supports FIFO, LRU, and LFU policies. Different expiration times require separate CacheBuilder instances; the implementation handles this automatically.

3. Distributed Cache (Redis)

Redis is a high‑performance key‑value store widely used for caching.

3.1 Add Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2 Configure Redis

# Redis cache configuration
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=123321
spring.redis.timeout=300000ms
spring.redis.jedis.pool.max-active=512
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms

3.3 Redis Cache Provider Implementation

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        return RedisCacheManager.create(connectionFactory);
    }

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

RedisTemplate supports various serializers; using StringRedisSerializer for keys improves operability.

@Configuration
@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
@Qualifier("redisCacheService")
public class RedisCacheProviderImpl implements CacheProviderService {

    @Resource
    private RedisTemplate<Serializable, Object> redisTemplate;

    @Override
    public <T extends Object> T get(String key) {
        T obj = get(key, null, null, AppConst.CACHE_MINUTE);
        return obj;
    }

    @Override
    public <T extends Object> T get(String key, Function<String, T> function) {
        T obj = get(key, function, key, AppConst.CACHE_MINUTE);
        return obj;
    }

    @Override
    public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
        T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
        return obj;
    }

    @Override
    public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
        T obj = get(key, function, key, expireTime);
        return obj;
    }

    @Override
    public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {
        if (StringUtils.isEmpty(key)) {
            return null;
        }
        expireTime = getExpireTime(expireTime);
        try {
            ValueOperations<Serializable, Object> ops = redisTemplate.opsForValue();
            T obj = (T) ops.get(key);
            if (function != null && obj == null) {
                obj = function.apply(funcParm);
                if (obj != null) {
                    set(key, obj, expireTime);
                }
            }
            return obj;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public <T extends Object> void set(String key, T obj) {
        set(key, obj, AppConst.CACHE_MINUTE);
    }

    @Override
    public <T extends Object> void set(String key, T obj, Long expireTime) {
        if (StringUtils.isEmpty(key) || obj == null) {
            return;
        }
        expireTime = getExpireTime(expireTime);
        ValueOperations<Serializable, Object> ops = redisTemplate.opsForValue();
        ops.set(key, obj);
        redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS);
    }

    @Override
    public void remove(String key) {
        if (StringUtils.isEmpty(key)) {
            return;
        }
        redisTemplate.delete(key);
    }

    @Override
    public boolean contains(String key) {
        if (StringUtils.isEmpty(key)) {
            return false;
        }
        Object obj = get(key);
        return obj != null;
    }

    private Long getExpireTime(Long expireTime) {
        if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {
            return AppConst.CACHE_MINUTE;
        }
        return expireTime;
    }
}

Note: RedisTemplate’s default serializers are JdkSerializationRedisSerializer ; using StringRedisSerializer for keys and Jackson2JsonRedisSerializer for values is recommended, though deserialization issues may arise.

4. Cache “Timely” Expiration Issue

Real‑time expiration can be achieved by adding a version suffix to cache keys; resetting the version forces all related entries to become stale.

5. Two‑Level Cache Builder

A façade class PowerCacheBuilder combines local and Redis providers, supports versioned keys, and offers get/set/remove methods that automatically update both layers.

public class PowerCacheBuilder {

    @Autowired @Qualifier("localCacheService")
    private CacheProviderService localCacheService;

    @Autowired @Qualifier("redisCacheService")
    private CacheProviderService redisCacheService;

    // Initialization, get, set, remove, version handling methods ...
}

Unit tests demonstrate cache version reset and key generation.

References: http://ifeve.com/google-guava/ http://www.cnblogs.com/luochengqiuse/p/4640932.html
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendCacheredisSpring BootGuavatwo-level cache
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.