时间:2022-03-26 09:02:16 | 栏目:.NET代码 | 点击:次
对锁的概念和应用场景在此就不阐述了,网上搜索有很多解释,只是我搜索到的使用C#利用Redis的SetNX命令实现的锁虽然能用,但是都不太适合我需要的场景。
Redis有三个最基本属性来保证分布式锁的有效实现:
基于ServiceStack.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 }); }
/// <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方法,并没有提供设置过期时间的方法,对于加锁业务又不能分开执行(如果加锁成功设置过期时间失败导致的永久死锁问题),所以就使用脚本实现,解决了异常情况死锁问题.
如果设置为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集群不能调用脚本执行)