经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » Redis » 查看文章
C#实现Redis的分布式锁
来源:jb51  时间:2021/8/4 11:26:53  对本文有异议

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

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

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

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

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

Redis连接池

  1. public static PooledRedisClientManager RedisClientPool = CreateManager();
  2.  
  3. private static PooledRedisClientManager CreateManager()
  4. {
  5. var redisHosts = System.Configuration.ConfigurationManager.AppSettings["redisHosts"];
  6. if (string.IsNullOrEmpty(redisHosts))
  7. {
  8. throw new Exception("AppSetting redisHosts no found");
  9. }
  10.  
  11. string[] redisHostarr = redisHosts.Split(new string[] { ",", "," }, StringSplitOptions.RemoveEmptyEntries);
  12.  
  13. return new PooledRedisClientManager(redisHostarr, redisHostarr, new RedisClientManagerConfig
  14. {
  15. MaxWritePoolSize = 1000,
  16. MaxReadPoolSize = 1000,
  17. AutoStart = true,
  18. DefaultDb = 0
  19. });
  20. }
  21.  

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

  1. /// <summary>
  2. /// 加锁
  3. /// </summary>
  4. /// <param name="key">锁key</param>
  5. /// <param name="selfMark">自己标记</param>
  6. /// <param name="lockExpirySeconds">锁自动过期时间[默认10](s)</param>
  7. /// <param name="waitLockMilliseconds">等待锁时间(ms)</param>
  8. /// <returns></returns>
  9. public static bool Lock(string key, out string selfMark, int lockExpirySeconds = 10, long waitLockMilliseconds = long.MaxValue)
  10. {
  11. DateTime begin = DateTime.Now;
  12. selfMark = Guid.NewGuid().ToString("N");//自己标记,释放锁时会用到,自己加的锁除非过期否则只能自己打开
  13. using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
  14. {
  15. string lockKey = "Lock:" + key;
  16. while (true)
  17. {
  18. 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);
  19. //循环获取取锁
  20. if (redisClient.ExecLuaAsInt(script, new[] { lockKey }, new[] { selfMark }) == 1)
  21. {
  22. return true;
  23. }
  24.  
  25. //不等待锁则返回
  26. if (waitLockMilliseconds == 0)
  27. {
  28. break;
  29. }
  30.  
  31. //超过等待时间,则不再等待
  32. if ((DateTime.Now - begin).TotalMilliseconds >= waitLockMilliseconds)
  33. {
  34. break;
  35. }
  36. Thread.Sleep(100);
  37. }
  38.  
  39. return false;
  40. }
  41. }
  42.  

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

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

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

释放锁

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

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

调用方式

悲观锁方式

  1. int num = 10;
  2. string lockkey = "xianseng";
  3.  
  4. //悲观锁开启20个人同时拿宝贝
  5. for (int i = 0; i < 20; i++)
  6. {
  7. Task.Run(() =>
  8. {
  9. string selfmark = "";
  10. try
  11. {
  12. if (PublicLockHelper.Lock(lockkey, out selfmark))
  13. {
  14. if (num > 0)
  15. {
  16. num--;
  17. Console.WriteLine($"我拿到了宝贝:宝贝剩余{num}个\t\t{selfmark}");
  18. }
  19. else
  20. {
  21. Console.WriteLine("宝贝已经没有了");
  22. }
  23. Thread.Sleep(100);
  24. }
  25. }
  26. finally
  27. {
  28. PublicLockHelper.UnLock(lockkey, selfmark);
  29. }
  30. });
  31. }

乐观锁方式

  1. int num = 10;
  2. string lockkey = "xianseng";
  3.  
  4. //乐观锁开启10个线程,每个线程拿5次
  5. for (int i = 0; i < 10; i++)
  6. {
  7. Task.Run(() =>
  8. {
  9. for (int j = 0; j < 5; j++)
  10. {
  11. string selfmark = "";
  12. try
  13. {
  14. if (PublicLockHelper.Lock(lockkey, out selfmark, 10, 0))
  15. {
  16. if (num > 0)
  17. {
  18. num--;
  19. Console.WriteLine($"我拿到了宝贝:宝贝剩余{num}个\t\t{selfmark}");
  20. }
  21. else
  22. {
  23. Console.WriteLine("宝贝已经没有了");
  24. }
  25.  
  26. Thread.Sleep(1000);
  27. }
  28. else
  29. {
  30. Console.WriteLine("没有拿到,不想等了");
  31. }
  32. }
  33. finally
  34. {
  35. PublicLockHelper.UnLock(lockkey, selfmark);
  36. }
  37. }
  38. });
  39. }

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

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

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

到此这篇关于C#实现Redis的分布式锁的文章就介绍到这了,更多相关C# Redis分布式锁内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号