redis事务在c#中需手动控制multi/exec命令链,stackexchange.redis的transaction实际执行lua脚本而非原生事务,watch依赖连接上下文且失效后需重试,未正确清理会导致连接状态污染。

Redis 事务在 C# 中不是“开箱即用”的原子黑盒,它依赖客户端库对 MULTI / EXEC 协议层的正确封装;直接调用命令或误用高层 API 容易导致事务失效、命令漏入队、甚至静默丢弃错误。
为什么 StackExchange.Redis 的 Transaction 不是传统事务
StackExchange.Redis 的 IDatabase.Transaction 接口看起来像事务,但它底层**不发送 MULTI/EXEC 命令序列**,而是将所有操作转为 Lua 脚本执行(默认启用 ScriptEvaluate)。这意味着:
- 你写的
transaction.StringSet("k1", "v1")实际不会进 Redis 的事务队列,而是在 Lua 中原子执行 -
WATCH在这种模式下被自动注入脚本头部,但你无法手动控制 WATCH 键的粒度或时机 - 如果脚本中某条命令报错(如
INCR作用于字符串值),整个脚本中止,返回null或异常,但不会像原生EXEC那样返回混合结果(成功+错误) - 调试困难:你看到的是脚本执行结果,不是原始
QUEUED→EXEC流程
如何真正触发 Redis 原生 MULTI/EXEC 流程
必须绕过 Transaction 高层封装,改用 IDatabase.Execute 手动发命令。关键点:
- 先调用
db.Execute("MULTI"),确认返回"OK" - 后续每条命令都用
db.Execute("SET", "key", "val")等形式发送,**不能用 StringSet/StringGet 等高层方法**——它们会跳过事务状态,直连执行 - 最后调用
db.Execute("EXEC"),返回RedisResult[]数组,每个元素对应队列中命令的执行结果 - 中间若想取消,发
db.Execute("DISCARD"),清空当前客户端的事务队列
示例片段:
var db = conn.GetDatabase();
db.Execute("MULTI");
db.Execute("SET", "user:1:name", "Alice");
db.Execute("INCR", "counter");
var results = (RedisResult[])db.Execute("EXEC"); // [true, 1L]
WATCH 命令必须和 EXEC 在同一连接中生效
WATCH 的作用域是「当前连接 + 当前事务生命周期」,断连、换 IDatabase 实例、或跨异步上下文都会让 WATCH 失效:
- WATCH 后不要 await 其他 I/O 操作(如 HTTP 请求、DB 查询),否则可能被线程切换,丢失连接上下文
- 不要在
async方法里分段写 WATCH → 业务逻辑 → EXEC,容易被调度打断 - WATCH 的键必须在 EXEC 前未被其他客户端修改,否则
EXEC返回null(不是异常!),需主动判断 - WATCH 多个键时,任一键被改就失败,没有部分成功概念
典型检查写法:
db.Execute("WATCH", "balance:1001");
var balance = db.StringGet("balance:1001");
if (balance.IsNullOrEmpty) throw new InvalidOperationException();
db.Execute("MULTI");
db.StringSet("balance:1001", balance - 100); // 注意:这里 StringSet 会跳过事务!应改用 Execute
db.Execute("EXEC"); // 若返回 null,说明 WATCH 失败,需重试
最容易被忽略的坑:连接复用与事务状态残留
StackExchange.Redis 使用连接池,IDatabase 是无状态的抽象,但 MULTI 状态实际绑定在底层物理连接上:
- 一次
MULTI后没发EXEC或DISCARD,该连接会卡在事务状态,后续从池中取出时可能继承这个脏状态 - 异常未捕获时(如网络中断、
EXEC前抛出异常),务必在finally中发DISCARD,否则连接污染 - 不要在长生命周期对象(如 Singleton Service)里缓存
IDatabase并反复用于事务——连接可能已被回收或复用 - 事务失败后重试,建议新建
IDatabase实例(或至少确保连接干净),而非复用旧实例
安全模板结构:
try {
db.Execute("MULTI");
db.Execute("SET", "a", "1");
db.Execute("DEL", "b");
var result = db.Execute("EXEC");
if (result.IsNull) { /* WATCH 失败,重试 */ }
} catch {
db.Execute("DISCARD"); // 清理
throw;
}
真正的 Redis 事务行为只在你亲手控制 MULTI → 命令队列 → EXEC 这条链路时才存在;任何封装层的“事务对象”都可能悄悄替换成 Lua,而你却以为自己在用原生命令。











