一、简介

熟悉.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学习系列七分布式锁的更多相关文章

  1. 分布式缓存技术redis学习系列(五)——redis实战(redis与spring整合,分布式锁实现)

    本文是redis学习系列的第五篇,点击下面链接可回看系列文章 <redis简介以及linux上的安装> <详细讲解redis数据结构(内存模型)以及常用命令> <redi ...

  2. 分布式缓存技术redis学习系列

    分布式缓存技术redis学习系列(一)--redis简介以及linux上的安装以及操作redis问题整理 分布式缓存技术redis学习系列(二)--详细讲解redis数据结构(内存模型)以及常用命令 ...

  3. 分布式缓存技术redis学习系列(四)——redis高级应用(集群搭建、集群分区原理、集群操作)

    本文是redis学习系列的第四篇,前面我们学习了redis的数据结构和一些高级特性,点击下面链接可回看 <详细讲解redis数据结构(内存模型)以及常用命令> <redis高级应用( ...

  4. redis系列:分布式锁

    redis系列:分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

  5. Redis整合Spring实现分布式锁

    spring把专门的数据操作独立封装在spring-data系列中,spring-data-redis是对Redis的封装 <dependencies> <!-- 添加spring- ...

  6. Python操作redis学习系列之(集合)set,redis set详解 (六)

    # -*- coding: utf-8 -*- import redis r = redis.Redis(host=") 1. Sadd 命令将一个或多个成员元素加入到集合中,已经存在于集合 ...

  7. 在 Redis 上实现的分布式锁

    由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客.这一个月里面接触到很多新知识,同时也遇到很多技术上的难点,在这我将对每一个有用 ...

  8. 【连载】redis库存操作,分布式锁的四种实现方式[一]--基于zookeeper实现分布式锁

    一.背景 在电商系统中,库存的概念一定是有的,例如配一些商品的库存,做商品秒杀活动等,而由于库存操作频繁且要求原子性操作,所以绝大多数电商系统都用Redis来实现库存的加减,最近公司项目做架构升级,以 ...

  9. Redis、Zookeeper实现分布式锁——原理与实践

    Redis与分布式锁的问题已经是老生常谈了,本文尝试总结一些Redis.Zookeeper实现分布式锁的常用方案,并提供一些比较好的实践思路(基于Java).不足之处,欢迎探讨. Redis分布式锁 ...

随机推荐

  1. 转:Loadrunner添加服务器监控

    一.监控windows系统:1.监视连接前的准备        1)进入被监视windows系统,开启以下二个服务Remote Procedure Call(RPC) 和Remote Registry ...

  2. PCIe 驱动流程(LTSSM)

     本次的工作是完成刚流片的FPGA中PCIe IP核的bring up,也就是芯片的中PCIe的第一个使用者,将PCIe IP核正常使用起来,并配合公司的EDA团队,完成PCIe IP核到用户的呈现. ...

  3. 激活prompt

    1.下载SQLPrompt 2. 断网, 打开注册机,拷贝验证码 2. 点击activate, 拷贝代码

  4. freemaker超详细 讲解 配置

    一.FreeMarker简介 二.第一个FreeMark示例 2.1.新建一个Maven项目 2.2.添加依赖 2.3.添加存放模板的文件夹 2.4.添加模板 2.5.解析模板 2.6.运行结果 三. ...

  5. Tomcat9.0.13 Bug引发的java.io.IOException:(打开的文件过多 Too many open files)导致服务假死

    问题背景: 笔者所在的项目组最近把生产环境Tomcat迁移到Linux,算是顺利运行了一段时间,最近一个低概率密度的(too many open files)问题导致服务假死并停止响应客户端客户端请求 ...

  6. redis 3.2.1 centos7

    设置外网访问 vmare:centos7redis:3.2.1 redis-cli shutdown 重启./redis-server 启动服务 启动之后无法访问 cd redis-3.2.1 vim ...

  7. javascript的异步编程

    同步与异步 介绍异步之前,回顾一下,所谓同步编程,就是计算机一行一行按顺序依次执行代码,当前代码任务耗时执行会阻塞后续代码的执行. 同步编程,即是一种典型的请求-响应模型,当请求调用一个函数或方法后, ...

  8. spring web参数传递

    spring boot 参数相关 ******************************************   @RequestParam 这个注解用来绑定单个请求数据,既可以是url中的 ...

  9. delphi 中record 的类操作符重载简介

    今天简单介绍一下 delphi 中record 的类操作符重载使用,就是如何 实现 record 之间的简单操作. 关于类操作符重载 ,大家可以看官方的文档. Delphi allows certai ...

  10. 关于CSS布局

    是时候单独列出一篇文章记录CSS布局了. http://www.imooc.com/article/2235  [史上最全Html和CSS布局技巧]