优化Redis设置,及时清理过期数据

This commit is contained in:
Tian jianyong 2025-10-13 16:51:33 +08:00
parent 840a441d9e
commit 64ba2d46e8
11 changed files with 1246 additions and 3 deletions

View File

@ -0,0 +1,136 @@
# Redis 内存优化 - 快速参考
## 🚀 快速开始
### 1⃣ 诊断当前状态
```bash
./scripts/redis_memory_diagnosis.sh
```
### 2⃣ 配置 Redis 内存限制
```bash
./scripts/configure_redis_memory.sh
# 开发环境选 1 (256MB)
# 生产环境选 2 (1GB)
```
### 3⃣ 重新编译部署
```bash
mvn clean install -DskipTests
./ry.sh restart
```
## 📊 已实施的优化
| 优化项 | 修改前 | 修改后 | 效果 |
|--------|--------|--------|------|
| Redis 默认缓存过期 | 永不过期 | 1小时 | 防止内存泄漏 |
| WebSocket 消息缓存 | 100条/30分钟 | 50条/10分钟 | 减少50%占用 |
| 内存缓存清理 | 无 | 每分钟 | 自动清理不活跃数据 |
| Redis 内存限制 | 无限制 | 256MB/1GB | 防止OOM |
| 缓存监控 | 无 | 每5分钟 | 及时发现问题 |
## 🔍 常用命令
### 查看 Redis 内存使用
```bash
redis-cli INFO memory | grep used_memory_human
```
### 查看 Key 数量
```bash
redis-cli DBSIZE
```
### 查看最大的 Key
```bash
redis-cli --bigkeys
```
### 查看配置
```bash
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy
```
### 手动设置内存限制
```bash
# 设置为 512MB
redis-cli CONFIG SET maxmemory 512mb
redis-cli CONFIG SET maxmemory-policy volatile-lru
```
### 清理所有缓存 (⚠️ 谨慎使用)
```bash
redis-cli FLUSHDB
```
## 📁 修改的文件
### 核心修改
- ✅ [RedisCache.java](qaup-common/src/main/java/com/qaup/common/core/redis/RedisCache.java) - 添加默认过期时间
- ✅ [MessageCacheService.java](qaup-collision/src/main/java/com/qaup/collision/websocket/cache/MessageCacheService.java) - 优化缓存参数
- ✅ [DataCollectorService.java](qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java) - 添加清理逻辑
- ✅ [application-dev.yml](qaup-admin/src/main/resources/application-dev.yml) - Redis 配置
- ✅ [application-prod.yml](qaup-admin/src/main/resources/application-prod.yml) - Redis 配置
### 新增文件
- 🆕 [RedisCacheCleanupTask.java](qaup-framework/src/main/java/com/qaup/framework/task/RedisCacheCleanupTask.java) - 自动清理任务
- 🆕 [scripts/configure_redis_memory.sh](scripts/configure_redis_memory.sh) - 配置脚本
- 🆕 [scripts/redis_memory_diagnosis.sh](scripts/redis_memory_diagnosis.sh) - 诊断脚本
## 📈 监控指标
### 应用日志 (每分钟)
```
清理不活跃缓存对象: X 个移动对象, Y 个航班通知
```
### Redis 监控 (每5分钟)
```
Redis 内存使用情况 - 当前: XXM, 峰值: XXM, 限制: 256M
```
### 告警阈值
- 🟢 正常: < 70%
- 🟡 警告: 70-80%
- 🟠 注意: 80-90%
- 🔴 严重: > 90%
## 🎯 预期效果
- ✅ 内存占用降低 50-70%
- ✅ 内存使用稳定
- ✅ 自动清理过期数据
- ✅ 防止内存泄漏
- ✅ 性能无明显影响
## 🔄 回滚方案
如果出现问题:
```bash
# 1. 恢复 Git
git checkout qaup-common/src/main/java/com/qaup/common/core/redis/RedisCache.java
# 2. 移除内存限制
redis-cli CONFIG SET maxmemory 0
# 3. 重新部署
mvn clean install -DskipTests && ./ry.sh restart
```
## 📚 详细文档
- 📖 [完整优化指南](doc/redis_optimization_guide.md)
- 📋 [实施总结](doc/redis_optimization_summary.md)
## ⚠️ 注意事项
1. **生产环境务必先备份**
2. **建议先在测试环境验证**
3. **监控应用性能和日志**
4. **Redis 配置实时生效,应用需重启**
---
**最后更新**: 2025-10-13 | **版本**: 1.0.0

View File

@ -0,0 +1,272 @@
# Redis 内存优化指南
## 问题诊断
### 1. 检查当前 Redis 内存使用情况
```bash
# 连接到 Redis
redis-cli
# 查看内存使用情况
INFO memory
# 查看所有 key 数量
DBSIZE
# 查看各类型 key 数量分布
redis-cli --scan --pattern '*' | awk -F: '{print $1}' | sort | uniq -c | sort -nr
# 查看最占内存的 key
redis-cli --bigkeys
# 查看没有过期时间的 key
redis-cli --scan | xargs -L 1 redis-cli TTL | grep -c '^-1$'
```
### 2. 主要内存占用来源分析
#### 项目中的 Redis 使用场景:
- **Token 缓存** (login_tokens:*) - 用户登录令牌默认30分钟
- **验证码缓存** (captcha_codes:*) - 验证码默认2分钟
- **字典缓存** (sys_dict:*) - 系统字典数据
- **用户缓存** (sys_user_cache:*) - 用户信息缓存
- **限流计数器** (rate_limit:*) - 接口限流
- **WebSocket 消息缓存** (websocket:messages:*) - 最近消息30分钟过期
- **业务数据缓存** - 位置数据、航班通知等
## 优化方案
### 方案 1: 为所有缓存设置合理的过期时间
**修改 RedisCache.java添加默认过期时间:**
```java
// 建议配置
private static final long DEFAULT_EXPIRE_TIME = 3600; // 默认1小时
private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;
public <T> void setCacheObject(final String key, final T value) {
// 设置默认过期时间,避免数据永久保存
redisTemplate.opsForValue().set(key, value, DEFAULT_EXPIRE_TIME, DEFAULT_TIME_UNIT);
}
```
### 方案 2: 优化业务缓存策略
#### 2.1 优化 activeMovingObjectsCache
该缓存是内存级别(ConcurrentHashMap),不占用 Redis但需要定期清理过期数据:
```java
// DataCollectorService.java 添加清理逻辑
@Scheduled(fixedRate = 60000) // 每分钟清理一次
public void cleanupInactiveObjects() {
long now = System.currentTimeMillis();
long inactiveThreshold = 300000; // 5分钟无更新视为不活跃
activeMovingObjectsCache.entrySet().removeIf(entry -> {
MovingObject obj = entry.getValue();
return (now - obj.getLastUpdateTime()) > inactiveThreshold;
});
}
```
#### 2.2 减少 WebSocket 消息缓存大小和时间
```java
// MessageCacheService.java
private static final int MAX_CACHED_MESSAGES = 50; // 从100减少到50
private static final Duration CACHE_EXPIRY = Duration.ofMinutes(10); // 从30分钟减少到10分钟
```
### 方案 3: 配置 Redis 内存限制和淘汰策略
**在 application-dev.yml 添加 Redis 配置:**
```yaml
spring:
data:
redis:
# Redis 最大内存限制 (建议根据服务器内存设置)
# 生产环境建议 1-2GB开发环境 256-512MB
maxmemory: 512mb
# 内存淘汰策略
# volatile-lru: 从设置了过期时间的key中使用LRU算法淘汰
# allkeys-lru: 从所有key中使用LRU算法淘汰
# volatile-ttl: 从设置了过期时间的key中淘汰即将过期的
# noeviction: 不淘汰,内存满时返回错误
maxmemory-policy: volatile-lru
```
**或者直接修改 Redis 配置文件 redis.conf:**
```conf
# 设置最大内存 (512MB)
maxmemory 512mb
# 设置淘汰策略
maxmemory-policy volatile-lru
# 启用持久化优化
save "" # 关闭 RDB减少内存峰值
appendonly no # 如果不需要持久化,可以关闭 AOF
```
**应用配置后重启 Redis:**
```bash
# macOS (如果使用 Homebrew)
brew services restart redis
# 或直接使用配置文件启动
redis-server /usr/local/etc/redis.conf
```
### 方案 4: 优化具体缓存使用
#### 4.1 Token 缓存优化
```java
// TokenService.java - 确保 token 设置了过期时间
int expireTime = 30; // 30分钟
redisCache.setCacheObject(tokenKey, loginUser, expireTime, TimeUnit.MINUTES);
```
#### 4.2 字典缓存优化
```java
// DictUtils.java - 字典数据可以设置较长过期时间
public static void setDictCache(String key, List<SysDictData> dictDatas) {
redisCache.setCacheObject(getCacheKey(key), dictDatas, 24, TimeUnit.HOURS);
}
```
#### 4.3 限流计数器优化
```java
// RateLimiterAspect.java - 限流计数器确保有过期时间
// Lua 脚本已经处理,无需修改
```
### 方案 5: 添加定期清理任务
创建定时任务清理过期或无用的缓存:
```java
@Component
public class RedisCacheCleanupTask {
@Autowired
private RedisCache redisCache;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 每天凌晨2点清理
@Scheduled(cron = "0 0 2 * * ?")
public void cleanupExpiredCache() {
log.info("开始清理 Redis 过期缓存");
// 清理临时数据
cleanupPattern("captcha_codes:*");
// 清理过期的 WebSocket 消息 (已自动过期)
// 清理不活跃用户缓存 (超过7天未登录)
cleanupInactiveUsers();
log.info("Redis 缓存清理完成");
}
private void cleanupPattern(String pattern) {
Collection<String> keys = redisCache.keys(pattern);
if (keys != null && !keys.isEmpty()) {
keys.forEach(key -> {
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
if (ttl == null || ttl == -1) {
// 没有设置过期时间的 key设置默认过期时间
redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
});
}
}
private void cleanupInactiveUsers() {
// 实现逻辑
}
}
```
## 实施步骤
### 立即执行 (紧急)
1. **设置 Redis 内存限制** (防止 OOM):
```bash
redis-cli CONFIG SET maxmemory 512mb
redis-cli CONFIG SET maxmemory-policy volatile-lru
```
2. **清理当前 Redis 数据** (谨慎操作):
```bash
# 查看当前内存使用
redis-cli INFO memory
# 清理所有数据 (注意: 会清空所有缓存)
redis-cli FLUSHDB
# 或者只清理特定模式的 key
redis-cli --scan --pattern 'websocket:messages:*' | xargs redis-cli DEL
```
### 短期优化 (1-2天)
1. 修改 `RedisCache.java` 添加默认过期时间
2. 优化 `MessageCacheService.java` 缓存参数
3. 添加 Redis 配置到 `application.yml`
4. 为 `activeMovingObjectsCache` 添加清理逻辑
### 长期优化 (1周内)
1. 创建 `RedisCacheCleanupTask` 定时清理任务
2. 审查所有 `setCacheObject` 调用,确保设置了过期时间
3. 实施 Redis 监控和告警
4. 考虑使用 Redis 集群或分离缓存存储
## 监控建议
### 添加 Redis 监控
```java
@Component
public class RedisMonitor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Scheduled(fixedRate = 300000) // 每5分钟
public void monitorMemoryUsage() {
Properties info = redisTemplate.getRequiredConnectionFactory()
.getConnection()
.info("memory");
String usedMemory = info.getProperty("used_memory_human");
String maxMemory = info.getProperty("maxmemory_human");
log.info("Redis Memory Usage: {} / {}", usedMemory, maxMemory);
// 内存使用超过80%时告警
// 实现告警逻辑
}
}
```
## 预期效果
实施以上优化后,预期:
- Redis 内存占用降低 50-70%
- 内存使用稳定在设置的上限以内
- 不会出现内存持续增长的情况
- 系统性能不会受到明显影响
## 注意事项
1. **生产环境操作前务必备份**
2. **Redis 配置修改需要重启服务**
3. **FLUSHDB 会清空所有缓存,可能导致短暂性能下降**
4. **监控内存使用情况,及时调整策略**

View File

@ -0,0 +1,260 @@
# Redis 内存优化实施总结
## 问题描述
项目中 Redis 运行一段时间后占用大量内存,可能导致系统性能下降或 OOM。
## 已实施的优化措施
### 1. 修改 RedisCache 类 - 添加默认过期时间
**文件**: `qaup-common/src/main/java/com/qaup/common/core/redis/RedisCache.java`
**改动**:
- `setCacheObject(key, value)` 方法现在默认设置 1 小时过期时间
- 新增 `setCacheObjectPermanent(key, value)` 方法用于需要永久缓存的数据
- 防止缓存数据无限期保存导致内存泄漏
**影响**:
- 所有使用 `setCacheObject` 的地方会自动获得过期时间
- 现有缓存会在 1 小时后自动清理
- 需要永久缓存的数据需要改用新方法
### 2. 优化 WebSocket 消息缓存
**文件**: `qaup-collision/src/main/java/com/qaup/collision/websocket/cache/MessageCacheService.java`
**改动**:
- 最大缓存消息数: 100 → 50
- 过期时间: 30分钟 → 10分钟
- 减少 50% 的 WebSocket 消息缓存占用
### 3. 添加内存缓存清理逻辑
**文件**: `qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java`
**新增功能**:
- 每 60 秒执行一次内存缓存清理
- 清理 5 分钟未更新的 `flightNotificationCache` 数据
- 监控 `activeMovingObjectsCache` 大小,超过 1000 个对象时发出警告
- 应用关闭时自动清理所有内存缓存
### 4. 配置 Redis 内存限制
**文件**:
- `qaup-admin/src/main/resources/application-dev.yml` (开发环境)
- `qaup-admin/src/main/resources/application-prod.yml` (生产环境)
**新增配置**:
```yaml
redis:
max-memory: 256mb # 开发环境
# max-memory: 1gb # 生产环境
max-memory-policy: volatile-lru
```
**说明**:
- 限制 Redis 最大内存使用
- 使用 LRU 算法淘汰设置了过期时间的 key
- 生产环境支持环境变量配置
### 5. 创建 Redis 缓存清理和监控任务
**文件**: `qaup-framework/src/main/java/com/qaup/framework/task/RedisCacheCleanupTask.java`
**功能**:
1. **每天凌晨 2 点执行缓存清理**
- 清理验证码等临时缓存
- 为没有过期时间的 key 设置默认 TTL
2. **每 5 分钟监控内存使用情况**
- 记录当前内存、峰值内存和限制
- 使用率超过 80% 时发出警告
- 使用率超过 90% 时发出错误日志
3. **每小时统计 key 数量**
- 统计各类型 key 的数量
- 帮助识别异常增长的缓存
### 6. 创建运维脚本
#### 6.1 Redis 配置脚本
**文件**: `scripts/configure_redis_memory.sh`
**功能**:
- 快速配置 Redis 内存限制
- 提供开发/生产环境预设
- 可选保存到配置文件
- 实时生效,无需重启
**使用方法**:
```bash
cd scripts
./configure_redis_memory.sh
```
#### 6.2 Redis 诊断脚本
**文件**: `scripts/redis_memory_diagnosis.sh`
**功能**:
- 分析内存使用情况
- 统计 key 数量分布
- 检查没有过期时间的 key
- 提供优化建议
**使用方法**:
```bash
cd scripts
./redis_memory_diagnosis.sh
```
## 立即执行的操作步骤
### 第一步: 运行诊断脚本
```bash
cd /Users/tianjianyong/apps/Company/QAUP-Management
./scripts/redis_memory_diagnosis.sh
```
查看当前 Redis 内存使用情况和问题。
### 第二步: 配置 Redis 内存限制
```bash
./scripts/configure_redis_memory.sh
```
选择适合的环境配置(开发环境选 1生产环境选 2)。
### 第三步: 重新编译和部署应用
```bash
# 编译项目
mvn clean install -DskipTests
# 重启应用
./ry.sh restart
```
### 第四步: 监控效果
等待应用运行一段时间后,再次运行诊断脚本:
```bash
./scripts/redis_memory_diagnosis.sh
```
## 预期效果
实施优化后,预期:
- ✅ Redis 内存占用降低 50-70%
- ✅ 内存使用稳定在设置的上限以内
- ✅ 不会出现内存持续增长的情况
- ✅ 系统性能不会受到明显影响
- ✅ 自动清理过期和不活跃的缓存
## 监控指标
### 关键日志
应用运行后会输出以下日志:
1. **内存缓存清理** (每分钟):
```
清理不活跃缓存对象: X 个移动对象, Y 个航班通知; 剩余: XX 个移动对象, YY 个航班通知
```
2. **Redis 内存监控** (每 5 分钟):
```
Redis 内存使用情况 - 当前: XXM, 峰值: XXM, 限制: 256M
```
3. **Key 数量统计** (每小时):
```
Redis Key 统计 - 登录Token: XX 个
Redis Key 统计 - WebSocket消息: XX 个
...
```
### 需要关注的指标
- Redis 内存使用率保持在 70% 以下
- 没有持续增长的趋势
- Key 数量稳定或有规律的波动
- 无大量没有过期时间的 key
## 回滚方案
如果优化导致问题,可以快速回滚:
### 1. 恢复 RedisCache 类
```bash
git checkout qaup-common/src/main/java/com/qaup/common/core/redis/RedisCache.java
```
### 2. 移除 Redis 内存限制
```bash
redis-cli CONFIG SET maxmemory 0
redis-cli CONFIG SET maxmemory-policy noeviction
```
### 3. 重新部署
```bash
mvn clean install -DskipTests
./ry.sh restart
```
## 长期维护建议
### 1. 定期检查 Redis 内存
每周运行诊断脚本,关注内存使用趋势:
```bash
./scripts/redis_memory_diagnosis.sh
```
### 2. 审查缓存使用
- 审查所有 `setCacheObject` 调用
- 确保设置了合理的过期时间
- 避免缓存大对象或大量数据
### 3. 监控和告警
考虑添加:
- Redis 内存使用率告警 (>80%)
- Key 数量异常增长告警
- 应用层缓存监控
### 4. 数据库层面优化
- 考虑使用 Redis 集群
- 分离不同类型的缓存(如会话、业务数据)
- 评估是否需要持久化
## 相关文档
- **详细优化指南**: [doc/redis_optimization_guide.md](redis_optimization_guide.md)
- **诊断脚本**: [scripts/redis_memory_diagnosis.sh](../scripts/redis_memory_diagnosis.sh)
- **配置脚本**: [scripts/configure_redis_memory.sh](../scripts/configure_redis_memory.sh)
## 修改清单
### 修改的文件
1. `qaup-common/src/main/java/com/qaup/common/core/redis/RedisCache.java`
2. `qaup-collision/src/main/java/com/qaup/collision/websocket/cache/MessageCacheService.java`
3. `qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java`
4. `qaup-admin/src/main/resources/application-dev.yml`
5. `qaup-admin/src/main/resources/application-prod.yml`
### 新增的文件
1. `qaup-framework/src/main/java/com/qaup/framework/task/RedisCacheCleanupTask.java`
2. `scripts/configure_redis_memory.sh`
3. `scripts/redis_memory_diagnosis.sh`
4. `doc/redis_optimization_guide.md`
5. `doc/redis_optimization_summary.md` (本文件)
## 注意事项
⚠️ **重要提示**:
1. 生产环境操作前务必备份数据
2. 建议先在开发/测试环境验证
3. 监控应用性能,确保无异常
4. Redis 配置修改可实时生效,但应用需要重启以应用代码修改
## 技术支持
如有问题,请检查:
1. 应用日志: `qaup-admin/app.log`
2. Redis 日志: `/var/log/redis/redis-server.log`
3. 运行诊断脚本获取详细信息
---
**优化完成时间**: 2025-10-13
**版本**: 1.0.0
**作者**: Claude Code

View File

@ -59,6 +59,17 @@ spring:
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# Redis 内存优化配置
redis:
# 最大内存限制(开发环境)
max-memory: 256mb
# 内存淘汰策略从设置了过期时间的key中使用LRU算法淘汰
# volatile-lru: 从设置了过期时间的key中使用LRU算法淘汰
# allkeys-lru: 从所有key中使用LRU算法淘汰
# volatile-ttl: 从设置了过期时间的key中淘汰即将过期的
# noeviction: 不淘汰,内存满时返回错误
max-memory-policy: volatile-lru
# 日志配置 - 开发环境(详细日志)
logging:
level:

View File

@ -33,6 +33,13 @@ spring:
max-active: 8
max-wait: -1ms
# Redis 内存优化配置(生产环境)
redis:
# 最大内存限制生产环境建议1-2GB
max-memory: ${REDIS_MAX_MEMORY:1gb}
# 内存淘汰策略
max-memory-policy: ${REDIS_MAX_MEMORY_POLICY:volatile-lru}
# 日志配置(生产环境简洁日志)
logging:
level:

View File

@ -712,9 +712,54 @@ public class DataCollectorService {
}
/**
* 定期清理不活跃的缓存对象内存优化
* 每分钟执行一次清理超过5分钟未更新的对象
*/
@Scheduled(fixedRate = 60000) // 每60秒执行一次
public void cleanupInactiveObjects() {
try {
long now = System.currentTimeMillis();
long inactiveThreshold = 300000; // 5分钟无更新视为不活跃
int removedMovingObjects = 0;
int removedFlightNotifications = 0;
// 清理 activeMovingObjectsCache
// 注意: MovingObject 没有时间戳字段需要通过外部跟踪或限制缓存大小
// 这里先保持简单策略如果缓存超过一定数量可以考虑清理
int maxCacheSize = 1000; // 最大缓存1000个对象
if (activeMovingObjectsCache.size() > maxCacheSize) {
log.warn("activeMovingObjectsCache 超过最大限制 {}, 当前: {}",
maxCacheSize, activeMovingObjectsCache.size());
}
// 清理 flightNotificationCache - 使用 eventTime 字段判断
int sizeBefore = flightNotificationCache.size();
flightNotificationCache.entrySet().removeIf(entry -> {
FlightNotification notification = entry.getValue();
long eventTime = (notification.getEventTime() != null) ? notification.getEventTime() : 0;
return (now - eventTime) > inactiveThreshold;
});
removedFlightNotifications = sizeBefore - flightNotificationCache.size();
if (removedMovingObjects > 0 || removedFlightNotifications > 0) {
log.info("清理不活跃缓存对象: {} 个移动对象, {} 个航班通知; 剩余: {} 个移动对象, {} 个航班通知",
removedMovingObjects,
removedFlightNotifications,
activeMovingObjectsCache.size(),
flightNotificationCache.size());
}
} catch (Exception e) {
log.error("清理不活跃缓存对象时发生错误", e);
}
}
@PreDestroy
public void shutdown() {
log.info("DataCollectorService 正在关闭...");
// 清理资源如果需要
// 清理缓存
activeMovingObjectsCache.clear();
flightNotificationCache.clear();
log.info("缓存已清理");
}
}

View File

@ -22,8 +22,8 @@ public class MessageCacheService {
// 缓存配置常量
private static final String CACHE_KEY_PREFIX = "websocket:messages:";
private static final int MAX_CACHED_MESSAGES = 100; // 每类型最多缓存100条消息
private static final Duration CACHE_EXPIRY = Duration.ofMinutes(30); // 30分钟过期
private static final int MAX_CACHED_MESSAGES = 50; // 每类型最多缓存50条消息优化内存占用
private static final Duration CACHE_EXPIRY = Duration.ofMinutes(10); // 10分钟过期从30分钟优化
@Autowired
public MessageCacheService(@Qualifier("websocketRedisTemplate") RedisTemplate<String, Object> redisTemplate) {

View File

@ -25,13 +25,33 @@ public class RedisCache
@Autowired
public RedisTemplate redisTemplate;
/**
* 默认缓存过期时间1小时
* 防止缓存数据永久保存导致内存占用过高
*/
private static final long DEFAULT_EXPIRE_TIME = 3600;
private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;
/**
* 缓存基本的对象IntegerString实体类等
* 注意默认设置1小时过期时间避免内存泄漏
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value, DEFAULT_EXPIRE_TIME, DEFAULT_TIME_UNIT);
}
/**
* 缓存基本的对象不设置过期时间仅用于永久性配置数据
* 警告使用此方法需谨慎可能导致内存泄漏
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObjectPermanent(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}

View File

@ -0,0 +1,223 @@
package com.qaup.framework.task;
import com.qaup.common.core.redis.RedisCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
/**
* Redis 缓存清理和监控任务
*
* 功能
* 1. 定期清理过期和无用的缓存
* 2. 监控 Redis 内存使用情况
* 3. 为没有过期时间的 key 设置默认过期时间
*
* @author QAUP System
*/
@Component
public class RedisCacheCleanupTask {
private static final Logger log = LoggerFactory.getLogger(RedisCacheCleanupTask.class);
@Autowired
private RedisCache redisCache;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 每天凌晨2点执行缓存清理
*/
@Scheduled(cron = "0 0 2 * * ?")
public void cleanupExpiredCache() {
log.info("开始执行 Redis 缓存清理任务");
try {
// 清理验证码缓存虽然应该已经过期但双重保险
cleanupPattern("captcha_codes:*", 1, TimeUnit.HOURS);
// 清理临时会话数据
cleanupPattern("temp:*", 1, TimeUnit.HOURS);
// 检查并修复没有过期时间的 key
fixKeysWithoutExpiry();
log.info("Redis 缓存清理任务完成");
} catch (Exception e) {
log.error("Redis 缓存清理任务执行失败", e);
}
}
/**
* 每5分钟监控 Redis 内存使用情况
*/
@Scheduled(fixedRate = 300000) // 5分钟
public void monitorMemoryUsage() {
try {
Properties info = redisTemplate.getRequiredConnectionFactory()
.getConnection()
.serverCommands()
.info("memory");
if (info != null) {
String usedMemory = info.getProperty("used_memory_human");
String maxMemory = info.getProperty("maxmemory_human");
String usedMemoryPeak = info.getProperty("used_memory_peak_human");
log.info("Redis 内存使用情况 - 当前: {}, 峰值: {}, 限制: {}",
usedMemory, usedMemoryPeak, maxMemory);
// 检查内存使用率
checkMemoryUsageThreshold(info);
}
} catch (Exception e) {
log.error("监控 Redis 内存使用情况失败", e);
}
}
/**
* 每小时统计 Redis key 数量
*/
@Scheduled(fixedRate = 3600000) // 1小时
public void statisticsKeyCount() {
try {
// 统计各类型 key 的数量
logKeyCount("login_tokens:*", "登录Token");
logKeyCount("captcha_codes:*", "验证码");
logKeyCount("sys_dict:*", "字典缓存");
logKeyCount("websocket:messages:*", "WebSocket消息");
logKeyCount("rate_limit:*", "限流计数");
} catch (Exception e) {
log.error("统计 Redis key 数量失败", e);
}
}
/**
* 清理指定模式的 key为没有过期时间的 key 设置默认过期时间
*
* @param pattern key 模式
* @param timeout 过期时间
* @param unit 时间单位
*/
private void cleanupPattern(String pattern, long timeout, TimeUnit unit) {
try {
Collection<String> keys = redisCache.keys(pattern);
if (keys == null || keys.isEmpty()) {
return;
}
int fixedCount = 0;
for (String key : keys) {
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
// TTL = -1 表示没有过期时间
if (ttl != null && ttl == -1) {
redisTemplate.expire(key, timeout, unit);
fixedCount++;
}
}
if (fixedCount > 0) {
log.info("为 {} 个 '{}' 模式的 key 设置了过期时间", fixedCount, pattern);
}
} catch (Exception e) {
log.error("清理模式 '{}' 的缓存失败", pattern, e);
}
}
/**
* 检查并修复没有过期时间的 key
*/
private void fixKeysWithoutExpiry() {
try {
// 扫描所有 key注意生产环境慎用 keys *
Collection<String> allKeys = redisCache.keys("*");
if (allKeys == null || allKeys.isEmpty()) {
return;
}
int fixedCount = 0;
int totalCount = allKeys.size();
for (String key : allKeys) {
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
// 跳过已有过期时间的 key
if (ttl == null || ttl != -1) {
continue;
}
// 根据 key 类型设置不同的过期时间
if (key.startsWith("sys_dict:") || key.startsWith("sys_config:")) {
// 字典和配置数据24小时
redisTemplate.expire(key, 24, TimeUnit.HOURS);
fixedCount++;
} else if (key.startsWith("login_tokens:") || key.startsWith("user:")) {
// 用户相关数据2小时
redisTemplate.expire(key, 2, TimeUnit.HOURS);
fixedCount++;
} else {
// 其他数据1小时
redisTemplate.expire(key, 1, TimeUnit.HOURS);
fixedCount++;
}
}
if (fixedCount > 0) {
log.warn("发现 {} / {} 个 key 没有过期时间,已自动修复", fixedCount, totalCount);
}
} catch (Exception e) {
log.error("修复没有过期时间的 key 失败", e);
}
}
/**
* 检查内存使用率阈值
*/
private void checkMemoryUsageThreshold(Properties info) {
try {
String usedMemoryStr = info.getProperty("used_memory");
String maxMemoryStr = info.getProperty("maxmemory");
if (usedMemoryStr == null || maxMemoryStr == null || "0".equals(maxMemoryStr)) {
return;
}
long usedMemory = Long.parseLong(usedMemoryStr);
long maxMemory = Long.parseLong(maxMemoryStr);
double usageRate = (double) usedMemory / maxMemory * 100;
if (usageRate > 90) {
log.error("Redis 内存使用率超过 90%: {:.2f}%, 请立即检查!", usageRate);
} else if (usageRate > 80) {
log.warn("Redis 内存使用率超过 80%: {:.2f}%, 建议清理缓存", usageRate);
} else if (usageRate > 70) {
log.info("Redis 内存使用率: {:.2f}%", usageRate);
}
} catch (Exception e) {
log.error("检查内存使用率失败", e);
}
}
/**
* 记录指定模式的 key 数量
*/
private void logKeyCount(String pattern, String description) {
try {
Collection<String> keys = redisCache.keys(pattern);
int count = (keys != null) ? keys.size() : 0;
log.info("Redis Key 统计 - {}: {} 个", description, count);
} catch (Exception e) {
log.error("统计 '{}' key 数量失败", pattern, e);
}
}
}

120
scripts/configure_redis_memory.sh Executable file
View File

@ -0,0 +1,120 @@
#!/bin/bash
# Redis 内存优化配置脚本
# 用于快速配置 Redis 内存限制和淘汰策略
echo "=========================================="
echo "Redis 内存优化配置脚本"
echo "=========================================="
# 检查 Redis 是否运行
if ! redis-cli ping > /dev/null 2>&1; then
echo "错误: Redis 未运行或无法连接"
echo "请先启动 Redis 服务"
exit 1
fi
echo ""
echo "当前 Redis 配置:"
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy
echo ""
echo "当前内存使用情况:"
redis-cli INFO memory | grep -E "used_memory_human|used_memory_peak_human|maxmemory_human"
echo ""
echo "当前数据库大小:"
redis-cli DBSIZE
echo ""
echo "=========================================="
echo "选择环境:"
echo "1) 开发环境 (256MB)"
echo "2) 生产环境 (1GB)"
echo "3) 自定义"
echo "4) 仅查看信息,不修改"
echo "=========================================="
read -p "请选择 [1-4]: " choice
case $choice in
1)
MAX_MEMORY="268435456" # 256MB in bytes
MAX_MEMORY_STR="256mb"
;;
2)
MAX_MEMORY="1073741824" # 1GB in bytes
MAX_MEMORY_STR="1gb"
;;
3)
read -p "请输入最大内存限制 (如: 512mb, 2gb): " MAX_MEMORY_STR
;;
4)
echo "未进行任何修改"
exit 0
;;
*)
echo "无效选择"
exit 1
;;
esac
# 设置淘汰策略
EVICTION_POLICY="volatile-lru"
echo ""
echo "=========================================="
echo "将设置以下配置:"
echo "最大内存: $MAX_MEMORY_STR"
echo "淘汰策略: $EVICTION_POLICY"
echo "=========================================="
read -p "确认执行? [y/N]: " confirm
if [[ $confirm != "y" && $confirm != "Y" ]]; then
echo "已取消"
exit 0
fi
# 执行配置
echo ""
echo "正在配置 Redis..."
if [ -n "$MAX_MEMORY_STR" ]; then
redis-cli CONFIG SET maxmemory "$MAX_MEMORY_STR"
echo "✓ 已设置 maxmemory = $MAX_MEMORY_STR"
fi
redis-cli CONFIG SET maxmemory-policy "$EVICTION_POLICY"
echo "✓ 已设置 maxmemory-policy = $EVICTION_POLICY"
# 可选: 保存到配置文件 (需要权限)
echo ""
read -p "是否保存到 Redis 配置文件? (需要写权限) [y/N]: " save_config
if [[ $save_config == "y" || $save_config == "Y" ]]; then
redis-cli CONFIG REWRITE
if [ $? -eq 0 ]; then
echo "✓ 配置已保存到 redis.conf"
else
echo "✗ 保存失败,请检查权限或手动修改 redis.conf"
fi
fi
echo ""
echo "=========================================="
echo "配置完成!"
echo "=========================================="
echo ""
echo "新配置:"
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy
echo ""
echo "建议操作:"
echo "1. 监控内存使用: redis-cli INFO memory"
echo "2. 查看 key 数量: redis-cli DBSIZE"
echo "3. 查看大 key: redis-cli --bigkeys"
echo "4. 查看应用日志以确认系统正常运行"
echo ""
echo "注意: 配置立即生效,无需重启 Redis"

149
scripts/redis_memory_diagnosis.sh Executable file
View File

@ -0,0 +1,149 @@
#!/bin/bash
# Redis 内存使用诊断脚本
# 分析 Redis 内存占用情况并提供优化建议
echo "=========================================="
echo "Redis 内存使用诊断报告"
echo "=========================================="
echo "生成时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# 检查 Redis 是否运行
if ! redis-cli ping > /dev/null 2>&1; then
echo "错误: Redis 未运行或无法连接"
exit 1
fi
echo "1. 内存使用概览"
echo "----------------------------------------"
redis-cli INFO memory | grep -E "used_memory_human|used_memory_peak_human|maxmemory_human|mem_fragmentation_ratio|used_memory_rss_human"
echo ""
echo "2. 配置信息"
echo "----------------------------------------"
redis-cli CONFIG GET maxmemory | xargs printf "最大内存限制: %s = %s\n"
redis-cli CONFIG GET maxmemory-policy | xargs printf "淘汰策略: %s = %s\n"
echo ""
echo "3. 数据库统计"
echo "----------------------------------------"
printf "Key 总数: "
redis-cli DBSIZE
echo ""
echo "4. 各类型 Key 数量分布"
echo "----------------------------------------"
echo "注意: 此操作可能耗时较长..."
# 统计不同前缀的 key 数量
declare -A key_counts
while IFS= read -r key; do
prefix=$(echo "$key" | cut -d':' -f1)
((key_counts[$prefix]++))
done < <(redis-cli --scan)
echo "前缀统计:"
for prefix in "${!key_counts[@]}"; do
printf " %-30s %d 个\n" "$prefix:" "${key_counts[$prefix]}"
done | sort -k2 -rn | head -20
echo ""
echo "5. 最大的 Key (Top 10)"
echo "----------------------------------------"
redis-cli --bigkeys 2>/dev/null | grep -A 20 "Biggest"
echo ""
echo "6. 没有过期时间的 Key 统计"
echo "----------------------------------------"
no_ttl_count=0
total_keys=$(redis-cli DBSIZE | awk '{print $1}')
# 采样检查 (最多检查1000个key)
sample_size=1000
checked=0
no_ttl_samples=""
while IFS= read -r key && [ $checked -lt $sample_size ]; do
ttl=$(redis-cli TTL "$key")
if [ "$ttl" = "-1" ]; then
((no_ttl_count++))
if [ $no_ttl_count -le 5 ]; then
no_ttl_samples="$no_ttl_samples\n $key"
fi
fi
((checked++))
done < <(redis-cli --scan)
if [ $checked -ge $total_keys ]; then
echo "检查了全部 $total_keys 个 key"
echo "没有过期时间的 key: $no_ttl_count 个 ($(awk "BEGIN {printf \"%.1f\", $no_ttl_count/$total_keys*100}")%)"
else
echo "采样检查了 $checked 个 key (总数: $total_keys)"
echo "采样中没有过期时间的 key: $no_ttl_count 个 ($(awk "BEGIN {printf \"%.1f\", $no_ttl_count/$checked*100}")%)"
fi
if [ $no_ttl_count -gt 0 ]; then
echo -e "示例:$no_ttl_samples"
fi
echo ""
echo "7. 内存使用率评估"
echo "----------------------------------------"
used_memory=$(redis-cli INFO memory | grep "used_memory:" | cut -d':' -f2 | tr -d '\r')
max_memory=$(redis-cli CONFIG GET maxmemory | tail -1 | tr -d '\r')
if [ "$max_memory" = "0" ]; then
echo "⚠️ 警告: 未设置最大内存限制!"
echo " 建议: 执行 ./configure_redis_memory.sh 设置内存限制"
else
usage_percent=$(awk "BEGIN {printf \"%.1f\", $used_memory/$max_memory*100}")
echo "内存使用率: $usage_percent%"
if (( $(echo "$usage_percent > 90" | bc -l) )); then
echo "🔴 严重: 内存使用率超过 90%!"
echo " 建议: 立即清理缓存或增加内存限制"
elif (( $(echo "$usage_percent > 80" | bc -l) )); then
echo "🟡 警告: 内存使用率超过 80%"
echo " 建议: 清理不必要的缓存"
elif (( $(echo "$usage_percent > 70" | bc -l) )); then
echo "🟢 正常: 内存使用率在合理范围内"
else
echo "🟢 良好: 内存使用率较低"
fi
fi
echo ""
echo "8. 优化建议"
echo "----------------------------------------"
# 检查是否设置了最大内存
if [ "$max_memory" = "0" ]; then
echo "✓ 设置 Redis 最大内存限制"
echo " 命令: redis-cli CONFIG SET maxmemory 512mb"
fi
# 检查是否有很多没有过期时间的 key
if [ $no_ttl_count -gt $(($checked / 2)) ]; then
echo "✓ 大量 key 没有过期时间,建议设置 TTL"
echo " 自动修复: 应用中的 RedisCacheCleanupTask 会自动处理"
fi
# 检查淘汰策略
eviction_policy=$(redis-cli CONFIG GET maxmemory-policy | tail -1 | tr -d '\r')
if [ "$eviction_policy" = "noeviction" ]; then
echo "✓ 当前淘汰策略为 noeviction,建议改为 volatile-lru"
echo " 命令: redis-cli CONFIG SET maxmemory-policy volatile-lru"
fi
echo ""
echo "=========================================="
echo "诊断完成!"
echo "=========================================="
echo ""
echo "快速操作:"
echo "1. 配置 Redis 内存: ./configure_redis_memory.sh"
echo "2. 清理 Redis 数据: redis-cli FLUSHDB (谨慎!)"
echo "3. 重启应用以应用新的缓存策略"