472 lines
12 KiB
Markdown
472 lines
12 KiB
Markdown
# 缓存系统指南
|
||
|
||
## 概述
|
||
|
||
本项目采用多级缓存架构,将原有的Redis缓存替换为基于Caffeine的本地缓存,以提升性能并减少对外部Redis服务的依赖。
|
||
|
||
## 缓存架构
|
||
|
||
### 多级缓存设计
|
||
|
||
```
|
||
应用层缓存
|
||
├── Caffeine缓存 (本地内存缓存)
|
||
│ ├── 用户缓存
|
||
│ ├── 角色缓存
|
||
│ ├── 岗位缓存
|
||
│ └── 登录用户缓存
|
||
├── Guava缓存 (配置缓存)
|
||
│ ├── 系统配置
|
||
│ ├── 字典数据
|
||
│ └── 部门数据
|
||
└── Redis缓存 (分布式缓存,可选)
|
||
├── 会话缓存
|
||
├── 验证码缓存
|
||
└── 分布式锁
|
||
```
|
||
|
||
## Caffeine缓存迁移
|
||
|
||
### 1. 迁移概述
|
||
|
||
本项目已将原有的Redis缓存替换为基于Caffeine的本地缓存,主要变更包括:
|
||
|
||
- **新增依赖**: Caffeine 2.9.3
|
||
- **新增组件**: AbstractCaffeineCacheTemplate、CaffeineCacheService
|
||
- **缓存中心更新**: CacheCenter使用Caffeine缓存替换Redis缓存
|
||
|
||
### 2. 缓存配置
|
||
|
||
#### 2.1 Caffeine缓存配置
|
||
|
||
```java
|
||
@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 缓存服务配置
|
||
|
||
```java
|
||
@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 从缓存中获取用户数据
|
||
|
||
```java
|
||
// 从缓存中获取用户数据
|
||
SysUserEntity user = CacheCenter.userCache.get("1");
|
||
|
||
// 从缓存中获取角色数据
|
||
SysRoleEntity role = CacheCenter.roleCache.get("1");
|
||
|
||
// 从缓存中获取岗位数据
|
||
SysPostEntity post = CacheCenter.postCache.get("1");
|
||
```
|
||
|
||
#### 1.2 获取登录用户信息
|
||
|
||
```java
|
||
public LoginUser getLoginUser(String token) {
|
||
LoginUser loginUser = CacheCenter.loginUserCache.get(token);
|
||
if (loginUser == null) {
|
||
throw new ApiException("用户登录状态已过期");
|
||
}
|
||
return loginUser;
|
||
}
|
||
```
|
||
|
||
### 2. 使缓存失效
|
||
|
||
#### 2.1 使单个缓存失效
|
||
|
||
```java
|
||
// 使单个用户缓存失效
|
||
CacheCenter.userCache.invalidate("1");
|
||
|
||
// 使单个登录用户缓存失效
|
||
CacheCenter.loginUserCache.invalidate(token);
|
||
```
|
||
|
||
#### 2.2 使所有缓存失效
|
||
|
||
```java
|
||
// 使所有用户缓存失效
|
||
CacheCenter.userCache.invalidateAll();
|
||
|
||
// 使所有登录用户缓存失效
|
||
CacheCenter.loginUserCache.invalidateAll();
|
||
```
|
||
|
||
### 3. 更新缓存数据
|
||
|
||
```java
|
||
@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
|
||
|
||
```java
|
||
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接口监控
|
||
|
||
访问缓存统计信息:
|
||
```bash
|
||
GET /monitor/caffeine/stats
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"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. 通过代码监控
|
||
|
||
```java
|
||
@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是本地缓存,在多实例部署时需要注意:
|
||
|
||
```java
|
||
@Service
|
||
public class CacheConsistencyService {
|
||
|
||
/**
|
||
* 在多实例环境下,需要同步缓存失效
|
||
*/
|
||
public void invalidateUserCache(Long userId) {
|
||
// 1. 使本地缓存失效
|
||
CacheCenter.userCache.invalidate(userId.toString());
|
||
|
||
// 2. 发布缓存失效事件(如果有多实例)
|
||
applicationEventPublisher.publishEvent(new UserCacheEvictEvent(userId));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 内存使用监控
|
||
|
||
```java
|
||
@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. 缓存配置调优
|
||
|
||
根据业务需求调整缓存参数:
|
||
|
||
```java
|
||
@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. 调试方法
|
||
|
||
```java
|
||
// 查看缓存统计
|
||
System.out.println(CacheCenter.userCache.getStats());
|
||
|
||
// 手动触发缓存加载
|
||
CacheCenter.userCache.get("test-key");
|
||
|
||
// 查看缓存内容(仅开发环境)
|
||
CacheCenter.userCache.asMap().forEach((key, value) -> {
|
||
System.out.println(key + ": " + value);
|
||
});
|
||
```
|
||
|
||
## 回滚方案
|
||
|
||
如需回滚到Redis缓存:
|
||
|
||
1. 恢复 `CacheCenter` 中的Redis缓存引用
|
||
2. 移除Caffeine相关依赖
|
||
3. 重启应用
|
||
|
||
```xml
|
||
<!-- 移除Caffeine依赖 -->
|
||
<!-- <dependency> -->
|
||
<!-- <groupId>com.github.ben-manes.caffeine</groupId> -->
|
||
<!-- <artifactId>caffeine</artifactId> -->
|
||
<!-- <version>2.9.3</version> -->
|
||
<!-- </dependency> -->
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 缓存键设计
|
||
- 使用有意义的键名
|
||
- 避免过长的键名
|
||
- 统一命名规范
|
||
|
||
### 2. 缓存失效策略
|
||
- 写操作时立即失效相关缓存
|
||
- 读操作时设置合理的过期时间
|
||
- 定期清理无用缓存
|
||
|
||
### 3. 性能监控
|
||
- 监控缓存命中率
|
||
- 监控内存使用情况
|
||
- 监控缓存加载时间
|
||
|
||
### 4. 安全考虑
|
||
- 避免缓存敏感数据
|
||
- 设置合理的缓存大小
|
||
- 实现缓存数据加密 |