12 KiB
12 KiB
缓存系统指南
概述
本项目采用多级缓存架构,将原有的Redis缓存替换为基于Caffeine的本地缓存,以提升性能并减少对外部Redis服务的依赖。
缓存架构
多级缓存设计
应用层缓存
├── Caffeine缓存 (本地内存缓存)
│ ├── 用户缓存
│ ├── 角色缓存
│ ├── 岗位缓存
│ └── 登录用户缓存
├── Guava缓存 (配置缓存)
│ ├── 系统配置
│ ├── 字典数据
│ └── 部门数据
└── Redis缓存 (分布式缓存,可选)
├── 会话缓存
├── 验证码缓存
└── 分布式锁
Caffeine缓存迁移
1. 迁移概述
本项目已将原有的Redis缓存替换为基于Caffeine的本地缓存,主要变更包括:
- 新增依赖: Caffeine 2.9.3
- 新增组件: AbstractCaffeineCacheTemplate、CaffeineCacheService
- 缓存中心更新: CacheCenter使用Caffeine缓存替换Redis缓存
2. 缓存配置
2.1 Caffeine缓存配置
@Configuration
public class CaffeineCacheConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
// 初始容量
.initialCapacity(128)
// 最大容量
.maximumSize(1024)
// 写入后过期时间
.expireAfterWrite(60, TimeUnit.MINUTES)
// 写入后刷新时间
.refreshAfterWrite(5, TimeUnit.MINUTES)
// 并发级别
.concurrencyLevel(16)
// 使用软引用
.softValues()
.build();
}
}
2.2 缓存服务配置
@Service
public class CaffeineCacheService {
// 验证码缓存
@Bean
public AbstractCaffeineCacheTemplate<String> captchaCache() {
return new AbstractCaffeineCacheTemplate<String>("captcha") {
@Override
protected String loadData(String key) {
// 从数据库加载数据的逻辑
return null;
}
};
}
// 登录用户缓存
@Bean
public AbstractCaffeineCacheTemplate<LoginUser> loginUserCache() {
return new AbstractCaffeineCacheTemplate<LoginUser>("loginUser") {
@Override
protected LoginUser loadData(String key) {
// 从数据库加载用户信息
return userService.getLoginUserByToken(key);
}
};
}
}
缓存使用
1. 获取缓存数据
1.1 从缓存中获取用户数据
// 从缓存中获取用户数据
SysUserEntity user = CacheCenter.userCache.get("1");
// 从缓存中获取角色数据
SysRoleEntity role = CacheCenter.roleCache.get("1");
// 从缓存中获取岗位数据
SysPostEntity post = CacheCenter.postCache.get("1");
1.2 获取登录用户信息
public LoginUser getLoginUser(String token) {
LoginUser loginUser = CacheCenter.loginUserCache.get(token);
if (loginUser == null) {
throw new ApiException("用户登录状态已过期");
}
return loginUser;
}
2. 使缓存失效
2.1 使单个缓存失效
// 使单个用户缓存失效
CacheCenter.userCache.invalidate("1");
// 使单个登录用户缓存失效
CacheCenter.loginUserCache.invalidate(token);
2.2 使所有缓存失效
// 使所有用户缓存失效
CacheCenter.userCache.invalidateAll();
// 使所有登录用户缓存失效
CacheCenter.loginUserCache.invalidateAll();
3. 更新缓存数据
@Service
@RequiredArgsConstructor
public class UserService {
public void updateUser(UpdateUserCommand command) {
// 1. 更新数据库
userModel.update(command);
// 2. 使缓存失效
CacheCenter.userCache.invalidate(command.getUserId().toString());
// 3. 如果用户正在登录,也需要更新登录用户缓存
String token = getTokenByUserId(command.getUserId());
if (token != null) {
CacheCenter.loginUserCache.invalidate(token);
}
}
}
缓存模板
1. AbstractCaffeineCacheTemplate
public abstract class AbstractCaffeineCacheTemplate<T> {
protected final Cache<String, T> cache;
protected final String cacheName;
public AbstractCaffeineCacheTemplate(String cacheName) {
this.cacheName = cacheName;
this.cache = Caffeine.newBuilder()
.maximumSize(1024)
.expireAfterWrite(60, TimeUnit.MINUTES)
.refreshAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
}
/**
* 从缓存获取数据,如果不存在则从数据库加载
*/
public T get(String key) {
try {
return cache.get(key, k -> loadData(k));
} catch (Exception e) {
log.error("从缓存{}获取数据失败: {}", cacheName, e.getMessage(), e);
return null;
}
}
/**
* 从数据库加载数据的抽象方法
*/
protected abstract T loadData(String key);
/**
* 使缓存失效
*/
public void invalidate(String key) {
cache.invalidate(key);
}
/**
* 使所有缓存失效
*/
public void invalidateAll() {
cache.invalidateAll();
}
/**
* 获取缓存统计信息
*/
public String getStats() {
CacheStats stats = cache.stats();
return String.format(
"缓存%s统计: 命中率=%.2f%%, 加载次数=%d, 加载成功次数=%d, 加载异常次数=%d, 总加载时间=%dms",
cacheName,
stats.hitRate() * 100,
stats.loadCount(),
stats.loadSuccessCount(),
stats.loadExceptionCount(),
stats.totalLoadTime()
);
}
}
缓存监控
1. 通过API接口监控
访问缓存统计信息:
GET /monitor/caffeine/stats
响应:
{
"code": 200,
"msg": "success",
"data": {
"userCache": "缓存user统计: 命中率=95.23%, 加载次数=100, 加载成功次数=98, 加载异常次数=2, 总加载时间=150ms",
"roleCache": "缓存role统计: 命中率=98.76%, 加载次数=50, 加载成功次数=50, 加载异常次数=0, 总加载时间=80ms",
"loginUserCache": "缓存loginUser统计: 命中率=92.45%, 加载次数=200, 加载成功次数=195, 加载异常次数=5, 总加载时间=300ms"
}
}
2. 通过代码监控
@Service
@RequiredArgsConstructor
public class CacheMonitorService {
@Autowired
private CaffeineCacheService caffeineCacheService;
public void printCacheStats() {
String stats = caffeineCacheService.getCacheStats();
System.out.println(stats);
}
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void monitorCachePerformance() {
log.info("缓存性能监控: {}", caffeineCacheService.getCacheStats());
}
}
性能对比
Redis vs Caffeine 性能对比
| 特性 | Redis | Caffeine |
|---|---|---|
| 访问延迟 | 网络延迟 (0.1-1ms) | 内存访问 (<0.01ms) |
| 数据一致性 | 分布式一致 | 本地不一致 |
| 内存使用 | 独立服务内存 | 应用进程内存 |
| 适用场景 | 分布式缓存 | 本地高速缓存 |
| 部署复杂度 | 需要独立部署 | 内置在应用中 |
适用场景建议
使用Caffeine缓存的场景
- 用户基本信息
- 角色权限数据
- 系统配置数据
- 字典数据
- 登录用户会话
使用Redis缓存的场景
- 分布式会话
- 分布式锁
- 消息队列
- 跨服务数据共享
迁移注意事项
1. 数据一致性
Caffeine是本地缓存,在多实例部署时需要注意:
@Service
public class CacheConsistencyService {
/**
* 在多实例环境下,需要同步缓存失效
*/
public void invalidateUserCache(Long userId) {
// 1. 使本地缓存失效
CacheCenter.userCache.invalidate(userId.toString());
// 2. 发布缓存失效事件(如果有多实例)
applicationEventPublisher.publishEvent(new UserCacheEvictEvent(userId));
}
}
2. 内存使用监控
@Service
public class MemoryMonitorService {
@Scheduled(fixedRate = 60000) // 每1分钟执行一次
public void monitorMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
double usageRate = (double) usedMemory / maxMemory * 100;
if (usageRate > 80) {
log.warn("内存使用率过高: {}%, 建议调整缓存配置", String.format("%.2f", usageRate));
}
}
}
3. 缓存配置调优
根据业务需求调整缓存参数:
@Configuration
public class CustomCacheConfig {
@Bean
public AbstractCaffeineCacheTemplate<SysUserEntity> userCache() {
return new AbstractCaffeineCacheTemplate<SysUserEntity>("user") {
@Override
protected SysUserEntity loadData(String key) {
return userService.getById(Long.valueOf(key));
}
} {
@Override
protected Cache<String, SysUserEntity> createCache() {
return Caffeine.newBuilder()
.maximumSize(500) // 用户数据较多,限制最大数量
.expireAfterWrite(30, TimeUnit.MINUTES) // 用户数据变化较少
.build();
}
};
}
@Bean
public AbstractCaffeineCacheTemplate<LoginUser> loginUserCache() {
return new AbstractCaffeineCacheTemplate<LoginUser>("loginUser") {
@Override
protected LoginUser loadData(String key) {
return userService.getLoginUserByToken(key);
}
} {
@Override
protected Cache<String, LoginUser> createCache() {
return Caffeine.newBuilder()
.maximumSize(1000) // 登录用户较多
.expireAfterWrite(2, TimeUnit.HOURS) // 登录会话较长
.build();
}
};
}
}
故障排查
1. 常见问题
1.1 缓存不生效
- 检查
@PostConstruct方法是否正确初始化 - 验证缓存键是否正确
- 检查缓存配置参数
1.2 内存溢出
- 调整
maximumSize参数 - 缩短
expireAfterWrite时间 - 监控内存使用情况
1.3 数据不一致
- 确保数据更新时正确调用
invalidate方法 - 在多实例环境下实现缓存同步
- 定期清理过期缓存
2. 调试方法
// 查看缓存统计
System.out.println(CacheCenter.userCache.getStats());
// 手动触发缓存加载
CacheCenter.userCache.get("test-key");
// 查看缓存内容(仅开发环境)
CacheCenter.userCache.asMap().forEach((key, value) -> {
System.out.println(key + ": " + value);
});
回滚方案
如需回滚到Redis缓存:
- 恢复
CacheCenter中的Redis缓存引用 - 移除Caffeine相关依赖
- 重启应用
<!-- 移除Caffeine依赖 -->
<!-- <dependency> -->
<!-- <groupId>com.github.ben-manes.caffeine</groupId> -->
<!-- <artifactId>caffeine</artifactId> -->
<!-- <version>2.9.3</version> -->
<!-- </dependency> -->
最佳实践
1. 缓存键设计
- 使用有意义的键名
- 避免过长的键名
- 统一命名规范
2. 缓存失效策略
- 写操作时立即失效相关缓存
- 读操作时设置合理的过期时间
- 定期清理无用缓存
3. 性能监控
- 监控缓存命中率
- 监控内存使用情况
- 监控缓存加载时间
4. 安全考虑
- 避免缓存敏感数据
- 设置合理的缓存大小
- 实现缓存数据加密