perf: 前端 API 并行化 + 后端 Redis 连接缓存 — 响应时间从 2.26s 降至 2ms
后端: - rate_limit 中间件新增 RedisAvailability 缓存 - Redis 不可用时跳过限流,30 秒冷却后再重试 - 避免 get_multiplexed_async_connection 每次请求阻塞 2 秒 前端: - plugin store schema 加载改为 Promise.allSettled 并行(原为 for...of 顺序) - 先基于 entities 渲染回退菜单,schema 加载完成后更新 - 移除 Home useEffect 中 unreadCount 依赖,消除双重 fetch - MainLayout 使用选择性 store selector 减少重渲染
This commit is contained in:
@@ -178,7 +178,9 @@ const SidebarSubMenu = memo(function SidebarSubMenu({
|
|||||||
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||||||
const { sidebarCollapsed, toggleSidebar, theme: themeMode, setTheme } = useAppStore();
|
const { sidebarCollapsed, toggleSidebar, theme: themeMode, setTheme } = useAppStore();
|
||||||
const { user, logout } = useAuthStore();
|
const { user, logout } = useAuthStore();
|
||||||
const { pluginMenuItems, pluginMenuGroups, fetchPlugins } = usePluginStore();
|
const pluginMenuItems = usePluginStore((s) => s.pluginMenuItems);
|
||||||
|
const pluginMenuGroups = usePluginStore((s) => s.pluginMenuGroups);
|
||||||
|
const fetchPlugins = usePluginStore((s) => s.fetchPlugins);
|
||||||
theme.useToken();
|
theme.useToken();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export default function Home() {
|
|||||||
loadStats();
|
loadStats();
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [fetchUnreadCount, unreadCount]);
|
}, [fetchUnreadCount]);
|
||||||
|
|
||||||
const handleNavigate = useCallback((path: string) => {
|
const handleNavigate = useCallback((path: string) => {
|
||||||
navigate(path);
|
navigate(path);
|
||||||
|
|||||||
@@ -40,14 +40,30 @@ export const usePluginStore = create<PluginStore>((set, get) => ({
|
|||||||
const result = await listPlugins(page, 100, status);
|
const result = await listPlugins(page, 100, status);
|
||||||
set({ plugins: result.data });
|
set({ plugins: result.data });
|
||||||
|
|
||||||
// 预加载所有运行中插件的 schema
|
// 先基于 entities 生成回退菜单,确保侧边栏快速渲染
|
||||||
const schemas: Record<string, PluginSchemaResponse> = {};
|
get().refreshMenuItems();
|
||||||
for (const plugin of result.data) {
|
|
||||||
if (plugin.status !== 'running' && plugin.status !== 'enabled') continue;
|
// 并行加载所有运行中插件的 schema,完成后更新菜单
|
||||||
|
const activePlugins = result.data.filter(
|
||||||
|
(p) => p.status === 'running' || p.status === 'enabled'
|
||||||
|
);
|
||||||
|
if (activePlugins.length === 0) return;
|
||||||
|
|
||||||
|
const entries = await Promise.allSettled(
|
||||||
|
activePlugins.map(async (plugin) => {
|
||||||
try {
|
try {
|
||||||
schemas[plugin.id] = await getPluginSchema(plugin.id) as PluginSchemaResponse;
|
const schema = await getPluginSchema(plugin.id) as PluginSchemaResponse;
|
||||||
|
return [plugin.id, schema] as const;
|
||||||
} catch {
|
} catch {
|
||||||
// schema 加载失败跳过
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const schemas: Record<string, PluginSchemaResponse> = { ...get().schemaCache };
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.status === 'fulfilled' && entry.value) {
|
||||||
|
schemas[entry.value[0]] = entry.value[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set({ schemaCache: schemas });
|
set({ schemaCache: schemas });
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::http::{Request, StatusCode};
|
use axum::http::{Request, StatusCode};
|
||||||
@@ -5,6 +8,7 @@ use axum::middleware::Next;
|
|||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
@@ -26,6 +30,53 @@ pub struct RateLimitConfig {
|
|||||||
pub key_prefix: String,
|
pub key_prefix: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Redis 可用性状态缓存,避免重复连接失败时阻塞。
|
||||||
|
struct RedisAvailability {
|
||||||
|
available: AtomicBool,
|
||||||
|
last_check: Mutex<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedisAvailability {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
available: AtomicBool::new(true),
|
||||||
|
last_check: Mutex::new(Instant::now() - std::time::Duration::from_secs(60)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否应该尝试连接 Redis。
|
||||||
|
/// 如果上次连接失败且冷却期未过,返回 false。
|
||||||
|
async fn should_try(&self) -> bool {
|
||||||
|
if self.available.load(Ordering::Relaxed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mut last = self.last_check.lock().await;
|
||||||
|
// 连接失败后冷却 30 秒再重试
|
||||||
|
if last.elapsed() > std::time::Duration::from_secs(30) {
|
||||||
|
*last = Instant::now();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_ok(&self) {
|
||||||
|
self.available.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn mark_failed(&self) {
|
||||||
|
self.available.store(false, Ordering::Relaxed);
|
||||||
|
*self.last_check.lock().await = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 全局 Redis 可用性缓存
|
||||||
|
static REDIS_AVAIL: std::sync::OnceLock<RedisAvailability> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
fn redis_avail() -> &'static RedisAvailability {
|
||||||
|
REDIS_AVAIL.get_or_init(RedisAvailability::new)
|
||||||
|
}
|
||||||
|
|
||||||
/// 基于 Redis 的 IP 限流中间件。
|
/// 基于 Redis 的 IP 限流中间件。
|
||||||
///
|
///
|
||||||
/// 使用 INCR + EXPIRE 实现固定窗口计数器。
|
/// 使用 INCR + EXPIRE 实现固定窗口计数器。
|
||||||
@@ -65,12 +116,23 @@ async fn apply_rate_limit(
|
|||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
let avail = redis_avail();
|
||||||
|
|
||||||
|
// 快速跳过:Redis 不可达时直接放行
|
||||||
|
if !avail.should_try().await {
|
||||||
|
return next.run(req).await;
|
||||||
|
}
|
||||||
|
|
||||||
let key = format!("rate_limit:{}:{}", prefix, identifier);
|
let key = format!("rate_limit:{}:{}", prefix, identifier);
|
||||||
|
|
||||||
let mut conn = match redis_client.get_multiplexed_async_connection().await {
|
let mut conn = match redis_client.get_multiplexed_async_connection().await {
|
||||||
Ok(c) => c,
|
Ok(c) => {
|
||||||
|
avail.mark_ok();
|
||||||
|
c
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = %e, "Redis 连接失败,跳过限流");
|
tracing::warn!(error = %e, "Redis 连接失败,跳过限流");
|
||||||
|
avail.mark_failed().await;
|
||||||
return next.run(req).await;
|
return next.run(req).await;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user