自己动手基于 Redis 实现一个 .NET 的分布式锁类库

时间:2022-12-09 12:05:47

分布式锁的核心其实就是采用一个集中式的服务,然后多个应用节点进行抢占式锁定来进行实现,今天介绍如何采用Redis作为基础服务,实现一个分布式锁的类库,本方案不考虑 Redis 集群多节点问题,如果引入集群多节点问题,会导致解决成本大幅上升,因为 Redis 单节点就可以很容易的处理10万并发量了,这对于日常开发中 99% 的项目足够使用了。

目标如下:

  1. 支持 using 语法,出 using 范围之后自动释放锁
  2. 支持 尝试行为,如果锁获取不到则直接跳过不等待
  3. 支持 等待行为,如果锁获取不到则持续等待直至超过设置的等待时间
  4. 支持信号量控制,实现一个锁可以同时获取到几次,方便对一些方法进行并发控制

代码整体结构图

自己动手基于 Redis 实现一个  .NET 的分布式锁类库


创建 DistributedLock 类库,然后定义接口文件 IDistributedLock ,方便我们后期扩展其他分布式锁的实现。

namespace DistributedLock
{
    public interface IDistributedLock
    {

        /// <summary>
        /// 获取锁
        /// </summary>
        /// <param name="key">锁的名称,不可重复</param>
        /// <param name="expiry">失效时长</param>
        /// <param name="semaphore">信号量</param>
        /// <returns></returns>
        public IDisposable Lock(string key, TimeSpan expiry = default, int semaphore = 1);

        /// <summary>
        /// 尝试获取锁
        /// </summary>
        /// <param name="key">锁的名称,不可重复</param>
        /// <param name="expiry">失效时长</param>
        /// <param name="semaphore">信号量</param>
        /// <returns></returns>
        public IDisposable? TryLock(string key, TimeSpan expiry = default, int semaphore = 1);

    }
}

创建 DistributedLock.Redis 类库,安装下面两个 Nuget 包

StackExchange.Redis
Microsoft.Extensions.Options

定义配置模型 RedisSetting

namespace DistributedLock.Redis.Models
{
    public class RedisSetting
    {
        public string Configuration { get; set; }

        public string InstanceName { get; set; }
    }
}

定义 RedisLockHandle

using StackExchange.Redis;

namespace DistributedLock.Redis
{
    public class RedisLockHandle : IDisposable
    {

        public IDatabase Database { get; set; }

        public string LockKey { get; set; }

        public void Dispose()
        {
            try
            {
                Database.LockRelease(LockKey, "123456");
            }
            catch
            {
            }

            GC.SuppressFinalize(this);
        }
    }
}

实现 RedisLock

using DistributedLock.Redis.Models;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using System.Security.Cryptography;
using System.Text;

namespace DistributedLock.Redis
{
    public class RedisLock : IDistributedLock
    {

        private readonly ConnectionMultiplexer connectionMultiplexer;

        private readonly RedisSetting redisSetting;

        public RedisLock(IOptionsMonitor<RedisSetting> config)
        {
            connectionMultiplexer = ConnectionMultiplexer.Connect(config.CurrentValue.Configuration);
            redisSetting = config.CurrentValue;
        }


        /// <summary>
        /// 获取锁
        /// </summary>
        /// <param name="key">锁的名称,不可重复</param>
        /// <param name="expiry">失效时长</param>
        /// <param name="semaphore">信号量</param>
        /// <returns></returns>
        public IDisposable Lock(string key, TimeSpan expiry = default, int semaphore = 1)
        {

            if (expiry == default)
            {
                expiry = TimeSpan.FromMinutes(1);
            }

            var endTime = DateTime.UtcNow + expiry;

            RedisLockHandle redisLockHandle = new();

        StartTag:
            {
                for (int i = 0; i < semaphore; i++)
                {
                    var keyMd5 = redisSetting.InstanceName + Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(key + i)));

                    try
                    {
                        var database = connectionMultiplexer.GetDatabase();

                        if (database.LockTake(keyMd5, "123456", expiry))
                        {
                            redisLockHandle.LockKey = keyMd5;
                            redisLockHandle.Database = database;
                            return redisLockHandle;
                        }
                    }
                    catch
                    {

                    }
                }


                if (redisLockHandle.LockKey == default)
                {

                    if (DateTime.UtcNow < endTime)
                    {
                        Thread.Sleep(1000);
                        goto StartTag;
                    }
                    else
                    {
                        throw new Exception("获取锁" + key + "超时失败");
                    }
                }
            }

            return redisLockHandle;
        }


        public IDisposable? TryLock(string key, TimeSpan expiry = default, int semaphore = 1)
        {

            if (expiry == default)
            {
                expiry = TimeSpan.FromMinutes(1);
            }


            for (int i = 0; i < semaphore; i++)
            {
                var keyMd5 = redisSetting.InstanceName + Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(key + i)));

                try
                {
                    var database = connectionMultiplexer.GetDatabase();

                    if (database.LockTake(keyMd5, "123456", expiry))
                    {
                        RedisLockHandle redisLockHandle = new()
                        {
                            LockKey = keyMd5,
                            Database = database
                        };
                        return redisLockHandle;
                    }
                }
                catch
                {
                }
            }
            return null;

        }
    }
}

定义 ServiceCollectionExtensions

using DistributedLock.Redis.Models;
using Microsoft.Extensions.DependencyInjection;

namespace DistributedLock.Redis
{
    public static class ServiceCollectionExtensions
    {
        public static void AddRedisLock(this IServiceCollection services, Action<RedisSetting> action)
        {
            services.Configure(action);
            services.AddSingleton<IDistributedLock, RedisLock>();
        }
    }
}

使用时只要在配置文件中加入 redis 连接字符串信息,然后注入服务即可。
appsettings.json

{
  "ConnectionStrings": {
    "redisConnection": "127.0.0.1,Password=123456,DefaultDatabase=0"
  }
}

注入示例代码:

//注册分布式锁 Redis模式
builder.Services.AddRedisLock(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("redisConnection")!;
    options.InstanceName = "lock";
});

使用示例

using DistributedLock;
using Microsoft.AspNetCore.Mvc;

namespace WebAPI.Controllers
{

    [Route("[controller]")]
    [ApiController]
    public class DemoController : ControllerBase
    {


        private readonly IDistributedLock distLock;

        public DemoController(IDistributedLock distLock)
        {
            this.distLock = distLock;
        }


        [HttpGet("Test")]
        public void Test()
        {

            //锁定键只要是一个字符串即可,可以简单理解为锁的标识名字,可以是用户名,用户id ,订单id 等等,根据业务需求自己定义
            string lockKey = "xx1";


            using (distLock.Lock(lockKey))
            {
                //代码块同时只有一个请求可以进来执行,其余没有获取到锁的全部处于等待状态
                //锁定时常1分钟,1分钟后无论代码块是否执行完成锁都会被释放,同时等待时常也为1分钟,1分钟后还没有获取到锁,则抛出异常
            }


            using (distLock.Lock(lockKey, TimeSpan.FromSeconds(300)))
            {
                //代码块同时只有一个请求可以进来执行,其余没有获取到锁的全部处于等待状态
                //锁定时常300秒,300秒后无论代码块是否执行完成锁都会被释放,同时等待时常也为300秒,300秒后还没有获取到锁,则抛出异常
            }


            using (distLock.Lock(lockKey, TimeSpan.FromSeconds(300), 5))
            {
                //代码块同时有五个请求可以进来执行,其余没有获取到锁的全部处于等待状态
                //锁定时常300秒,300秒后无论代码块是否执行完成锁都会被释放,同时等待时常也为300秒,300秒后还没有获取到锁,则抛出异常

                //该代码块有5个请求同时拿到锁,签发出去的5把锁,每把锁的时间都是单独计算的,并非300秒后 5个锁会全部同时释放,可能只会释放 2个或3个,释放之后心的请求又可以获取到,总之最多只有5个请求可以进入
            }


            var lockHandle1 = distLock.TryLock(lockKey);

            if (lockHandle1 != null)
            {
                //代码块同时只有一个请求可以进来执行,其余没有获取到锁的直接为 null 不等待,也不执行
                //锁定时常1分钟,1分钟后无论代码块是否执行完成锁都会被释放
            }

            var lockHandle2 = distLock.TryLock(lockKey, TimeSpan.FromSeconds(300));

            if (lockHandle2 != null)
            {
                //代码块同时只有一个请求可以进来执行,其余没有获取到锁的直接为 null 不等待,也不执行
                //锁定时常300秒,300秒后无论代码块是否执行完成锁都会被释放
            }


            var lockHandle3 = distLock.TryLock(lockKey, TimeSpan.FromSeconds(300), 5);

            if (lockHandle3 != null)
            {
                //代码块同时有五个请求可以进来执行,其余没有获取到锁的直接为 null 不等待,也不执行
                //锁定时常300秒,300秒后无论代码块是否执行完成锁都会被释放

                //该代码块有5个请求同时拿到锁,签发出去的5把锁,每把锁的时间都是单独计算的,并非300秒后 5个锁会全部同时释放,可能只会释放 2个或3个,释放之后心的请求又可以获取到,总之最多只有5个请求可以进入
            }
        }

    }
}

至此 关于 自己动手基于 Redis 实现一个 .NET 的分布式锁类库 就讲解完了,有任何不明白的,可以在文章下面评论或者私信我,欢迎大家积极的讨论交流,有兴趣的朋友可以关注我目前在维护的一个 .NET 基础框架项目,项目地址如下
https://github.com/berkerdong/NetEngine.git
https://gitee.com/berkerdong/NetEngine.git