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

  • 自己动手基于 Redis 实现一个 .NET 的分布式锁类库已关闭评论
  • 138 次浏览
  • A+
所属分类:.NET技术
摘要

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

分布式锁的核心其实就是采用一个集中式的服务,然后多个应用节点进行抢占式锁定来进行实现,今天介绍如何采用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