聊聊分布式锁
一、简介
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
分布式锁的目的:
保证同一时间只有一个客户端可以对共享资源进行操作。
分布式锁特点:
- 互斥性,在任意时刻,只有一个客户端可以获得锁(排他性)
- 容错性,当一个持有锁的线程异常退出,没来的及释放锁,其它客户端仍可获得锁
- 隔离性,线程只能解自己的锁,不能解其他线程的锁
分布式锁服务,你需要考虑下面几个设计:
- 需要给一个锁被释放的方式,以避免请求者不把锁还回来,导致死锁的问题。Redis 使用超时时间,ZooKeeper 可以依靠自身的 sessionTimeout 来删除节点。
- 分布式锁服务应该是高可用的,而且需要持久化
- 非阻塞方式的锁服务。
- 支持锁的可重入性。
二、实现方式
2.1 单实例 Redis锁
Redis 2.6.12 版本开始支持
SET resource_name my_random_value NX PX 30000
- my_random_value:是由客户端生成的一个随机字符串,相当于是客户端持有锁的标志
- NX:表示只有当resource_name对应的key值不存在的时候才能SET成功,相当于只有第一个请求的客户端才能获得锁
- PX:单位毫秒,30000表示这个锁有一个30秒的自动过期
至于解锁,为了防止客户端1 获得的锁,被客户端2 给释放,采用下面的Lua脚本来释放锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
在执行这段LUA脚本的时候,KEYS[1]的值为resource_name,ARGV[1]的值为my_random_value。原理就是先获取锁对应的value值,保证和客户端传进去的my_random_value值相等,这样就能避免自己的锁被其他人释放。另外,采取Lua脚本操作保证了原子性。
例如,下面的例子演示了不区分 Client 会导致的错误
- Client A 获得了一个锁。
- 当尝试释放锁的请求发送给 Redis 时被阻塞,没有及时到达Redis
- 锁定时间超时,Redis 认为锁的租约到期,释放了这个锁。
- Client B 重新申请到了这个锁。
- Client A 的解锁请求到达,将 Client B 锁定的key解锁
- Client C 也获得了锁。
- Client B 和 Client C 同时持有锁。
通过执行上面脚本的方式释放锁,Client 的解锁操作只会解锁自己曾经加锁的资源,所以是安全的。
特意注意:由于释放锁涉及多个redis操作,考虑到并发问题,采用lua脚本打包执行。
2.2 新的分布式锁算法 Redlock
如果 Redis节点宕机了,服务不可用,所有客户端都无法获得锁。为了提高可用性,我们可以给这个Redis节点挂一个Slave,当Master节点挂了,系统自动切到Slave上(failover)。但由于主从复制(replication)是异步的,数据可能会丢失,导致丧失锁的安全性。下面会介绍一种升级版:Redis 的官方文档
Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。
算法原理:
- 1、客户端获取服务器当前的的时间t0,毫秒数。
- 2、使用相同的key和value依次向5个实例获取锁。客户端在获取锁的时候自身设置一个远小于业务锁需要的持续时间的超时时间。举个例子,假设锁需要10秒,超时时间可以设置成比如5-50毫秒。这个避免某个Redis本身已经挂了,但是客户端一直在尝试获取锁的情况。超时了之后就直接跳到下一个节点。
- 3、客户端通过当前时间(t1)减去t0,计算获取锁所消耗的时间t2(=t1-t0)。只有t2小于锁的业务有效时间(也就是第二步的10秒),并且,客户端在至少3(5/2+1)台上获取到锁我们才认为锁获取成功。
- 4、如果锁已经获取,那么锁的业务有效时间为10s-t2。
- 5、如果客户端没有获取到锁,可能是没有在大于等于N/2+1个实例上获取锁,也可能是有效时间(10s-t2)为负数,我们就尝试去释放锁,即使是并没有在那个节点上获取到。
redisson框架对 Redlock 算法封装。pom依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.3.2</version>
</dependency>
详细的获取锁、释放锁的API方法可以参考文章 链接
2.3 基于curator的zookeeper分布式锁(推荐这种方式)
基于ZooKeeper的锁和基于Redis的锁比较:
- 在正常情况下,客户端可以持有锁任意长的时间,这可以确保它执行完所有操作之后再释放锁。这避免了基于Redis的锁对于过期时间到底设置多长的两难问题。实际上,基于ZooKeeper的锁是依靠Session(心跳)来维持锁的持有状态的,而Redis不支持Session。
- Redis 客户端如果获取锁失败后,
只能通过不断的轮询尝试获取锁
。而ZooKeeper支持watch
机制,获取锁失败之后等待锁释放的事件,这让客户端对锁的使用更加灵活。
Maven依赖:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
提供方法:
- public void acquire() throws Exception; // 阻塞方式获取锁、可重入
- public boolean acquire(long time, TimeUnit unit) throws Exception; // 超时方式获取锁
- public void release() throws Exception; // 释放锁
- boolean isAcquiredInThisProcess(); // 是否当前线程操作
- 关于上述方法的源码解读,链接
public class ZkDistributedLock {
// zookeeper地址
private String zkAddr;
// session超时时间
private int sessionTimeOutMs;
// zk名称空间
private String nameSpace;
private CuratorFramework cf;
private final ThreadLocal<Pair<InterProcessMutex, String>> threadLocal = new ThreadLocal<Pair<InterProcessMutex, String>>();
public ZkDistributedLock(String zkAddr, int sessionTimeOutMs, String nameSpace) {
this.zkAddr = zkAddr;
this.sessionTimeOutMs = sessionTimeOutMs;
this.nameSpace = nameSpace;
//1. 重试策略:重试时间为0s 重试3次 [默认重试策略:无需等待一直抢,抢3次]
RetryPolicy retryPolicy = new ExponentialBackoffRetry(0, 3);
//2. 通过工厂创建连接
cf = CuratorFrameworkFactory.builder()
.connectString(this.zkAddr)
.sessionTimeoutMs(this.sessionTimeOutMs)
.retryPolicy(retryPolicy)
.namespace(this.nameSpace)
.build();
//3. 开启连接
cf.start();
}
// 获取分布式锁
public boolean acquire(String lockKey) {
try {
InterProcessMutex lock = new InterProcessMutex(cf, "/" + lockKey);
lock.acquire();
threadLocal.set(new Pair<InterProcessMutex, String>(lock, lockKey));
return true;
} catch (Exception e) {
return false;
}
}
// 获取分布式锁(支持等待时间)
public boolean acquire(String lockKey, long time, TimeUnit unit) {
try {
InterProcessMutex lock = new InterProcessMutex(cf, "/" + lockKey);
if (lock.acquire(time, unit)) {
threadLocal.set(new Pair<InterProcessMutex, String>(lock, lockKey));
return true;
}
return false;
} catch (Exception e) {
return false;
}
}
// 释放锁
public void release() {
String lockKey = null;
try {
// 前线程中获取到pair 如果没有获取到锁 没有必要做释放
Pair<InterProcessMutex, String> pair = threadLocal.get();
if (pair == null) {
return;
}
InterProcessMutex lock = pair.getKey();
lockKey = pair.getValue();
if (lock == null) {
return;
}
if (!lock.isAcquiredInThisProcess()) {
return;
}
lock.release();
} catch (Exception e) {
} finally {
threadLocal.remove();
}
}
}
- [git]撤销的相关命令:reset、revert、checkout
- Thrift教程初级篇——thrift安装环境变量配置第一个实例
- 1083: [SCOI2005]繁忙的都市
- 1015: [JSOI2008]星球大战starwar
- Tyvj P1813 [JSOI2008]海战训练
- 1820: [JSOI2010]Express Service 快递服务
- 3038: 上帝造题的七分钟2
- 1854: [Scoi2010]游戏
- Javascript字符串
- Codevs3278[NOIP2013]货车运输
- 关于使用lazytag的线段树两种查询方式的比较研究
- Java 持久化操作之 --XML
- 算法模板——splay区间反转 1
- 3223: Tyvj 1729 文艺平衡树
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 02 . Zabbix配置监控项及聚合图形
- 01 . GitLab简介及环境部署
- 03 . Prometheus监控容器和HTTP探针应用及服务发现
- java编程思想第四版第九章习题
- 03 . Django之腾讯云短信
- ESP32 MQTT连接到中移OneNET物联网平台(附源码)
- 01 . Docker原理部署及常用操作命令
- SSH原理常见应用升级及端口转发
- 01 . Linux常用命令
- 私人订制属于自己的Linux系统
- 04 . Docker安全与Docker底层实现
- 03 . Docker数据资源管理与网络
- 02 . DockerFile构建镜像和Docker仓库
- Mysql通过MHA实现高可用
- ProxySQL简介原理及读写分离应用