事务、WATCH 与自动流水线
事务解决“命令按顺序作为一组执行”的问题,流水线解决“减少网络往返”的问题。两者目的不同。
MULTI / EXEC
const [setReply, value] = await client
.multi()
.set("nloop:transaction:key", "value")
.get("nloop:transaction:key")
.exec();
console.log(setReply, value);
Redis 会依次执行事务中的命令,其他客户端的命令不会插入这组执行过程。
但 Redis 事务与 MySQL 事务不同:
- 没有通用回滚机制。
- 命令入队时成功,不代表执行时一定成功。
- 不提供关系数据库那样的隔离级别概念。
乐观锁 WATCH
当更新依赖旧值时,可以监视键:
async function addPoints(userId: number, points: number): Promise<void> {
const key: string = `nloop:user:${userId}:points`;
for (let attempt = 1; attempt <= 3; attempt += 1) {
await client.watch(key);
const current: number = Number(await client.get(key) ?? "0");
const result = await client
.multi()
.set(key, String(current + points))
.exec();
if (result !== null) {
return;
}
}
throw new Error("积分更新冲突次数过多");
}
如果 WATCH 后键被其他客户端修改,exec() 返回 null。应用需要重试整个“读取—计算—提交”过程。
WATCH 状态属于连接。高并发下应使用独占连接或连接池,避免不同请求共享监视状态。
优先使用原子命令
上面的积分累加其实不需要 WATCH:
await client.incrBy(key, points);
选择顺序通常是:
- Redis 已有的原子命令。
- 条件参数,例如
NX、XX。 MULTI/EXEC。WATCH或 Lua 脚本。
自动流水线
Node-Redis 会将同一个事件循环 tick 中发出的命令自动流水线发送。
推荐用 Promise.all() 明确处理 Promise:
const [name, email, status] = await Promise.all([
client.hGet("nloop:user:42", "name"),
client.hGet("nloop:user:42", "email"),
client.hGet("nloop:user:42", "status"),
]);
反例:
const name = await client.hGet("nloop:user:42", "name");
const email = await client.hGet("nloop:user:42", "email");
const status = await client.hGet("nloop:user:42", "status");
后者产生顺序等待,通常需要更多网络往返。
流水线不是事务
流水线只优化传输:
- 命令不保证作为不可插入的一组执行。
- 一个命令失败不会自动撤销其他命令。
- 适合相互独立的批量操作。
事务强调执行边界,流水线强调吞吐量。
并发数量也要受控
不要一次 Promise.all() 数十万条命令。应分批处理,例如每批 100 或 500 条,并根据值大小、Redis 延迟和应用内存进行压测。