移除Redis依赖并改造为内存缓存
This commit is contained in:
264
bin/restart_java_backend.sh
Executable file
264
bin/restart_java_backend.sh
Executable file
@@ -0,0 +1,264 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||||
|
LOG_DIR="$ROOT_DIR/logs"
|
||||||
|
CONSOLE_LOG="$LOG_DIR/backend-console.log"
|
||||||
|
PID_FILE="$LOG_DIR/backend-java.pid"
|
||||||
|
TARGET_DIR="$ROOT_DIR/ruoyi-admin/target"
|
||||||
|
JAR_NAME="ruoyi-admin.jar"
|
||||||
|
SERVER_PORT=63310
|
||||||
|
STOP_WAIT_SECONDS=30
|
||||||
|
APP_MARKER="-Dccdi.backend.root=$ROOT_DIR"
|
||||||
|
JAVA_OPTS="$APP_MARKER -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
|
||||||
|
|
||||||
|
timestamp() {
|
||||||
|
date "+%Y-%m-%d %H:%M:%S"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
printf '[%s] %s\n' "$(timestamp)" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
printf '[%s] %s\n' "$(timestamp)" "$1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
用法: ./bin/restart_java_backend.sh [start|stop|restart|status]
|
||||||
|
|
||||||
|
默认动作:
|
||||||
|
restart 重新构建后端并重启,随后持续输出运行日志
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_command() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
log_error "缺少命令: $1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_managed_backend_pid() {
|
||||||
|
pid="$1"
|
||||||
|
if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
args=$(ps -o args= -p "$pid" 2>/dev/null || true)
|
||||||
|
if [ -z "${args:-}" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$args" in
|
||||||
|
*"$APP_MARKER"*"$JAR_NAME"*|*"$JAR_NAME"*"$APP_MARKER"*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
file_pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
||||||
|
if [ "${file_pid:-}" = "$pid" ]; then
|
||||||
|
case "$args" in
|
||||||
|
*"java"*"-jar"*"$JAR_NAME"*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_pids() {
|
||||||
|
all_pids=""
|
||||||
|
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
file_pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
||||||
|
if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then
|
||||||
|
all_pids="$all_pids $file_pid"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
marker_pids=$(ps -ef | awk -v marker="$APP_MARKER" -v jar="$JAR_NAME" '
|
||||||
|
index($0, "<defunct>") == 0 && index($0, marker) > 0 {
|
||||||
|
for (i = 1; i < NF; i++) {
|
||||||
|
if ($i == "-jar" && $(i + 1) == jar) {
|
||||||
|
print $2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' | xargs 2>/dev/null || true)
|
||||||
|
if [ -n "${marker_pids:-}" ]; then
|
||||||
|
for pid in $marker_pids; do
|
||||||
|
if is_managed_backend_pid "$pid"; then
|
||||||
|
all_pids="$all_pids $pid"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
unique_pids=""
|
||||||
|
for pid in $all_pids; do
|
||||||
|
case " $unique_pids " in
|
||||||
|
*" $pid "*) ;;
|
||||||
|
*)
|
||||||
|
unique_pids="$unique_pids $pid"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s\n' "$(echo "$unique_pids" | xargs 2>/dev/null || true)"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_backend() {
|
||||||
|
log_info "开始构建后端: mvn -pl ruoyi-admin -am clean package -DskipTests"
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
mvn -pl ruoyi-admin -am clean package -DskipTests
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_backend() {
|
||||||
|
pids=$(collect_pids)
|
||||||
|
|
||||||
|
if [ -z "${pids:-}" ]; then
|
||||||
|
log_info "未发现运行中的后端进程"
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "准备停止后端进程: $pids"
|
||||||
|
for pid in $pids; do
|
||||||
|
kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
remaining_pids="$pids"
|
||||||
|
elapsed=0
|
||||||
|
while [ -n "${remaining_pids:-}" ] && [ "$elapsed" -lt "$STOP_WAIT_SECONDS" ]; do
|
||||||
|
sleep 1
|
||||||
|
elapsed=$((elapsed + 1))
|
||||||
|
remaining_pids=""
|
||||||
|
for pid in $pids; do
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
remaining_pids="$remaining_pids $pid"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
remaining_pids=$(echo "$remaining_pids" | xargs 2>/dev/null || true)
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "${remaining_pids:-}" ]; then
|
||||||
|
log_info "仍有进程未退出,执行强制停止: $remaining_pids"
|
||||||
|
for pid in $remaining_pids; do
|
||||||
|
kill -KILL "$pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
log_info "后端停止完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_backend() {
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
touch "$CONSOLE_LOG"
|
||||||
|
|
||||||
|
printf '\n===== %s restart =====\n' "$(timestamp)" >> "$CONSOLE_LOG"
|
||||||
|
|
||||||
|
log_info "开始启动后端,控制台日志输出到: $CONSOLE_LOG"
|
||||||
|
if [ ! -f "$TARGET_DIR/$JAR_NAME" ]; then
|
||||||
|
log_error "未找到打包产物: $TARGET_DIR/$JAR_NAME"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$TARGET_DIR"
|
||||||
|
nohup java $JAVA_OPTS -jar "$JAR_NAME" >> "$CONSOLE_LOG" 2>&1 &
|
||||||
|
echo $! > "$PID_FILE"
|
||||||
|
)
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
starter_pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
||||||
|
if [ -z "${starter_pid:-}" ] || ! kill -0 "$starter_pid" 2>/dev/null; then
|
||||||
|
log_error "启动命令未保持运行,请检查日志: $CONSOLE_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "启动命令已提交,PID: $starter_pid"
|
||||||
|
}
|
||||||
|
|
||||||
|
status_backend() {
|
||||||
|
pids=$(collect_pids)
|
||||||
|
if [ -n "${pids:-}" ]; then
|
||||||
|
log_info "后端正在运行,进程: $pids"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
port_pids=$(lsof -tiTCP:"$SERVER_PORT" -sTCP:LISTEN 2>/dev/null || true)
|
||||||
|
if [ -n "${port_pids:-}" ]; then
|
||||||
|
log_info "未发现脚本托管的后端进程,但端口 $SERVER_PORT 被其他进程占用: $port_pids"
|
||||||
|
else
|
||||||
|
log_info "后端未运行"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
follow_logs() {
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
touch "$CONSOLE_LOG"
|
||||||
|
log_info "持续输出日志中,按 Ctrl+C 仅退出日志查看"
|
||||||
|
tail -n 200 -F "$CONSOLE_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_action() {
|
||||||
|
running_pids=$(collect_pids)
|
||||||
|
if [ -n "${running_pids:-}" ]; then
|
||||||
|
log_error "检测到已有后端进程在运行: $running_pids,请先执行 stop 或 restart"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
build_backend
|
||||||
|
start_backend
|
||||||
|
follow_logs
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_action() {
|
||||||
|
build_backend
|
||||||
|
stop_backend
|
||||||
|
start_backend
|
||||||
|
follow_logs
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
ensure_command mvn
|
||||||
|
ensure_command lsof
|
||||||
|
ensure_command ps
|
||||||
|
ensure_command tail
|
||||||
|
|
||||||
|
action="${1:-restart}"
|
||||||
|
case "$action" in
|
||||||
|
start)
|
||||||
|
start_action
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_backend
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart_action
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status_backend
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
116
doc/2026-04-15-前端缓存监控页适配内存缓存实施记录.md
Normal file
116
doc/2026-04-15-前端缓存监控页适配内存缓存实施记录.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# 前端缓存监控页适配内存缓存实施记录
|
||||||
|
|
||||||
|
## 1. 实际改动内容
|
||||||
|
|
||||||
|
### 1.1 调整缓存监控首页
|
||||||
|
|
||||||
|
修改文件:
|
||||||
|
|
||||||
|
- `ruoyi-ui/src/views/monitor/cache/index.vue`
|
||||||
|
|
||||||
|
改动内容:
|
||||||
|
|
||||||
|
- 页面展示从 Redis 服务器指标改为内存缓存统计
|
||||||
|
- 增加默认数据结构:
|
||||||
|
- `cache_type`
|
||||||
|
- `cache_mode`
|
||||||
|
- `key_size`
|
||||||
|
- `hit_count`
|
||||||
|
- `miss_count`
|
||||||
|
- `expired_count`
|
||||||
|
- `write_count`
|
||||||
|
- 新增格式化与归一化方法:
|
||||||
|
- `normalizeCacheData`
|
||||||
|
- `normalizeCommandStats`
|
||||||
|
- `formatCacheType`
|
||||||
|
- `formatCacheMode`
|
||||||
|
- `formatSampleTime`
|
||||||
|
- `toNumber`
|
||||||
|
- 基本信息区改为展示:
|
||||||
|
- 缓存类型
|
||||||
|
- 运行模式
|
||||||
|
- 总键数
|
||||||
|
- 写入次数
|
||||||
|
- 命中次数
|
||||||
|
- 未命中次数
|
||||||
|
- 过期清理次数
|
||||||
|
- 命中率
|
||||||
|
- 图表改为展示内存缓存统计分布与条形概览
|
||||||
|
|
||||||
|
### 1.2 调整缓存列表页清理全部后的状态
|
||||||
|
|
||||||
|
修改文件:
|
||||||
|
|
||||||
|
- `ruoyi-ui/src/views/monitor/cache/list.vue`
|
||||||
|
|
||||||
|
改动内容:
|
||||||
|
|
||||||
|
- 清理全部缓存后主动清空:
|
||||||
|
- `cacheKeys`
|
||||||
|
- `cacheForm`
|
||||||
|
- `nowCacheName`
|
||||||
|
- 重新加载缓存分类列表,避免页面残留旧数据显示
|
||||||
|
|
||||||
|
### 1.3 API 文件
|
||||||
|
|
||||||
|
文件:
|
||||||
|
|
||||||
|
- `ruoyi-ui/src/api/monitor/cache.js`
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
- 接口路径与交互方式保持不变
|
||||||
|
- 本次未对 API 文件做额外改动
|
||||||
|
|
||||||
|
## 2. 验证结果
|
||||||
|
|
||||||
|
### 2.1 Node 版本
|
||||||
|
|
||||||
|
项目中未提供 `.nvmrc`,因此未能直接执行 `nvm use` 自动切换。
|
||||||
|
|
||||||
|
本机 `nvm` 已安装版本:
|
||||||
|
|
||||||
|
- `v14.21.3`
|
||||||
|
- `v25.9.0`
|
||||||
|
|
||||||
|
实际使用版本:
|
||||||
|
|
||||||
|
- `nvm use 14.21.3`
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- Vue CLI 4 + Vue 2 对较新的 Node 版本存在潜在兼容风险,优先使用更稳妥的 `v14.21.3`
|
||||||
|
|
||||||
|
### 2.2 构建命令
|
||||||
|
|
||||||
|
已执行:
|
||||||
|
|
||||||
|
- `cd ruoyi-ui && source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod`
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- 构建成功
|
||||||
|
- 输出 `DONE Build complete. The dist directory is ready to be deployed.`
|
||||||
|
|
||||||
|
### 2.3 构建告警
|
||||||
|
|
||||||
|
存在 webpack 资源体积告警:
|
||||||
|
|
||||||
|
- `asset size limit`
|
||||||
|
- `entrypoint size limit`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 这些是现有项目静态资源体积告警,不影响本次缓存监控页适配结果
|
||||||
|
- 本次未对该类历史体积问题做额外处理
|
||||||
|
|
||||||
|
## 3. 与计划差异
|
||||||
|
|
||||||
|
- 计划中预期通过 `nvm use` 自动切换版本,但项目没有 `.nvmrc`,因此改为显式执行 `nvm use 14.21.3`
|
||||||
|
- 除此之外,前端实施路径与计划一致
|
||||||
|
|
||||||
|
## 4. 当前结论
|
||||||
|
|
||||||
|
- 缓存监控首页已适配后端内存缓存统计结构
|
||||||
|
- 缓存列表页与现有接口保持兼容
|
||||||
|
- 前端生产构建已通过
|
||||||
131
doc/2026-04-15-移除Redis依赖改造为内存缓存后端实施记录.md
Normal file
131
doc/2026-04-15-移除Redis依赖改造为内存缓存后端实施记录.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# 移除 Redis 依赖改造为内存缓存后端实施记录
|
||||||
|
|
||||||
|
## 1. 实际改动内容
|
||||||
|
|
||||||
|
### 1.1 新增内存缓存底座
|
||||||
|
|
||||||
|
新增文件:
|
||||||
|
|
||||||
|
- `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java`
|
||||||
|
- `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java`
|
||||||
|
- `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java`
|
||||||
|
|
||||||
|
实现内容:
|
||||||
|
|
||||||
|
- 使用 `ConcurrentHashMap` 存储缓存值
|
||||||
|
- 支持 TTL、过期清理、按前缀查询 key、批量删除
|
||||||
|
- 支持计数器递增能力,供限流使用
|
||||||
|
- 提供命中、未命中、过期、写入、总键数统计快照
|
||||||
|
|
||||||
|
### 1.2 改造缓存门面
|
||||||
|
|
||||||
|
修改文件:
|
||||||
|
|
||||||
|
- `ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java`
|
||||||
|
|
||||||
|
改造内容:
|
||||||
|
|
||||||
|
- 保留原 `RedisCache` 业务入口
|
||||||
|
- 移除 `RedisTemplate` 依赖
|
||||||
|
- 底层改为委托 `InMemoryCacheStore`
|
||||||
|
- 补充 `increment`、`getCacheStats`、`clear` 能力
|
||||||
|
|
||||||
|
### 1.3 改造限流与缓存监控
|
||||||
|
|
||||||
|
修改文件:
|
||||||
|
|
||||||
|
- `ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java`
|
||||||
|
- `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java`
|
||||||
|
|
||||||
|
改造内容:
|
||||||
|
|
||||||
|
- `RateLimiterAspect` 删除 Lua + Redis 依赖,改为内存计数
|
||||||
|
- `CacheController` 改为返回内存缓存统计与缓存内容
|
||||||
|
- 缓存清理接口改为操作本地缓存门面
|
||||||
|
|
||||||
|
### 1.4 删除 Redis 配置与依赖
|
||||||
|
|
||||||
|
修改文件:
|
||||||
|
|
||||||
|
- `ruoyi-common/pom.xml`
|
||||||
|
- `ruoyi-framework/pom.xml`
|
||||||
|
- `ruoyi-admin/pom.xml`
|
||||||
|
- `ruoyi-admin/src/main/resources/application.yml`
|
||||||
|
|
||||||
|
删除文件:
|
||||||
|
|
||||||
|
- `ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java`
|
||||||
|
- `ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java`
|
||||||
|
|
||||||
|
处理结果:
|
||||||
|
|
||||||
|
- 删除 `spring-boot-starter-data-redis`
|
||||||
|
- 删除 `commons-pool2`
|
||||||
|
- 删除 `spring.redis` 配置块
|
||||||
|
- 删除 Redis 专属配置类
|
||||||
|
- 为 `ruoyi-common`、`ruoyi-framework`、`ruoyi-admin` 增加 `spring-boot-starter-test` 测试依赖
|
||||||
|
|
||||||
|
### 1.5 新增后端测试
|
||||||
|
|
||||||
|
新增文件:
|
||||||
|
|
||||||
|
- `ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java`
|
||||||
|
- `ruoyi-common/src/test/java/com/ruoyi/common/core/redis/RedisCacheTest.java`
|
||||||
|
- `ruoyi-framework/src/test/java/com/ruoyi/framework/aspectj/RateLimiterAspectTest.java`
|
||||||
|
- `ruoyi-framework/src/test/java/com/ruoyi/framework/web/service/TokenServiceLocalCacheTest.java`
|
||||||
|
- `ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java`
|
||||||
|
|
||||||
|
覆盖内容:
|
||||||
|
|
||||||
|
- 内存缓存读写 / TTL / keys / 统计
|
||||||
|
- 缓存门面能力
|
||||||
|
- 限流计数
|
||||||
|
- token 登录态缓存
|
||||||
|
- 验证码删除
|
||||||
|
- 防重提交毫秒级窗口
|
||||||
|
- 缓存监控接口
|
||||||
|
|
||||||
|
## 2. 执行结果
|
||||||
|
|
||||||
|
### 2.1 验证命令
|
||||||
|
|
||||||
|
已执行:
|
||||||
|
|
||||||
|
- `mvn -pl ruoyi-common -Dtest=InMemoryCacheStoreTest,RedisCacheTest test`
|
||||||
|
- `mvn -pl ruoyi-framework -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=RateLimiterAspectTest,TokenServiceLocalCacheTest test`
|
||||||
|
- `mvn -pl ruoyi-admin -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CacheControllerTest test`
|
||||||
|
- `mvn clean package -DskipTests`
|
||||||
|
- `mvn -pl ruoyi-common,ruoyi-framework,ruoyi-admin -am test`
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- 上述命令全部执行成功
|
||||||
|
|
||||||
|
### 2.2 运行态冒烟
|
||||||
|
|
||||||
|
已执行:
|
||||||
|
|
||||||
|
- `java -jar ruoyi-admin/target/ruoyi-admin.jar --server.port=18080`
|
||||||
|
- `curl http://127.0.0.1:18080/captchaImage`
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- 应用可正常启动
|
||||||
|
- 启动过程未出现 Redis 连接错误
|
||||||
|
- `captchaImage` 返回:`{"msg":"操作成功","code":200,"captchaEnabled":false}`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 默认端口 `8080` 被本机其它进程占用,因此改用临时端口 `18080` 做冒烟
|
||||||
|
- 冒烟结束后已手动停止本次启动的 Java 进程
|
||||||
|
|
||||||
|
## 3. 与计划差异
|
||||||
|
|
||||||
|
- 计划中建议使用 `mvn -pl ruoyi-admin -am spring-boot:run` 启动应用,但本地 Maven 环境未解析到 `spring-boot` 插件前缀,因此改为直接运行已构建完成的 `ruoyi-admin.jar`
|
||||||
|
- 除上述启动方式调整外,其余后端实施路径与计划一致
|
||||||
|
|
||||||
|
## 4. 当前结论
|
||||||
|
|
||||||
|
- 后端已完成 Redis 依赖移除
|
||||||
|
- Redis 相关能力已切换为单实例 JVM 内存实现
|
||||||
|
- 构建、单元测试、运行态公开接口冒烟均已完成
|
||||||
@@ -55,13 +55,19 @@
|
|||||||
<artifactId>ruoyi-quartz</artifactId>
|
<artifactId>ruoyi-quartz</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 代码生成-->
|
<!-- 代码生成-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.ruoyi</groupId>
|
<groupId>com.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-generator</artifactId>
|
<artifactId>ruoyi-generator</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
@@ -93,4 +99,4 @@
|
|||||||
<finalName>${project.artifactId}</finalName>
|
<finalName>${project.artifactId}</finalName>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,121 +1,148 @@
|
|||||||
package com.ruoyi.web.controller.monitor;
|
package com.ruoyi.web.controller.monitor;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import java.util.Collection;
|
import com.ruoyi.common.constant.CacheConstants;
|
||||||
import java.util.HashMap;
|
import com.ruoyi.common.core.cache.InMemoryCacheStats;
|
||||||
import java.util.List;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import java.util.Map;
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
import java.util.Properties;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import java.util.Set;
|
import com.ruoyi.system.domain.SysCache;
|
||||||
import java.util.TreeSet;
|
import java.util.ArrayList;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import java.util.Collection;
|
||||||
import org.springframework.data.redis.core.RedisCallback;
|
import java.util.HashMap;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import java.util.LinkedHashMap;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import java.util.List;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import java.util.Map;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import java.util.Set;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import java.util.TreeSet;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import com.ruoyi.common.constant.CacheConstants;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import com.ruoyi.system.domain.SysCache;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缓存监控
|
* 缓存监控
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/monitor/cache")
|
@RequestMapping("/monitor/cache")
|
||||||
public class CacheController
|
public class CacheController
|
||||||
{
|
{
|
||||||
@Autowired
|
private final RedisCache redisCache;
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
|
||||||
|
private final static List<SysCache> caches = new ArrayList<SysCache>();
|
||||||
private final static List<SysCache> caches = new ArrayList<SysCache>();
|
{
|
||||||
{
|
caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
|
||||||
caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
|
caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
|
||||||
caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
|
caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
|
||||||
caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
|
caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
|
||||||
caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
|
caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
|
||||||
caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
|
caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
|
||||||
caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
|
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
|
||||||
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
|
}
|
||||||
}
|
|
||||||
|
public CacheController(RedisCache redisCache)
|
||||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
{
|
||||||
@GetMapping()
|
this.redisCache = redisCache;
|
||||||
public AjaxResult getInfo() throws Exception
|
}
|
||||||
{
|
|
||||||
Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||||
Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
|
@GetMapping()
|
||||||
Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
|
public AjaxResult getInfo()
|
||||||
|
{
|
||||||
Map<String, Object> result = new HashMap<>(3);
|
InMemoryCacheStats stats = redisCache.getCacheStats();
|
||||||
result.put("info", info);
|
Map<String, Object> info = new LinkedHashMap<String, Object>();
|
||||||
result.put("dbSize", dbSize);
|
info.put("cache_type", stats.getCacheType());
|
||||||
|
info.put("cache_mode", stats.getMode());
|
||||||
List<Map<String, String>> pieList = new ArrayList<>();
|
info.put("key_size", stats.getKeySize());
|
||||||
commandStats.stringPropertyNames().forEach(key -> {
|
info.put("hit_count", stats.getHitCount());
|
||||||
Map<String, String> data = new HashMap<>(2);
|
info.put("miss_count", stats.getMissCount());
|
||||||
String property = commandStats.getProperty(key);
|
info.put("expired_count", stats.getExpiredCount());
|
||||||
data.put("name", StringUtils.removeStart(key, "cmdstat_"));
|
info.put("write_count", stats.getWriteCount());
|
||||||
data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
|
|
||||||
pieList.add(data);
|
Map<String, Object> result = new HashMap<String, Object>(3);
|
||||||
});
|
result.put("info", info);
|
||||||
result.put("commandStats", pieList);
|
result.put("dbSize", stats.getKeySize());
|
||||||
return AjaxResult.success(result);
|
|
||||||
}
|
List<Map<String, String>> pieList = new ArrayList<Map<String, String>>();
|
||||||
|
pieList.add(statEntry("hit_count", stats.getHitCount()));
|
||||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
pieList.add(statEntry("miss_count", stats.getMissCount()));
|
||||||
@GetMapping("/getNames")
|
pieList.add(statEntry("expired_count", stats.getExpiredCount()));
|
||||||
public AjaxResult cache()
|
pieList.add(statEntry("write_count", stats.getWriteCount()));
|
||||||
{
|
result.put("commandStats", pieList);
|
||||||
return AjaxResult.success(caches);
|
return AjaxResult.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||||
@GetMapping("/getKeys/{cacheName}")
|
@GetMapping("/getNames")
|
||||||
public AjaxResult getCacheKeys(@PathVariable String cacheName)
|
public AjaxResult cache()
|
||||||
{
|
{
|
||||||
Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
|
return AjaxResult.success(caches);
|
||||||
return AjaxResult.success(new TreeSet<>(cacheKeys));
|
}
|
||||||
}
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
@GetMapping("/getKeys/{cacheName}")
|
||||||
@GetMapping("/getValue/{cacheName}/{cacheKey}")
|
public AjaxResult getCacheKeys(@PathVariable String cacheName)
|
||||||
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
|
{
|
||||||
{
|
Set<String> cacheKeys = new TreeSet<String>(redisCache.keys(cacheName + "*"));
|
||||||
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
return AjaxResult.success(cacheKeys);
|
||||||
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
|
}
|
||||||
return AjaxResult.success(sysCache);
|
|
||||||
}
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||||
|
@GetMapping("/getValue/{cacheName}/{cacheKey}")
|
||||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
|
||||||
@DeleteMapping("/clearCacheName/{cacheName}")
|
{
|
||||||
public AjaxResult clearCacheName(@PathVariable String cacheName)
|
Object cacheValue = redisCache.getCacheObject(cacheKey);
|
||||||
{
|
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValueToString(cacheValue));
|
||||||
Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
|
return AjaxResult.success(sysCache);
|
||||||
redisTemplate.delete(cacheKeys);
|
}
|
||||||
return AjaxResult.success();
|
|
||||||
}
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||||
|
@DeleteMapping("/clearCacheName/{cacheName}")
|
||||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
public AjaxResult clearCacheName(@PathVariable String cacheName)
|
||||||
@DeleteMapping("/clearCacheKey/{cacheKey}")
|
{
|
||||||
public AjaxResult clearCacheKey(@PathVariable String cacheKey)
|
Collection<String> cacheKeys = redisCache.keys(cacheName + "*");
|
||||||
{
|
redisCache.deleteObject(cacheKeys);
|
||||||
redisTemplate.delete(cacheKey);
|
return AjaxResult.success();
|
||||||
return AjaxResult.success();
|
}
|
||||||
}
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
@DeleteMapping("/clearCacheKey/{cacheKey}")
|
||||||
@DeleteMapping("/clearCacheAll")
|
public AjaxResult clearCacheKey(@PathVariable String cacheKey)
|
||||||
public AjaxResult clearCacheAll()
|
{
|
||||||
{
|
redisCache.deleteObject(cacheKey);
|
||||||
Collection<String> cacheKeys = redisTemplate.keys("*");
|
return AjaxResult.success();
|
||||||
redisTemplate.delete(cacheKeys);
|
}
|
||||||
return AjaxResult.success();
|
|
||||||
}
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||||
}
|
@DeleteMapping("/clearCacheAll")
|
||||||
|
public AjaxResult clearCacheAll()
|
||||||
|
{
|
||||||
|
redisCache.clear();
|
||||||
|
return AjaxResult.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> statEntry(String name, long value)
|
||||||
|
{
|
||||||
|
Map<String, String> data = new HashMap<String, String>(2);
|
||||||
|
data.put("name", name);
|
||||||
|
data.put("value", String.valueOf(value));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cacheValueToString(Object cacheValue)
|
||||||
|
{
|
||||||
|
if (cacheValue == null)
|
||||||
|
{
|
||||||
|
return StringUtils.EMPTY;
|
||||||
|
}
|
||||||
|
if (cacheValue instanceof String)
|
||||||
|
{
|
||||||
|
return (String) cacheValue;
|
||||||
|
}
|
||||||
|
return JSON.toJSONString(cacheValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,36 +60,14 @@ spring:
|
|||||||
max-file-size: 10MB
|
max-file-size: 10MB
|
||||||
# 设置总上传的文件大小
|
# 设置总上传的文件大小
|
||||||
max-request-size: 20MB
|
max-request-size: 20MB
|
||||||
# 服务模块
|
# 服务模块
|
||||||
devtools:
|
devtools:
|
||||||
restart:
|
restart:
|
||||||
# 热部署开关
|
# 热部署开关
|
||||||
enabled: true
|
enabled: true
|
||||||
# redis 配置
|
|
||||||
redis:
|
# token配置
|
||||||
# 地址
|
token:
|
||||||
host: 116.62.17.81
|
|
||||||
# 端口,默认为6379
|
|
||||||
port: 6379
|
|
||||||
# 数据库索引
|
|
||||||
database: 0
|
|
||||||
# 密码
|
|
||||||
password: Kfcx@1234
|
|
||||||
# 连接超时时间
|
|
||||||
timeout: 10s
|
|
||||||
lettuce:
|
|
||||||
pool:
|
|
||||||
# 连接池中的最小空闲连接
|
|
||||||
min-idle: 0
|
|
||||||
# 连接池中的最大空闲连接
|
|
||||||
max-idle: 8
|
|
||||||
# 连接池的最大数据库连接数
|
|
||||||
max-active: 8
|
|
||||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
|
||||||
max-wait: -1ms
|
|
||||||
|
|
||||||
# token配置
|
|
||||||
token:
|
|
||||||
# 令牌自定义标识
|
# 令牌自定义标识
|
||||||
header: Authorization
|
header: Authorization
|
||||||
# 令牌密钥
|
# 令牌密钥
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ruoyi.web.controller.monitor;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||||
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
class CacheControllerTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
void shouldReturnInMemoryCacheSummary() throws Exception
|
||||||
|
{
|
||||||
|
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||||
|
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CacheController(redisCache)).build();
|
||||||
|
|
||||||
|
mockMvc.perform(get("/monitor/cache"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.data.info.cache_type").value("IN_MEMORY"))
|
||||||
|
.andExpect(jsonPath("$.data.info.cache_mode").value("single-instance"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldClearCacheKeysByPrefix() throws Exception
|
||||||
|
{
|
||||||
|
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||||
|
redisCache.setCacheObject("login_tokens:a", "A");
|
||||||
|
redisCache.setCacheObject("login_tokens:b", "B");
|
||||||
|
|
||||||
|
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CacheController(redisCache)).build();
|
||||||
|
|
||||||
|
mockMvc.perform(delete("/monitor/cache/clearCacheName/login_tokens:"))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/monitor/cache/getKeys/login_tokens:"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.data").isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,30 +89,24 @@
|
|||||||
<artifactId>jaxb-api</artifactId>
|
<artifactId>jaxb-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- redis 缓存操作 -->
|
<!-- 解析客户端操作系统、浏览器等 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>nl.basjes.parse.useragent</groupId>
|
||||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
<artifactId>yauaa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- pool 对象池 -->
|
<!-- servlet包 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>javax.servlet</groupId>
|
||||||
<artifactId>commons-pool2</artifactId>
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 解析客户端操作系统、浏览器等 -->
|
<dependency>
|
||||||
<dependency>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<groupId>nl.basjes.parse.useragent</groupId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<artifactId>yauaa</artifactId>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- servlet包 -->
|
</dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>javax.servlet</groupId>
|
</project>
|
||||||
<artifactId>javax.servlet-api</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
</project>
|
|
||||||
|
|||||||
29
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java
vendored
Normal file
29
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ruoyi.common.core.cache;
|
||||||
|
|
||||||
|
class InMemoryCacheEntry
|
||||||
|
{
|
||||||
|
private final Object value;
|
||||||
|
|
||||||
|
private final Long expireAtMillis;
|
||||||
|
|
||||||
|
InMemoryCacheEntry(Object value, Long expireAtMillis)
|
||||||
|
{
|
||||||
|
this.value = value;
|
||||||
|
this.expireAtMillis = expireAtMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object getValue()
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long getExpireAtMillis()
|
||||||
|
{
|
||||||
|
return expireAtMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isExpired(long now)
|
||||||
|
{
|
||||||
|
return expireAtMillis != null && expireAtMillis.longValue() <= now;
|
||||||
|
}
|
||||||
|
}
|
||||||
65
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java
vendored
Normal file
65
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package com.ruoyi.common.core.cache;
|
||||||
|
|
||||||
|
public class InMemoryCacheStats
|
||||||
|
{
|
||||||
|
private final String cacheType;
|
||||||
|
|
||||||
|
private final String mode;
|
||||||
|
|
||||||
|
private final long keySize;
|
||||||
|
|
||||||
|
private final long hitCount;
|
||||||
|
|
||||||
|
private final long missCount;
|
||||||
|
|
||||||
|
private final long expiredCount;
|
||||||
|
|
||||||
|
private final long writeCount;
|
||||||
|
|
||||||
|
public InMemoryCacheStats(String cacheType, String mode, long keySize, long hitCount, long missCount,
|
||||||
|
long expiredCount, long writeCount)
|
||||||
|
{
|
||||||
|
this.cacheType = cacheType;
|
||||||
|
this.mode = mode;
|
||||||
|
this.keySize = keySize;
|
||||||
|
this.hitCount = hitCount;
|
||||||
|
this.missCount = missCount;
|
||||||
|
this.expiredCount = expiredCount;
|
||||||
|
this.writeCount = writeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCacheType()
|
||||||
|
{
|
||||||
|
return cacheType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMode()
|
||||||
|
{
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getKeySize()
|
||||||
|
{
|
||||||
|
return keySize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getHitCount()
|
||||||
|
{
|
||||||
|
return hitCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMissCount()
|
||||||
|
{
|
||||||
|
return missCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpiredCount()
|
||||||
|
{
|
||||||
|
return expiredCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getWriteCount()
|
||||||
|
{
|
||||||
|
return writeCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
290
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java
vendored
Normal file
290
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java
vendored
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
package com.ruoyi.common.core.cache;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class InMemoryCacheStore
|
||||||
|
{
|
||||||
|
private final ConcurrentHashMap<String, InMemoryCacheEntry> entries = new ConcurrentHashMap<String, InMemoryCacheEntry>();
|
||||||
|
|
||||||
|
private final AtomicLong hitCount = new AtomicLong();
|
||||||
|
|
||||||
|
private final AtomicLong missCount = new AtomicLong();
|
||||||
|
|
||||||
|
private final AtomicLong expiredCount = new AtomicLong();
|
||||||
|
|
||||||
|
private final AtomicLong writeCount = new AtomicLong();
|
||||||
|
|
||||||
|
public void set(String key, Object value)
|
||||||
|
{
|
||||||
|
putEntry(key, new InMemoryCacheEntry(value, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(String key, Object value, long timeout, TimeUnit unit)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(unit, "TimeUnit must not be null");
|
||||||
|
long expireAtMillis = System.currentTimeMillis() + Math.max(0L, unit.toMillis(timeout));
|
||||||
|
putEntry(key, new InMemoryCacheEntry(value, Long.valueOf(expireAtMillis)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T get(String key)
|
||||||
|
{
|
||||||
|
InMemoryCacheEntry entry = readEntry(key);
|
||||||
|
return entry == null ? null : (T) entry.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasKey(String key)
|
||||||
|
{
|
||||||
|
return readEntry(key) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean delete(String key)
|
||||||
|
{
|
||||||
|
return entries.remove(key) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean delete(Collection<String> keys)
|
||||||
|
{
|
||||||
|
boolean deleted = false;
|
||||||
|
if (keys == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String key : keys)
|
||||||
|
{
|
||||||
|
deleted = delete(key) || deleted;
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> keys(String pattern)
|
||||||
|
{
|
||||||
|
purgeExpiredEntries();
|
||||||
|
Set<String> matchedKeys = new TreeSet<String>();
|
||||||
|
for (Map.Entry<String, InMemoryCacheEntry> entry : entries.entrySet())
|
||||||
|
{
|
||||||
|
if (matches(pattern, entry.getKey()))
|
||||||
|
{
|
||||||
|
matchedKeys.add(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchedKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean expire(String key, long timeout, TimeUnit unit)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(unit, "TimeUnit must not be null");
|
||||||
|
long expireAtMillis = System.currentTimeMillis() + Math.max(0L, unit.toMillis(timeout));
|
||||||
|
return entries.computeIfPresent(key, (cacheKey, entry) -> entry.isExpired(System.currentTimeMillis())
|
||||||
|
? null
|
||||||
|
: new InMemoryCacheEntry(entry.getValue(), Long.valueOf(expireAtMillis))) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpire(String key)
|
||||||
|
{
|
||||||
|
return getExpire(key, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpire(String key, TimeUnit unit)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(unit, "TimeUnit must not be null");
|
||||||
|
InMemoryCacheEntry entry = readEntry(key);
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
return -2L;
|
||||||
|
}
|
||||||
|
if (entry.getExpireAtMillis() == null)
|
||||||
|
{
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
long remainingMillis = Math.max(0L, entry.getExpireAtMillis().longValue() - System.currentTimeMillis());
|
||||||
|
long unitMillis = Math.max(1L, unit.toMillis(1L));
|
||||||
|
return (remainingMillis + unitMillis - 1L) / unitMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long increment(String key, long timeout, TimeUnit unit)
|
||||||
|
{
|
||||||
|
Objects.requireNonNull(unit, "TimeUnit must not be null");
|
||||||
|
AtomicLong result = new AtomicLong();
|
||||||
|
entries.compute(key, (cacheKey, currentEntry) -> {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
boolean missingOrExpired = currentEntry == null || currentEntry.isExpired(now);
|
||||||
|
long nextValue = missingOrExpired ? 1L : toLong(currentEntry.getValue()) + 1L;
|
||||||
|
Long expireAtMillis = missingOrExpired || currentEntry.getExpireAtMillis() == null
|
||||||
|
? Long.valueOf(now + Math.max(0L, unit.toMillis(timeout)))
|
||||||
|
: currentEntry.getExpireAtMillis();
|
||||||
|
if (missingOrExpired && currentEntry != null)
|
||||||
|
{
|
||||||
|
expiredCount.incrementAndGet();
|
||||||
|
}
|
||||||
|
result.set(nextValue);
|
||||||
|
return new InMemoryCacheEntry(Long.valueOf(nextValue), expireAtMillis);
|
||||||
|
});
|
||||||
|
writeCount.incrementAndGet();
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear()
|
||||||
|
{
|
||||||
|
entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InMemoryCacheStats snapshot()
|
||||||
|
{
|
||||||
|
purgeExpiredEntries();
|
||||||
|
return new InMemoryCacheStats("IN_MEMORY", "single-instance", entries.size(), hitCount.get(), missCount.get(),
|
||||||
|
expiredCount.get(), writeCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> Map<String, T> getMap(String key)
|
||||||
|
{
|
||||||
|
Map<String, T> value = get(key);
|
||||||
|
return value == null ? null : new HashMap<String, T>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void putMap(String key, Map<String, T> dataMap)
|
||||||
|
{
|
||||||
|
set(key, new HashMap<String, T>(dataMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void putMapValue(String key, String mapKey, T value)
|
||||||
|
{
|
||||||
|
long ttl = getExpire(key, TimeUnit.MILLISECONDS);
|
||||||
|
Map<String, T> current = getMap(key);
|
||||||
|
if (current == null)
|
||||||
|
{
|
||||||
|
current = new HashMap<String, T>();
|
||||||
|
}
|
||||||
|
current.put(mapKey, value);
|
||||||
|
setWithOptionalTtl(key, current, ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteMapValue(String key, String mapKey)
|
||||||
|
{
|
||||||
|
long ttl = getExpire(key, TimeUnit.MILLISECONDS);
|
||||||
|
Map<String, Object> current = getMap(key);
|
||||||
|
if (current == null || !current.containsKey(mapKey))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
current.remove(mapKey);
|
||||||
|
setWithOptionalTtl(key, current, ttl);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> Set<T> getSet(String key)
|
||||||
|
{
|
||||||
|
Set<T> value = get(key);
|
||||||
|
return value == null ? null : new HashSet<T>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void putSet(String key, Set<T> dataSet)
|
||||||
|
{
|
||||||
|
set(key, new HashSet<T>(dataSet));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> List<T> getList(String key)
|
||||||
|
{
|
||||||
|
List<T> value = get(key);
|
||||||
|
return value == null ? null : new ArrayList<T>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void putList(String key, List<T> dataList)
|
||||||
|
{
|
||||||
|
set(key, new ArrayList<T>(dataList));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putEntry(String key, InMemoryCacheEntry entry)
|
||||||
|
{
|
||||||
|
entries.put(key, entry);
|
||||||
|
writeCount.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InMemoryCacheEntry readEntry(String key)
|
||||||
|
{
|
||||||
|
InMemoryCacheEntry entry = entries.get(key);
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
missCount.incrementAndGet();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (entry.isExpired(System.currentTimeMillis()))
|
||||||
|
{
|
||||||
|
removeExpiredEntry(key, entry);
|
||||||
|
missCount.incrementAndGet();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
hitCount.incrementAndGet();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void purgeExpiredEntries()
|
||||||
|
{
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (Map.Entry<String, InMemoryCacheEntry> entry : entries.entrySet())
|
||||||
|
{
|
||||||
|
if (entry.getValue().isExpired(now))
|
||||||
|
{
|
||||||
|
removeExpiredEntry(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeExpiredEntry(String key, InMemoryCacheEntry expectedEntry)
|
||||||
|
{
|
||||||
|
if (entries.remove(key, expectedEntry))
|
||||||
|
{
|
||||||
|
expiredCount.incrementAndGet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matches(String pattern, String key)
|
||||||
|
{
|
||||||
|
if ("*".equals(pattern))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (pattern.endsWith("*"))
|
||||||
|
{
|
||||||
|
return key.startsWith(pattern.substring(0, pattern.length() - 1));
|
||||||
|
}
|
||||||
|
return key.equals(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long toLong(Object value)
|
||||||
|
{
|
||||||
|
if (value instanceof Number)
|
||||||
|
{
|
||||||
|
return ((Number) value).longValue();
|
||||||
|
}
|
||||||
|
return Long.parseLong(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setWithOptionalTtl(String key, Object value, long ttlMillis)
|
||||||
|
{
|
||||||
|
if (ttlMillis > 0L)
|
||||||
|
{
|
||||||
|
set(key, value, ttlMillis, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,42 @@
|
|||||||
package com.ruoyi.common.core.redis;
|
package com.ruoyi.common.core.redis;
|
||||||
|
|
||||||
import java.util.Collection;
|
import com.ruoyi.common.core.cache.InMemoryCacheStats;
|
||||||
import java.util.Iterator;
|
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||||
import java.util.List;
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Collections;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import java.util.Map;
|
||||||
import org.springframework.data.redis.core.BoundSetOperations;
|
import java.util.Set;
|
||||||
import org.springframework.data.redis.core.HashOperations;
|
import java.util.concurrent.TimeUnit;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.data.redis.core.ValueOperations;
|
|
||||||
import org.springframework.stereotype.Component;
|
/**
|
||||||
|
* 本地缓存门面,保留原有 RedisCache 业务入口。
|
||||||
/**
|
*
|
||||||
* spring redis 工具类
|
* @author ruoyi
|
||||||
*
|
**/
|
||||||
* @author ruoyi
|
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
||||||
**/
|
@Component
|
||||||
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
public class RedisCache
|
||||||
@Component
|
{
|
||||||
public class RedisCache
|
private final InMemoryCacheStore cacheStore;
|
||||||
{
|
|
||||||
@Autowired
|
public RedisCache(InMemoryCacheStore cacheStore)
|
||||||
public RedisTemplate redisTemplate;
|
{
|
||||||
|
this.cacheStore = cacheStore;
|
||||||
/**
|
}
|
||||||
* 缓存基本的对象,Integer、String、实体类等
|
|
||||||
|
/**
|
||||||
|
* 缓存基本的对象,Integer、String、实体类等
|
||||||
*
|
*
|
||||||
* @param key 缓存的键值
|
* @param key 缓存的键值
|
||||||
* @param value 缓存的值
|
* @param value 缓存的值
|
||||||
*/
|
*/
|
||||||
public <T> void setCacheObject(final String key, final T value)
|
public <T> void setCacheObject(final String key, final T value)
|
||||||
{
|
{
|
||||||
redisTemplate.opsForValue().set(key, value);
|
cacheStore.set(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缓存基本的对象,Integer、String、实体类等
|
* 缓存基本的对象,Integer、String、实体类等
|
||||||
@@ -44,10 +46,10 @@ public class RedisCache
|
|||||||
* @param timeout 时间
|
* @param timeout 时间
|
||||||
* @param timeUnit 时间颗粒度
|
* @param timeUnit 时间颗粒度
|
||||||
*/
|
*/
|
||||||
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
|
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
|
||||||
{
|
{
|
||||||
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
|
cacheStore.set(key, value, timeout.longValue(), timeUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置有效时间
|
* 设置有效时间
|
||||||
@@ -69,10 +71,10 @@ public class RedisCache
|
|||||||
* @param unit 时间单位
|
* @param unit 时间单位
|
||||||
* @return true=设置成功;false=设置失败
|
* @return true=设置成功;false=设置失败
|
||||||
*/
|
*/
|
||||||
public boolean expire(final String key, final long timeout, final TimeUnit unit)
|
public boolean expire(final String key, final long timeout, final TimeUnit unit)
|
||||||
{
|
{
|
||||||
return redisTemplate.expire(key, timeout, unit);
|
return cacheStore.expire(key, timeout, unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取有效时间
|
* 获取有效时间
|
||||||
@@ -80,10 +82,10 @@ public class RedisCache
|
|||||||
* @param key Redis键
|
* @param key Redis键
|
||||||
* @return 有效时间
|
* @return 有效时间
|
||||||
*/
|
*/
|
||||||
public long getExpire(final String key)
|
public long getExpire(final String key)
|
||||||
{
|
{
|
||||||
return redisTemplate.getExpire(key);
|
return cacheStore.getExpire(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断 key是否存在
|
* 判断 key是否存在
|
||||||
@@ -91,10 +93,10 @@ public class RedisCache
|
|||||||
* @param key 键
|
* @param key 键
|
||||||
* @return true 存在 false不存在
|
* @return true 存在 false不存在
|
||||||
*/
|
*/
|
||||||
public Boolean hasKey(String key)
|
public Boolean hasKey(String key)
|
||||||
{
|
{
|
||||||
return redisTemplate.hasKey(key);
|
return cacheStore.hasKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得缓存的基本对象。
|
* 获得缓存的基本对象。
|
||||||
@@ -102,21 +104,20 @@ public class RedisCache
|
|||||||
* @param key 缓存键值
|
* @param key 缓存键值
|
||||||
* @return 缓存键值对应的数据
|
* @return 缓存键值对应的数据
|
||||||
*/
|
*/
|
||||||
public <T> T getCacheObject(final String key)
|
public <T> T getCacheObject(final String key)
|
||||||
{
|
{
|
||||||
ValueOperations<String, T> operation = redisTemplate.opsForValue();
|
return cacheStore.get(key);
|
||||||
return operation.get(key);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除单个对象
|
* 删除单个对象
|
||||||
*
|
*
|
||||||
* @param key
|
* @param key
|
||||||
*/
|
*/
|
||||||
public boolean deleteObject(final String key)
|
public boolean deleteObject(final String key)
|
||||||
{
|
{
|
||||||
return redisTemplate.delete(key);
|
return cacheStore.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除集合对象
|
* 删除集合对象
|
||||||
@@ -124,10 +125,19 @@ public class RedisCache
|
|||||||
* @param collection 多个对象
|
* @param collection 多个对象
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public boolean deleteObject(final Collection collection)
|
public boolean deleteObject(final Collection collection)
|
||||||
{
|
{
|
||||||
return redisTemplate.delete(collection) > 0;
|
if (collection == null || collection.isEmpty())
|
||||||
}
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<String> keys = new ArrayList<String>(collection.size());
|
||||||
|
for (Object item : collection)
|
||||||
|
{
|
||||||
|
keys.add(String.valueOf(item));
|
||||||
|
}
|
||||||
|
return cacheStore.delete(keys);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缓存List数据
|
* 缓存List数据
|
||||||
@@ -136,11 +146,11 @@ public class RedisCache
|
|||||||
* @param dataList 待缓存的List数据
|
* @param dataList 待缓存的List数据
|
||||||
* @return 缓存的对象
|
* @return 缓存的对象
|
||||||
*/
|
*/
|
||||||
public <T> long setCacheList(final String key, final List<T> dataList)
|
public <T> long setCacheList(final String key, final List<T> dataList)
|
||||||
{
|
{
|
||||||
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
|
cacheStore.putList(key, dataList);
|
||||||
return count == null ? 0 : count;
|
return dataList == null ? 0 : dataList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得缓存的list对象
|
* 获得缓存的list对象
|
||||||
@@ -148,10 +158,10 @@ public class RedisCache
|
|||||||
* @param key 缓存的键值
|
* @param key 缓存的键值
|
||||||
* @return 缓存键值对应的数据
|
* @return 缓存键值对应的数据
|
||||||
*/
|
*/
|
||||||
public <T> List<T> getCacheList(final String key)
|
public <T> List<T> getCacheList(final String key)
|
||||||
{
|
{
|
||||||
return redisTemplate.opsForList().range(key, 0, -1);
|
return cacheStore.getList(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缓存Set
|
* 缓存Set
|
||||||
@@ -160,16 +170,11 @@ public class RedisCache
|
|||||||
* @param dataSet 缓存的数据
|
* @param dataSet 缓存的数据
|
||||||
* @return 缓存数据的对象
|
* @return 缓存数据的对象
|
||||||
*/
|
*/
|
||||||
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
|
public <T> long setCacheSet(final String key, final Set<T> dataSet)
|
||||||
{
|
{
|
||||||
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
|
cacheStore.putSet(key, dataSet);
|
||||||
Iterator<T> it = dataSet.iterator();
|
return dataSet == null ? 0 : dataSet.size();
|
||||||
while (it.hasNext())
|
}
|
||||||
{
|
|
||||||
setOperation.add(it.next());
|
|
||||||
}
|
|
||||||
return setOperation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得缓存的set
|
* 获得缓存的set
|
||||||
@@ -177,10 +182,10 @@ public class RedisCache
|
|||||||
* @param key
|
* @param key
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public <T> Set<T> getCacheSet(final String key)
|
public <T> Set<T> getCacheSet(final String key)
|
||||||
{
|
{
|
||||||
return redisTemplate.opsForSet().members(key);
|
return cacheStore.getSet(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缓存Map
|
* 缓存Map
|
||||||
@@ -188,12 +193,13 @@ public class RedisCache
|
|||||||
* @param key
|
* @param key
|
||||||
* @param dataMap
|
* @param dataMap
|
||||||
*/
|
*/
|
||||||
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
|
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
|
||||||
{
|
{
|
||||||
if (dataMap != null) {
|
if (dataMap != null)
|
||||||
redisTemplate.opsForHash().putAll(key, dataMap);
|
{
|
||||||
}
|
cacheStore.putMap(key, dataMap);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得缓存的Map
|
* 获得缓存的Map
|
||||||
@@ -201,10 +207,10 @@ public class RedisCache
|
|||||||
* @param key
|
* @param key
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public <T> Map<String, T> getCacheMap(final String key)
|
public <T> Map<String, T> getCacheMap(final String key)
|
||||||
{
|
{
|
||||||
return redisTemplate.opsForHash().entries(key);
|
return cacheStore.getMap(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 往Hash中存入数据
|
* 往Hash中存入数据
|
||||||
@@ -213,10 +219,10 @@ public class RedisCache
|
|||||||
* @param hKey Hash键
|
* @param hKey Hash键
|
||||||
* @param value 值
|
* @param value 值
|
||||||
*/
|
*/
|
||||||
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
|
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
|
||||||
{
|
{
|
||||||
redisTemplate.opsForHash().put(key, hKey, value);
|
cacheStore.putMapValue(key, hKey, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取Hash中的数据
|
* 获取Hash中的数据
|
||||||
@@ -225,11 +231,11 @@ public class RedisCache
|
|||||||
* @param hKey Hash键
|
* @param hKey Hash键
|
||||||
* @return Hash中的对象
|
* @return Hash中的对象
|
||||||
*/
|
*/
|
||||||
public <T> T getCacheMapValue(final String key, final String hKey)
|
public <T> T getCacheMapValue(final String key, final String hKey)
|
||||||
{
|
{
|
||||||
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
|
Map<String, T> map = cacheStore.getMap(key);
|
||||||
return opsForHash.get(key, hKey);
|
return map == null ? null : map.get(hKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取多个Hash中的数据
|
* 获取多个Hash中的数据
|
||||||
@@ -238,10 +244,20 @@ public class RedisCache
|
|||||||
* @param hKeys Hash键集合
|
* @param hKeys Hash键集合
|
||||||
* @return Hash对象集合
|
* @return Hash对象集合
|
||||||
*/
|
*/
|
||||||
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
|
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
|
||||||
{
|
{
|
||||||
return redisTemplate.opsForHash().multiGet(key, hKeys);
|
Map<String, T> map = cacheStore.getMap(key);
|
||||||
}
|
if (map == null || hKeys == null || hKeys.isEmpty())
|
||||||
|
{
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<T> values = new ArrayList<T>(hKeys.size());
|
||||||
|
for (Object hKey : hKeys)
|
||||||
|
{
|
||||||
|
values.add(map.get(String.valueOf(hKey)));
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除Hash中的某条数据
|
* 删除Hash中的某条数据
|
||||||
@@ -250,10 +266,10 @@ public class RedisCache
|
|||||||
* @param hKey Hash键
|
* @param hKey Hash键
|
||||||
* @return 是否成功
|
* @return 是否成功
|
||||||
*/
|
*/
|
||||||
public boolean deleteCacheMapValue(final String key, final String hKey)
|
public boolean deleteCacheMapValue(final String key, final String hKey)
|
||||||
{
|
{
|
||||||
return redisTemplate.opsForHash().delete(key, hKey) > 0;
|
return cacheStore.deleteMapValue(key, hKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得缓存的基本对象列表
|
* 获得缓存的基本对象列表
|
||||||
@@ -261,8 +277,23 @@ public class RedisCache
|
|||||||
* @param pattern 字符串前缀
|
* @param pattern 字符串前缀
|
||||||
* @return 对象列表
|
* @return 对象列表
|
||||||
*/
|
*/
|
||||||
public Collection<String> keys(final String pattern)
|
public Collection<String> keys(final String pattern)
|
||||||
{
|
{
|
||||||
return redisTemplate.keys(pattern);
|
return cacheStore.keys(pattern);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public long increment(String key, long timeout, TimeUnit unit)
|
||||||
|
{
|
||||||
|
return cacheStore.increment(key, timeout, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InMemoryCacheStats getCacheStats()
|
||||||
|
{
|
||||||
|
return cacheStore.snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear()
|
||||||
|
{
|
||||||
|
cacheStore.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
58
ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java
vendored
Normal file
58
ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package com.ruoyi.common.core.cache;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class InMemoryCacheStoreTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
void shouldExpireEntryAfterTtl() throws Exception
|
||||||
|
{
|
||||||
|
InMemoryCacheStore store = new InMemoryCacheStore();
|
||||||
|
store.set("captcha_codes:1", "1234", 20L, TimeUnit.MILLISECONDS);
|
||||||
|
Thread.sleep(40L);
|
||||||
|
assertNull(store.get("captcha_codes:1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnPrefixKeysInSortedOrder()
|
||||||
|
{
|
||||||
|
InMemoryCacheStore store = new InMemoryCacheStore();
|
||||||
|
store.set("login_tokens:a", "A");
|
||||||
|
store.set("login_tokens:b", "B");
|
||||||
|
store.set("sys_dict:x", "X");
|
||||||
|
|
||||||
|
Set<String> expected = new HashSet<String>();
|
||||||
|
expected.add("login_tokens:a");
|
||||||
|
expected.add("login_tokens:b");
|
||||||
|
assertEquals(expected, store.keys("login_tokens:*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldTrackHitsMissesWritesAndExpiredEntries() throws Exception
|
||||||
|
{
|
||||||
|
InMemoryCacheStore store = new InMemoryCacheStore();
|
||||||
|
store.set("captcha_codes:2", "5678", 20L, TimeUnit.MILLISECONDS);
|
||||||
|
assertTrue(store.hasKey("captcha_codes:2"));
|
||||||
|
assertEquals("5678", store.get("captcha_codes:2"));
|
||||||
|
Thread.sleep(40L);
|
||||||
|
assertNull(store.get("captcha_codes:2"));
|
||||||
|
assertFalse(store.hasKey("captcha_codes:2"));
|
||||||
|
|
||||||
|
InMemoryCacheStats stats = store.snapshot();
|
||||||
|
assertEquals("IN_MEMORY", stats.getCacheType());
|
||||||
|
assertEquals("single-instance", stats.getMode());
|
||||||
|
assertEquals(0, stats.getKeySize());
|
||||||
|
assertEquals(2L, stats.getHitCount());
|
||||||
|
assertEquals(2L, stats.getMissCount());
|
||||||
|
assertEquals(1L, stats.getExpiredCount());
|
||||||
|
assertEquals(1L, stats.getWriteCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.ruoyi.common.core.redis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class RedisCacheTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
void shouldSupportSetGetDeleteAndExpireThroughRedisCacheFacade()
|
||||||
|
{
|
||||||
|
RedisCache cache = new RedisCache(new InMemoryCacheStore());
|
||||||
|
cache.setCacheObject("login_tokens:abc", "payload", 1, TimeUnit.SECONDS);
|
||||||
|
assertEquals("payload", cache.getCacheObject("login_tokens:abc"));
|
||||||
|
assertTrue(cache.hasKey("login_tokens:abc"));
|
||||||
|
assertTrue(cache.deleteObject("login_tokens:abc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSupportKeysBulkDeleteAndRemainingTtl()
|
||||||
|
{
|
||||||
|
RedisCache cache = new RedisCache(new InMemoryCacheStore());
|
||||||
|
cache.setCacheObject("login_tokens:a", "A", 5, TimeUnit.SECONDS);
|
||||||
|
cache.setCacheObject("login_tokens:b", "B", 5, TimeUnit.SECONDS);
|
||||||
|
cache.setCacheObject("sys_dict:x", "X");
|
||||||
|
|
||||||
|
assertNotNull(cache.getExpire("login_tokens:a"));
|
||||||
|
assertTrue(cache.getExpire("login_tokens:a") > 0L);
|
||||||
|
assertEquals(2, cache.keys("login_tokens:*").size());
|
||||||
|
|
||||||
|
Collection<String> keys = new ArrayList<String>();
|
||||||
|
keys.add("login_tokens:a");
|
||||||
|
keys.add("login_tokens:b");
|
||||||
|
assertTrue(cache.deleteObject(keys));
|
||||||
|
assertFalse(cache.hasKey("login_tokens:a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldIncrementCounterWithinTtlWindow() throws Exception
|
||||||
|
{
|
||||||
|
RedisCache cache = new RedisCache(new InMemoryCacheStore());
|
||||||
|
assertEquals(1L, cache.increment("rate_limit:test", 50L, TimeUnit.MILLISECONDS));
|
||||||
|
assertEquals(2L, cache.increment("rate_limit:test", 50L, TimeUnit.MILLISECONDS));
|
||||||
|
Thread.sleep(70L);
|
||||||
|
assertNull(cache.getCacheObject("rate_limit:test"));
|
||||||
|
assertEquals(1L, cache.increment("rate_limit:test", 50L, TimeUnit.MILLISECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,12 +53,18 @@
|
|||||||
<artifactId>oshi-core</artifactId>
|
<artifactId>oshi-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 系统模块-->
|
<!-- 系统模块-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.ruoyi</groupId>
|
<groupId>com.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-system</artifactId>
|
<artifactId>ruoyi-system</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
</project>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
|||||||
@@ -1,89 +1,75 @@
|
|||||||
package com.ruoyi.framework.aspectj;
|
package com.ruoyi.framework.aspectj;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import com.ruoyi.common.annotation.RateLimiter;
|
||||||
import java.util.Collections;
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
import java.util.List;
|
import com.ruoyi.common.enums.LimitType;
|
||||||
import org.aspectj.lang.JoinPoint;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
import com.ruoyi.common.utils.ip.IpUtils;
|
||||||
import org.aspectj.lang.annotation.Before;
|
import java.lang.reflect.Method;
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
import java.util.concurrent.TimeUnit;
|
||||||
import org.slf4j.Logger;
|
import org.aspectj.lang.JoinPoint;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.aspectj.lang.annotation.Before;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
import org.springframework.data.redis.core.script.RedisScript;
|
import org.slf4j.Logger;
|
||||||
import org.springframework.stereotype.Component;
|
import org.slf4j.LoggerFactory;
|
||||||
import com.ruoyi.common.annotation.RateLimiter;
|
import org.springframework.stereotype.Component;
|
||||||
import com.ruoyi.common.enums.LimitType;
|
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
/**
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
* 限流处理
|
||||||
import com.ruoyi.common.utils.ip.IpUtils;
|
*
|
||||||
|
* @author ruoyi
|
||||||
/**
|
*/
|
||||||
* 限流处理
|
@Aspect
|
||||||
*
|
@Component
|
||||||
* @author ruoyi
|
public class RateLimiterAspect
|
||||||
*/
|
{
|
||||||
@Aspect
|
private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
|
||||||
@Component
|
|
||||||
public class RateLimiterAspect
|
private final RedisCache redisCache;
|
||||||
{
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
|
public RateLimiterAspect(RedisCache redisCache)
|
||||||
|
{
|
||||||
private RedisTemplate<Object, Object> redisTemplate;
|
this.redisCache = redisCache;
|
||||||
|
}
|
||||||
private RedisScript<Long> limitScript;
|
|
||||||
|
@Before("@annotation(rateLimiter)")
|
||||||
@Autowired
|
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
|
||||||
public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
|
{
|
||||||
{
|
int time = rateLimiter.time();
|
||||||
this.redisTemplate = redisTemplate;
|
int count = rateLimiter.count();
|
||||||
}
|
|
||||||
|
String combineKey = getCombineKey(rateLimiter, point);
|
||||||
@Autowired
|
try
|
||||||
public void setLimitScript(RedisScript<Long> limitScript)
|
{
|
||||||
{
|
long number = redisCache.increment(combineKey, time, TimeUnit.SECONDS);
|
||||||
this.limitScript = limitScript;
|
if (number > count)
|
||||||
}
|
{
|
||||||
|
throw new ServiceException("访问过于频繁,请稍候再试");
|
||||||
@Before("@annotation(rateLimiter)")
|
}
|
||||||
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
|
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number, combineKey);
|
||||||
{
|
}
|
||||||
int time = rateLimiter.time();
|
catch (ServiceException e)
|
||||||
int count = rateLimiter.count();
|
{
|
||||||
|
throw e;
|
||||||
String combineKey = getCombineKey(rateLimiter, point);
|
}
|
||||||
List<Object> keys = Collections.singletonList(combineKey);
|
catch (Exception e)
|
||||||
try
|
{
|
||||||
{
|
throw new RuntimeException("服务器限流异常,请稍候再试");
|
||||||
Long number = redisTemplate.execute(limitScript, keys, count, time);
|
}
|
||||||
if (StringUtils.isNull(number) || number.intValue() > count)
|
}
|
||||||
{
|
|
||||||
throw new ServiceException("访问过于频繁,请稍候再试");
|
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
|
||||||
}
|
{
|
||||||
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
|
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
|
||||||
}
|
if (rateLimiter.limitType() == LimitType.IP)
|
||||||
catch (ServiceException e)
|
{
|
||||||
{
|
stringBuffer.append(IpUtils.getIpAddr()).append("-");
|
||||||
throw e;
|
}
|
||||||
}
|
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||||
catch (Exception e)
|
Method method = signature.getMethod();
|
||||||
{
|
Class<?> targetClass = method.getDeclaringClass();
|
||||||
throw new RuntimeException("服务器限流异常,请稍候再试");
|
stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
|
||||||
}
|
return stringBuffer.toString();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
|
|
||||||
{
|
|
||||||
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
|
|
||||||
if (rateLimiter.limitType() == LimitType.IP)
|
|
||||||
{
|
|
||||||
stringBuffer.append(IpUtils.getIpAddr()).append("-");
|
|
||||||
}
|
|
||||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
|
||||||
Method method = signature.getMethod();
|
|
||||||
Class<?> targetClass = method.getDeclaringClass();
|
|
||||||
stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
|
|
||||||
return stringBuffer.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package com.ruoyi.framework.config;
|
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
|
||||||
import org.springframework.data.redis.serializer.SerializationException;
|
|
||||||
import com.alibaba.fastjson2.JSON;
|
|
||||||
import com.alibaba.fastjson2.JSONReader;
|
|
||||||
import com.alibaba.fastjson2.JSONWriter;
|
|
||||||
import com.alibaba.fastjson2.filter.Filter;
|
|
||||||
import com.ruoyi.common.constant.Constants;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis使用FastJson序列化
|
|
||||||
*
|
|
||||||
* @author ruoyi
|
|
||||||
*/
|
|
||||||
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
|
|
||||||
{
|
|
||||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
|
||||||
|
|
||||||
static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
|
|
||||||
|
|
||||||
private Class<T> clazz;
|
|
||||||
|
|
||||||
public FastJson2JsonRedisSerializer(Class<T> clazz)
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.clazz = clazz;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] serialize(T t) throws SerializationException
|
|
||||||
{
|
|
||||||
if (t == null)
|
|
||||||
{
|
|
||||||
return new byte[0];
|
|
||||||
}
|
|
||||||
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public T deserialize(byte[] bytes) throws SerializationException
|
|
||||||
{
|
|
||||||
if (bytes == null || bytes.length <= 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String str = new String(bytes, DEFAULT_CHARSET);
|
|
||||||
|
|
||||||
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package com.ruoyi.framework.config;
|
|
||||||
|
|
||||||
import org.springframework.cache.annotation.CachingConfigurerSupport;
|
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* redis配置
|
|
||||||
*
|
|
||||||
* @author ruoyi
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EnableCaching
|
|
||||||
public class RedisConfig extends CachingConfigurerSupport
|
|
||||||
{
|
|
||||||
@Bean
|
|
||||||
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
|
||||||
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
|
|
||||||
{
|
|
||||||
RedisTemplate<Object, Object> template = new RedisTemplate<>();
|
|
||||||
template.setConnectionFactory(connectionFactory);
|
|
||||||
|
|
||||||
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
|
|
||||||
|
|
||||||
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
|
||||||
template.setKeySerializer(new StringRedisSerializer());
|
|
||||||
template.setValueSerializer(serializer);
|
|
||||||
|
|
||||||
// Hash的key也采用StringRedisSerializer的序列化方式
|
|
||||||
template.setHashKeySerializer(new StringRedisSerializer());
|
|
||||||
template.setHashValueSerializer(serializer);
|
|
||||||
|
|
||||||
template.afterPropertiesSet();
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public DefaultRedisScript<Long> limitScript()
|
|
||||||
{
|
|
||||||
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
|
|
||||||
redisScript.setScriptText(limitScriptText());
|
|
||||||
redisScript.setResultType(Long.class);
|
|
||||||
return redisScript;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 限流脚本
|
|
||||||
*/
|
|
||||||
private String limitScriptText()
|
|
||||||
{
|
|
||||||
return "local key = KEYS[1]\n" +
|
|
||||||
"local count = tonumber(ARGV[1])\n" +
|
|
||||||
"local time = tonumber(ARGV[2])\n" +
|
|
||||||
"local current = redis.call('get', key);\n" +
|
|
||||||
"if current and tonumber(current) > count then\n" +
|
|
||||||
" return tonumber(current);\n" +
|
|
||||||
"end\n" +
|
|
||||||
"current = redis.call('incr', key)\n" +
|
|
||||||
"if tonumber(current) == 1 then\n" +
|
|
||||||
" redis.call('expire', key, time)\n" +
|
|
||||||
"end\n" +
|
|
||||||
"return tonumber(current);";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.ruoyi.framework.aspectj;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.ruoyi.common.annotation.RateLimiter;
|
||||||
|
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||||
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.Signature;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class RateLimiterAspectTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
void shouldRejectThirdRequestWithinWindow() throws Throwable
|
||||||
|
{
|
||||||
|
RateLimiterAspect aspect = new RateLimiterAspect(new RedisCache(new InMemoryCacheStore()));
|
||||||
|
Method method = TestRateLimitTarget.class.getDeclaredMethod("limited");
|
||||||
|
JoinPoint joinPoint = mockJoinPoint(method);
|
||||||
|
RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> aspect.doBefore(joinPoint, rateLimiter));
|
||||||
|
assertDoesNotThrow(() -> aspect.doBefore(joinPoint, rateLimiter));
|
||||||
|
assertThrows(ServiceException.class, () -> aspect.doBefore(joinPoint, rateLimiter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JoinPoint mockJoinPoint(Method method)
|
||||||
|
{
|
||||||
|
MethodSignature signature = mock(MethodSignature.class);
|
||||||
|
when(signature.getMethod()).thenReturn(method);
|
||||||
|
|
||||||
|
JoinPoint joinPoint = mock(JoinPoint.class);
|
||||||
|
when(joinPoint.getSignature()).thenReturn((Signature) signature);
|
||||||
|
return joinPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TestRateLimitTarget
|
||||||
|
{
|
||||||
|
@RateLimiter(count = 2, time = 60)
|
||||||
|
public void limited()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.ruoyi.framework.web.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.ruoyi.common.annotation.RepeatSubmit;
|
||||||
|
import com.ruoyi.common.constant.CacheConstants;
|
||||||
|
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||||
|
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||||
|
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||||
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
|
import com.ruoyi.framework.interceptor.impl.SameUrlDataInterceptor;
|
||||||
|
import com.ruoyi.system.service.ISysConfigService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
class TokenServiceLocalCacheTest
|
||||||
|
{
|
||||||
|
@AfterEach
|
||||||
|
void clearRequestContext()
|
||||||
|
{
|
||||||
|
RequestContextHolder.resetRequestAttributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldStoreLoginUserWithTokenTtl()
|
||||||
|
{
|
||||||
|
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||||
|
TokenService tokenService = new TokenService();
|
||||||
|
ReflectionTestUtils.setField(tokenService, "redisCache", redisCache);
|
||||||
|
ReflectionTestUtils.setField(tokenService, "header", "Authorization");
|
||||||
|
ReflectionTestUtils.setField(tokenService, "secret", "unit-test-secret");
|
||||||
|
ReflectionTestUtils.setField(tokenService, "expireTime", 30);
|
||||||
|
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||||
|
request.setRemoteAddr("127.0.0.1");
|
||||||
|
request.addHeader("User-Agent", "Mozilla/5.0");
|
||||||
|
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
|
||||||
|
|
||||||
|
SysUser user = new SysUser();
|
||||||
|
user.setUserName("admin");
|
||||||
|
user.setPassword("password");
|
||||||
|
LoginUser loginUser = new LoginUser();
|
||||||
|
loginUser.setUser(user);
|
||||||
|
|
||||||
|
String jwt = tokenService.createToken(loginUser);
|
||||||
|
|
||||||
|
assertNotNull(jwt);
|
||||||
|
assertNotNull(redisCache.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + loginUser.getToken()));
|
||||||
|
assertTrue(redisCache.getExpire(CacheConstants.LOGIN_TOKEN_KEY + loginUser.getToken()) > 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeleteCaptchaAfterValidation()
|
||||||
|
{
|
||||||
|
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||||
|
SysLoginService loginService = new SysLoginService();
|
||||||
|
ISysConfigService configService = mock(ISysConfigService.class);
|
||||||
|
when(configService.selectCaptchaEnabled()).thenReturn(true);
|
||||||
|
ReflectionTestUtils.setField(loginService, "redisCache", redisCache);
|
||||||
|
ReflectionTestUtils.setField(loginService, "configService", configService);
|
||||||
|
|
||||||
|
redisCache.setCacheObject(CacheConstants.CAPTCHA_CODE_KEY + "uuid", "ABCD", 1, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
loginService.validateCaptcha("admin", "ABCD", "uuid");
|
||||||
|
|
||||||
|
assertFalse(redisCache.hasKey(CacheConstants.CAPTCHA_CODE_KEY + "uuid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSupportMillisecondRepeatSubmitWindow() throws Exception
|
||||||
|
{
|
||||||
|
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||||
|
SameUrlDataInterceptor interceptor = new SameUrlDataInterceptor();
|
||||||
|
ReflectionTestUtils.setField(interceptor, "redisCache", redisCache);
|
||||||
|
ReflectionTestUtils.setField(interceptor, "header", "Authorization");
|
||||||
|
|
||||||
|
RepeatSubmit repeatSubmit = RepeatSubmitTarget.class.getDeclaredMethod("submit")
|
||||||
|
.getAnnotation(RepeatSubmit.class);
|
||||||
|
|
||||||
|
MockHttpServletRequest firstRequest = createRepeatRequest();
|
||||||
|
assertFalse(interceptor.isRepeatSubmit(firstRequest, repeatSubmit));
|
||||||
|
|
||||||
|
MockHttpServletRequest secondRequest = createRepeatRequest();
|
||||||
|
assertTrue(interceptor.isRepeatSubmit(secondRequest, repeatSubmit));
|
||||||
|
|
||||||
|
Thread.sleep(70L);
|
||||||
|
MockHttpServletRequest thirdRequest = createRepeatRequest();
|
||||||
|
assertFalse(interceptor.isRepeatSubmit(thirdRequest, repeatSubmit));
|
||||||
|
}
|
||||||
|
|
||||||
|
private MockHttpServletRequest createRepeatRequest()
|
||||||
|
{
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||||
|
request.setRequestURI("/loan/pricing");
|
||||||
|
request.addHeader("Authorization", "token-1");
|
||||||
|
request.addParameter("loanId", "1001");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class RepeatSubmitTarget
|
||||||
|
{
|
||||||
|
@RepeatSubmit(interval = 50)
|
||||||
|
public void submit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
283
ruoyi-ui/src/views/monitor/cache/index.vue
vendored
283
ruoyi-ui/src/views/monitor/cache/index.vue
vendored
@@ -8,34 +8,34 @@
|
|||||||
<table cellspacing="0" style="width: 100%">
|
<table cellspacing="0" style="width: 100%">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">Redis版本</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">缓存类型</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cacheTypeText }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">运行模式</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">运行模式</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "单机" : "集群" }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cacheModeText }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">端口</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">总键数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ displayDbSize }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">客户端数</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">写入次数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.write_count }}</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">运行时间(天)</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">命中次数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.hit_count }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">使用内存</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">未命中次数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.miss_count }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">使用CPU</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">过期清理次数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.expired_count }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">内存配置</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">命中率</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ hitRateText }}</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">AOF是否开启</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">统计说明</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "否" : "是" }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">进程内累计统计</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">RDB是否成功</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">监控范围</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">当前应用实例</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">Key数量</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">图表数据项</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ commandStats.length }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">网络入口/出口</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">监控采样时间</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ sampleTime }}</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
<el-col :span="12" class="card-box">
|
<el-col :span="12" class="card-box">
|
||||||
<el-card>
|
<el-card>
|
||||||
<div slot="header"><span><i class="el-icon-pie-chart"></i> 命令统计</span></div>
|
<div slot="header"><span><i class="el-icon-pie-chart"></i> 缓存统计</span></div>
|
||||||
<div class="el-table el-table--enable-row-hover el-table--medium">
|
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||||
<div ref="commandstats" style="height: 420px" />
|
<div ref="commandstats" style="height: 420px" />
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
|
|
||||||
<el-col :span="12" class="card-box">
|
<el-col :span="12" class="card-box">
|
||||||
<el-card>
|
<el-card>
|
||||||
<div slot="header"><span><i class="el-icon-odometer"></i> 内存信息</span></div>
|
<div slot="header"><span><i class="el-icon-odometer"></i> 命中概览</span></div>
|
||||||
<div class="el-table el-table--enable-row-hover el-table--medium">
|
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||||
<div ref="usedmemory" style="height: 420px" />
|
<div ref="usedmemory" style="height: 420px" />
|
||||||
</div>
|
</div>
|
||||||
@@ -72,76 +72,201 @@ export default {
|
|||||||
name: "Cache",
|
name: "Cache",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 统计命令信息
|
|
||||||
commandstats: null,
|
commandstats: null,
|
||||||
// 使用内存
|
|
||||||
usedmemory: null,
|
usedmemory: null,
|
||||||
// cache信息
|
cache: {
|
||||||
cache: []
|
info: {
|
||||||
|
cache_type: "IN_MEMORY",
|
||||||
|
cache_mode: "single-instance",
|
||||||
|
key_size: 0,
|
||||||
|
hit_count: 0,
|
||||||
|
miss_count: 0,
|
||||||
|
expired_count: 0,
|
||||||
|
write_count: 0
|
||||||
|
},
|
||||||
|
dbSize: 0,
|
||||||
|
commandStats: []
|
||||||
|
},
|
||||||
|
sampleTime: "-",
|
||||||
|
resizeHandler: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getList()
|
|
||||||
this.openLoading()
|
this.openLoading()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.resizeHandler) {
|
||||||
|
window.removeEventListener("resize", this.resizeHandler)
|
||||||
|
}
|
||||||
|
if (this.commandstats) {
|
||||||
|
this.commandstats.dispose()
|
||||||
|
}
|
||||||
|
if (this.usedmemory) {
|
||||||
|
this.usedmemory.dispose()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
cacheTypeText() {
|
||||||
|
return this.formatCacheType(this.cache.info.cache_type)
|
||||||
|
},
|
||||||
|
cacheModeText() {
|
||||||
|
return this.formatCacheMode(this.cache.info.cache_mode)
|
||||||
|
},
|
||||||
|
displayDbSize() {
|
||||||
|
return this.toNumber(this.cache.dbSize || this.cache.info.key_size)
|
||||||
|
},
|
||||||
|
hitRateText() {
|
||||||
|
const hitCount = this.toNumber(this.cache.info.hit_count)
|
||||||
|
const missCount = this.toNumber(this.cache.info.miss_count)
|
||||||
|
const total = hitCount + missCount
|
||||||
|
if (!total) {
|
||||||
|
return "0.00%"
|
||||||
|
}
|
||||||
|
return ((hitCount / total) * 100).toFixed(2) + "%"
|
||||||
|
},
|
||||||
|
commandStats() {
|
||||||
|
return this.normalizeCommandStats(this.cache.commandStats)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/** 查缓存询信息 */
|
|
||||||
getList() {
|
getList() {
|
||||||
getCache().then((response) => {
|
getCache().then((response) => {
|
||||||
this.cache = response.data
|
this.cache = this.normalizeCacheData(response.data)
|
||||||
|
this.sampleTime = this.formatSampleTime(new Date())
|
||||||
this.$modal.closeLoading()
|
this.$modal.closeLoading()
|
||||||
|
this.$nextTick(() => {
|
||||||
this.commandstats = echarts.init(this.$refs.commandstats, "macarons")
|
this.renderCharts()
|
||||||
this.commandstats.setOption({
|
|
||||||
tooltip: {
|
|
||||||
trigger: "item",
|
|
||||||
formatter: "{a} <br/>{b} : {c} ({d}%)",
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: "命令",
|
|
||||||
type: "pie",
|
|
||||||
roseType: "radius",
|
|
||||||
radius: [15, 95],
|
|
||||||
center: ["50%", "38%"],
|
|
||||||
data: response.data.commandStats,
|
|
||||||
animationEasing: "cubicInOut",
|
|
||||||
animationDuration: 1000,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
this.usedmemory = echarts.init(this.$refs.usedmemory, "macarons")
|
|
||||||
this.usedmemory.setOption({
|
|
||||||
tooltip: {
|
|
||||||
formatter: "{b} <br/>{a} : " + this.cache.info.used_memory_human,
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: "峰值",
|
|
||||||
type: "gauge",
|
|
||||||
min: 0,
|
|
||||||
max: 1000,
|
|
||||||
detail: {
|
|
||||||
formatter: this.cache.info.used_memory_human,
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: parseFloat(this.cache.info.used_memory_human),
|
|
||||||
name: "内存消耗",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
this.commandstats.resize()
|
|
||||||
this.usedmemory.resize()
|
|
||||||
})
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
this.cache = this.normalizeCacheData()
|
||||||
|
this.sampleTime = "-"
|
||||||
|
this.$modal.closeLoading()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// 打开加载层
|
|
||||||
openLoading() {
|
openLoading() {
|
||||||
this.$modal.loading("正在加载缓存监控数据,请稍候!")
|
this.$modal.loading("正在加载缓存监控数据,请稍候!")
|
||||||
|
},
|
||||||
|
normalizeCacheData(data) {
|
||||||
|
const source = data || {}
|
||||||
|
const info = source.info || {}
|
||||||
|
return {
|
||||||
|
info: {
|
||||||
|
cache_type: info.cache_type || "IN_MEMORY",
|
||||||
|
cache_mode: info.cache_mode || "single-instance",
|
||||||
|
key_size: this.toNumber(info.key_size),
|
||||||
|
hit_count: this.toNumber(info.hit_count),
|
||||||
|
miss_count: this.toNumber(info.miss_count),
|
||||||
|
expired_count: this.toNumber(info.expired_count),
|
||||||
|
write_count: this.toNumber(info.write_count)
|
||||||
|
},
|
||||||
|
dbSize: this.toNumber(source.dbSize || info.key_size),
|
||||||
|
commandStats: source.commandStats || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeCommandStats(commandStats) {
|
||||||
|
const stats = Array.isArray(commandStats) ? commandStats : []
|
||||||
|
return stats.map((item) => ({
|
||||||
|
name: this.statLabel(item.name),
|
||||||
|
value: this.toNumber(item.value)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
statLabel(name) {
|
||||||
|
const labelMap = {
|
||||||
|
hit_count: "命中次数",
|
||||||
|
miss_count: "未命中次数",
|
||||||
|
expired_count: "过期清理次数",
|
||||||
|
write_count: "写入次数"
|
||||||
|
}
|
||||||
|
return labelMap[name] || name || "未命名统计"
|
||||||
|
},
|
||||||
|
formatCacheType(type) {
|
||||||
|
const typeMap = {
|
||||||
|
IN_MEMORY: "进程内缓存"
|
||||||
|
}
|
||||||
|
return typeMap[type] || type || "-"
|
||||||
|
},
|
||||||
|
formatCacheMode(mode) {
|
||||||
|
const modeMap = {
|
||||||
|
"single-instance": "单实例"
|
||||||
|
}
|
||||||
|
return modeMap[mode] || mode || "-"
|
||||||
|
},
|
||||||
|
formatSampleTime(date) {
|
||||||
|
const current = date || new Date()
|
||||||
|
const pad = (value) => String(value).padStart(2, "0")
|
||||||
|
return `${current.getFullYear()}-${pad(current.getMonth() + 1)}-${pad(current.getDate())} ${pad(current.getHours())}:${pad(current.getMinutes())}:${pad(current.getSeconds())}`
|
||||||
|
},
|
||||||
|
toNumber(value) {
|
||||||
|
const parsedValue = Number(value)
|
||||||
|
return Number.isFinite(parsedValue) ? parsedValue : 0
|
||||||
|
},
|
||||||
|
renderCharts() {
|
||||||
|
if (!this.commandstats) {
|
||||||
|
this.commandstats = echarts.init(this.$refs.commandstats, "macarons")
|
||||||
|
}
|
||||||
|
if (!this.usedmemory) {
|
||||||
|
this.usedmemory = echarts.init(this.$refs.usedmemory, "macarons")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commandstats.setOption({
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
formatter: "{a} <br/>{b} : {c} ({d}%)"
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "缓存统计",
|
||||||
|
type: "pie",
|
||||||
|
roseType: "radius",
|
||||||
|
radius: [15, 95],
|
||||||
|
center: ["50%", "38%"],
|
||||||
|
data: this.commandStats,
|
||||||
|
animationEasing: "cubicInOut",
|
||||||
|
animationDuration: 1000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
this.usedmemory.setOption({
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
axisPointer: {
|
||||||
|
type: "shadow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
data: this.commandStats.map((item) => item.name),
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
minInterval: 1
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "统计次数",
|
||||||
|
type: "bar",
|
||||||
|
barMaxWidth: 48,
|
||||||
|
data: this.commandStats.map((item) => item.value)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.resizeHandler) {
|
||||||
|
this.resizeHandler = () => {
|
||||||
|
if (this.commandstats) {
|
||||||
|
this.commandstats.resize()
|
||||||
|
}
|
||||||
|
if (this.usedmemory) {
|
||||||
|
this.usedmemory.resize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", this.resizeHandler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
ruoyi-ui/src/views/monitor/cache/list.vue
vendored
20
ruoyi-ui/src/views/monitor/cache/list.vue
vendored
@@ -230,12 +230,16 @@ export default {
|
|||||||
this.cacheForm = response.data
|
this.cacheForm = response.data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
/** 清理全部缓存 */
|
/** 清理全部缓存 */
|
||||||
handleClearCacheAll() {
|
handleClearCacheAll() {
|
||||||
clearCacheAll().then(response => {
|
clearCacheAll().then(response => {
|
||||||
this.$modal.msgSuccess("清理全部缓存成功")
|
this.cacheKeys = []
|
||||||
})
|
this.cacheForm = {}
|
||||||
}
|
this.nowCacheName = ""
|
||||||
}
|
this.getCacheNames()
|
||||||
}
|
this.$modal.msgSuccess("清理全部缓存成功")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user