从零到一手写基于Redis的分布式锁框架
1.分布式锁缘由
学习编程初期,我们做的诸如教务系统、成绩管理系统大多是单机架构,单机架构在处理并发的问题上一般是依赖于JDK内置的并发编程类库,如synchronize关键字、Lock类等。随着业务以及需求的提高,单机架构不再满足我们的要求,这个时候我们不免要进行业务上的分离,例如基于Maven进行多模块开发。业务与业务分离之后,遇到的首要问题就是业务之间如何进行通信,相信会有不少读者了解诸如Dubbo、SpringCloud之类的RPC框架,但这些RPC框架并没有自带处理分布式并发问题的功能,所以,分布式并发问题还需要我们自己去实现分布式锁。
2.分布式锁条件
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
3.分布式锁方式
分布式锁一般有三种实现方式:
- 数据库乐观锁
- 基于Redis的分布式锁
- 基于Zookeeper的分布式锁
下面我按个提一下这三种方式的大致实现思路。
3.1 数据库乐观锁
数据库乐观锁的实现方式是先使用SELECT语句查询某字段的值(版本号),该字段即理解为要获取的分布式锁。然后在使用UPDATE语句对正常业务数据进行更新,在UPDATE语句执行时一定要用WHERE条件对版本号进行判断,若版本号在这段时间内并没有发生变化则该语句默认执行成功,否则循环执行即可。
示例代码:
select (status,version) from goods where id=#{id}
update goods set status=2,version=version+1 where id=#{id} and version=#{version};
3.2 基于Zookeeper的分布式锁
基于Zookeeper实现分布式锁的算法思路大致如下假设锁空间的根节点为/lock:
- 客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。
- 客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁。
- 执行业务代码。
- 完成业务流程后,删除对应的子节点释放锁。
3.3 基于Redis的分布式锁
基于Redis的分布式锁实现是基于Redis自带的 setnx 命令。该命令只有在要设置的字段不存在的情况下才能设置成功,也就是获得分布式锁,否则失败。为了防止客户端异常导致的锁未释放问题,还需要对该字段设置过期时间。
本文将基于Redis分布式锁的实现思路设计一个spring-boot-starter-redis-lock框架。
核心代码如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate template;
@Autowired
private DefaultRedisScript<Long> redisScript;
private static final Long RELEASE_SUCCESS = 1L;
private long timeout = 3000;
public boolean lock(String key, String value) {
//执行set命令
Boolean absent = template.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);//1
//其实没必要判NULL,这里是为了程序的严谨而加的逻辑
if (absent == null) {
return false;
}
//是否成功获取锁
return true;
}
public boolean unlock(String key, String value) {
//使用Lua脚本:先判断是否是自己设置的锁,再执行删除
Long result = template.execute(redisScript, Arrays.asList(key,value));
//返回最终结果
return RELEASE_SUCCESS.equals(result);
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
@Bean
public DefaultRedisScript<Long> defaultRedisScript() {
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setResultType(Long.class);
defaultRedisScript.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end");
return defaultRedisScript;
}
}
执行上面的setIfAbsent()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
回顾上面提到的分布式锁的四个条件,在任意时刻,该代码都能保证只有一个客户端能持有锁,并且每一个分布式锁都加了过期时间,保证不会出现死锁,容错性暂时不考虑的话,加锁和解锁通过key保证了对多个客户端而言都是同一把锁,value的作用则是保证对同一把锁的加锁和解锁操作都是同一个客户端。
4.为什么上述方案不够好
为了理解我们想要提高的到底是什么,我们先看下当前大多数基于Redis的分布式锁三方库的现状。 用Redis来实现分布式锁最简单的方式就是在实例里创建一个键值,创建出来的键值一般都是有一个超时时间的(这个是Redis自带的超时特性),所以每个锁最终都会释放(参见前文属性2)。而当一个客户端想要释放锁时,它只需要删除这个键值即可。 表面来看,这个方法似乎很管用,但是这里存在一个问题:在我们的系统架构里存在一个单点故障,如果Redis的master节点宕机了怎么办呢?有人可能会说:加一个slave节点!在master宕机时用slave就行了!但是其实这个方案明显是不可行的,因为这种方案无法保证第1个安全互斥属性,因为Redis的复制是异步的。 总的来说,这个方案里有一个明显的竞争条件(race condition),举例来说:
- 客户端A在master节点拿到了锁。
- master节点在把A创建的key写入slave之前宕机了。
- slave变成了master节点
- B也得到了和A还持有的相同的锁(因为原来的slave里还没有A持有锁的信息)
当然,在某些特殊场景下,前面提到的这个方案则完全没有问题,比如在宕机期间,多个客户端允许同时都持有锁,如果你可以容忍这个问题的话,那用这个基于复制的方案就完全没有问题,否则的话我还是建议你对上述方案进行改进。比如,考虑使用Redlock算法。
5.Redlock算法
在分布式版本的算法里我们假设我们有N个Redis master节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法。我们已经描述了如何在单节点环境下安全地获取和释放锁。因此我们理所当然地应当用这个方法在每个单节点里来获取和释放锁。在我们的例子里面我们把N设成5,这个数字是一个相对比较合理的数值,因此我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁:
- 获取当前时间(单位是毫秒)。
- 轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
- 客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
- 如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
- 如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。
本文代码仓库:https://github.com/yueshutong/spring-boot-starter-redis-lock
参考文章:https://www.cnblogs.com/ironPhoenix/p/6048467.html
从零到一手写基于Redis的分布式锁框架的更多相关文章
- 基于Redis的分布式锁真的安全吗?
说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...
- 基于 Redis 的分布式锁
前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三. 首先谈到分布式锁自然也就联想到分布式应用. 在我们将应用拆分为分布式应用之前的单机系统 ...
- 基于redis的分布式锁的分析与实践
前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了.乐观锁和悲观锁最根本的区别在于 ...
- [Redis] 基于redis的分布式锁
前言分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁. 可靠性首先,为了确保 ...
- asp.net core mvc基于Redis实现分布式锁,C# WebApi接口防止高并发重复请求,分布式锁的接口幂等性实现
使用背景:在使用app或者pc网页时,可能由于网络原因,api接口可能被前端调用一个接口重复2次的情况,但是请求内容是一样的.这样在同一个短暂的时间内,就会有两个相同请求,而程序只希望处理第一个请求, ...
- 基于Redis的分布式锁到底安全吗(下)?
2017-02-24 自从我写完这个话题的上半部分之后,就感觉头脑中出现了许多细小的声音,久久挥之不去.它们就像是在为了一些鸡毛蒜皮的小事而相互争吵个不停.的确,有关分布式的话题就是这样,琐碎异常,而 ...
- 基于Redis的分布式锁到底安全吗(上)?
基于Redis的分布式锁到底安全吗(上)? 2017-02-11 网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词“Redis 分布式锁”随便到哪个搜索引擎上去搜索一下就知道了 ...
- 基于Redis的分布式锁安全性分析-转
基于Redis的分布式锁到底安全吗(上)? 2017-02-11 网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词“Redis 分布式锁”随便到哪个搜索引擎上去搜索一下就知道了 ...
- 不用找了,基于 Redis 的分布式锁实战来了!
Java技术栈 www.javastack.cn 优秀的Java技术公众号 作者:菜蚜 my.oschina.net/wnjustdoit/blog/1606215 前言:在分布式环境中,我们经常使用 ...
随机推荐
- Android高可用移动网络连接---(转载自http://wingjay.com/2019/01/16/mobile-network-connection/)
读者好,前面我们在 <Android 架构之网络连接与加速> 和<Android 架构之长连接技术>两篇文章中,讲解了 Http 短连接.TCP 长连接.连接复用与速度优化.数 ...
- OpenGL入门1.5:矩阵与变换
每一个小步骤的源码都放在了Github 的内容为插入注释,可以先跳过 前言 在阅读本篇博客之前,你必须对向量和矩阵有基本的认识,并且能熟练进行向量和矩阵的运算 我们已经知道了如何创建一个物体.着色.加 ...
- PlayJava Day006
今日所学: /* 2019.08.19开始学习,此为补档. */ 构造方法没有返回值(即return为空). this:实例(对象)的引用. JVM:①static方法区:存静态数据 ②栈区:引用 ...
- JavaScript几种继承方式
我们先构建一个Person的构造函数 function Person(name) { this.name=name; } Person.prototype.sayHi=function () { co ...
- 做一个vue轮播图组件
根据huangyi老师的慕课网vue项目跟着做的,下面大概记录了下思路 1.轮播图的图 先不做轮播图逻辑部分,先把数据导进来,看看什么效果.在recommend组件新建一个recommends的数组, ...
- c冒泡排序
外层循环需要循环和len一样的次数 //定义一个函数,该函数返回NSString void bubbleSort(int nums[],unsigned long len) { //控制本轮循环是否发 ...
- 算法基础:BFS和DFS的直观解释
算法基础:BFS和DFS的直观解释 https://cuijiahua.com/blog/2018/01/alogrithm_10.html 一.前言 我们首次接触 BFS 和 DFS 时,应该是在数 ...
- win10连接共享打印机
一.在运行中输入“\\共享打印机的主机ip”. 二.如果出现下面弹窗: 1.按Win键弹出开始菜单,直接在键盘上按zucelue,这个时候开始菜单里会检索到“编辑组策略”这个程序,按回车运行该程序.2 ...
- 日志分析利器Splunk的搭建、使用、破解
博主对splunk的了解不多,博主的使用目的是为了同步,分析日志.当初的搭建也是为了公司申请牌照需要日志服务器分析日志,顺便自己也对这方面感兴趣就买了本书看了看搭建玩的,后来检查来了博主也给他们演示了 ...
- Python3面向对象编程实例
/*认真研究完这个实例,理解python面向对象编程的方法*/class Person: """人员信息""" # 姓名(共有属性) nam ...