Redis学习系列七分布式锁
一、简介
熟悉.Net多线程的都知道,当多个线程同时操作一个全局缓存对象(static对象实例、Dictionary、List等)时,会存在多线程争用问题,包括EF、Dapper等本身的缓存机制,都存在多线程争用问题,当我们在享受多线程带来的好处的同时,千万要注意这个问题.如果不了解多线程,请移步到我的C#多线程分类下.但是实际的业务场景中经常存在需要根据每个缓存对象的状态,进行一系列判断之后,在进行修改的操作,但是这个操作必须保证有序性,不能多个线程同时去读,否则就乱套了.比如你要进行一个数据库表字段的递增操作,首先可能时先去把最后一条记录读出来,然后拿到对应的字段,然后更新回数据库,但是这个时候如果在多线程环境下,多个线程可能同时去读,如果用了EF、Dapeer等ORM,它们会把数据读到缓存中,这个时候多个线程拿到了相同的数据,然后同步+1操作,那么这个时候如果有三个线程,那么只会进行一次+1操作,而不是三次.
Redis也是如此,所以这个时候就需要使用Redis的分布式锁,来限制这个行为,如果你用了他的异步Api,我前面的随笔用的都是异步的.
二、分布式锁实战
哇,踩坑踩了还久,终于明白了StackExchange.Redis怎么处理分布式锁,我刚开始使用Api时,是这么认为的.当一个线程使用Redis的数据时(该数据已加上了分布式锁),另外一个线程则不能操作这个数据,会等待前面的锁释放(这个过程Redis帮助我们完成),但是事实证明我太年轻了.Redis还是Redis,即使时分布式锁,也是一种缓存数据(这一点和C#完全不一样,所以需要注意).为什么会这样呢?看下面的代码:
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, );
await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { kv }); //对用户的信息进行加锁操作
var time = ;
//请求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, );
if (await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { newKv }))
{
var newResult = await RedisClient.StringGetAsync(new RedisKey[] { key });
Console.WriteLine("数据修改成功,修改后的数据为:{0}",newResult[]);
}
}
}

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

多了一条数据,里面记录了锁的相关信息,现在我终于明白了,这个事情还是得自己来,他不会帮你做,他只是个缓存,所以修改代码如下:
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, );
await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { kv }); //对用户的信息进行加锁操作
var time = ;
//请求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 >= )
{
Console.WriteLine("当前用户键为:{0}的数据被别的线程(或者进程占用了),无法进行操作,请等待锁释放!");
}
else
{
var result = await RedisClient.StringGetAsync(key);
Console.WriteLine("成功查到了数据,值为:{0}", result);
var newKv = new KeyValuePair<RedisKey, RedisValue>(key, );
if (await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { newKv }))
{
var newResult = await RedisClient.StringGetAsync(new RedisKey[] { key });
Console.WriteLine("数据修改成功,修改后的数据为:{0}", newResult[]);
}
} }
}

操作前,自己去查当前用户信息有没有被加锁,Redis会把锁的数据写入缓存,并在过期时间到了时,自动回收对应锁的内存,这样当下个线程进来时,查不到锁,那么就可以操作这个数据了.这里我选择轮询判断,当然也可以使用队列,将请求插入到队列中,开启一个线程去消费这个队列,轮询代码如下:
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, );
await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { kv }); //对用户的信息进行加锁操作
var time = ;
//请求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();
Console.WriteLine("当前用户被加锁了,不能修改数据!");
}
else
{
//锁被释放了,这个时候可以操作数据了
var result = await RedisClient.StringGetAsync(key);
Console.WriteLine("成功查到了数据,值为:{0}", result);
var newKv = new KeyValuePair<RedisKey, RedisValue>(key, );
if (await RedisClient.StringSetAsync(new KeyValuePair<RedisKey, RedisValue>[] { newKv }))
{
var newResult = await RedisClient.StringGetAsync(new RedisKey[] { key });
Console.WriteLine("数据修改成功,修改后的数据为:{0}", newResult[]);
}
break;
} }
}
}

10秒后,操作数据成功!当然队列更加友好.不阻塞线程!这个例子举得也不是很好,大多数情况下,锁不会被一个线程占用这么久,一般用完就被释放,1秒已经很长了.
注:这个过程不会存在死锁的问题(除非Redis内部的设置过期的进程挂了),因为现在这个版本的Redis支持setnc和expire一起执行,属于原子指令,即在设置锁的同时设置过期时间.这个过程是同步的,据我所知老版本的Redis可能这两个指令时分开的,可能会存在"长生不老"的锁.
三、分布式锁超时问题
如果你理解上面的内容,就会发现分布式锁,并不能解决超时问题,感觉这一点和C#自带的Timer类的问题很像,线程不会等待你执行完毕,在开启第二轮的轮询任务,线程不会等你.Timer中我提供了解决方案,Redis也存在相同的问题,但是两者的解决方案不一样,Timer是通过回调的方式,当第一轮循环任务做完,在重启Timer,执行第二轮任务.
而Redis则需要通过守护线程的方式去做,代码如下:
Redis学习系列七分布式锁的更多相关文章
- 分布式缓存技术redis学习系列(五)——redis实战(redis与spring整合,分布式锁实现)
本文是redis学习系列的第五篇,点击下面链接可回看系列文章 <redis简介以及linux上的安装> <详细讲解redis数据结构(内存模型)以及常用命令> <redi ...
- 分布式缓存技术redis学习系列
分布式缓存技术redis学习系列(一)--redis简介以及linux上的安装以及操作redis问题整理 分布式缓存技术redis学习系列(二)--详细讲解redis数据结构(内存模型)以及常用命令 ...
- 分布式缓存技术redis学习系列(四)——redis高级应用(集群搭建、集群分区原理、集群操作)
本文是redis学习系列的第四篇,前面我们学习了redis的数据结构和一些高级特性,点击下面链接可回看 <详细讲解redis数据结构(内存模型)以及常用命令> <redis高级应用( ...
- redis系列:分布式锁
redis系列:分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...
- Redis整合Spring实现分布式锁
spring把专门的数据操作独立封装在spring-data系列中,spring-data-redis是对Redis的封装 <dependencies> <!-- 添加spring- ...
- Python操作redis学习系列之(集合)set,redis set详解 (六)
# -*- coding: utf-8 -*- import redis r = redis.Redis(host=") 1. Sadd 命令将一个或多个成员元素加入到集合中,已经存在于集合 ...
- 在 Redis 上实现的分布式锁
由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客.这一个月里面接触到很多新知识,同时也遇到很多技术上的难点,在这我将对每一个有用 ...
- 【连载】redis库存操作,分布式锁的四种实现方式[一]--基于zookeeper实现分布式锁
一.背景 在电商系统中,库存的概念一定是有的,例如配一些商品的库存,做商品秒杀活动等,而由于库存操作频繁且要求原子性操作,所以绝大多数电商系统都用Redis来实现库存的加减,最近公司项目做架构升级,以 ...
- Redis、Zookeeper实现分布式锁——原理与实践
Redis与分布式锁的问题已经是老生常谈了,本文尝试总结一些Redis.Zookeeper实现分布式锁的常用方案,并提供一些比较好的实践思路(基于Java).不足之处,欢迎探讨. Redis分布式锁 ...
随机推荐
- POJ-3268.SilverCowParty.(最短路 + 图的转置)
本题思路:对原图和原图的逆图分别用一次最短路,找出最大值即可. 一开始是我是对每个顶点spfa搜了一波,结果判题时间巨长,还好这个题的数据量不是很大,所以就用了另一种思路. 参考代码:spfa单结点爆 ...
- Js学习(6) 标准库-Array对象
Array是Js的原生对象,同时也是一个构造函数,可以用它生成新的数组 用不用new结果都一样 var arr = new Array(2); // 等同于 var arr = Array(2); 但 ...
- Spring源码-循环依赖源码解读
Spring源码-循环依赖源码解读 笔者最近无论是看书还是从网上找资料,都没发现对Spring源码是怎么解决循环依赖这一问题的详解,大家都是解释了Spring解决循环依赖的想法(有的解释也不准确,在& ...
- CSS的六大选择器
选择器:选择器是一种模式,用于选择需要添加样式的元素. 首先简述六大选择器 基本选择器 标签选择器 类选择器 ID选择器 高级选择器 层次选择器 结构伪类选择器 属性选择器 其中基本选择器与层次选择器 ...
- Android app中存储文件的路径
// 获得缓存文件路径,磁盘空间不足或清除缓存时数据会被删掉,一般存放一些临时文件 // /data/data/<application package>/cache目录 File cac ...
- iframe和选项卡面板切换
<div class="col-sm-6"> <div class="tabs-container"> <ul class=&qu ...
- 11.5 vmstat:虚拟内存统计
vmstat vmstat是Virtual Memory Statistics(虚拟内存统计)的缩写,利用vmstat命令可以对操作系统的内存信息.进程状态和CPU活动等进行监视.但是只能对系统的整体 ...
- 软件推荐-有限元开发软件FELAC
首页:http://yuanjisuan.cn/ 有限元语言及其编译器(Finite Element Language And it’s Compiler),以下简称FELAC是中国科学院数学与系统科 ...
- php循环
while 例子: /* example 1 */ $a = 0; while (true) { $a++; echo $a.'<br>'; if($a >= 10){ break; ...
- MySQL优化--NOT EXISTS和LEFT JOIN方式差异
在MySQL中,我们可以将NOT EXISTS语句转换为LEFT JOIN语句来进行优化,哪为什么会有性能提升呢? 使用NOT EXISTS方式SQL为: ) FROM t_monitor m WHE ...