C# 实现Redis的分布式锁

 

Redis实现分布式锁(悲观锁/乐观锁)

对锁的概念和应用场景在此就不阐述了,网上搜索有很多解释,只是我搜索到的使用C#利用Redis的SetNX命令实现的锁虽然能用,但是都不太适合我需要的场景。

Redis有三个最基本属性来保证分布式锁的有效实现:

  • 安全性: 互斥,在任何时候,只有一个客户端能持有锁。
  • 活跃性A:没有死锁,即使客户端在持有锁的时候崩溃,最后也会有其他客户端能获得锁,超时机制。
  • 活跃性B:故障容忍,只有大多数Redis节点时存活的,客户端仍可以获得锁和释放锁。

基于ServiceStack.Redis写了一个帮助类

 

Redis连接池

      public static PooledRedisClientManager RedisClientPool = CreateManager();

      private static PooledRedisClientManager CreateManager()
      {
          var redisHosts = System.Configuration.ConfigurationManager.AppSettings["redisHosts"];
          if (string.IsNullOrEmpty(redisHosts))
          {
              throw new Exception("AppSetting redisHosts no found");
          }

          string[] redisHostarr = redisHosts.Split(new string[] { ",", "," }, StringSplitOptions.RemoveEmptyEntries);

          return new PooledRedisClientManager(redisHostarr, redisHostarr, new RedisClientManagerConfig
          {
              MaxWritePoolSize = 1000,
              MaxReadPoolSize = 1000,
              AutoStart = true,
              DefaultDb = 0
          });
      }

 

使用Redis的SetNX命令实现加锁,

      /// <summary>
      /// 加锁
      /// </summary>
      /// <param name="key">锁key</param>
      /// <param name="selfMark">自己标记</param>
      /// <param name="lockExpirySeconds">锁自动过期时间[默认10](s)</param>
      /// <param name="waitLockMilliseconds">等待锁时间(ms)</param>
      /// <returns></returns>
      public static bool Lock(string key, out string selfMark, int lockExpirySeconds = 10, long waitLockMilliseconds = long.MaxValue)
      {
          DateTime begin = DateTime.Now;
          selfMark = Guid.NewGuid().ToString("N");//自己标记,释放锁时会用到,自己加的锁除非过期否则只能自己打开
          using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
          {
              string lockKey = "Lock:" + key;
              while (true)
              {
                  string script = string.Format("if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('PEXPIRE',KEYS[1],{0}) return 1 else return 0 end", lockExpirySeconds * 1000);
                  //循环获取取锁
                  if (redisClient.ExecLuaAsInt(script, new[] { lockKey }, new[] { selfMark }) == 1)
                  {
                      return true;
                  }

                  //不等待锁则返回
                  if (waitLockMilliseconds == 0)
                  {
                      break;
                  }

                  //超过等待时间,则不再等待
                  if ((DateTime.Now - begin).TotalMilliseconds >= waitLockMilliseconds)
                  {
                      break;
                  }
                  Thread.Sleep(100);
              }

              return false;
          }
      }

因为ServiceStack.Redis提供的SetNX方法,并没有提供设置过期时间的方法,对于加锁业务又不能分开执行(如果加锁成功设置过期时间失败导致的永久死锁问题),所以就使用脚本实现,解决了异常情况死锁问题.

  • 参数key:锁的key
  • 参数selfMark:在设置锁的时候会产生一个自己的标识,在释放锁的时候会用到,所谓解铃还须系铃人。防止锁被误释放,导致锁无效.
  • 参数lockExpirySeconds:锁的默认过期时间,防止被永久死锁.
  • 参数waitLockMilliseconds:循环获取锁的等待时间.

如果设置为0,为乐观锁机制,获取不到锁,直接返回未获取到锁.
默认值为long最大值,为悲观锁机制,约等于很多很多天,可以理解为一直等待.

释放锁

      /// <summary>
      /// 释放锁
      /// </summary>
      /// <param name="key">锁key</param>
      /// <param name="selfMark">自己标记</param>
      public void UnLock(string key, string selfMark)
      {
          using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
          {
              string lockKey = "Lock:" + key;
              var script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
              redisClient.ExecLuaAsString(script, new[] { lockKey }, new[] { selfMark });
          }
      }

参数key:锁的key
参数selfMark:在设置锁的时候返回的自己标识,用来解锁自己加的锁(此值不能随意传,必须是加锁时返回的值)

 

调用方式

悲观锁方式

          int num = 10;
          string lockkey = "xianseng";

          //悲观锁开启20个人同时拿宝贝
          for (int i = 0; i < 20; i++)
          {
              Task.Run(() =>
              {
                  string selfmark = "";
                  try
                  {
                      if (PublicLockHelper.Lock(lockkey, out selfmark))
                      {
                          if (num > 0)
                          {
                              num--;
                              Console.WriteLine($"我拿到了宝贝:宝贝剩余{num}个\t\t{selfmark}");
                          }
                          else
                          {
                              Console.WriteLine("宝贝已经没有了");
                          }
                          Thread.Sleep(100);
                      }
                  }
                  finally
                  {
                      PublicLockHelper.UnLock(lockkey, selfmark);
                  }
              });
          }

乐观锁方式

          int num = 10;
          string lockkey = "xianseng";

          //乐观锁开启10个线程,每个线程拿5次
          for (int i = 0; i < 10; i++)
          {
              Task.Run(() =>
              {
                  for (int j = 0; j < 5; j++)
                  {
                      string selfmark = "";
                      try
                      {
                          if (PublicLockHelper.Lock(lockkey, out selfmark, 10, 0))
                          {
                              if (num > 0)
                              {
                                  num--;
                                  Console.WriteLine($"我拿到了宝贝:宝贝剩余{num}个\t\t{selfmark}");
                              }
                              else
                              {
                                  Console.WriteLine("宝贝已经没有了");
                              }

                              Thread.Sleep(1000);
                          }
                          else
                          {
                              Console.WriteLine("没有拿到,不想等了");
                          }
                      }
                      finally
                      {
                          PublicLockHelper.UnLock(lockkey, selfmark);
                      }
                  }
              });
          }

单机只能用多线模拟使用分布式锁了

此锁已经可以满足大多数场景了,若有不妥,还请多多指出,以免误别人!

(次方案不支持Redis集群,Redis集群不能调用脚本执行)

关于C#实现Redis的分布式锁的文章就介绍至此,更多相关C# Redis分布式锁内容请搜索编程教程以前的文章,希望大家多多支持编程教程

C# 是一门现代化的编程语言,与Java十分的相似。熟练的开发者甚至能三天无缝切换到Java。生态性能也是遍地开花。今天, 让我们来学习一下C#中的Span相关的性能优化吧 什么是SpanSystem. ...