一、前言
在高性能服务架构设计里,缓存是关键环节。常规做法是将热点数据存于 Redis/MemCache 等远程缓存,缓存未命中时再查数据库,以此提升访问速度、降低数据库压力 。
随着发展,架构有了改进,部分场景下单纯远程缓存不够,需结合本地缓存(如 Guava cache、Caffeine ),形成本地缓存(一级缓存) + 远程缓存(二级缓存)的两级缓存架构,进一步提升程序响应与服务性能,其基础访问流程如下(暂不考虑并发等复杂问题):
此处插入原流程图片,替换路径为合理展示形式,如保留`./assets/2.jpeg`,若为线上展示需处理为可访问链接
二、优点与待解决问题
(一)优势
相比单纯远程缓存,两级缓存有以下优势:
- 访问速度快:本地缓存基于内存,对变更 / 实时性要求低的数据,存入本地可大幅提升访问速度。
- 减少网络开销:降低与远程缓存的数据交互,减少网络 I/O 耗时,节省网络通信成本。
(二)需解决问题
设计时要考虑诸多问题,核心是数据一致性:
- 两级缓存与数据库需同步,数据修改时,本地、远程缓存应同步更新。
- 分布式环境下,一级缓存节点间存在一致性问题。一个节点修改本地缓存后,需通知其他节点刷新,否则会读过期数据,可借 Redis 发布 / 订阅功能解决。
此外,缓存过期时间、策略及多线程访问等问题也需关注,本文先聚焦两级缓存的代码实现。
三、准备工作
(一)技术选型
整合Caffeine(最强本地缓存) 为一级缓存、Redis(性能优) 为二级缓存,搭建 SpringBoot 项目,引入依赖:
XML
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.1</version>
</dependency>
(二)Redis 配置
配置RedisTemplate
,处理连接工厂、序列化等:
java
/**
* Redis缓存配置类
* @author ZhuZiKai
* @date 2022/3/31 0014
*/
@Configuration
@EnableCaching
@EnableAspectJAutoProxy
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public FastJsonRedisSerializer fastJson2JsonRedisSerializer() {
return new FastJsonRedisSerializer(Object.class);
}
@Bean
public StringRedisSerializer StringRedisSerializer() {
return new StringRedisSerializer();
}
@Bean("redis")
@Primary
public RedisTemplate initRedisTemplate(RedisConnectionFactory redisConnectionFactory,
StringRedisSerializer stringRedisSerializer,
FastJsonRedisSerializer fastJson2JsonRedisSerializer) throws Exception {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(fastJson2JsonRedisSerializer);
redisTemplate.setDefaultSerializer(stringRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
四、代码实战:两级缓存管理实现
(一)V1.0 版本:Spring 缓存注解方案
Spring 提供CacheManager
接口与注解(@Cacheable
/@CachePut
/@CacheEvict
),简化缓存操作。
1. 注解说明
@Cacheable
:按 key 查缓存,命中则直接返回;未命中则执行方法,结果入缓存。@CachePut
:无论缓存是否存在,执行方法并强制更新缓存。@CacheEvict
:执行方法后,移除对应缓存数据。
2. 配置CacheManager
java
@Configuration
public class CacheManagerConfig {
@Bean
public CacheManager cacheManager(){
CaffeineCacheManager cacheManager=new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(128)
.maximumSize(1024)
.expireAfterWrite(60, TimeUnit.SECONDS));
return cacheManager;
}
}
启动类添加@EnableCaching
,开启缓存支持。
3. Service 层改造
- 查询方法 :用
@Cacheable
,保留业务与 Redis 操作逻辑,Caffeine 缓存交 Spring 管理:
java
@Cacheable(value = "order",key = "#id")
public Order getOrderById(Long id) {
String key= CacheConstant.ORDER + id;
// 查Redis
Object obj = redisTemplate.opsForValue().get(key);
if (Objects.nonNull(obj)){
log.info("get data from redis");
return (Order) obj;
}
// Redis无则查DB
log.info("get data from database");
Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
.eq(Order::getId, id));
// 结果入Redis
redisTemplate.opsForValue().set(key,myOrder,120, TimeUnit.SECONDS);
return myOrder;
}
- 更新方法 :用
@CachePut
,移除手动更新 Caffeine 操作(由 Spring 管理):
java
@CachePut(cacheNames = "order",key = "#order.id")
public Order updateOrder(Order order) {
log.info("update order data");
orderMapper.updateById(order);
// 更新Redis
redisTemplate.opsForValue().set(CacheConstant.ORDER + order.getId(),
order, 120, TimeUnit.SECONDS);
return order;
}
注意:方法需定义返回值,否则可能缓存空对象,影响后续查询。
- 删除方法 :用
@CacheEvict
,仅处理 Redis 缓存删除:
java
@CacheEvict(cacheNames = "order",key = "#id")
public void deleteOrder(Long id) {
log.info("delete order");
orderMapper.deleteById(id);
redisTemplate.delete(CacheConstant.ORDER + id);
}
此方案将本地缓存交 Spring 管理,Redis 缓存手动操作,降低业务入侵性。
(二)V2.0 版本:自定义注解 + 切面方案
进一步解耦,通过自定义注解与切面,实现对业务无入侵的缓存管理。
1. 自定义注解
定义@DoubleCache
,支持缓存存取、删除,适配 SpringEL 表达式:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {
String cacheName();
String key(); // 支持SpringEL
long l2TimeOut() default 120;
CacheType type() default CacheType.FULL;
}
// 缓存操作类型枚举
public enum CacheType {
FULL, // 存取
PUT, // 只存
DELETE // 删除
}
2. SpringEL 表达式解析
解析注解中key
的表达式,适配方法参数:
java
public static String parse(String elString, TreeMap<String,Object> map){
elString=String.format("#{%s}",elString);
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
map.entrySet().forEach(entry->
context.setVariable(entry.getKey(),entry.getValue())
);
Expression expression = parser.parseExpression(elString, new TemplateParserContext());
return expression.getValue(context, String.class);
}
3. 配置 Caffeine 缓存
java
@Configuration
public class CaffeineConfig {
@Bean
public Cache<String,Object> caffeineCache(){
return Caffeine.newBuilder()
.initialCapacity(128)// 初始容量
.maximumSize(1024)// 最大容量
.expireAfterWrite(60, TimeUnit.SECONDS)// 过期时间
.build();
}
}
4. 切面实现缓存逻辑
通过切面拦截@DoubleCache
注解,统一处理缓存读写、删除:
java
@Slf4j
@Component
@Aspect
@AllArgsConstructor
public class CacheAspect {
private final Cache<String,Object> cache;
private final RedisTemplate redisTemplate;
@Pointcut("@annotation(com.cn.dc.annotation.DoubleCache)")
public void cacheAspect() {}
@Around("cacheAspect()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 解析方法参数,封装SpringEL上下文
String[] paramNames = signature.getParameterNames();
Object[] args = point.getArgs();
TreeMap<String, Object> treeMap = new TreeMap<>();
for (int i = 0; i < paramNames.length; i++) {
treeMap.put(paramNames[i],args[i]);
}
DoubleCache annotation = method.getAnnotation(DoubleCache.class);
String elResult = ElParser.parse(annotation.key(), treeMap);
String realKey = annotation.cacheName() + CacheConstant.COLON + elResult;
// 按操作类型处理
if (annotation.type()== CacheType.PUT){
Object object = point.proceed();
redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);
cache.put(realKey, object);
return object;
} else if (annotation.type()== CacheType.DELETE){
redisTemplate.delete(realKey);
cache.invalidate(realKey);
return point.proceed();
}
// 读写流程:先查Caffeine
Object caffeineCache = cache.getIfPresent(realKey);
if (Objects.nonNull(caffeineCache)) {
log.info("get data from caffeine");
return caffeineCache;
}
// 再查Redis
Object redisCache = redisTemplate.opsForValue().get(realKey);
if (Objects.nonNull(redisCache)) {
log.info("get data from redis");
cache.put(realKey, redisCache);
return redisCache;
}
// 都无则查数据库,结果入缓存
log.info("get data from database");
Object object = point.proceed();
if (Objects.nonNull(object)){
redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);
cache.put(realKey, object);
}
return object;
}
}
5. Service 层简化
只需添加自定义注解,专注业务逻辑:
java
@DoubleCache(cacheName = "order", key = "#id",type = CacheType.FULL)
public Order getOrderById(Long id) {
return orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id));
}
@DoubleCache(cacheName = "order",key = "#order.id",type = CacheType.PUT)
public Order updateOrder(Order order) {
orderMapper.updateById(order);
return order;
}
@DoubleCache(cacheName = "order",key = "#id",type = CacheType.DELETE)
public void deleteOrder(Long id) {
orderMapper.deleteById(id);
}
五、总结与思考
本文按业务入侵程度,介绍两种两级缓存实现:
- V1.0:借 Spring 缓存注解,半入侵式管理,本地缓存交 Spring,Redis 手动操作。
- V2.0:自定义注解 + 切面,完全解耦缓存逻辑,业务代码更简洁。
实际项目中,是否用两级缓存需结合业务。若 Redis 满足需求,无需强制引入,因实际使用涉及并发、事务回滚、缓存策略适配等复杂问题。需权衡数据特性(如哪些适合一级 / 二级缓存)、一致性保障成本,再决定架构方案,让缓存真正成为服务性能的 "助推器" 而非 "负担" 。