Implementing a Two-Level Cache System with Guava and Redis in Java
This article explains how to build a simple two‑level caching solution in Java, covering generic cache interfaces, FIFO/LRU/LFU algorithms, implementing local Guava cache and distributed Redis cache, configuring Spring Boot, handling expiration, and providing a unified cache builder for seamless use.
Overview
Cache is a direct way to improve system performance. The article walks through building a simple two‑level cache system using a generic cache interface, common eviction algorithms (FIFO, LFU, LRU), a local Guava cache and a distributed Redis cache.
Generic Cache Interface
Defines methods for get, set, remove and contains, with overloads supporting functional callbacks, additional parameters and expiration times.
package com.power.demo.cache.contract;
import java.util.function.Function;
/**
* 缓存提供者接口
*/
public interface CacheProviderService {
/**
* 查询缓存
* @param key 缓存键 不可为空
*/
<T extends Object> T get(String key);
/**
* 查询缓存
* @param key 缓存键 不可为空
* @param function 如没有缓存,调用该callable函数返回对象 可为空
*/
<T extends Object> T get(String key, Function<String, T> function);
/**
* 查询缓存
* @param key 缓存键 不可为空
* @param function 如没有缓存,调用该callable函数返回对象 可为空
* @param funcParm function函数的调用参数
*/
<T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm);
/**
* 查询缓存
* @param key 缓存键 不可为空
* @param function 如没有缓存,调用该callable函数返回对象 可为空
* @param expireTime 过期时间(单位:毫秒) 可为空
*/
<T extends Object> T get(String key, Function<String, T> function, Long expireTime);
/**
* 查询缓存
* @param key 缓存键 不可为空
* @param function 如没有缓存,调用该callable函数返回对象 可为空
* @param funcParm function函数的调用参数
* @param expireTime 过期时间(单位:毫秒) 可为空
*/
<T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime);
/**
* 设置缓存键值
* @param key 缓存键 不可为空
* @param obj 缓存值 不可为空
*/
<T extends Object> void set(String key, T obj);
/**
* 设置缓存键值
* @param key 缓存键 不可为空
* @param obj 缓存值 不可为空
* @param expireTime 过期时间(单位:毫秒) 可为空
*/
<T extends Object> void set(String key, T obj, Long expireTime);
/**
* 移除缓存
* @param key 缓存键 不可为空
*/
void remove(String key);
/**
* 是否存在缓存
* @param key 缓存键 不可为空
*/
boolean contains(String key);
}Local Cache with Guava
Shows how to add the Guava dependency, create a CacheBuilder with maximum size and expiration policies, and implement the CacheProviderService methods using Guava's Cache API.
package com.power.demo.cache.impl;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Maps;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
@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);
}
private static 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);
try {
lock.lock();
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;
}
@Override
public <T extends Object> T get(String key) {
return get(key, null, null, AppConst.CACHE_MINUTE);
}
@Override
public <T extends Object> T get(String key, Function<String, T> function) {
return get(key, function, key, AppConst.CACHE_MINUTE);
}
@Override
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
return get(key, function, funcParm, AppConst.CACHE_MINUTE);
}
@Override
public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
return get(key, function, key, expireTime);
}
@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);
Object obj = null;
try {
if (function == null) {
obj = cacheContainer.getIfPresent(key);
} else {
obj = cacheContainer.get(key, () -> function.apply(funcParm));
}
} catch (Exception e) { e.printStackTrace(); }
return (T) 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;
Cache<String, Object> cacheContainer = getCacheContainer(AppConst.CACHE_MINUTE);
cacheContainer.invalidate(key);
}
@Override
public boolean contains(String key) {
if (StringUtils.isEmpty(key)) return false;
Object obj = get(key);
return obj != null;
}
}Distributed Cache with Redis
Describes Redis as an in‑memory key‑value store, adds the Spring Boot starter dependency, configures connection properties, and implements CacheProviderService using RedisTemplate and ValueOperations.
package com.power.demo.cache.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.StringRedisSerializer;
/**
* Redis缓存配置类
*/
@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;
}
} package com.power.demo.cache.impl;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@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) {
return get(key, null, null, AppConst.CACHE_MINUTE);
}
@Override
public <T extends Object> T get(String key, Function<String, T> function) {
return get(key, function, key, AppConst.CACHE_MINUTE);
}
@Override
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
return get(key, function, funcParm, AppConst.CACHE_MINUTE);
}
@Override
public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
return get(key, function, key, expireTime);
}
@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);
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;
}
@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;
return get(key) != null;
}
private Long getExpireTime(Long expireTime) {
if (expireTime == null || expireTime < AppConst.CACHE_MINUTE/10) return AppConst.CACHE_MINUTE;
return expireTime;
}
}Cache Expiration and Versioning
The article discusses the difficulty of timely expiration, introduces a cache version key stored in Redis, and shows how to generate versioned cache keys to force refresh across all nodes.
Two‑Level Cache Builder
A PowerCacheBuilder aggregates the local and Redis providers, allows optional enabling via configuration flags, and offers unified get, set, remove and contains methods. It also handles cache version generation and resetting.
package com.power.demo.cache;
import com.google.common.collect.Lists;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import com.power.demo.common.AppField;
import com.power.demo.util.ConfigUtil;
import com.power.demo.util.PowerLogger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
/**
* 支持多缓存提供程序多级缓存的缓存帮助类
*/
@Configuration
@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
public class PowerCacheBuilder {
@Autowired
@Qualifier("localCacheService")
private CacheProviderService localCacheService;
@Autowired
@Qualifier("redisCacheService")
private CacheProviderService redisCacheService;
private static List<CacheProviderService> _listCacheProvider = Lists.newArrayList();
private static final Lock providerLock = new ReentrantLock();
private List<CacheProviderService> getCacheProviders() {
if (_listCacheProvider.size()>0) return _listCacheProvider;
try {
if (providerLock.tryLock(1000, TimeUnit.MILLISECONDS)) {
if (_listCacheProvider.size()>0) return _listCacheProvider;
String isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_LOCAL_CACHE);
if ("1".equalsIgnoreCase(isUseCache)) _listCacheProvider.add(localCacheService);
isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
if ("1".equalsIgnoreCase(isUseCache)) {
_listCacheProvider.add(redisCacheService);
resetCacheVersion();
}
PowerLogger.info("初始化缓存提供者成功,共有" + _listCacheProvider.size() + "个");
}
} catch (Exception e) { e.printStackTrace(); _listCacheProvider = Lists.newArrayList(); }
finally { providerLock.unlock(); }
return _listCacheProvider;
}
public <T extends Object> T get(String key) { T obj = null; for (CacheProviderService p : getCacheProviders()) { obj = p.get(key); if (obj!=null) return obj; } return obj; }
public <T extends Object> T get(String key, Function<String, T> function) { T obj = null; for (CacheProviderService p : getCacheProviders()) { if (obj==null) obj = p.get(key, function); else if (function!=null && obj!=null) p.get(key, function); if (function==null && obj!=null) return obj; } return obj; }
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) { T obj = null; for (CacheProviderService p : getCacheProviders()) { if (obj==null) obj = p.get(key, function, funcParm); else if (function!=null && obj!=null) p.get(key, function, funcParm); if (function==null && obj!=null) return obj; } return obj; }
public <T extends Object> T get(String key, Function<String, T> function, long expireTime) { T obj = null; for (CacheProviderService p : getCacheProviders()) { if (obj==null) obj = p.get(key, function, expireTime); else if (function!=null && obj!=null) p.get(key, function, expireTime); if (function==null && obj!=null) return obj; } return obj; }
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, long expireTime) { T obj = null; for (CacheProviderService p : getCacheProviders()) { if (obj==null) obj = p.get(key, function, funcParm, expireTime); else if (function!=null && obj!=null) p.get(key, function, funcParm, expireTime); if (function==null && obj!=null) return obj; } return obj; }
public <T extends Object> void set(String key, T obj) { for (CacheProviderService p : getCacheProviders()) p.set(key, obj); }
public <T extends Object> void set(String key, T obj, Long expireTime) { for (CacheProviderService p : getCacheProviders()) p.set(key, obj, expireTime); }
public void remove(String key) { if (StringUtils.isEmpty(key)) return; for (CacheProviderService p : getCacheProviders()) p.remove(key); }
public boolean contains(String key) { if (StringUtils.isEmpty(key)) return false; Object obj = get(key); return obj != null; }
public String getCacheVersion() { String version = ""; if (!checkUseRedisCache()) return version; return redisCacheService.get(AppConst.CACHE_VERSION_KEY); }
public String resetCacheVersion() { if (!checkUseRedisCache()) return ""; String version = String.valueOf(Math.abs(UUID.randomUUID().hashCode())); redisCacheService.set(AppConst.CACHE_VERSION_KEY, version); return version; }
public String generateVerKey(String key) { if (StringUtils.isEmpty(key)) return key; if (!checkUseRedisCache()) return key; String version = redisCacheService.get(AppConst.CACHE_VERSION_KEY); if (StringUtils.isEmpty(version)) return key; return String.format("%s_%s", key, version); }
private boolean checkUseRedisCache() { return "1".equalsIgnoreCase(ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE)); }
}Testing
A JUnit test demonstrates retrieving the cache version, generating versioned keys, storing and retrieving objects, and resetting the version to verify that keys are refreshed.
@Test
public void testCacheVerson() throws Exception {
String version = cacheBuilder.getCacheVersion();
System.out.println(String.format("当前缓存版本:%s", version));
String cacheKey = cacheBuilder.generateVerKey("goods778899");
GoodsVO goodsVO = new GoodsVO();
goodsVO.setGoodsId(UUID.randomUUID().toString());
goodsVO.setCreateTime(new Date());
goodsVO.setCreateDate(new DateTime(new Date()));
goodsVO.setGoodsType(1024);
goodsVO.setGoodsCode("123456789");
goodsVO.setGoodsName("我的测试商品");
cacheBuilder.set(cacheKey, goodsVO);
GoodsVO goodsVO1 = cacheBuilder.get(cacheKey);
Assert.assertNotNull(goodsVO1);
version = cacheBuilder.resetCacheVersion();
System.out.println(String.format("重置后的缓存版本:%s", version));
cacheKey = cacheBuilder.generateVerKey("goods112233");
cacheBuilder.set(cacheKey, goodsVO);
GoodsVO goodsVO2 = cacheBuilder.get(cacheKey);
Assert.assertNotNull(goodsVO2);
Assert.assertTrue("两个缓存对象的主键相同", goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId()));
}Conclusion
The article delivers a functional multi‑level caching framework for Java Spring Boot applications, combining fast in‑process Guava caching with scalable Redis storage, and outlines future topics such as scheduled tasks, MongoDB, Elasticsearch, and distributed file systems.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
