优化数据库读写
This commit is contained in:
Vendored
+55
-10
@@ -5,26 +5,69 @@ function positiveInteger(value, fallback, maximum = Number.MAX_SAFE_INTEGER) {
|
||||
return Number.isInteger(parsed) && parsed > 0 ? Math.min(parsed, maximum) : fallback;
|
||||
}
|
||||
|
||||
function disabledCache(status = 'disabled') {
|
||||
function disabledCache(status = 'disabled', { ttlSeconds = 60, maxEntries = 200 } = {}) {
|
||||
const values = new Map();
|
||||
const pending = new Map();
|
||||
const generations = new Map();
|
||||
|
||||
const cacheKey = (namespace, key) => `${namespace}:${key}`;
|
||||
const generation = namespace => generations.get(namespace) || 0;
|
||||
const prune = () => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of values) if (entry.expiresAt <= now) values.delete(key);
|
||||
while (values.size > maxEntries) values.delete(values.keys().next().value);
|
||||
};
|
||||
|
||||
return {
|
||||
enabled: false,
|
||||
status,
|
||||
async remember(_namespace, _key, loader) {
|
||||
return loader();
|
||||
async remember(namespace, key, loader, options = {}) {
|
||||
const fullKey = cacheKey(namespace, key);
|
||||
const cached = values.get(fullKey);
|
||||
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
||||
if (cached) values.delete(fullKey);
|
||||
const startedGeneration = generation(namespace);
|
||||
const active = pending.get(fullKey);
|
||||
if (active?.generation === startedGeneration) return active.promise;
|
||||
|
||||
const loading = Promise.resolve(loader()).then(value => {
|
||||
if (generation(namespace) === startedGeneration) {
|
||||
const lifetime = positiveInteger(options.ttlSeconds, ttlSeconds, 86400);
|
||||
values.set(fullKey, { value, expiresAt: Date.now() + lifetime * 1000 });
|
||||
prune();
|
||||
}
|
||||
return value;
|
||||
}).finally(() => {
|
||||
if (pending.get(fullKey)?.promise === loading) pending.delete(fullKey);
|
||||
});
|
||||
pending.set(fullKey, { generation: startedGeneration, promise: loading });
|
||||
return loading;
|
||||
},
|
||||
async invalidate() { return false; },
|
||||
async close() {}
|
||||
async invalidate(namespace) {
|
||||
generations.set(namespace, generation(namespace) + 1);
|
||||
const prefix = `${namespace}:`;
|
||||
for (const key of values.keys()) if (key.startsWith(prefix)) values.delete(key);
|
||||
// Preserve the public meaning of this return value: no Redis namespace
|
||||
// was refreshed, even though the local fallback was invalidated.
|
||||
return false;
|
||||
},
|
||||
async close() {
|
||||
values.clear();
|
||||
pending.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function createRedisCache({ env = process.env, logger = console, clientFactory = createClient } = {}) {
|
||||
const url = String(env.REDIS_URL || '').trim();
|
||||
if (!url) return disabledCache();
|
||||
const defaultTtlSeconds = positiveInteger(env.REDIS_CACHE_TTL_SECONDS, 60, 86400);
|
||||
const localMaxEntries = positiveInteger(env.LOCAL_CACHE_MAX_ENTRIES, 200, 5000);
|
||||
if (!url) return disabledCache('disabled', { ttlSeconds: defaultTtlSeconds, maxEntries: localMaxEntries });
|
||||
const fallback = disabledCache('unavailable', { ttlSeconds: defaultTtlSeconds, maxEntries: localMaxEntries });
|
||||
|
||||
const prefix = String(env.REDIS_CACHE_PREFIX || 'exam-information')
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9:_-]/g, '-') || 'exam-information';
|
||||
const defaultTtlSeconds = positiveInteger(env.REDIS_CACHE_TTL_SECONDS, 60, 86400);
|
||||
const connectTimeout = positiveInteger(env.REDIS_CONNECT_TIMEOUT_MS, 1500, 30000);
|
||||
const pending = new Map();
|
||||
let warningReported = false;
|
||||
@@ -54,7 +97,7 @@ export async function createRedisCache({ env = process.env, logger = console, cl
|
||||
} catch (error) {
|
||||
warn(error);
|
||||
if (client.isOpen) client.destroy();
|
||||
return disabledCache('unavailable');
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const versionKey = namespace => `${prefix}:namespace:${namespace}`;
|
||||
@@ -75,7 +118,7 @@ export async function createRedisCache({ env = process.env, logger = console, cl
|
||||
return client.isReady ? 'ready' : 'unavailable';
|
||||
},
|
||||
async remember(namespace, key, loader, { ttlSeconds = defaultTtlSeconds } = {}) {
|
||||
if (!client.isReady) return loader();
|
||||
if (!client.isReady) return fallback.remember(namespace, key, loader, { ttlSeconds });
|
||||
try {
|
||||
const version = await namespaceVersion(namespace);
|
||||
const cacheKey = `${prefix}:${namespace}:${version}:${key}`;
|
||||
@@ -99,10 +142,11 @@ export async function createRedisCache({ env = process.env, logger = console, cl
|
||||
return loading;
|
||||
} catch (error) {
|
||||
warn(error);
|
||||
return loader();
|
||||
return fallback.remember(namespace, key, loader, { ttlSeconds });
|
||||
}
|
||||
},
|
||||
async invalidate(namespace) {
|
||||
await fallback.invalidate(namespace);
|
||||
if (!client.isReady) return false;
|
||||
try {
|
||||
await client.incr(versionKey(namespace));
|
||||
@@ -113,6 +157,7 @@ export async function createRedisCache({ env = process.env, logger = console, cl
|
||||
}
|
||||
},
|
||||
async close() {
|
||||
await fallback.close();
|
||||
if (client.isOpen) await client.quit();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user