C# Redis分布式锁的应用

分布式锁Redis实现
本文探讨了在分布式环境中如何使用Redis实现高性能、可靠的分布式锁。详细介绍了实现分布式锁的必要性,Redis实现分布式锁的原理,以及C#代码实现。通过使用SETNX、expire和delete命令,确保了锁的安全性和效率。

1、背景

我们在开发很多业务场景会使用到锁,例如库存控制,抽奖等。一般我们会使用内存锁的方式来保证线性的执行。但现在大多站点都会使用分布式部署,那多台服务器间的就必须使用同一个目标来判断锁。分布式与单机情况下最大的不同在于其不是多线程而是多进程。

2、演变

[分布式站点使用内存锁方式如下图]
clipboard.png
假设有3个用户同时购买一件商品,商品库存只剩下1,如果3个用户同时购买,负载均衡把3个用户分别指向站点1、2、3,那结果将会是3个用户都购买成功。下面我们使用分布式锁解决这个问题。

[分布式站点使用分布式锁如下图]
clipboard.png
单台服务器由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而多服务器之间都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。

3、实现

选型

想想我们实现分布式锁要满足哪些条件?
1、在分布式系统环境下,一个锁在同一时间只能被一个服务器获取;(这是所有分布式锁的基础)
2、高性能的获取锁和释放锁;(锁用完了,要及时释放,以供别人继续使用)
3、具备锁失效机制,防止死锁;(防止因为某些意外,锁没有得到释放,那别人将永远无法使用)
4、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。(满足等待锁的同时,也要满足非阻塞锁特性,便于多样性的业务场景使用)

分布式锁有很多的实现方式:数据库排他锁、Zookeeper、缓存(redis、memcached)等,本文选用Redis实现。原因如下:
1、Redis有很高的性能;
2、Redis命令对此支持较好,实现起来比较方便;
Redis官网上对C#的使用推荐有ServiceStack.Redis和StackExchange.Redis
由于StackExchange.Redis虽然有提供LockTake方法,很方便的使用锁,但是只支持.Net4.5以上。公司很多站点都是3.5和4.0的,所以选用ServiceStack.Redis,自己封装一下。

Redis使用命令介绍

1、SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
2、expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
3、delete
delete key:删除key
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

C#代码

1、锁方法

        /// <summary>
        /// 分布式锁
        /// </summary>
        /// <param name="key">锁key</param>
        /// <param name="lockExpirySeconds">锁自动超时时间(秒)</param>
        /// <param name="waitLockMs">等待锁时间(秒)</param>
        /// <returns></returns>
        public static bool Lock(string key, int lockExpirySeconds = 10, double waitLockSeconds = 0)
        {
            //间隔等待50毫秒
            int waitIntervalMs = 50;

            RedisClient client = (RedisClient)RedisClientManager.GetClient(RedisProviderName.RedisCommon, 0);

            string lockKey = "LockForSetNX:" + key;

            DateTime begin = DateTime.Now;
            using (client)
            {
                while (true)
                {
                    //循环获取取锁
                    if (client.SetNX(lockKey, new byte[] { 1 }) == 1)
                    {
                        //设置锁的过期时间
                        client.Expire(lockKey, lockExpirySeconds);
                        return true;
                    }

                    //不等待锁则返回
                    if (waitLockSeconds == 0) break;

                    //超过等待时间,则不再等待
                    if ((DateTime.Now - begin).TotalSeconds >= waitLockSeconds) break;

                    Thread.Sleep(waitIntervalMs);
                }
                return false;
            }
        }

2、释放锁

        /// <summary>
        /// 删除锁 执行完代码以后调用释放锁
        /// </summary>
        /// <param name="key"></param>
        public static void DelLock(string key)
        {
            RedisClient client = (RedisClient)RedisClientManager.GetClient(RedisProviderName.RedisCommon, 0);
            string lockKey = "LockForSetNX:" + key;
            using (client)
            {
                client.Del(lockKey);
            }
        }

3、业务应用代码

            try
            {
                //取锁,设置key10秒后失效,最大等待锁5秒
                if (RedisHelper.Lock("LockKey", 10, 5))
                {
                    //取到锁,执行具体业务
                    //例如商品购买,库存-1
                }
            }
            finally
            {
                //释放锁
                DelLock("LockKey");
            }

【如果文章对你有所帮助,请在评论区留言点赞,以资鼓励。】

<think>好的,我现在需要帮助用户在C#中实现Redis分布式锁的解决方案或示例代码。首先,我得回顾一下用户提供的引用内容,特别是引用[3],里面有一个C#的示例代码,这可能是一个重要的参考点。用户之前的问题也提到了C#,所以需要确保代码符合最新的实践和标准。 首先,分布式锁的基本原理是通过Redis的原子操作来确保只有一个客户端能获取。关键点包括设置的键值、唯一标识(防止误删)、过期时间以及释放时的Lua脚本验证。引用[3]中的代码使用了StackExchange.Redis库,这是一个常用的C# Redis客户端。代码中TryAcquireLock方法使用StringSet命令,并带有When.NotExists条件,这对应Redis的SETNX命令,确保只在键不存在时设置值,保证原子性。同时,的值使用GUID生成,确保每个客户端实例的唯一性,避免其他客户端误删。 接下来需要考虑的是的自动释放,即设置过期时间。在示例代码中,expiryTime参数用于设置键的过期时间,这样即使客户端崩溃,也会在过期后自动释放,防止死。这一点很重要,因为如果客户端在释放之前崩溃,没有过期时间的话,将永远无法释放。 然后是释放的部分,代码中使用了Lua脚本,通过比较的值是否匹配来删除键。这是必要的,因为直接删除可能会删除其他客户端持有的。Lua脚本在Redis中原子执行,确保检查和删除操作的原子性,避免竞态条件。 用户可能需要更详细的步骤说明,比如如何配置Redis连接、如何处理异常情况、如何确保的可重入性等。不过,引用中的代码已经涵盖了基本实现,但可能需要补充一些最佳实践,比如使用using语句管理ConnectionMultiplexer的生命周期,避免资源泄漏。此外,重试机制和续期(比如看门狗机制)也是实际应用中常见的优化点,但可能在基础示例中没有涉及。 另外,需要注意StackExchange.Redis的线程安全,ConnectionMultiplexer是设计为单例的,应该在应用程序中共享,而不是每次创建都新建一个连接。示例中的构造函数每次创建新的连接,这可能不是最佳做法,应该改为接收一个已存在的ConnectionMultiplexer实例,以提高性能。 最后,需要确保代码示例的完整性和可运行性,可能需要添加异常处理和资源释放的代码。例如,在释放时,需要确保即使发生异常,资源也能正确释放。此外,测试代码的编写也是帮助用户验证实现正确性的重要部分,但用户可能没有提到这一点,所以可能不需要包含在回答中。 总结下来,我需要提供一个基于引用[3]但改进后的C#示例代码,确保连接管理的正确性,并解释关键部分的工作原理,如原子操作、唯一标识、过期时间和Lua脚本的使用。同时,需要提到可能的优化点和注意事项,比如连接复用、异常处理和续期机制。</think>以下是在C#中实现Redis分布式锁的完整解决方案,基于最佳实践并结合实际应用场景: ### 一、C# Redis分布式锁实现步骤 1. **添加依赖包** 使用NuGet安装StackExchange.Redis包: ```bash Install-Package StackExchange.Redis ``` 2. **核心实现类**(改进版) ```csharp using StackExchange.Redis; using System; public class RedisDistributedLock : IDisposable { private readonly IDatabase _database; private readonly string _lockKey; private readonly string _lockValue; private readonly TimeSpan _expiry; private bool _isLockAcquired; // 建议复用ConnectionMultiplexer[^3] public RedisDistributedLock(IConnectionMultiplexer connection, string lockKey, TimeSpan expiry) { _database = connection.GetDatabase(); _lockKey = lockKey; _lockValue = Guid.NewGuid().ToString(); _expiry = expiry; } public bool TryAcquireLock(TimeSpan? timeout = null) { var timeoutTime = DateTime.UtcNow.Add(timeout ?? TimeSpan.Zero); while (DateTime.UtcNow < timeoutTime) { // 原子操作设置[^3] if (_database.StringSet(_lockKey, _lockValue, _expiry, When.NotExists)) { _isLockAcquired = true; return true; } Thread.Sleep(100); // 轮询间隔 } return false; } public void ReleaseLock() { if (!_isLockAcquired) return; // 使用Lua脚本保证原子性[^3] var script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) else return 0 end"; _database.ScriptEvaluate(script, new RedisKey[] { _lockKey }, new RedisValue[] { _lockValue }); } public void Dispose() { ReleaseLock(); GC.SuppressFinalize(this); } } ``` ### 二、使用示例 ```csharp var redis = ConnectionMultiplexer.Connect("localhost"); var redisLock = new RedisDistributedLock( redis, "order_lock_123", TimeSpan.FromSeconds(30)); try { if (redisLock.TryAcquireLock(TimeSpan.FromSeconds(5))) { // 执行业务逻辑 ProcessOrder(); } else { Console.WriteLine("获取失败"); } } finally { redisLock.Dispose(); } ``` ### 三、关键实现要点 1. **原子性操作**: - 使用`When.NotExists`参数保证`SETNX`操作的原子性[^3] - 释放时使用Lua脚本保证验证与删除的原子性 2. **安全特性**: - 每个使用唯一GUID作为值,防止误删其他客户端的 - 实现IDisposable接口确保的释放 3. **连接管理**: - 复用ConnectionMultiplexer实例(推荐单例模式) - 连接字符串建议配置超时参数:`syncTimeout=5000,connectTimeout=10000` 4. **异常处理**: ```csharp catch (RedisConnectionException ex) { // 处理Redis连接异常 Logger.Error("Redis连接失败", ex); } ``` ### 四、生产环境建议 1. **续期机制**: ```csharp // 在单独线程中执行 while (_isLockAcquired) { _database.KeyExpire(_lockKey, _expiry); Thread.Sleep(_expiry - TimeSpan.FromSeconds(5)); } ``` 2. **监控指标**: - 获取成功率 - 平均等待时间 - 竞争频率[^2] 3. **集群环境处理**: ```csharp var endpoints = new EndPointCollection { { "redis1", 6379 }, { "redis2", 6380 } }; var config = new ConfigurationOptions { EndPoints = endpoints, CommandMap = CommandMap.Create(new HashSet<string> { "INFO", "PING" }) }; ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值