You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

156 lines
3.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import {
ADDRESS_CANDIDATES_CACHE_TTL_SECONDS,
L1_TTL_SECONDS,
L2_TTL_BASE_SECONDS,
L2_TTL_JITTER_MAX_SECONDS,
L3_TTL_SECONDS,
LOCK_TTL_SECONDS,
LOCK_WAIT_MS,
} from "@/lib/constants/cache";
import { getRedis } from "@/lib/redis";
export const cacheKeys = {
l1: (requestId: string) => `idem:${requestId}`,
l2: (cargoHash: string) => `quote:${cargoHash}`,
l3: (cargoHash: string) => `stale:${cargoHash}`,
lock: (cargoHash: string) => `lock:quote:${cargoHash}`,
addrCand: (hash: string) => `addr-cand:${hash}`,
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function l2TtlWithJitter(): number {
const jitter = Math.floor(Math.random() * (L2_TTL_JITTER_MAX_SECONDS + 1));
return L2_TTL_BASE_SECONDS + jitter;
}
export async function getL1<T = unknown>(
requestId: string,
): Promise<T | null> {
const raw = await getRedis().get(cacheKeys.l1(requestId));
if (!raw) {
return null;
}
return JSON.parse(raw) as T;
}
/** SETNX + TTL已存在时不覆盖返回 false */
export async function setL1(
requestId: string,
data: unknown,
): Promise<boolean> {
const key = cacheKeys.l1(requestId);
const value = JSON.stringify(data);
const result = await getRedis().set(key, value, "EX", L1_TTL_SECONDS, "NX");
return result === "OK";
}
/** 覆盖写入 L1RPA 完成后更新幂等响应) */
export async function overwriteL1(
requestId: string,
data: unknown,
): Promise<void> {
await getRedis().set(
cacheKeys.l1(requestId),
JSON.stringify(data),
"EX",
L1_TTL_SECONDS,
);
}
export async function getL2<T = unknown>(
cargoHash: string,
): Promise<T | null> {
const raw = await getRedis().get(cacheKeys.l2(cargoHash));
if (!raw) {
return null;
}
return JSON.parse(raw) as T;
}
export async function setL2(
cargoHash: string,
data: unknown,
): Promise<number> {
const ttl = l2TtlWithJitter();
await getRedis().set(
cacheKeys.l2(cargoHash),
JSON.stringify(data),
"EX",
ttl,
);
return ttl;
}
export async function getL3<T = unknown>(
cargoHash: string,
): Promise<T | null> {
const raw = await getRedis().get(cacheKeys.l3(cargoHash));
if (!raw) {
return null;
}
return JSON.parse(raw) as T;
}
/** 仅 RPA 成功时写入;正常路径禁止读取 L3 */
export async function setL3(
cargoHash: string,
data: unknown,
): Promise<void> {
await getRedis().set(
cacheKeys.l3(cargoHash),
JSON.stringify(data),
"EX",
L3_TTL_SECONDS,
);
}
/** SETNX 获取击穿锁TTL 5s */
export async function acquireLock(cargoHash: string): Promise<boolean> {
const result = await getRedis().set(
cacheKeys.lock(cargoHash),
"1",
"EX",
LOCK_TTL_SECONDS,
"NX",
);
return result === "OK";
}
export async function releaseLock(cargoHash: string): Promise<void> {
await getRedis().del(cacheKeys.lock(cargoHash));
}
/** 未获锁时等待 2s 后读 L2 */
export async function waitAndGetL2<T = unknown>(
cargoHash: string,
): Promise<T | null> {
await sleep(LOCK_WAIT_MS);
return getL2<T>(cargoHash);
}
export async function getAddressCandidatesCache<T = unknown>(
hash: string,
): Promise<T | null> {
const raw = await getRedis().get(cacheKeys.addrCand(hash));
if (!raw) {
return null;
}
return JSON.parse(raw) as T;
}
/** 地址联想成功结果短缓存(默认 120s */
export async function setAddressCandidatesCache(
hash: string,
data: unknown,
): Promise<void> {
await getRedis().set(
cacheKeys.addrCand(hash),
JSON.stringify(data),
"EX",
ADDRESS_CANDIDATES_CACHE_TTL_SECONDS,
);
}