Browse Source

feat(cache): implement caching system with Redis and memory fallback; update API endpoints to utilize caching

beauty-auth
npmrun 1 week ago
parent
commit
bfc76b8e7f
  1. 2
      .env.example
  2. 1
      bun.lock
  3. 1
      package.json
  4. 188
      packages/cache/readme.md
  5. 2
      packages/cache/tsconfig.json
  6. BIN
      packages/drizzle-pkg/db.sqlite
  7. 10
      server/api/cards.get.ts
  8. 5
      server/api/scheduler/executions.get.ts
  9. 11
      server/api/scheduler/stats.get.ts
  10. 3
      server/api/scheduler/tasks/[id].delete.ts
  11. 8
      server/api/scheduler/tasks/[id].get.ts
  12. 4
      server/api/scheduler/tasks/[id].put.ts
  13. 4
      server/api/scheduler/tasks/[id]/toggle.post.ts
  14. 10
      server/api/scheduler/tasks/index.get.ts
  15. 3
      server/api/scheduler/tasks/index.post.ts
  16. 22
      server/plugins/04.cache.ts
  17. 7
      server/types/index.d.ts

2
.env.example

@ -3,6 +3,4 @@ STATIC_DIR=static
UPLOAD_SUBDIR=upload
NITRO_PORT=3399
SCHEDULER_MAX_CONCURRENCY=5
SCHEDULER_LOG_RETENTION_DAYS=30
SCHEDULER_MAX_CONCURRENCY=5
SCHEDULER_LOG_RETENTION_DAYS=30

1
bun.lock

@ -7,6 +7,7 @@
"dependencies": {
"@libsql/client": "0.17.3",
"bcryptjs": "3.0.3",
"cache": "workspace:*",
"croner": "10.0.1",
"dotenv": "17.4.1",
"drizzle-orm": "0.45.2",

1
package.json

@ -22,6 +22,7 @@
"@libsql/client": "0.17.3",
"bcryptjs": "3.0.3",
"croner": "10.0.1",
"cache": "workspace:*",
"dotenv": "17.4.1",
"drizzle-orm": "0.45.2",
"drizzle-pkg": "workspace:*",

188
packages/cache/readme.md

@ -0,0 +1,188 @@
# Cache System 使用文档
`packages/cache` 提供一个支持 Redis 主存储 + Memory 内存降级的通用 K-V 缓存系统。
## 安装
cache 包已作为 workspace 依赖安装,直接使用即可:
```typescript
import { createCache } from 'cache'
```
## 快速开始
```typescript
import { createCache } from 'cache'
// 初始化
const cache = createCache({
redis: {
host: '127.0.0.1',
port: 6379,
},
defaultTtl: 300, // 默认 5 分钟
})
// 在 Nitro API 中使用
export default defineEventHandler(async (event) => {
const key = 'users:list'
// 尝试从缓存读取
const cached = await cache.get(key)
if (cached) return cached
// 缓存未命中,从数据源获取
const data = await fetchUsers()
// 写入缓存
await cache.set(key, data)
return data
})
```
## API
### createCache(options)
创建 CacheManager 实例。
```typescript
interface CacheFactoryOptions {
redis?: RedisDriverOptions // Redis 配置,省略则只用内存
memory?: boolean // 是否启用内存兜底,默认 true
defaultTtl?: number // 默认 TTL(秒),默认 0(永不过期)
}
interface RedisDriverOptions {
host: string
port: number
password?: string
db?: number
}
```
**行为:**
- `redis` 配置存在时,Redis 作为主存储(优先尝试)
- `memory !== false` 时,Memory 作为兜底(Redis 不可用时自动降级)
- 两者都不配置则抛出错误
### CacheManager 实例方法
```typescript
// 读取缓存,未命中返回 null
get<T = unknown>(key: string): Promise<T | null>
// 写入缓存
// ttl 单位为秒,0 表示永不过期
set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>
// 删除指定 key
del(key: string): Promise<void>
// 检查 key 是否存在
exists(key: string): Promise<boolean>
// 清空所有缓存(谨慎使用)
clear(): Promise<void>
```
## 降级策略
采用**手动显式降级**:按驱动数组顺序尝试,返回第一个成功结果。
```
RedisDriver (优先)
↓ 失败时
MemoryDriver (兜底)
```
- `get` / `exists`:遍历 drivers,返回第一个成功结果
- `set` / `del` / `clear`:并发写入/删除所有 drivers(保证各层数据一致)
- 单个 driver 失败时 warn 并继续,不影响整体操作
## 使用场景
### 场景一:纯内存缓存(开发/无 Redis 环境)
```typescript
const cache = createCache({
defaultTtl: 60,
})
```
### 场景二:Redis + 内存降级(生产环境)
```typescript
const cache = createCache({
redis: {
host: process.env.REDIS_HOST!,
port: Number(process.env.REDIS_PORT ?? 6379),
password: process.env.REDIS_PASSWORD,
db: Number(process.env.REDIS_DB ?? 0),
},
defaultTtl: 300,
})
```
### 场景三:禁用内存兜底(Redis 专用)
```typescript
const cache = createCache({
redis: { host: '127.0.0.1', port: 6379 },
memory: false,
})
```
### 场景四:Nitro Plugin 全局注入
`server/plugins/cache.ts`:
```typescript
import { createCache } from 'cache'
export default defineNitroPlugin(() => {
const cache = createCache({
redis: {
host: '127.0.0.1',
port: 6379,
},
defaultTtl: 300,
})
// 注入到 H3 event context
event.context.cache = cache
})
```
`server/api/users.get.ts`:
```typescript
export default defineEventHandler(async (event) => {
const cache = event.context.cache
const cached = await cache.get('users:list')
if (cached) return cached
const data = await fetchUsers()
await cache.set('users:list', data)
return data
})
```
## 数据结构
所有值以 JSON 序列化存储到 Redis:
```typescript
await cache.set('user:1', { id: 1, name: 'Alice' })
// Redis: SET 'user:1' '{"id":1,"name":"Alice"}' EX 300
```
## 注意事项
- **TTL 单位是秒**,Redis `EX` 参数单位也是秒
- `clear()` 在 Redis 上执行 `FLUSHDB`,会清空整个数据库,请确认 namespace 隔离
- MemoryDriver 使用进程内存,重启进程后缓存丢失
- 不支持存储 `undefined`、Symbol 或含循环引用的对象(JSON 序列化限制)

2
packages/cache/tsconfig.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "tsconfig/tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

10
server/api/cards.get.ts

@ -125,16 +125,22 @@ function genItem(globalIdx: number): CardData {
}
}
export default defineEventHandler(async (event) => {
export default defineWrappedResponseHandler(async (event) => {
const query = getQuery(event)
const page = Math.max(1, parseInt(String(query.page || '1')))
const pageSize = Math.min(30, Math.max(1, parseInt(String(query.pageSize || '12'))))
const cacheKey = `cards:list:${page}:${pageSize}`
const cached = await event.context.cache.get<{ items: CardData[], hasMore: boolean, page: number }>(cacheKey)
if (cached) return cached
const start = (page - 1) * pageSize
const items = Array.from({ length: pageSize }, (_, i) => genItem(start + i))
const total = 60
const hasMore = start + pageSize < total
return { items, hasMore, page }
const result = { items, hasMore, page }
await event.context.cache.set(cacheKey, result, 300)
return result
})

5
server/api/scheduler/executions.get.ts

@ -5,6 +5,10 @@ export default defineWrappedResponseHandler(async (event) => {
const page = query.page ? Number(query.page) : 1;
const pageSize = query.pageSize ? Number(query.pageSize) : 20;
const cacheKey = `scheduler:executions:${page}:${pageSize}:${query.taskId ?? 'all'}:${query.status ?? 'all'}`
const cached = await event.context.cache.get<{ list: unknown[], total: number, page: number, pageSize: number }>(cacheKey)
if (cached) return R.success(cached)
const result = await listExecutions({
page,
pageSize,
@ -12,5 +16,6 @@ export default defineWrappedResponseHandler(async (event) => {
status: query.status as string | undefined,
});
await event.context.cache.set(cacheKey, result, 60)
return R.success(result);
});

11
server/api/scheduler/stats.get.ts

@ -2,9 +2,12 @@ import { getStats } from "../../service/scheduler";
import { getJobCount } from "../../scheduler/engine";
export default defineWrappedResponseHandler(async () => {
const cacheKey = 'scheduler:stats'
const cached = await event.context.cache.get<{ totalTasks: number, enabledTasks: number, last24hExecutions: number, activeJobs: number }>(cacheKey)
if (cached) return R.success({ ...cached, activeJobs: getJobCount() })
const stats = await getStats();
return R.success({
...stats,
activeJobs: getJobCount(),
});
const result = { ...stats, activeJobs: getJobCount() }
await event.context.cache.set(cacheKey, result, 60)
return R.success(result);
});

3
server/api/scheduler/tasks/[id].delete.ts

@ -8,5 +8,8 @@ export default defineWrappedResponseHandler(async (event) => {
removeTask(id);
await deleteTask(id);
await event.context.cache.del(`scheduler:task:${id}`)
await event.context.cache.del('scheduler:tasks:1:20:all:all')
return R.success(null);
});

8
server/api/scheduler/tasks/[id].get.ts

@ -4,10 +4,16 @@ export default defineWrappedResponseHandler(async (event) => {
const id = getRouterParam(event, "id");
if (!id) return R.throwError(400, "Missing id", null);
const cacheKey = `scheduler:task:${id}`
const cached = await event.context.cache.get<{ task: unknown, recentExecutions: unknown[] }>(cacheKey)
if (cached) return R.success(cached)
const task = await getTaskById(id);
if (!task) return R.throwError(404, "Task not found", null);
const recentExecutions = await getRecentExecutions(id, 20);
return R.success({ task, recentExecutions });
const result = { task, recentExecutions }
await event.context.cache.set(cacheKey, result, 60)
return R.success(result);
});

4
server/api/scheduler/tasks/[id].put.ts

@ -54,5 +54,9 @@ export default defineWrappedResponseHandler(async (event) => {
reloadTask(task.id);
}
await event.context.cache.del(`scheduler:task:${id}`)
await event.context.cache.del('scheduler:tasks:1:20:all:all')
await event.context.cache.del('scheduler:stats')
return R.success(task);
});

4
server/api/scheduler/tasks/[id]/toggle.post.ts

@ -16,5 +16,9 @@ export default defineWrappedResponseHandler(async (event) => {
removeTask(id);
}
await event.context.cache.del(`scheduler:task:${id}`)
await event.context.cache.del('scheduler:tasks:1:20:all:all')
await event.context.cache.del('scheduler:stats')
return R.success(task);
});

10
server/api/scheduler/tasks/index.get.ts

@ -6,6 +6,10 @@ export default defineWrappedResponseHandler(async (event) => {
const page = query.page ? Number(query.page) : 1;
const pageSize = query.pageSize ? Number(query.pageSize) : 20;
const cacheKey = `scheduler:tasks:${page}:${pageSize}:${query.type ?? 'all'}:${query.enabled ?? 'all'}`
const cached = await event.context.cache.get<{ list: unknown[], total: number, page: number, pageSize: number, registeredFunctions: unknown[] }>(cacheKey)
if (cached) return R.success(cached)
const result = await listTasks({
page,
pageSize,
@ -13,8 +17,10 @@ export default defineWrappedResponseHandler(async (event) => {
enabled: query.enabled !== undefined ? Number(query.enabled) : undefined,
});
return R.success({
const fullResult = {
...result,
registeredFunctions: listRegisteredTasks(),
});
}
await event.context.cache.set(cacheKey, fullResult, 120)
return R.success(fullResult);
});

3
server/api/scheduler/tasks/index.post.ts

@ -55,5 +55,8 @@ export default defineWrappedResponseHandler(async (event) => {
addTask(task.id);
}
await event.context.cache.del('scheduler:tasks:1:20:all:all')
await event.context.cache.del('scheduler:stats')
return R.success(task);
});

22
server/plugins/04.cache.ts

@ -0,0 +1,22 @@
import { createCache } from 'cache'
const cache = createCache({
// redis: {
// host: process.env.REDIS_HOST ?? '127.0.0.1',
// port: Number(process.env.REDIS_PORT ?? 6379),
// password: process.env.REDIS_PASSWORD,
// db: Number(process.env.REDIS_DB ?? 0),
// },
// defaultTtl: 300,
memory: true
})
if (import.meta.dev) {
console.log('plugin: 04.cache')
}
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', async (event) => {
event.context.cache = cache
})
})

7
server/types/index.d.ts

@ -0,0 +1,7 @@
import type { CacheManager } from 'cache'
declare module 'h3' {
interface H3EventContext {
cache: CacheManager
}
}
Loading…
Cancel
Save