Spring Boot + Caffeine(本地缓存) + Redis(分布式缓存) 二级缓存
·
一、完整代码实现
步骤 1:引入 Maven 依赖(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>two-level-cache</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>two-level-cache</name>
<dependencies>
<!-- Spring Boot Web(提供接口测试) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis 分布式缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 连接池(提升Redis性能) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Caffeine 本地缓存(性能最优) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- FastJSON(序列化/反序列化) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<!-- Lombok(简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
步骤 2:配置文件(application.yml)
server:
port: 8080 # 服务端口
spring:
# Redis 配置
redis:
host: 127.0.0.1 # 替换为你的Redis地址
port: 6379
password: # 你的Redis密码(无则留空)
database: 0
lettuce:
pool:
max-active: 10 # 最大连接数
max-idle: 5 # 最大空闲连接
min-idle: 2 # 最小空闲连接
max-wait: 3000ms # 连接等待时间
# 自定义缓存配置
cache:
# Caffeine本地缓存配置
caffeine:
initial-capacity: 100 # 初始缓存大小
maximum-size: 1000 # 最大缓存数量
expire-after-write: 5m # 写入后5分钟过期(本地缓存过期时间短于Redis)
# Redis缓存配置
redis:
expire-after-write: 1h # 写入后1小时过期
null-value-expire: 60s # 空值缓存60秒(防穿透)
步骤 3:核心配置类(缓存配置)
package com.example.twolevelcache.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* 二级缓存配置类:初始化Caffeine本地缓存和Redis模板
*/
@Configuration
public class CacheConfig {
// 从配置文件读取Caffeine参数
@Value("${cache.caffeine.initial-capacity}")
private int initialCapacity;
@Value("${cache.caffeine.maximum-size}")
private long maximumSize;
@Value("${cache.caffeine.expire-after-write}")
private long expireAfterWrite;
/**
* 初始化Caffeine本地缓存
*/
@Bean
public com.github.benmanes.caffeine.cache.Cache<String, String> caffeineCache() {
return Caffeine.newBuilder()
.initialCapacity(initialCapacity) // 初始容量
.maximumSize(maximumSize) // 最大缓存数
.expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES) // 过期时间
.recordStats() // 记录缓存统计(可选)
.build();
}
/**
* 二级缓存管理器(封装本地+Redis缓存逻辑)
*/
@Bean
public TwoLevelCacheManager twoLevelCacheManager(StringRedisTemplate stringRedisTemplate,
com.github.benmanes.caffeine.cache.Cache<String, String> caffeineCache) {
return new TwoLevelCacheManager(stringRedisTemplate, caffeineCache);
}
}
步骤 4:二级缓存核心实现类
package com.example.twolevelcache.config;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 二级缓存管理器:封装本地缓存(Caffeine)+ Redis缓存的核心逻辑
*/
@Slf4j
@Component
public class TwoLevelCacheManager {
private final StringRedisTemplate stringRedisTemplate;
private final com.github.benmanes.caffeine.cache.Cache<String, String> caffeineCache;
// Redis缓存过期时间
@Value("${cache.redis.expire-after-write}")
private long redisExpireTime;
// 空值缓存过期时间
@Value("${cache.redis.null-value-expire}")
private long nullValueExpireTime;
public TwoLevelCacheManager(StringRedisTemplate stringRedisTemplate,
com.github.benmanes.caffeine.cache.Cache<String, String> caffeineCache) {
this.stringRedisTemplate = stringRedisTemplate;
this.caffeineCache = caffeineCache;
}
// ==================== 读取缓存 ====================
/**
* 读取二级缓存(先本地,后Redis)
*/
public <T> T get(String key, Class<T> clazz) {
// 1. 先查本地缓存
String value = caffeineCache.getIfPresent(key);
if (value != null) {
log.info("本地缓存命中,key:{}", key);
// 空值处理
if (value.isEmpty()) {
return null;
}
return JSON.parseObject(value, clazz);
}
log.info("本地缓存未命中,key:{}", key);
// 2. 再查Redis缓存
value = stringRedisTemplate.opsForValue().get(key);
if (value != null) {
log.info("Redis缓存命中,key:{}", key);
// 将Redis数据回写到本地缓存
caffeineCache.put(key, value);
// 空值处理
if (value.isEmpty()) {
return null;
}
return JSON.parseObject(value, clazz);
}
log.info("Redis缓存未命中,key:{}", key);
// 3. 两级缓存都未命中
return null;
}
// ==================== 写入缓存 ====================
/**
* 写入二级缓存(本地+Redis)
*/
public <T> void put(String key, T value) {
if (value == null) {
// 缓存空值(防穿透)
caffeineCache.put(key, "");
stringRedisTemplate.opsForValue().set(key, "", nullValueExpireTime, TimeUnit.SECONDS);
return;
}
String jsonValue = JSON.toJSONString(value);
// 1. 写入本地缓存
caffeineCache.put(key, jsonValue);
// 2. 写入Redis缓存(设置过期时间)
stringRedisTemplate.opsForValue().set(key, jsonValue, redisExpireTime, TimeUnit.HOURS);
log.info("写入二级缓存成功,key:{}", key);
}
// ==================== 删除缓存 ====================
/**
* 删除二级缓存(本地+Redis)
*/
public void delete(String key) {
// 1. 删除本地缓存
caffeineCache.invalidate(key);
// 2. 删除Redis缓存
stringRedisTemplate.delete(key);
log.info("删除二级缓存成功,key:{}", key);
}
}
步骤 5:实体类(示例:用户)
package com.example.twolevelcache.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
private String phone;
}
步骤 6:模拟数据库 DAO 层
package com.example.twolevelcache.dao;
import com.example.twolevelcache.entity.User;
import org.springframework.stereotype.Repository;
import java.util.HashMap;
import java.util.Map;
/**
* 模拟数据库操作(实际项目替换为MyBatis/MyBatis-Plus/JPA)
*/
@Repository
public class UserDAO {
// 模拟数据库表
private static final Map<Long, User> USER_DB = new HashMap<>();
// 初始化测试数据
static {
USER_DB.put(1L, new User(1L, "张三", 25, "13800138000"));
USER_DB.put(2L, new User(2L, "李四", 30, "13900139000"));
USER_DB.put(3L, new User(3L, "王五", 28, "13700137000"));
}
/**
* 根据ID查询用户
*/
public User getById(Long id) {
// 模拟数据库查询耗时(50ms)
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return USER_DB.get(id);
}
/**
* 更新用户
*/
public void update(User user) {
USER_DB.put(user.getId(), user);
}
/**
* 删除用户
*/
public void deleteById(Long id) {
USER_DB.remove(id);
}
}
步骤 7:业务服务层
package com.example.twolevelcache.service;
import com.example.twolevelcache.config.TwoLevelCacheManager;
import com.example.twolevelcache.dao.UserDAO;
import com.example.twolevelcache.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 用户业务服务:封装二级缓存的核心使用逻辑
*/
@Slf4j
@Service
public class UserService {
@Resource
private UserDAO userDAO;
@Resource
private TwoLevelCacheManager twoLevelCacheManager;
// 缓存key前缀(避免key冲突)
private static final String CACHE_KEY_PREFIX = "user:";
/**
* 查询用户(二级缓存核心逻辑)
*/
public User getUserById(Long userId) {
String cacheKey = CACHE_KEY_PREFIX + userId;
// 1. 查二级缓存
User user = twoLevelCacheManager.get(cacheKey, User.class);
if (user != null) {
return user;
}
// 2. 缓存未命中:查数据库
user = userDAO.getById(userId);
// 3. 回写二级缓存(包括空值)
twoLevelCacheManager.put(cacheKey, user);
return user;
}
/**
* 更新用户(先更库,再删缓存)
*/
public void updateUser(User user) {
// 1. 先更新数据库
userDAO.update(user);
// 2. 删除二级缓存(而非更新,避免并发脏数据)
String cacheKey = CACHE_KEY_PREFIX + user.getId();
twoLevelCacheManager.delete(cacheKey);
}
/**
* 删除用户(先删库,再删缓存)
*/
public void deleteUser(Long userId) {
// 1. 先删除数据库数据
userDAO.deleteById(userId);
// 2. 删除二级缓存
String cacheKey = CACHE_KEY_PREFIX + userId;
twoLevelCacheManager.delete(cacheKey);
}
}
步骤 8:控制器(接口测试)
package com.example.twolevelcache.controller;
import com.example.twolevelcache.entity.User;
import com.example.twolevelcache.service.UserService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 用户接口控制器:提供测试接口
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
/**
* 查询用户
* 示例:http://localhost:8080/user/1
*/
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
/**
* 更新用户
* 示例:POST http://localhost:8080/user
* Body:{"id":1,"name":"张三-update","age":26,"phone":"13800138001"}
*/
@PostMapping
public String updateUser(@RequestBody User user) {
userService.updateUser(user);
return "更新成功";
}
/**
* 删除用户
* 示例:DELETE http://localhost:8080/user/1
*/
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return "删除成功";
}
}
步骤 9:启动类
package com.example.twolevelcache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TwoLevelCacheApplication {
public static void main(String[] args) {
SpringApplication.run(TwoLevelCacheApplication.class, args);
}
}
二、测试验证
- 启动项目:确保 Redis 已启动,运行
TwoLevelCacheApplication - 第一次访问:
http://localhost:8080/user/1- 日志会显示:本地缓存未命中 → Redis 缓存未命中 → 查数据库 → 写入二级缓存
- 响应耗时约 50ms(模拟数据库耗时)
- 第二次访问:
http://localhost:8080/user/1- 日志会显示:本地缓存命中
- 响应耗时 < 1ms(极致速度)
- 更新用户:POST 请求更新用户后,再次查询会重新查库并更新缓存
- 删除用户:DELETE 请求后,缓存会被清空,再次查询返回 null
三、生产级优化建议(代码已预留扩展点)
- 本地缓存更新通知:多实例部署时,可通过 Redis Pub/Sub 广播缓存删除消息,保证所有实例本地缓存一致
- 分布式锁:热点 key 查询时加 Redis 锁,避免缓存击穿
- 缓存统计:Caffeine 的
recordStats()可统计缓存命中率,便于优化缓存策略 - Redis 集群:生产环境替换为 Redis 主从 + 哨兵 / Cluster,保证高可用
总结
- 核心流程:二级缓存遵循「本地缓存优先 → Redis 缓存兜底 → 数据库最终源」的查询逻辑,更新 / 删除遵循「先操作数据库,再删除缓存」的原则;
- 关键配置:本地缓存(Caffeine)过期时间短于 Redis,避免本地缓存数据不一致;
- 防坑要点:缓存空值防穿透、删除缓存而非更新缓存防脏数据、本地缓存 + Redis 组合兼顾性能与分布式一致性。
更多推荐


所有评论(0)