Redis系列文章总结:ASP.Net Core 中如何借助CSRedis实现一个安全高效的分布式锁

引言:最近回头看了看开发的.Net Core 2.1项目的复盘总结,其中在多处用到Redis实现的分布式锁,虽然在OnResultExecuting方法中作了防止死锁的处理,但在某些场景下仍是会发生死锁的问题,下面我只展现部分代码:redis

 

问题:服务器

(1)这里setnx设置的值“1”,我想问,你最后del的这个值必定是你本身建立的吗?数据结构

(2)图中标注的步骤1和步骤2不是原子操做,会有死锁的几率吗?多线程

你们能够思考一下先,下面让咱们带着这两个问题往下看,下面介绍一下使用Redis实现分布式锁经常使用的几个命令。分布式

 

1、使用Redis实现分布式锁常见的几个命令

► Setnxurl

命令:SETNX key value
说明:将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不作任何动做。SETNX 是『SET if Not eXists』(若是不存在,则 SET)的简写。
时间复杂度:O(1)
返回值:设置成功,返回1 ; 设置失败,返回 0spa

► Getset线程

命令:GETSET key value
说明:将给定 key 的值设为 value ,并返回 key 的旧值(old value)。当 key 存在但不是字符串类型时,返回一个错误。
时间复杂度:O(1)
返回值:返回给定 key 的旧值; 当 key 没有旧值时,也便是, key 不存在时,返回 nil 。调试


► Expirecode

命令: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 为以上数据结构内的元素数量。

返回值:被删除 key 的数量。

好了,命令熟悉以后,下面咱们就开始一步一步实现分布式锁。

 

2、使用Redis实现分布式锁版本一:与时间戳的结合

对于上面的setnx设置的默认值1,咱们采用时间戳来防止问题一,下面先让咱们来看下想固然写法流程图。

流程图:

C#代码实现:

static void Main(string[] args)
        {
            var lockTimeout = 5000;//单位是毫秒
            var currentTime = DateTime.Now.ToUnixTime(true);
            if (SetNx("lockkey", currentTime+ lockTimeout,lockTimeout))
            {
                //TODO:一些业务逻辑代码
                //.....
                //.....
                //最后释放锁
                Remove("lockkey");
            }
            else
            {
                Console.WriteLine("没有得到分布式锁");
            }
            Console.ReadKey();
        }

        public static bool SetNx(string key,long time ,double expireMS)
        {
            if (redisClient.SetNx(key, time))
            {
                if (expireMS > 0)
                    redisClient.Expire(key, TimeSpan.FromMilliseconds(expireMS));
                return true;
            }
            return false;
        }

        public static bool Remove(string key)
        {
            return redisClient.Del(key) > 0;
        }

上面的代码中value的值咱们使用时间戳,不是一个固定的值了,至少能保证你删除的key确实是你本身的,因此,建议你们在设value的值时,不要设置一个固定的值,最好是随机的。可是这样写虽然解决了问题一,可是这种写法仍是存在必定的风险,虽然Redis是单线程的而且setnx、expire是原子操做,可是先setnx再expire就不是原子操做了!!!咱们要考虑多线程环境和容器部署时多实例环境等等,那这样的写法就会出现问题。

好比:如今有A、B两台服务器在跑这个应用,当A台应用跑到:setnx成功可是尚未设置过时时间的时候,忽然重启服务,这个时候在分布式环境中就会发生死锁的问题,由于你没有设置过时时间。

下面咱们经过调试来展现死锁的场景:

A应用:在执行到setnx成功可是在执行expire以前宕机了,此时的Redis已经有数据了,可是没有过时时间

 

B应用:运行正常

可是B应用就会一直获取不到锁,致使死锁。

因此上面在获取锁的逻辑仍是有问题的,为了解决这个问题,咱们采用下面的方式来处理。

 

3、使用Redis实现分布式锁版本二:双重防死锁

 

流程图:

C#代码实现:

public static void RedisLockV2()
        {
            var lockTimeout = 5000;//单位是毫秒
            var currentTime = DateTime.Now.ToUnixTime(true);

            if (SetNxV2("lockkey",DateTime.Now.ToUnixTime(true)+lockTimeout))
            {
                //设置过时时间
                redisClient.Expire("lockkey", TimeSpan.FromMilliseconds(5000));
                //TODO:一些业务逻辑代码
                
                Console.WriteLine("处理业务ing");
                Thread.Sleep(100000);

                Console.WriteLine("处理业务ed");
                //最后释放锁
                Remove("lockkey");
            }
            else
            {
                //未获取到锁,继续判断,判断时间戳看看是否能够重置并获取锁
                var lockValue = redisClient.Get("lockkey");
                var time = DateTime.Now.ToUnixTime(true);

                if (!string.IsNullOrEmpty(lockValue) &&  time> lockValue.ToInt64())
                {
                    //再次用当前时间戳getset
                    //返回固定key的旧值,旧值判断是否能够获取锁
                    var getsetResult = redisClient.GetSet("lockkey", time);
                    if (getsetResult == null || (getsetResult != null && getsetResult == lockValue))
                    {
                        Console.WriteLine("获取到Redis锁了");
                        //真正获取到锁
                        redisClient.Expire("lockkey", TimeSpan.FromMilliseconds(5000));
                        //TODO:一些业务逻辑代码
                        //.....
                        //.....
                        Console.WriteLine("处理业务");
                        //最后释放锁
                        Remove("lockkey");
                    }
                    else
                    {
                        Console.WriteLine("没有获取到锁");
                    }

                }
                else
                {
                    Console.WriteLine("没有获取到锁");
                }
            }
            
        }

 

 如今,Redis中的状况以下:

 

咱们运行上面的代码,结果以下:

 副本.exe中添加一行代码。来模拟这种场景:有A、B两台服务器在跑这个应用,当A台应用跑到:setnx成功可是尚未设置过时时间的时候,忽然重启服务,这个时候在分布式环境中就会发生死锁的问题,由于你没有设置过时时间

咱们先执行Lottery.ThriftRpc - 副本.exe,等Redis里面有值了,而且这个key是没有过时时间,再关闭掉该程序:

而后,再执行Lottery.ThriftRpc.exe

 

看,咱们是否是解决了该问题,至于过时时间设置为多少要结合你的具体业务处理时间来计算出一个合理的值,好了,聊到这里关于Redis的分布式锁就讲完了,但愿对你有帮助,谢谢。

 

4、总结:

 

 上面的示例中Redis的组件用的是CSRedisCore,这里只是本身的一点体会,若是你有更好的办法,能够在评论区讨论,关于Redis的理论讲解有太多的文章了,你们能够参考,关于Redis的文章我只总结工做中遇到的一些问题,关于文章中的源码,我就不提供了,太简单了。后面我会不按期分享一些Redis的问题,但愿你们多多支持。

 

 

做者:郭峥

出处:http://www.cnblogs.com/runningsmallguo/

本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文连接。

相关文章
相关标签/搜索