.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

posted on 2021-11-19 11:58  白码一号  阅读(3165)  评论(0编辑  收藏  举报