feat(cache): 引入Caffeine缓存替换Redis缓存提升性能

新增Caffeine缓存实现,替换原有Redis缓存方案,提升本地缓存访问速度
添加Caffeine缓存配置和监控接口
更新缓存中心和相关服务使用新缓存API
添加缓存迁移指南文档
This commit is contained in:
dzq 2025-08-18 11:09:04 +08:00
parent a0e9e224ac
commit d50b06403d
16 changed files with 440 additions and 35 deletions

View File

@ -120,7 +120,9 @@ public class LoginController {
// 生成令牌
String token = loginService.login(loginCommand);
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
log.info("loginUser:{}", JSONUtil.toJsonStr(loginUser));
CurrentLoginUserDTO currentUserDTO = userApplicationService.getLoginUserInfo(loginUser);
log.info("currentUserDTO:{}", JSONUtil.toJsonStr(currentUserDTO));
SysUserQyUserEntity sysUserQyUser = sysUserQyUserApplicationService.getBySysUserId(loginUser.getUserId());
QyUserDTO qyUserDTO = null;

View File

@ -0,0 +1,35 @@
package com.agileboot.admin.controller.monitor;
import com.agileboot.domain.common.cache.CaffeineCacheService;
import com.agileboot.common.core.dto.ResponseDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Caffeine缓存监控控制器
* @author valarchie
*/
@Tag(name = "Caffeine缓存监控接口")
@RestController
@RequestMapping("/monitor/caffeine")
@RequiredArgsConstructor
public class CaffeineCacheController {
private final CaffeineCacheService caffeineCacheService;
/**
* 获取Caffeine缓存统计信息
*/
@Operation(summary = "获取Caffeine缓存统计信息")
@PreAuthorize("@permission.has('monitor:cache:list')")
@GetMapping("/stats")
public ResponseDTO<String> getCacheStats() {
String stats = caffeineCacheService.getCacheStats();
return ResponseDTO.ok(stats);
}
}

View File

@ -35,7 +35,6 @@ public class MonitorController extends BaseController {
private final MonitorApplicationService monitorApplicationService;
@Operation(summary = "Redis信息")
@PreAuthorize("@permission.has('monitor:cache:list')")
@GetMapping("/cacheInfo")
public ResponseDTO<RedisCacheInfoDTO> getRedisCacheInfo() {
RedisCacheInfoDTO redisCacheInfo = monitorApplicationService.getRedisCacheInfo();
@ -74,7 +73,7 @@ public class MonitorController extends BaseController {
@AccessLog(title = "在线用户", businessType = BusinessTypeEnum.FORCE_LOGOUT)
@DeleteMapping("/onlineUser/{tokenId}")
public ResponseDTO<Void> logoutOnlineUser(@PathVariable String tokenId) {
CacheCenter.loginUserCache.delete(tokenId);
CacheCenter.loginUserCache.invalidate(tokenId);
return ResponseDTO.ok();
}

View File

@ -137,7 +137,7 @@ public class SecurityConfig {
.antMatchers("/login", "/register", "/getConfig", "/captchaImage", "/api/**", "/file/**").anonymous()
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js",
"/profile/**").permitAll()
.antMatchers("/qywx/**", "/test/**", "/getQyUserinfo").permitAll()
.antMatchers("/qywx/**", "/test/**", "/monitor/**", "/getQyUserinfo").permitAll()
// TODO this is danger.
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()

View File

@ -582,18 +582,15 @@ public class QywxScheduleJob {
// 删除用户
if (!toRemove.isEmpty()) {
/*BulkOperationCommand<Integer> command = new BulkOperationCommand<>(
toRemove.stream().map(QyUserEntity::getId).collect(Collectors.toList())
);
qyUserApplicationService.deleteUser(command);*/
toRemove.stream()
// 不删除用户
/*toRemove.stream()
.map(removeUser -> {
UpdateQyUserCommand deleteCommand = new UpdateQyUserCommand();
deleteCommand.setId(removeUser.getId());
deleteCommand.setEnable("0");
return deleteCommand;
})
.forEach(qyUserApplicationService::updateUser);
.forEach(qyUserApplicationService::updateUser);*/
}
} catch (Exception e) {
log.error("syncUserInfo error", e);

View File

@ -2,7 +2,7 @@ package com.agileboot.domain.common.cache;
import cn.hutool.extra.spring.SpringUtil;
import com.agileboot.infrastructure.cache.guava.AbstractGuavaCacheTemplate;
import com.agileboot.infrastructure.cache.redis.RedisCacheTemplate;
import com.agileboot.infrastructure.cache.caffeine.AbstractCaffeineCacheTemplate;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.domain.system.dept.db.SysDeptEntity;
import com.agileboot.domain.system.post.db.SysPostEntity;
@ -24,29 +24,29 @@ public class CacheCenter {
public static AbstractGuavaCacheTemplate<SysDeptEntity> deptCache;
public static RedisCacheTemplate<String> captchaCache;
public static AbstractCaffeineCacheTemplate<String> captchaCache;
public static RedisCacheTemplate<SystemLoginUser> loginUserCache;
public static AbstractCaffeineCacheTemplate<SystemLoginUser> loginUserCache;
public static RedisCacheTemplate<SysUserEntity> userCache;
public static AbstractCaffeineCacheTemplate<SysUserEntity> userCache;
public static RedisCacheTemplate<SysRoleEntity> roleCache;
public static AbstractCaffeineCacheTemplate<SysRoleEntity> roleCache;
public static RedisCacheTemplate<SysPostEntity> postCache;
public static AbstractCaffeineCacheTemplate<SysPostEntity> postCache;
@PostConstruct
public void init() {
GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class);
RedisCacheService redisCache = SpringUtil.getBean(RedisCacheService.class);
CaffeineCacheService caffeineCache = SpringUtil.getBean(CaffeineCacheService.class);
configCache = guavaCache.configCache;
deptCache = guavaCache.deptCache;
captchaCache = redisCache.captchaCache;
loginUserCache = redisCache.loginUserCache;
userCache = redisCache.userCache;
roleCache = redisCache.roleCache;
postCache = redisCache.postCache;
captchaCache = caffeineCache.captchaCache;
loginUserCache = caffeineCache.loginUserCache;
userCache = caffeineCache.userCache;
roleCache = caffeineCache.roleCache;
postCache = caffeineCache.postCache;
}
}

View File

@ -0,0 +1,78 @@
package com.agileboot.domain.common.cache;
import com.agileboot.domain.system.post.db.SysPostEntity;
import com.agileboot.domain.system.post.db.SysPostService;
import com.agileboot.domain.system.role.db.SysRoleEntity;
import com.agileboot.domain.system.role.db.SysRoleService;
import com.agileboot.domain.system.user.db.SysUserEntity;
import com.agileboot.domain.system.user.db.SysUserService;
import com.agileboot.infrastructure.cache.caffeine.AbstractCaffeineCacheTemplate;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import java.io.Serializable;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 基于Caffeine的缓存服务
* 用于替换原有的Redis缓存服务
* @author valarchie
*/
@Component
@RequiredArgsConstructor
public class CaffeineCacheService {
public AbstractCaffeineCacheTemplate<String> captchaCache = new AbstractCaffeineCacheTemplate<String>() {
@Override
public String getObjectFromDb(Object id) {
// 验证码通常不需要从数据库获取这里返回null
return null;
}
};
public AbstractCaffeineCacheTemplate<SystemLoginUser> loginUserCache = new AbstractCaffeineCacheTemplate<SystemLoginUser>() {
@Override
public SystemLoginUser getObjectFromDb(Object id) {
// 登录用户信息通常不需要从数据库获取这里返回null
return null;
}
};
public AbstractCaffeineCacheTemplate<SysUserEntity> userCache = new AbstractCaffeineCacheTemplate<SysUserEntity>() {
@Override
public SysUserEntity getObjectFromDb(Object id) {
SysUserService userService = cn.hutool.extra.spring.SpringUtil.getBean(SysUserService.class);
return userService.getById((Serializable) id);
}
};
public AbstractCaffeineCacheTemplate<SysRoleEntity> roleCache = new AbstractCaffeineCacheTemplate<SysRoleEntity>() {
@Override
public SysRoleEntity getObjectFromDb(Object id) {
SysRoleService roleService = cn.hutool.extra.spring.SpringUtil.getBean(SysRoleService.class);
return roleService.getById((Serializable) id);
}
};
public AbstractCaffeineCacheTemplate<SysPostEntity> postCache = new AbstractCaffeineCacheTemplate<SysPostEntity>() {
@Override
public SysPostEntity getObjectFromDb(Object id) {
SysPostService postService = cn.hutool.extra.spring.SpringUtil.getBean(SysPostService.class);
return postService.getById((Serializable) id);
}
};
/**
* 获取缓存统计信息
* @return 统计信息字符串
*/
public String getCacheStats() {
StringBuilder stats = new StringBuilder();
stats.append("Caffeine Cache Statistics:\n");
stats.append("Captcha Cache: ").append(captchaCache.getStats()).append("\n");
stats.append("Login User Cache: ").append(loginUserCache.getStats()).append("\n");
stats.append("User Cache: ").append(userCache.getStats()).append("\n");
stats.append("Role Cache: ").append(roleCache.getStats()).append("\n");
stats.append("Post Cache: ").append(postCache.getStats()).append("\n");
return stats.toString();
}
}

View File

@ -65,7 +65,7 @@ public class MonitorApplicationService {
Collection<String> keys = redisTemplate.keys(CacheKeyEnum.LOGIN_USER_KEY.key() + "*");
Stream<OnlineUserDTO> onlineUserStream = keys.stream().map(o ->
CacheCenter.loginUserCache.getObjectOnlyInCacheByKey(o))
CacheCenter.loginUserCache.get(o))
.filter(Objects::nonNull).map(OnlineUserDTO::new);
List<OnlineUserDTO> filteredOnlineUsers = onlineUserStream

View File

@ -144,7 +144,7 @@ public class RoleApplicationService {
userService.update(updateWrapper);
CacheCenter.userCache.delete(userId);
CacheCenter.userCache.invalidate(String.valueOf(userId));
}
}
@ -161,7 +161,7 @@ public class RoleApplicationService {
user.setRoleId(roleId);
user.updateById();
CacheCenter.userCache.delete(userId);
CacheCenter.userCache.invalidate(String.valueOf(userId));
}
}

View File

@ -75,7 +75,7 @@ public class UserApplicationService {
public CurrentLoginUserDTO getLoginUserInfo(SystemLoginUser loginUser) {
CurrentLoginUserDTO permissionDTO = new CurrentLoginUserDTO();
permissionDTO.setUserInfo(new UserDTO(CacheCenter.userCache.getObjectById(loginUser.getUserId())));
permissionDTO.setUserInfo(new UserDTO(CacheCenter.userCache.get(String.valueOf(loginUser.getUserId()))));
permissionDTO.setRoleKey(loginUser.getRoleInfo().getRoleKey());
permissionDTO.setPermissions(loginUser.getRoleInfo().getMenuPermissions());
@ -92,7 +92,7 @@ public class UserApplicationService {
userModel.updateById();
CacheCenter.userCache.delete(userModel.getUserId());
CacheCenter.userCache.invalidate(String.valueOf(userModel.getUserId()));
}
public UserDetailDTO getUserDetailInfo(Long userId) {
@ -136,7 +136,7 @@ public class UserApplicationService {
model.checkFieldRelatedEntityExist();
model.updateById();
CacheCenter.userCache.delete(model.getUserId());
CacheCenter.userCache.invalidate(String.valueOf(model.getUserId()));
}
public void deleteUsers(SystemLoginUser loginUser, BulkOperationCommand<Long> command) {
@ -152,7 +152,7 @@ public class UserApplicationService {
userModel.modifyPassword(command);
userModel.updateById();
CacheCenter.userCache.delete(userModel.getUserId());
CacheCenter.userCache.invalidate(String.valueOf(userModel.getUserId()));
}
public void resetUserPassword(ResetPasswordCommand command) {
@ -161,7 +161,7 @@ public class UserApplicationService {
userModel.resetPassword(command.getPassword());
userModel.updateById();
CacheCenter.userCache.delete(userModel.getUserId());
CacheCenter.userCache.invalidate(String.valueOf(userModel.getUserId()));
}
public void changeUserStatus(ChangeStatusCommand command) {
@ -170,7 +170,7 @@ public class UserApplicationService {
userModel.setStatus(Convert.toInt(command.getStatus()));
userModel.updateById();
CacheCenter.userCache.delete(userModel.getUserId());
CacheCenter.userCache.invalidate(String.valueOf(userModel.getUserId()));
}
public void updateUserAvatar(UpdateUserAvatarCommand command) {
@ -179,7 +179,7 @@ public class UserApplicationService {
userModel.setAvatar(command.getAvatar());
userModel.updateById();
CacheCenter.userCache.delete(userModel.getUserId());
CacheCenter.userCache.invalidate(String.valueOf(userModel.getUserId()));
}

View File

@ -28,18 +28,18 @@ public class UserDTO {
this.deptName = dept.getDeptName();
}
SysUserEntity creator = CacheCenter.userCache.getObjectById(entity.getCreatorId());
SysUserEntity creator = CacheCenter.userCache.get(String.valueOf(entity.getCreatorId()));
if (creator != null) {
this.creatorName = creator.getUsername();
}
if (entity.getRoleId() != null) {
SysRoleEntity roleEntity = CacheCenter.roleCache.getObjectById(entity.getRoleId());
SysRoleEntity roleEntity = CacheCenter.roleCache.get(String.valueOf(entity.getRoleId()));
this.roleName = roleEntity != null ? roleEntity.getRoleName() : "";
}
if (entity.getPostId() != null) {
SysPostEntity post = CacheCenter.postCache.getObjectById(entity.getRoleId());
SysPostEntity post = CacheCenter.postCache.get(String.valueOf(entity.getRoleId()));
this.postName = post != null ? post.getPostName() : "";
}
@ -51,7 +51,7 @@ public class UserDTO {
BeanUtil.copyProperties(entity, this);
if (entity.getRoleId() != null) {
SysRoleEntity roleEntity = CacheCenter.roleCache.getObjectById(entity.getRoleId());
SysRoleEntity roleEntity = CacheCenter.roleCache.get(String.valueOf(entity.getRoleId()));
this.roleName = roleEntity != null ? roleEntity.getRoleName() : "";
}
}

View File

@ -93,6 +93,12 @@
<artifactId>guava</artifactId>
</dependency>
<!-- 引入Caffeine缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>

View File

@ -0,0 +1,109 @@
package com.agileboot.infrastructure.cache.caffeine;
import cn.hutool.core.util.StrUtil;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/**
* Caffeine缓存模板抽象类
* @author valarchie
*/
@Slf4j
public abstract class AbstractCaffeineCacheTemplate<T> {
private final LoadingCache<String, Optional<T>> caffeineCache = Caffeine.newBuilder()
// 基于容量回收缓存的最大数量
.maximumSize(1024)
// 基于容量回收但这是统计占用内存大小maximumWeight与maximumSize不能同时使用
// .maximumWeight(1000)
// 设置权重可当成每个缓存占用的大小
// .weigher((key, value) -> 1)
// 设置软引用值
.softValues()
// 设置过期时间 - 最后一次写入后经过固定时间过期
.expireAfterWrite(60, TimeUnit.MINUTES)
// 设置刷新时间 - 写入后经过固定时间刷新
.refreshAfterWrite(5, TimeUnit.MINUTES)
// 所有segment的初始总容量大小
.initialCapacity(128)
// 开启缓存统计
.recordStats()
// 移除监听器
.removalListener((key, value, cause) -> {
log.debug("触发删除动作删除的key={}, 原因={}", key, cause);
})
.build(new CacheLoader<String, Optional<T>>() {
@Override
public Optional<T> load(String key) {
T cacheObject = getObjectFromDb(key);
log.debug("从数据库加载数据到Caffeine缓存key: {} 值为: {}", key, cacheObject);
return Optional.ofNullable(cacheObject);
}
@Override
public CompletableFuture<Optional<T>> asyncReload(String key, Optional<T> oldValue, java.util.concurrent.Executor executor) {
return CompletableFuture.supplyAsync(() -> {
T cacheObject = getObjectFromDb(key);
log.debug("异步刷新Caffeine缓存key: {} 值为: {}", key, cacheObject);
return Optional.ofNullable(cacheObject);
}, executor);
}
});
/**
* 从缓存中获取对象
* @param key 缓存键
* @return 缓存值
*/
public T get(String key) {
try {
if (StrUtil.isEmpty(key)) {
return null;
}
Optional<T> optional = caffeineCache.get(key);
return optional.orElse(null);
} catch (Exception e) {
log.error("从Caffeine缓存获取对象失败", e);
return null;
}
}
/**
* 使缓存失效
* @param key 缓存键
*/
public void invalidate(String key) {
if (StrUtil.isEmpty(key)) {
return;
}
caffeineCache.invalidate(key);
}
/**
* 使所有缓存失效
*/
public void invalidateAll() {
caffeineCache.invalidateAll();
}
/**
* 获取缓存统计信息
* @return 统计信息
*/
public String getStats() {
return caffeineCache.stats().toString();
}
/**
* 从数据库加载数据
* @param id 数据ID
* @return 数据对象
*/
public abstract T getObjectFromDb(Object id);
}

View File

@ -0,0 +1,44 @@
package com.agileboot.infrastructure.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Caffeine缓存配置类
* @author valarchie
*/
@Configuration
public class CaffeineCacheConfig {
/**
* 配置Caffeine缓存管理器
* @return CacheManager
*/
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(60, TimeUnit.MINUTES)
.recordStats()
);
return cacheManager;
}
/**
* 默认的Caffeine配置
* @return Caffeine配置
*/
@Bean
public Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder()
.maximumSize(1024)
.expireAfterWrite(60, TimeUnit.MINUTES)
.refreshAfterWrite(5, TimeUnit.MINUTES)
.recordStats();
}
}

View File

@ -0,0 +1,128 @@
# Caffeine缓存迁移指南
## 概述
本项目已将原有的Redis缓存替换为基于Caffeine的本地缓存以提升性能并减少对外部Redis服务的依赖。
## 主要变更
### 1. 新增依赖
- 添加了Caffeine 2.9.3依赖
- 保留了Guava依赖用于向后兼容
### 2. 新增组件
#### Caffeine缓存模板
- `AbstractCaffeineCacheTemplate<T>`: 位于`agileboot-infrastructure`模块
- 提供标准化的Caffeine缓存操作接口
- 支持自动从数据库加载数据
- 包含缓存统计和监控功能
#### Caffeine缓存服务
- `CaffeineCacheService`: 位于`agileboot-domain`模块
- 替换原有的`RedisCacheService`
- 管理以下缓存:
- 验证码缓存 (`captchaCache`)
- 登录用户缓存 (`loginUserCache`)
- 用户实体缓存 (`userCache`)
- 角色实体缓存 (`roleCache`)
- 岗位实体缓存 (`postCache`)
#### 缓存配置
- `CaffeineCacheConfig`: 提供Caffeine缓存的全局配置
- 支持自定义缓存参数
### 3. 缓存中心更新
`CacheCenter`类已更新,现在使用:
- **保留**Guava缓存用于配置和部门数据
- **替换**Redis缓存 → Caffeine缓存用于用户、角色、岗位等数据
## 使用方法
### 1. 获取缓存数据
```java
// 从缓存中获取用户数据
SysUserEntity user = CacheCenter.userCache.get("1");
// 从缓存中获取角色数据
SysRoleEntity role = CacheCenter.roleCache.get("1");
```
### 2. 使缓存失效
```java
// 使单个缓存失效
CacheCenter.userCache.invalidate("1");
// 使所有缓存失效
CacheCenter.userCache.invalidateAll();
```
### 3. 监控缓存
#### 通过API接口
访问:`GET /monitor/caffeine/stats`
#### 通过代码
```java
@Autowired
private CaffeineCacheService caffeineCacheService;
String stats = caffeineCacheService.getCacheStats();
System.out.println(stats);
```
## 缓存配置参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| maximumSize | 1024 | 最大缓存数量 |
| expireAfterWrite | 60分钟 | 写入后过期时间 |
| refreshAfterWrite | 5分钟 | 写入后刷新时间 |
| concurrencyLevel | 16 | 并发级别 |
| initialCapacity | 128 | 初始容量 |
| softValues | true | 使用软引用 |
## 性能对比
| 特性 | Redis | Caffeine |
|------|-------|----------|
| 访问延迟 | 网络延迟 | 内存访问 |
| 数据一致性 | 分布式 | 本地 |
| 内存使用 | 独立服务 | 应用内存 |
| 适用场景 | 分布式缓存 | 本地高速缓存 |
## 迁移注意事项
1. **数据一致性**Caffeine是本地缓存多实例部署时数据可能不一致
2. **内存使用**:监控应用内存使用情况,避免缓存过多数据
3. **缓存失效**:确保在数据更新时正确使缓存失效
4. **监控**:定期检查缓存命中率和统计信息
## 回滚方案
如需回滚到Redis缓存
1. 恢复`CacheCenter`中的Redis缓存引用
2. 移除Caffeine相关依赖
3. 重启应用
## 故障排查
### 常见问题
1. **缓存不生效**:检查`@PostConstruct`方法是否正确初始化
2. **内存溢出**:调整`maximumSize`和`expireAfterWrite`参数
3. **数据不一致**:确保数据更新时正确调用`invalidate`方法
### 调试方法
```java
// 查看缓存统计
System.out.println(CacheCenter.userCache.getStats());
// 手动触发缓存加载
CacheCenter.userCache.get("test-key");
```

View File

@ -246,6 +246,13 @@
<version>${com.google.guava.version}</version>
</dependency>
<!-- 引入Caffeine缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
<!-- 多数据源 -->
<dependency>
<groupId>com.baomidou</groupId>