.netCore如何借助CSRedis实现一个安全高效的分布式锁
分布式是锁是分布式中重要且会遇到的问题,分布式锁的难点在于,是多个进行访问同一个资源,出现资源竞争的情况(普通的多个线程是在一个进程中 可以加锁就能解决)
如何借助CSRedis实现一个安全高效的分布式锁?往下看
一、Redis实现分布式锁常见的几个命令
► Setnx
命令:SETNX key value
说明:将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
时间复杂度:O(1)
返回值:设置成功,返回1 ; 设置失败,返回 0
► Getset
命令:GETSET key value
说明:将给定 key 的值设为 value ,并返回 key 的旧值(old value)。当 key 存在但不是字符串类型时,返回一个错误。
时间复杂度:O(1)
返回值:返回给定 key 的旧值; 当 key 没有旧值时,也即是, key 不存在时,返回 nil 。
► Expire
命令:EXPIRE key seconds
说明:为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。
时间复杂度:O(1)
返回值:设置成功返回 1 ;当 key 不存在或者不能为 key 设置生存时间时(比如在低于 2.1.3 版本的 Redis 中你尝试更新 key 的生存时间),返回 0 。
► Del
命令:DEL key [key ...]
说明:删除给定的一个或多个 key 。不存在的 key 会被忽略。
时间复杂度:O(N); N 为被删除的 key 的数量。
删除单个字符串类型的 key ,时间复杂度为O(1)。
删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为O(M), M 为以上数据结构内的元素数量。
锁按照不同的维度,有多种分类.比如
1.悲观锁,乐观锁;
2.公平锁,非公平锁;
3.独享锁,共享锁;
4.线程锁,进程锁;
案例一 (次案例要安装CsRedis的依赖包)实现方式 redis 本身是个单线程,在某个进程进入redis后会把当前连接的库 加上锁防止其他进程进入(redis 可以设置锁的时常,业务处理完后也可以解除当前锁,避免死锁的发生)其他进程在访问redis 后发现有锁可以设置 等待 或者重试时间 大致逻辑是这样
using CSRedis; using System; using System.Threading; using System.Threading.Tasks; namespace Test0002 { internal class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); CSRedisClient redisClient = new CSRedis.CSRedisClient("120.0.0.0.1:6380,defaultDatabase=1,password=mixdo2018"); var lockKey = "lockKey"; var stockKey = "stock"; redisClient.Set(stockKey, 5);//商品库存 var releaseLockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";//释放锁的redis脚本 redisClient.Del(lockKey);//测试前,先把锁删了. Parallel.For(0, 10, i => { var id = Guid.NewGuid().ToString("N"); //获取锁 do { //set : key存在则失败,不存在才会成功,并且过期时间5秒 var success = redisClient.Set(lockKey, id, expireSeconds: 5, exists: RedisExistence.Nx); if (success == true) { break; } Thread.Sleep(TimeSpan.FromSeconds(1));//休息1秒再尝试获取锁 } while (true); Console.WriteLine($"线程:{Task.CurrentId} 拿到了锁,开始消费"); //扣减库存 var currentStock = redisClient.IncrBy(stockKey, -1); if (currentStock < 0) { Console.WriteLine($"库存不足,线程:{Task.CurrentId} 抢购失败!"); redisClient.Eval(releaseLockScript, lockKey, id); return; } //模拟处理业务,这里不考虑失败的情况 Thread.Sleep(TimeSpan.FromSeconds(new Random().Next(1, 3))); Console.WriteLine($"线程:{Task.CurrentId} 消费完毕!剩余 {currentStock} 个"); //业务处理完后,释放锁. redisClient.Eval(releaseLockScript, lockKey, id); }); } } }
///上面这个只是简单的在单个进程里面模拟
把上面的方式进行处理之后得到下面简练的方式
///下面这个可以在本地通过 dotnet TimePlan.dll --urls http://localhost:5002 、 dotnet TimePlan.dll --urls http://localhost:5003 、dotnet TimePlan.dll --urls http://localhost:5004 、dotnet TimePlan.dll --urls http://localhost:5005 开多台进程进行测试
using CSRedis; using System; using System.Threading; namespace Test0004 { internal class Program { static void Main(string[] args) { Thread.Sleep(500); Console.WriteLine("开始循环10次"); for (int i = 0; i < 1000; i++) { Thread.Sleep(500); Console.WriteLine("Hello World!"); CSRedisClient redisClient = new CSRedis.CSRedisClient("120.0.0.0.1:6380,defaultDatabase=1,password=mixdo2018"); var lockKey = "lockKey"; var releaseLockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";//释放锁的redis脚本 var id = Guid.NewGuid().ToString("N"); var success = redisClient.Set(lockKey, id, expireSeconds: -1, exists: RedisExistence.Nx); if (success != true) { Console.WriteLine("当前资源以被占用,没机会了,等10秒钟再次尝试"); Thread.Sleep(500);//休息1秒再尝试获取锁 continue; } else { Console.WriteLine("已获取当前资源锁的钥匙,可放心使用"); } ///业务操作 Console.WriteLine("我好开心,我拿到key了:" + id); Thread.Sleep(500);//休息1秒再尝试获取锁 Console.WriteLine("哈哈哈"); Console.WriteLine("救命救命"); redisClient.Eval(releaseLockScript, lockKey, id); redisClient.Del(lockKey); redisClient.Dispose(); } } } }
案例二(此案例要安装StackExchange.Redis的包) 实现方式(思路大致一样)
using Microsoft.Extensions.Configuration; using StackExchange.Redis; using System; using System.Threading; namespace Redis { public class RedisHelper { #region Fileds private static string _redisConnection; private static int _db = 0; private static ConnectionMultiplexer connection; #endregion #region Constructors public RedisHelper(IConfiguration configuration) { _redisConnection = configuration["RedisConfigHost.Connection"]?.ToString() ?? ""; } public static ConnectionMultiplexer CacheConnection { get { try { if (connection == null || !connection.IsConnected) { connection = new Lazy<ConnectionMultiplexer>(() => ConnectionMultiplexer.Connect(_redisConnection)).Value; } } catch (Exception ex) { return null; } return connection; } } #endregion #region Methons /// <summary> /// 缓存当前数据库 /// </summary> public static IDatabase CacheRedis => CacheConnection.GetDatabase(_db); /// <summary> /// 新增单条值 /// </summary> /// <param name="values"></param> /// <returns></returns> public static bool StringSet(string key, string values) { if (string.IsNullOrEmpty(key) && string.IsNullOrEmpty(values)) throw new AggregateException("values or is null"); return CacheRedis.StringSet(key, values); } /// <summary> /// 查询单个key值 /// </summary> /// <param name="key"></param> /// <returns></returns> public static RedisValue GetStringKey(string key) { return CacheRedis.StringGet(key); } /// <summary> /// 判断key是否存储 /// </summary> /// <param name="key">redis key</param> /// <returns></returns> public bool KeyExists(string key) { return CacheRedis.KeyExists(key); } /// <summary> /// 删除单个key /// </summary> /// <param name="key">redis key</param> /// <returns>是否删除成功</returns> public bool KeyDelete(string key) { return CacheRedis.KeyDelete(key); } /// <summary> /// redis 枷锁 /// </summary> /// <param name="key">需要加锁的锁名</param> /// <param name="expireTimeSeconds">该锁自动到期时间 如果没其他要求可设置为最大时常 该方式一定要手动解锁</param> /// <exception cref="Exception"></exception> #region 分布式锁 public static bool LockByRedis(string key, string values) { try { //expireTimeSeconds = expireTimeSeconds > 20 ? 10 : expireTimeSeconds; //var data = TimeSpan.FromSeconds(expireTimeSeconds); //var token = Environment.MachineName; //bool lockflag = CacheRedis.LockTake(key, Thread.CurrentThread.ManagedThreadId, TimeSpan.FromSeconds(expireTimeSeconds)); bool lockflag = CacheRedis.LockTake(key, values, TimeSpan.MaxValue); if (!lockflag) { return false; } return true; } catch (Exception ex) { throw new Exception($"Redis加锁异常:原因{ex.Message}"); } } /// <summary> /// 解锁 /// </summary> /// <param name="key">需要解锁的锁名</param>
/// <param name="values">需要解锁的值</param>
/// <returns></returns> /// <exception cref="Exception"></exception> public static bool UnLockByRedis(string key, string valuse) { try { // Thread.CurrentThread.ManagedThreadId //Environment.MachineName return CacheRedis.LockRelease(key, valuse); } catch (Exception ex) { throw new Exception($"Redis加锁异常:原因{ex.Message}"); } } #endregion #endregion #region Utilities #endregion } }
下面是调用方式
public static void Alternative() { count++; Console.WriteLine($"进入Alternative进入时间{DateTime.Now}"); var lockKey = "lockKey"; var lockValue = Guid.NewGuid().ToString("N"); bool result = Redis.RedisHelper.LockByRedis(lockKey, lockValue); if (!result) { Console.WriteLine("没有抢到锁,这次请求停止"); return; } string data = Redis.RedisHelper.GetStringKey("Sum").ToString(); int sum = int.Parse(string.IsNullOrEmpty(data) ? "0" : data); Console.WriteLine("读取到的sum是:" + sum.ToString()); if (sum <= 0) { Console.WriteLine("sum 小于0 直接插入"); Redis.RedisHelper.StringSet("Sum", count.ToString()); SendTimed(); } else { Console.WriteLine("sum 大于0 开始判断;当前的count是:" + count.ToString()); if (sum < count) { Console.WriteLine("count大于sum开始插入"); Redis.RedisHelper.StringSet("Sum", count.ToString()); SendTimed(); } } var unlock = Redis.RedisHelper.UnLockByRedis(lockKey, lockValue); Console.WriteLine($"进入Alternative结束时间{DateTime.Now}"); }
示例2
介绍c#操作redis的文章 http://www.cnblogs.com/axel10/p/8459434.html ,这篇文章中的案例使用了StringIncrement来实现了高并发情况下key值的稳定增加,但如果要用锁的方式而不是StringIncrement方法,那该怎么做呢?
LockTake涉及到三个参数:key,token和TimeSpan,分别表示redis数据库中该锁的名称、锁的持有者标识和有效时间。下面将用一个多线程增加key值的案例来演示LockTake/LockRelease的用法。
using StackExchange.Redis; using StackExchange.Redis.Extensions.Core; using StackExchange.Redis.Extensions.Core.Configuration; using StackExchange.Redis.Extensions.Newtonsoft; using System; using System.Threading; namespace RedisTest { class Program { static RedisValue Token = Environment.MachineName; static RedisKey Key = "lock"; static void Ins() { Thread thread = new Thread(() => { for (int i = ; i < ; i++) { if (client.Database.LockTake(Key, Token, TimeSpan.MaxValue)) //key表示的是redis数据库中该锁的名称,不可重复。 Token用来标识谁拥有该锁并用来释放锁。TimeSpan表示该锁的有效时间。 { try { int key = client.Get<int>("key"); client.Add("key", key + ); } catch (Exception e) { Console.WriteLine(e); throw; } finally { client.Database.LockRelease(Key, Token); } } else { Console.WriteLine("locking"); while (!TryAgain()) { Thread.Sleep(); } } } }); thread.Start(); } private static StackExchangeRedisCacheClient client; static void Main(string[] args) { var redisConfiguration = new RedisConfiguration() //配置 { Hosts = new RedisHost[] { new RedisHost() {Host = "127.0.0.1", Port = } } }; client = new StackExchangeRedisCacheClient(new NewtonsoftSerializer(), redisConfiguration); client.Add("key", ); for (int j = ; j < ; j++) { Ins(); } Thread.Sleep(); int i = client.Get<int>("key"); Console.WriteLine(i); Console.ReadKey(); } static bool TryAgain(int value) { if (client.Database.LockTake(Key, Token, TimeSpan.MaxValue)) { try { int key = client.Get<int>("key"); client.Add("key", key + value); } catch (Exception e) { Console.WriteLine(e); throw; } finally { client.Database.LockRelease(Key, Token); } return true; } else { return false; } } } }
案例三 实现方式
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; } }
- 参数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); } } }); }
单机只能用多线模拟使用分布式锁了
此锁已经可以满足大多数场景了,若有不妥,还请多多指出,以免误别人!
()
借鉴:https://cloud.tencent.com/developer/article/1742360
借鉴:https://www.cnblogs.com/simoncai/p/11477177.html
借鉴:https://www.cnblogs.com/runningsmallguo/p/10322315.html