feat(content): eager invalidation and optimistic lock for cqrs

M4: ADR-038 Eager Invalidation + ADR-039 Optimistic Lock

- EagerInvalidationService: Redis DEL after MySQL commit

- Cache key conventions for textbook/chapter/kp/question

- Version header check (If-Match/X-Expected-Version) on write endpoints

- 409 Conflict on version mismatch

- All write endpoints return updatedAt timestamp
This commit is contained in:
SpecialX
2026-07-15 01:28:20 +08:00
parent 47a062606f
commit a75527be80
26 changed files with 673 additions and 72 deletions

View File

@@ -0,0 +1,44 @@
import { Redis } from "ioredis";
import type { Redis as RedisClient } from "ioredis";
import { env } from "./env.js";
// Redis Client 惰性初始化:未配置 REDIS_URL 时 client 保持 null
// 服务仍可正常启动。所有依赖 Redis 的功能Eager Invalidation
// client 为 null 时优雅降级(跳过失效,由 Kafka projector 兜底),
// 不会阻塞主流程。
let client: RedisClient | null = null;
let initAttempted = false;
export function getRedis(): RedisClient | null {
if (initAttempted) {
return client;
}
initAttempted = true;
if (!env.REDIS_URL) {
return null;
}
try {
client = new Redis(env.REDIS_URL, {
maxRetriesPerRequest: 1,
enableReadyCheck: true,
lazyConnect: false,
});
} catch (err) {
console.warn(
"Redis client init failed, running without Redis:",
err instanceof Error ? err.message : String(err),
);
client = null;
}
return client;
}
/**
* 关闭 Redis Client 连接。
*/
export async function closeRedis(): Promise<void> {
if (client) {
await client.quit();
client = null;
}
}