Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Redis学习系列七分布式锁

Redis学习系列七分布式锁

作者头像
郑小超.
发布于 2019-01-03 08:35:37
发布于 2019-01-03 08:35:37
98900
代码可运行
举报
文章被收录于专栏:GreenLeavesGreenLeaves
运行总次数:0
代码可运行

一、简介

熟悉.Net多线程的都知道,当多个线程同时操作一个全局缓存对象(static对象实例、Dictionary、List等)时,会存在多线程争用问题,包括EF、Dapper等本身的缓存机制,都存在多线程争用问题,当我们在享受多线程带来的好处的同时,千万要注意这个问题.如果不了解多线程,请移步到我的C#多线程分类下.但是实际的业务场景中经常存在需要根据每个缓存对象的状态,进行一系列判断之后,在进行修改的操作,但是这个操作必须保证有序性,不能多个线程同时去读,否则就乱套了.比如你要进行一个数据库表字段的递增操作,首先可能时先去把最后一条记录读出来,然后拿到对应的字段,然后更新回数据库,但是这个时候如果在多线程环境下,多个线程可能同时去读,如果用了EF、Dapeer等ORM,它们会把数据读到缓存中,这个时候多个线程拿到了相同的数据,然后同步+1操作,那么这个时候如果有三个线程,那么只会进行一次+1操作,而不是三次.

Redis也是如此,所以这个时候就需要使用Redis的分布式锁,来限制这个行为,如果你用了他的异步Api,我前面的随笔用的都是异步的.

二、分布式锁实战

哇,踩坑踩了还久,终于明白了StackExchange.Redis怎么处理分布式锁,我刚开始使用Api时,是这么认为的.当一个线程使用Redis的数据时(该数据已加上了分布式锁),另外一个线程则不能操作这个数据,会等待前面的锁释放(这个过程Redis帮助我们完成),但是事实证明我太年轻了.Redis还是Redis,即使时分布式锁,也是一种缓存数据(这一点和C#完全不一样,所以需要注意).为什么会这样呢?看下面的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    class Program
    {
        static Program()
        {
            //链式配置Redis
            AppConfiguration.Current.ConfigureRedis<RedisConfig>();
        }

        static void Main(string[] args)
        {
            StringSetGetAsync();
            Console.ReadKey();
        }

        static async void StringSetGetAsync()
        {
            //需要锁的Redis数据的键
            var key = "用户信息Id";

            //先异步写入一个
            var kv=new KeyValuePair<RedisKey, RedisValue>(key, 1);
            await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { kv });
             
            //对用户的信息进行加锁操作
            var time = 100000;
            //请求Id,这里最好能区分客户端的调用标识,方便异常时定位错误
            var requestId = "锁的键";
            var lockResult = await RedisClient.LockTakeAsync(requestId, key, TimeSpan.FromMilliseconds(time));
            if (lockResult)
            {
                Console.WriteLine("对用户信息加锁成功,请求的锁Id为:{0},锁的时间周期为:{1}毫秒", key, TimeSpan.FromMilliseconds(time));
            }
            else
            {
                Console.WriteLine("加锁失败,key为{0}的数据结构已被其它请求占用!", key);
            }

            //这里开启一个新的线程去访问Redis,先查,在修改上面的用户数据,这里如果Redis帮我们判断的话,那我们是读不出数据的,而且不能修改该数据的
            //因为上面的线程已经对当前用户数据加锁了
            var result=await RedisClient.StringGetAsync(key);
            Console.WriteLine("成功查到了数据,值为:{0}", result);
            var newKv = new KeyValuePair<RedisKey, RedisValue>(key, 2);
            if (await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { newKv }))
            {
                var newResult = await RedisClient.StringGetAsync(new RedisKey[] { key });
                Console.WriteLine("数据修改成功,修改后的数据为:{0}",newResult[0]);
            }
        }
    }

look,Redis并没有帮助我们做了这个事情,他还是让第二个线程去修改了用户的数据.但是,打开Redis桌面管理工具,如下信息:

多了一条数据,里面记录了锁的相关信息,现在我终于明白了,这个事情还是得自己来,他不会帮你做,他只是个缓存,所以修改代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    class Program
    {
        static Program()
        {
            //链式配置Redis
            AppConfiguration.Current.ConfigureRedis<RedisConfig>();
        }

        static void Main(string[] args)
        {
            StringSetGetAsync();
            Console.ReadKey();
        }

        static async void StringSetGetAsync()
        {
            //需要锁的Redis数据的键
            var key = "用户信息Id";

            //先异步写入一个
            var kv=new KeyValuePair<RedisKey, RedisValue>(key, 1);
            await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { kv });
             
            //对用户的信息进行加锁操作
            var time = 100000;
            //请求Id,这里最好能区分客户端的调用标识,方便异常时定位错误
            var requestId = "锁的键";
            var lockResult = await RedisClient.LockTakeAsync(requestId, key, TimeSpan.FromMilliseconds(time));
            if (lockResult)
            {
                Console.WriteLine("对用户信息加锁成功,请求的锁Id为:{0},锁的时间周期为:{1}毫秒", key, TimeSpan.FromMilliseconds(time));
            }
            else
            {
                Console.WriteLine("加锁失败,key为{0}的数据结构已被其它请求占用!", key);
            }

            //修改前判断当前用户数据有没有被加锁
            var userInfoLockObj = (int)(await RedisClient.LockQueryAsync(key));
            if (userInfoLockObj >= 1)
            {
                Console.WriteLine("当前用户键为:{0}的数据被别的线程(或者进程占用了),无法进行操作,请等待锁释放!");
            }
            else
            {
                var result = await RedisClient.StringGetAsync(key);
                Console.WriteLine("成功查到了数据,值为:{0}", result);
                var newKv = new KeyValuePair<RedisKey, RedisValue>(key, 2);
                if (await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { newKv }))
                {
                    var newResult = await RedisClient.StringGetAsync(new RedisKey[] { key });
                    Console.WriteLine("数据修改成功,修改后的数据为:{0}", newResult[0]);
                }
            }
            
        }
    }

操作前,自己去查当前用户信息有没有被加锁,Redis会把锁的数据写入缓存,并在过期时间到了时,自动回收对应锁的内存,这样当下个线程进来时,查不到锁,那么就可以操作这个数据了.这里我选择轮询判断,当然也可以使用队列,将请求插入到队列中,开启一个线程去消费这个队列,轮询代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    class Program
    {
        static Program()
        {
            //链式配置Redis
            AppConfiguration.Current.ConfigureRedis<RedisConfig>();
        }

        static void Main(string[] args)
        {
            StringSetGetAsync();
            Console.ReadKey();
        }

        static async void StringSetGetAsync()
        {
            //需要锁的Redis数据的键
            var key = "用户信息Id";

            //先异步写入一个
            var kv = new KeyValuePair<RedisKey, RedisValue>(key, 1);
            await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { kv });

            //对用户的信息进行加锁操作
            var time = 10000;
            //请求Id,这里最好能区分客户端的调用标识,方便异常时定位错误
            var requestId = "锁的键";
            var lockResult = await RedisClient.LockTakeAsync(requestId, key, TimeSpan.FromMilliseconds(time));
            if (lockResult)
            {
                Console.WriteLine("对用户信息加锁成功,请求的锁Id为:{0},锁的时间周期为:{1}毫秒", key, TimeSpan.FromMilliseconds(time));
            }
            else
            {
                Console.WriteLine("加锁失败,key为{0}的数据结构已被其它请求占用!", key);
            }

            //轮询判断锁是否被释放,释放就修改数据
            while (true)
            {
                var userInfoLockObj =await RedisClient.LockQueryAsync(requestId);
                if (userInfoLockObj== key)
                {
                    //如果加锁,休息一秒后重试
                    Thread.Sleep(1000);
                    Console.WriteLine("当前用户被加锁了,不能修改数据!");
                }
                else
                {
                    //锁被释放了,这个时候可以操作数据了
                    var result = await RedisClient.StringGetAsync(key);
                    Console.WriteLine("成功查到了数据,值为:{0}", result);
                    var newKv = new KeyValuePair<RedisKey, RedisValue>(key, 2);
                    if (await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { newKv }))
                    {
                        var newResult = await RedisClient.StringGetAsync(new RedisKey[] { key });
                        Console.WriteLine("数据修改成功,修改后的数据为:{0}", newResult[0]);
                    }
                    break;
                }
              
            }
        }
    }

10秒后,操作数据成功!当然队列更加友好.不阻塞线程!这个例子举得也不是很好,大多数情况下,锁不会被一个线程占用这么久,一般用完就被释放,1秒已经很长了.

注:这个过程不会存在死锁的问题(除非Redis内部的设置过期的进程挂了),因为现在这个版本的Redis支持setnc和expire一起执行,属于原子指令,即在设置锁的同时设置过期时间.这个过程是同步的,据我所知老版本的Redis可能这两个指令时分开的,可能会存在"长生不老"的锁.

三、分布式锁超时问题

如果你理解上面的内容,就会发现分布式锁,并不能解决超时问题,感觉这一点和C#自带的Timer类的问题很像,线程不会等待你执行完毕,在开启第二轮的轮询任务,线程不会等你.Timer中我提供了解决方案,Redis也存在相同的问题,但是两者的解决方案不一样,Timer是通过回调的方式,当第一轮循环任务做完,在重启Timer,执行第二轮任务.

而Redis则需要通过守护线程的方式去做,代码如下:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-12-29 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Redis学习系列二之.Net开发环境搭建及基础数据结构String字符串
Redis有5种基本数据结构,分别是string、list(列表)、hash(字典)、set(集合)、zset(有序集合),这是必须掌握的5种基本数据结构.注意Redis作为一个键值对缓存系统,其所有的数据结构,都以唯一的key(字符串)作为名称,然后通过key来获取对应的数据.
郑小超.
2018/12/28
6750
Redis学习系列六ZSet(有序列表)及Redis数据结构的过期
ZSet可以说是Redis中最有趣的数据结构了,因为他兼具了Hash集合和Set的双重特性,也是用的最多的,保证了value值的唯一性的同时,,同时又保证了高性能,最主要的是还可以给每个Value设置Source(权重),那么我们就可以通过权重进行排序,这在业务上是非常常见的,比如很多地方需要,比如我们需要对所有用户的数学成绩进行排序.对英语等等地例子比比皆是,那么通过ZSet,你将会得到一个响应速度非常快的过程.下面会介绍.
郑小超.
2018/12/28
1.9K0
Redis学习系列四Hash(字典)
Redis中的Hash字典相当于C#中的Hashtable,是一种无序字典,内存存储了很对的键值对,实现上和Hashtable一样,都是"数组+链表"二维结构,都是对关键字(键值)进行散列操作,讲关键字散列到Hashtable中的某一个槽位中去,这个过程中如果发生了碰撞,散列函数可能将不同的关键字散列到Hashtable中的同一个槽位中去,通过"链表的方式"进行连接。
郑小超.
2018/12/28
6410
C# Redis分布式锁 - 单节点
我们平时用的锁,比如 lock,它是线程锁,主要用来给方法,代码块加锁.由于进程的内存单元是被其所有线程共享的,所以线程锁控制的实际是多个线程对同一块内存区域的访问.
梁规晓
2020/11/05
1.3K0
C# Redis分布式锁 - 单节点
在 .NET Core中如何使用 Redis 创建分布式锁
在分布式系统中,多个服务实例可能会同时访问共享资源。为了避免数据不一致或其他并发问题,需要使用分布式锁来保证同一时间只有一个服务实例能够访问该资源。Redis 是一个高性能的键值存储数据库,由于其单线程的特性,非常适合用来实现分布式锁。本文将详细介绍在 .NET Core 中如何使用 Redis 创建分布式锁。
郑子铭
2025/03/20
2550
在 .NET Core中如何使用 Redis 创建分布式锁
使用Redis创建分布式锁
当我们构建分布式系统时,我们将面临多个进程一起处理共享资源,由于其中只有一个可以一次使用共享资源,因此会导致一些意外问题!
leon公众号精选
2022/04/27
4850
使用Redis创建分布式锁
【愚公系列】2023年01月 .NET CORE工具案例-RedLock.net实现分布式锁
在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。
愚公搬代码
2023/01/03
6340
【愚公系列】2023年01月 .NET CORE工具案例-RedLock.net实现分布式锁
Redis分布式锁抽丝剥茧
最近首度应用"分布式锁",现在想想,分布式锁不是孤立的技能点,这其实就是跨主机的线程同步。
有态度的马甲
2021/07/12
3840
Redis学习系列三List列表
Redis中的列表相当于C#中的LinkedList,也就是链表,如果你研究过链表这个数据结构,肯定知道.它的插入和删除是非常快的,但是定位却很慢,因为必须遍历所有的元素,才能找到对应的值,所以当你需要对列表进行统计的时候,建立跑后台服务去做,而不是使用Redis去遍历,因为开销很大.
郑小超.
2018/12/28
7030
Redis学习系列五Set(集合)
Redis中的Set(集合)相当于C#中的HashSet,它内部的键值对时无序的、唯一的。用过Dictionary的都知道,Dictionary都知道,里面的每个键值对肯定是唯一的,因为键不允许重复.而Redis中的Set相当于一个特殊的字典,字典中所有的Value值都是null.当集合中最后一个元素被移除,该集合的内存会被回收.
郑小超.
2018/12/28
7180
StackExchange.Redis通用封装类分享
首先是 ConnectionMultiplexer 的封装,ConnectionMultiplexer对象是StackExchange.Redis最中枢的对象。这个类的实例需要被整个应用程序域共享和重用的,所以不需要在每个操作中不停的创建该对象的实例,一般都是使用单例来创建和存放这个对象,这个在官网上也有说明。
yaphetsfang
2020/07/30
1.4K0
StackExchange.Redis通用封装类分享
分布式锁原理与实现
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huyuyang6688/article/details/89526572
DannyHoo
2019/04/27
7860
分布式锁的实现(redis)
1、单机锁 考虑在并发场景并且存在竞态的状况下,我们就要实现同步机制了,最简单的同步机制就是加锁。 加锁可以帮我们锁住资源,如内存中的变量,或者锁住临界区(线程中的一段代码),使得同一个时刻只有一个线程能访问某一个区域。 如果是单实例(单进程部署),那么单机锁就可以满足我们的要求了,如synchronized,ReentrantLock。 因为在一个进程中的不同线程可以共享这个锁。 2、分布式锁 但是如果场景来到了分布式系统呢? 分布式系统部署在不同的机器上,或者只是简单的多进程部署。这样各个进程之间无法共
用户1225216
2018/03/05
1.6K0
.NET开源分布式锁DistributedLock
更多的线程同步锁,可以看这篇文章:cnblogs.com/Z7TS/p/16463494.html
郑子铭
2023/08/30
5210
.NET开源分布式锁DistributedLock
【愚公系列】2022年11月 .NET CORE工具案例-StackExchange.Redis代码变量方式实现商品秒杀
文章目录 前言 一、StackExchange.Redis执行Lua脚本实现商品秒杀 1.StackExchange.Redis封装 2.秒杀代码 3.效果 ---- 前言 下面是Redis分布式锁常用的概念说明:设置、获取、过期时间、删除。 1、 Setnx 命令:SETNX key value 说明:将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写
愚公搬代码
2022/11/28
4820
【愚公系列】2022年11月 .NET CORE工具案例-StackExchange.Redis代码变量方式实现商品秒杀
redis分布式锁的应用场景_redis为什么可以做分布式锁
“分布式锁”是用来解决分布式应用中“并发冲突”的一种常用手段,实现方式一般有基于zookeeper及基于redis二种。
全栈程序员站长
2022/10/04
2990
redis分布式锁的应用场景_redis为什么可以做分布式锁
redis实现分布式锁的原理_Redis作为分布式锁原理
现在面试,一般都会聊聊分布式系统这块的东西。通常面试官都会从服务框架(Spring Cloud、Dubbo)聊起,一路聊到分布式事务、分布式锁、ZooKeeper等知识。
全栈程序员站长
2022/11/17
1K0
redis实现分布式锁的原理_Redis作为分布式锁原理
基于redis分布式锁实现“秒杀”
最近在项目中遇到了类似“秒杀”的业务场景,在本篇博客中,我将用一个非常简单的demo,阐述实现所谓“秒杀”的基本思路。
Java高级架构
2019/01/02
9000
【愚公系列】2022年11月 .NET CORE工具案例-CSRedis执行Lua脚本实现商品秒杀
文章目录 前言 一、CSRedis执行Lua脚本实现商品秒杀 1.单线程模拟多线程进行秒杀 2.多线程进行秒杀 ---- 前言 下面是Redis分布式锁常用的概念说明:设置、获取、过期时间、删除。 1、 Setnx 命令:SETNX key value 说明:将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。 时间复杂度:O(1) 返回值:设置成功,返回
愚公搬代码
2022/11/20
5640
【愚公系列】2022年11月 .NET CORE工具案例-CSRedis执行Lua脚本实现商品秒杀
手撕redis分布式锁,隔壁张小帅都看懂了!
看到标题,有人可能会满心疑惑?张小帅是谁?咳咳,老猫在此先卖个关子,关于张小帅,老猫后续会向大家正式介绍的,标题算是彩蛋了。
程序员老猫
2021/01/12
4850
手撕redis分布式锁,隔壁张小帅都看懂了!
推荐阅读
相关推荐
Redis学习系列二之.Net开发环境搭建及基础数据结构String字符串
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验