一、为什么需要Redis事务

在分布式系统中,数据一致性是永恒的话题。就像超市收银员结账时既要扫码商品又要扣款的操作必须原子化,Redis事务能确保多个命令要么全部执行成功,要么全部不执行。通过StackExchange.Redis这个高性能.NET客户端库,我们可以在C#中轻松实现这一特性。

相较于ServiceStack.Redis等其他库,StackExchange.Redis具有更轻量级的设计和更强大的异步支持。它通过ConnectionMultiplexer实现连接复用,单个连接即可处理所有操作,这对需要高并发的电商库存扣减、秒杀系统等场景尤为重要。

二、Redis事务核心操作步骤

1. 创建连接多路复用器

// 创建全局唯一的连接复用器
var connection = ConnectionMultiplexer.Connect("localhost:6379,password=yourpassword");
IDatabase db = connection.GetDatabase();

2. 开启事务环境

// 创建事务对象
ITransaction transaction = db.CreateTransaction();

3. 添加事务命令

// 定义条件:当库存大于0时执行扣减
var condition = Condition.HashEqual("product:1001", "stock", 5); 

// 添加操作命令
transaction.AddCondition(condition);
transaction.HashDecrementAsync("product:1001", "stock");
transaction.StringIncrementAsync("order:total");

4. 执行事务提交

bool committed = await transaction.ExecuteAsync();
Console.WriteLine($"事务执行结果:{committed}");

5. 完整示例演示

public async Task ProcessOrder(string productId, int quantity)
{
    var redis = ConnectionMultiplexer.Connect("localhost:6379");
    var db = redis.GetDatabase();
    
    var trans = db.CreateTransaction();
    
    try 
    {
        // 添加库存校验条件
        trans.AddCondition(Condition.HashGreaterThan(productId, "stock", 0));
        
        // 扣减库存
        trans.HashDecrementAsync(productId, "stock", quantity);
        
        // 记录订单日志
        trans.ListRightPushAsync($"orders:{DateTime.Now:yyyyMMdd}", 
            JsonConvert.SerializeObject(new {
                productId,
                quantity,
                time = DateTime.UtcNow
            }));
            
        // 提交事务
        if (await trans.ExecuteAsync()) 
        {
            Console.WriteLine("订单处理成功");
        }
        else 
        {
            Console.WriteLine("库存不足,事务已回滚");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"事务执行异常:{ex.Message}");
    }
}

三、典型应用场景分析

1. 电商库存扣减

在双十一大促场景中,某爆款商品库存仅剩100件。当500个用户同时点击购买时,通过事务确保:

  • 读取库存值
  • 校验库存是否充足
  • 执行库存扣减 这三个操作必须原子化执行,避免超卖问题。

2. 金融账户转账

// 转账事务示例
var trans = db.CreateTransaction();

// 校验转出账户余额
trans.AddCondition(Condition.StringEqual("account:A", "1000"));
trans.StringDecrementAsync("account:A", 500); // 转出
trans.StringIncrementAsync("account:B", 500); // 转入

if (!await trans.ExecuteAsync()) 
{
    throw new InvalidOperationException("账户余额不足或数据已变更");
}

3. 秒杀系统实现

结合Redis的WATCH命令实现乐观锁:

var key = "seckill:item_2023";
var db = redis.GetDatabase();

while (true) 
{
    // 监视库存键
    await db.WatchAsync(key);
    
    int stock = (int)await db.StringGetAsync(key);
    if (stock <= 0) break;

    var trans = db.CreateTransaction();
    trans.StringDecrementAsync(key);
    
    if (await trans.ExecuteAsync()) 
    {
        Console.WriteLine("秒杀成功");
        break;
    }
    
    // 如果失败则重试
    await Task.Delay(100);
}

四、技术方案优缺点对比

优势特性

  1. 原子性保证:所有命令序列化执行,不会被其他客户端命令打断
  2. 高性能:事务命令打包发送,减少网络往返次数
  3. 条件约束:通过Condition类实现复杂校验逻辑
  4. 连接复用:单个ConnectionMultiplexer可处理6000+并发请求

使用限制

  1. 不支持回滚:已执行的命令不会撤销(与关系型数据库不同)
  2. 命令队列限制:所有命令需要预先添加到队列,无法动态调整
  3. Lua脚本对比:复杂逻辑建议使用Lua脚本,但事务更易维护

五、关键注意事项

1. 网络稳定性处理

// 配置重试策略
var config = new ConfigurationOptions
{
    EndPoints = { "localhost:6379" },
    ConnectRetry = 3,
    ReconnectRetryPolicy = new ExponentialRetry(1000)
};

2. 错误处理机制

try
{
    var trans = db.CreateTransaction();
    // ...添加命令...
    await trans.ExecuteAsync();
}
catch (RedisConnectionException ex)
{
    // 处理网络中断
}
catch (RedisTimeoutException ex)
{
    // 处理超时
}

3. 性能优化建议

  • 事务内命令数量控制在100个以内
  • 避免在事务中执行耗时操作(如KEYS命令)
  • 使用Pipeline模式批量提交命令

六、扩展技术关联

1. 与Lua脚本结合

var script = @"local stock = redis.call('HGET', KEYS[1], 'stock')
               if tonumber(stock) >= tonumber(ARGV[1]) then
                   redis.call('HINCRBY', KEYS[1], 'stock', -ARGV[1])
                   return 1
               else
                   return 0
               end";

var result = await db.ScriptEvaluateAsync(script, 
    new { KEYS = new RedisKey[] { "product:1001" }, ARGV = new RedisValue[] { 2 } });

2. 分布式锁配合

var lockKey = "resource_lock";
var token = Guid.NewGuid().ToString();

// 获取锁
if (db.LockTake(lockKey, token, TimeSpan.FromSeconds(10)))
{
    try 
    {
        // 执行事务操作
    }
    finally 
    {
        db.LockRelease(lockKey, token);
    }
}

七、总结与建议

通过本文的完整示例,我们已经掌握了使用StackExchange.Redis实现Redis事务的核心方法。在实际项目中使用时需要注意:

  1. 事务不是银弹,要评估是否真的需要事务特性
  2. 监控Redis服务器内存和CPU使用率
  3. 重要操作建议记录操作日志
  4. 生产环境务必配置持久化和备份策略

当遇到秒级十万并发场景时,建议结合Lua脚本+集群分片+本地缓存的多级方案。希望本文的实战经验能帮助你在分布式系统中构建更健壮的事务处理机制。