服务中的数据访问与缓存
建立事务和连接边界,连接 ORM、分页、缓存键、失效、序列化与多实例故障。
数据访问怎样接到一次操作
驱动提供数据库协议,连接池复用连接,ORM 或查询构建器表达访问,业务流程决定提交范围。Drizzle 的 schema 与查询保持接近 SQL;Prisma、Kysely 等具有各自表示和生成规则。具体数据库语义由数据库承担,ORM 类型无法替代约束和隔离。
按一次业务操作选择事务范围,同一事务中的访问使用事务对象,避免内部依赖悄悄换成全局连接。错误传播应使事务正确退出,并保留重试所需性质;外部 HTTP、邮件和对象存储没有自动进入数据库事务。Drizzle Transactions
连接池上限按所有实例与数据库总容量安排。serverless 实例增减可以增加连接总量,需要平台适合的连接或代理策略。finally 释放借用连接,应用停机关闭池;超时及断连后确认事务和连接状态。
查询的参数与结果
值通过参数绑定,排序字段或方向通过可控映射。分页应给确定顺序与上限;offset 深页与数据变动有成本和漂移,游标需要稳定排序键和相应比较规则。返回的总数还需声明是否与行来自同一观察条件。
公开 DTO 可以从查询结果映射,避免个人信息、内部状态和秘密字段进入响应。日期、十进制和大整数需要可传输表示。数据库生成值、默认值与客户端默认值也需要判断实际来源。
模式迁移由明确执行者管理。应用多个副本同时启动并执行迁移可能竞争;迁移结果、兼容窗口和回滚条件按目标环境保留证据。生成迁移文件与数据迁移成功属于不同结果。
缓存怎样命中正确结果
键包括资源、参数、主体范围和表示版本。读公开汇总和读当前用户私有记录具有不同边界。缓存 miss 要按实际库的缺失表示判断,if (!value) 会把合法 0、空串和 false 当成缺失。
序列化改变 Date、BigInt、Buffer 和实例等表示,读取时按存储契约恢复。不同 cache-manager、Keyv 和 Redis 适配器版本的 TTL 单位与 miss 值可能变化,明确读取实际接口,避免把秒当毫秒。Nest Caching
写入提交后删除缓存是一种失效方法,但删除失败或并发回填旧值可能保留陈旧数据。可使用可恢复失效意图、版本条件、事件传播和 TTL 组合,按所需新鲜度建立保证。TTL 只限定复用时长,缓存库不会自动知道业务依赖。
下面以 node-postgres 和 node-redis 连接一个可允许短暂陈旧的记录摘要缓存。配置在启动前已验证;传入的 owner 来自可信主体,且调用方在进入缓存前完成当前访问政策检查。数据库表为 records(id, owner_id, title, version),version 使用可安全表示的整数。
import { Pool } from 'pg';
import { createClient } from 'redis';
import { z } from 'zod';
const config = z.object({
DATABASE_URL: z.string().min(1),
REDIS_URL: z.string().min(1),
}).parse(process.env);
const pool = new Pool({ connectionString: config.DATABASE_URL, max: 5 });
const redis = createClient({ url: config.REDIS_URL });
redis.on('error', () => console.error('Redis connection error'));
await redis.connect();
const recordSchema = z.object({
id: z.string(), title: z.string(), version: z.number().int(),
});
export async function readRecord(owner: string, id: string) {
const key = `record:v1:${encodeURIComponent(owner)}:${encodeURIComponent(id)}`;
const cached = await redis.get(key);
if (cached !== null) {
try {
const parsed = recordSchema.safeParse(JSON.parse(cached));
if (parsed.success) return parsed.data;
} catch {
// 损坏的 JSON 回到数据库来源。
}
await redis.del(key);
}
const result = await pool.query(
'SELECT id, title, version FROM records WHERE id = $1 AND owner_id = $2',
[id, owner],
);
const row = result.rows[0];
if (row === undefined) return null;
const record = recordSchema.parse(row);
await redis.set(key, JSON.stringify(record), { PX: 30_000 });
return record;
}
export async function closeDataConnections() {
try { await redis.close(); }
finally { await pool.end(); }
}键编码了主体、对象和表示版本,miss 用 null 判断,PX 的单位为毫秒。示例没有负缓存,并选择缓存连接失败时让调用失败;需要故障回源时应显式捕获缓存操作并限制数据库压力。读取损坏 JSON 或不匹配的表示时删除坏键后回源;删除失败仍按本例的缓存故障政策让调用失败。停机协调器停止新请求、等待在途后调用关闭函数;这里使用 node-redis v5 的 close 接口,采用其他版本时核对其退出接口。代码未运行新数据库或 Redis 实验。node-redis 连接与调用
这里的 30 秒是允许陈旧的示例配置。缓存命中期间记录可能已经修改或删除;要求当前权限、删除即时生效或读到最新版本时,需要在效果入口重查对应事实或采用更强协调,不能宣称这段缓存提供强一致性。
多层缓存与真实能力
本地缓存速度快且按进程隔离,Redis 提供共享键值及其他能力。一个 stores 数组可能表示多个写入层,也可能表示默认与回退,必须核对采用版本真实语义;配置出现内存和 Redis 不能直接宣称实现了一致的 L1/L2。
热点读取可合并同一进程的并发回源,多实例仍可能同时回源。缓存穿透、击穿与大量同时失效需要不同政策,负缓存期限和有效性应跟随数据语义。分布式锁还需租约、持有者和失效后续动作的约束。
缓存可以允许逐出,可靠任务队列依赖的数据通常不能被相同策略随意逐出。分开实例或明确资源策略,使缓存压力不会删除任务数据。命中率、错误率、回源成本与数据新鲜度分别观察。
最后更新于