当前位置:首页 > 技术 > 正文内容

使用StackExchange.Redis实现C#分布式锁的两种方式

访客 技术 2026年7月6日 1

分布式锁在集群架构中扮演着关键角色,主要用于控制并发访问。以下是一些主要应用场景:

  1. 在秒杀、抢购等高并发场景下,多个用户可能同时下单同一商品,导致库存超卖。
  2. 支付、转账等金融操作需要保证同一账户的资金变动是串行执行的。
  3. 分布式环境下,多个节点可能同时触发同一任务(如定时报表生成)。
  4. 用户因网络延迟重复提交表单,可能导致数据重复插入。
自定义分布式锁实现
获取锁

以下代码示例展示了如何对特定订单进行扣款处理,防止用户重复提交导致不同节点处理同一订单:

public static async Task<(bool Success, string LockId)> AcquireLock(string cacheKey, int timeoutSeconds = 5)
{
    var lockKey = BuildLockKey(cacheKey);
    var lockId = Guid.NewGuid().ToString();
    var timeoutMilliseconds = timeoutSeconds * 1000;
    var expiresIn = TimeSpan.FromMilliseconds(timeoutMilliseconds);
    bool acquired = await _redisDb.StringSetAsync(lockKey, lockId, expiresIn, When.NotExists);

    return (acquired, acquired ? lockId : string.Empty);
}
private static string BuildLockKey(string cacheKey)
{
    return $"MyApp:locker:{cacheKey}";
}

上述代码在请求时将订单号作为Redis键的一部分存储,并生成一个唯一的锁标识。只有在Redis中不存在该键时才能成功设置,即成功获取锁。

释放锁

释放锁时需要确保加锁和释放锁是同一个来源:

public static async Task<bool> ReleaseLock(string cacheKey, string lockId)
{
    var lockKey = BuildLockKey(cacheKey);
    var script = @"local expected = @lockId
                      local current = redis.call('get', @lockKey)
                      if current == expected then
                          redis.call('del', @lockKey)
                          return 1
                      else
                          return 0
                      end";
    var parameters = new { lockKey, lockId };
    var prepared = LuaScript.Prepare(script);
    var result = (int)await _redisDb.ScriptEvaluateAsync(prepared, parameters);

    return result == 1;
}

使用Lua脚本的原因:

  1. 如果锁过期或被释放,直接删除可能导致其他节点的锁被误删。
  2. 脚本在Redis单线程执行,确保操作原子性。
自动续期

对于耗时任务,需要实现自动续期:

public async static Task Renew(IDatabase db, string key, string lockId, int milliseconds)
{
    if (!Locker renewLocker = AutoRenewHandler.Instance.GetOrAdd(key, () => new Locker()))
    {
        return;
    }

    var script = @"local current = redis.call('get', @key)
                      if current == @lockId then
                          redis.call('pexpire', @key, @milliseconds)
                          return 1
                      else
                          return 0
                      end";
    var parameters = new { key, lockId, milliseconds };
    var prepared = LuaScript.Prepare(script);
    var result = await db.ScriptEvaluateAsync(prepared, parameters, CommandFlags.None);
    if ((int)result == 0)
    {
        AutoRenewHandler.Instance.Remove(key);
    }
}

自动续期处理器:

public class AutoRenewHandler
{
    private static readonly ConcurrentDictionary<string, Locker> _handlers = new();

    public static Locker GetOrAdd(string key, Func<Locker> factory)
    {
        return _handlers.AddOrUpdate(key, factory, (k, _) => factory());
    }

    public static bool Remove(string key)
    {
        return _handlers.TryRemove(key, out _);
    }
}

public class Locker : IDisposable
{
    private readonly CancellationTokenSource _token = new();
    private Task _task;

    public void Start()
    {
        _task = Task.Run(RenewTask, _token.Token);
    }

    public void Dispose()
    {
        _token.Cancel();
        _task.Wait();
    }

    private async Task RenewTask()
    {
        while (!_token.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromMilliseconds(5000));
            await Renew(_redisDb, _key, _lockId, _timeout);
        }
    }
}

完整的带自动续期的锁获取代码:

public static async Task<(bool Acquired, string LockId)> AcquireLock(string cacheKey, int timeoutSeconds = 5, bool autoRenew = false)
{
    var lockKey = BuildLockKey(cacheKey);
    var lockId = Guid.NewGuid().ToString();
    var expiresIn = TimeSpan.FromSeconds(timeoutSeconds);
    var acquired = await _redisDb.StringSetAsync(lockKey, lockId, expiresIn, When.NotExists);

    if (acquired && autoRenew)
    {
        var handler = new AutoRenewHandler();
        handler.Start(lockKey, lockId, timeoutSeconds);
    }

    return (acquired, lockId);
}

Redis的过期精度约为1秒,续期应选择TTL的一半间隔以确保在过期检查前完成续期。

使用StackExchange.Redis的内置锁
获取锁
string lockKey = "order:12345:lock";
string lockId = Guid.NewGuid().ToString();
TimeSpan expiry = TimeSpan.FromSeconds(30);

bool acquired = _redisDb.LockTake(lockKey, lockId, expiry);
释放锁
bool released = _redisDb.LockRelease(lockKey, lockId);
特点
  • 使用Redis的原生LOCK命令,提供原子操作保证。
  • 内置自动过期机制,避免死锁。
  • 使用OPTIMISTIC锁模式,提高性能。
标签: RedisC#

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。