首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >自己动手基于 Redis 实现一个 .NET 的分布式锁类库

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

作者头像
乌拉栋
发布2022-12-10 15:06:14
2720
发布2022-12-10 15:06:14
举报
文章被收录于专栏:编程宝典编程宝典

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

目标如下:

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

代码整体结构图


创建 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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-12-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档