R2M分布式锁原理及实践
作者:京东科技 张石磊
1 案例引入
名词简介:
资源:可以理解为一条内容,或者图+文字+链接的载体。
档位ID: 资源的分类组,资源必须归属于档位。
问题描述:当同一个档位下2条资源同时审批通过时,收到擎天审批系统2条消息,消费者应用部署了2台机器,此时正好由2台机器分别消费,在并发消费时,先更新资源状态,然后写缓存,每次取前100条资源,类似select * from resource where gear_id=xxx limit 100 order by id desc;
在写档位缓存,此时事务未提交,并发查询时根据档位Id查询时查询不到对方的数据,全量写缓存时导致后写的缓存覆盖了先写的缓存,即缓存被覆盖,导致投放资源缺失。
方案思考 :
方案1:一台机器消费mq–单点问题
方案2:将同档位ID的资源路由到同一个queue,需要审批系统配合根据档位Id做路由,审批系统发的消息不只是cms审批数据,此方案不适用。
方案3:在档位级别加分布式锁。
经比较,最终采用方案3是合适的方案.
2 锁说明和分布式锁选择
synchronized锁的粒度是JVM进程维度,集群模式下,不能对共享资源加锁,此时需要跨JVM进程的分布式锁。
分布式锁方式核心实现方式优点缺点分析
1 数据库:
悲观锁,lock
乐观锁,通过版本号实现version
实现简单,不依赖中间件
数据库IO瓶颈,性能差
单实例存在单点问题,主从架构存在数据不一致,主从切换时其他客户端可重复加锁。
2 zookeeper
创建临时节点
CP模型,可靠性高,不存在主从切换不一致问题
频繁创建和销毁临时节点,且
集群模式下,leader数据需要同步到follower才算加锁成功,性能不如redis
主从切换服务不可用
3 redis集群
setnx+expire
性能高
有封装好的框架redission
支持超时自动删除锁
集群支持高可用,AP模型
主从切换时其他客户端可重复加锁。
R2M是基于开源的Redis cluster(Redis 3.0以上版本)构建的高性能分布式缓存系统,我们系统一直在使用,3.2.5版本开始支持分布式锁。
3 r2m分布式锁原理
示例代码:
String lockKey = CacheKeyHelper.getGearLockKey(EnvEnum.getEnvFlagEnum(envCode),resource.getGearId());
R2mLock lock = (R2mLock) r2mClusterClient.getLock(lockKey);
//获取锁,锁的默认有效期30s,获取不到锁就阻塞
lock.lock();
try {
//业务代码
resourceService.afterApprovedHandle(resource);
} finally {
//释放锁
lock.unlock();
}
1 加锁核心流程:
加锁流程图:
1):尝试获取锁,通过执行加锁Lua脚本来做;
2):若第一步未获取到锁,则去redis订阅解锁消息
3):一旦持有锁的线程释放了锁,就会广播解锁消息,其他线程自旋重新尝试获取锁。
核心加锁原理:使用lua脚本封装了hset和pexpire命令,保证是一个原子操作, KEYS[1]是加锁的key,argv[2]是加锁的客户端ID(UUID+线程ID),ARGV[1]是锁的有效期,默认30s.
private Object acquireInternal(List<String> args) {
if (!this.setLocked() && this.getHolderId() != Thread.currentThread().getId()) {
return -1L;
} else {
try {
//hash结构,hash的key是加锁的key,键值对的key为客户端的UUID+线程id,value为锁的重入计数器值。
return this.lockSha() != null ? this.executor.evalR2mLockSha(this.lockSha(),
"if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;
return -2;", Collections.singletonList(this.lockName), args) : this.executor. == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return -2;", Collections.singletonList(this.lockName), args);
} catch (Exception var3) {
this.setUnlocked();
throw new R2mLockException("Failed to acquire lock " + this.lockName + ".", var3);
}
}
}
args参数
private List<String> acquireArgs(long leaseTime) {
List<String> args = new ArrayList();
if (leaseTime != -1L) {
args.add(String.valueOf(leaseTime));
} else {
//默认30s
args.add(String.valueOf(this.internalLockLeaseTime()));
}
//UUID+当前线程id
args.add(this.currentThreadLockId(Thread.currentThread().getId()));
return args;
}
获取锁失败订阅锁的channel
//获取锁失败,订阅释放锁的消息
private boolean failedAcquire() {
this.subLock();
return false;
}
private void subLock() {
CompletableFuture<Void> cf = R2mLock.this.executor.lockSubscribe(R2mLock.this.lockPubSub(), R2mLock.this.getLockChannelName(), R2mLock.this);
if (cf != null) {
cf.handleAsync(this::reSubIfEx);
}
}
锁释放后,订阅者通过自旋尝试获取锁。
//tryAcquire获取锁,!tryAcquire就是获取锁失败,锁释放后,通知线程唤醒后返回false,然后通过自旋,尝试获取锁,
public final void acquire(long arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, long arg) {
boolean failed = true;
try {
boolean interrupted = false;
//内部自旋获取锁
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2 释放锁核心逻辑:
1)删除分布式锁key(如果可重入锁计数为0)
- 发释放锁的广播消息
3)取消watchdog
private Object unlockInternal(List<String> args) {
logger.debug("{} trying to unlock.", Thread.currentThread().getId());
Object var2;
try {
//判断锁 key 是否存在,如果存在,然后递减hash的value值,当value值为0时再删除锁key,并且广播释放锁的消息
if (this.unlockSha() == null) {
var2 = this.executor. == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.lockName, this.getLockChannelName()), args);
return var2;
}
var2 = this.executor.evalR2mLockSha(this.unlockSha(), "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.lockName, this.getLockChannelName()), args);
} catch (Exception var6) {
throw new R2mLockException("Failed to unlock " + this.lockName + ".", var6);
} finally {
this.finalizeRelease();
}
return var2;
}
//取消当前线程的watchdog
private void finalizeRelease() {
long threadId = Thread.currentThread().getId();
R2mLock.ExpirableEntry entry = (R2mLock.ExpirableEntry)this.entryCache.get(threadId);
if (entry != null) {
entry.release(threadId);
if (entry.isReleased()) {
//取消这个线程watchdog定时任务
entry.getExtTask().cancel();
this.expEntry.compareAndSet(entry, (Object)null);
//从缓存watchdog线程的map中删除该线程
this.entryCache.remove(threadId);
}
}
}
3 锁的健壮性思考
1 业务没执行完,锁超时过期怎么办?
客户端加锁默认有效期30s,超过有效期后如果业务没执行完,还需要持有这把锁,r2m客户端提供了续期机制,也就是watchdog机制。
watchdog原理:客户端线程维度(UUID+线程ID,客户端维护一个MAP,key就是UUID+线程ID)的后台定时线程,获取锁成功后,如果客户端还持有当前锁,每隔10s(this.internalLockLeaseTime() / 3L),去延长锁的有效期(internalLockLeaseTime)
//watchdog核心机制 ,internalLockLeaseTime默认30s
private void extendLock(long holderId) {
if (this.expEntry.get() != null) {
R2mLock.ExpirableEntry holderEntry = (R2mLock.ExpirableEntry)this.entryCache.get(holderId);
if (holderEntry != null) {
//每隔10s,如果当前线程持有锁,则续期30s
if (this.expEntry.compareAndSet(holderEntry, holderEntry)) {
Timeout task = this.timer().newTimeout((timeout) -> {
if (this.extendLockInternal(holderId)) {
this.extendLock(holderId);
}
}, this.internalLockLeaseTime() / 3L, TimeUnit.MILLISECONDS);
if (this.expEntry.get() != null) {
((R2mLock.ExpirableEntry)this.expEntry.get()).setExtTask(task);
}
}
}
}
}
//执行续期lua脚本
private boolean extendLockInternal(long threadId) {
Object result;
try {
//只续期
if (this.extendLockSha() != null) {
result = this.executor.evalR2mLockSha(this.extendLockSha(), "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.lockName), this.extendLockArgs(threadId));
} else {
result = this.executor. == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.lockName), this.extendLockArgs(threadId));
}
} catch (Exception var5) {
return false;
}
return Long.parseLong(result.toString()) == 1L;
}
2 客户端宕机,锁如何释放?
分布式锁是有效期的,客户端宕机后,watchdog机制失效,锁过期自动失效。
3 redis分布式锁集群模式下缺陷
r2m集群模式,极端情况,master加锁成功,宕机,还未来得及同步到slave,主从切换,slave切换成master,可以继续加锁,对于非及其严格加锁场景,该方案可满足,属于AP;对于严格场景下的分布式锁,可采用基于zookeeper的分布式锁,属于CP,leader宕机,folllower选举时不可用。性能上redis更优。
4 锁的释放问题
注意锁的释放在finally中释放,必须由锁的持有者释放,不能由其他线程释放别人的锁,示例代码中lock放到try的外面。
R2M分布式锁原理及实践的更多相关文章
- Redis、Zookeeper实现分布式锁——原理与实践
Redis与分布式锁的问题已经是老生常谈了,本文尝试总结一些Redis.Zookeeper实现分布式锁的常用方案,并提供一些比较好的实践思路(基于Java).不足之处,欢迎探讨. Redis分布式锁 ...
- 从Paxos到Zookeeper 分布式一致性原理与实践读书心得
一 本书作者介绍 此书名为从Paxos到ZooKeeper分布式一致性原理与实践,作者倪超,阿里巴巴集团高级研发工程师,国家认证系统分析师,毕业于杭州电子科技大学计算机系.2010年加入阿里巴巴中间件 ...
- 利用多写Redis实现分布式锁原理与实现分析(转)
利用多写Redis实现分布式锁原理与实现分析 一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子:场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能 ...
- 《从Paxos到Zookeeper:分布式一致性原理与实践》【PDF】下载
内容简介 Paxos到Zookeeper分布式一致性原理与实践从分布式一致性的理论出发,向读者简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了Paxos和ZAB协议. ...
- 女朋友也能看懂的Zookeeper分布式锁原理
前言 关于分布式锁,在互联网行业的使用场景还是比较多的,比如电商的库存扣减,秒杀活动,集群定时任务执行等需要进程互斥的场景.而实现分布式锁的手段也很多,大家比较常见的就是redis跟zookeep ...
- zookeeper 分布式锁原理
zookeeper 分布式锁原理: 1 大家也许都很熟悉了多个线程或者多个进程间的共享锁的实现方式了,但是在分布式场景中我们会面临多个Server之间的锁的问题,实现的复杂度比较高.利用基于googl ...
- Zookeeper--0300--java操作Zookeeper,临时节点实现分布式锁原理
删除Zookeeper的java客户端有 : 1,Zookeeper官方提供的原生API, 2,zkClient,在原生api上进行扩展的开源java客户端 3, 一.Zookeeper原生API ...
- Redis分布式锁原理
1. Redis分布式锁原理 1.1. Redisson 现在最流行的redis分布式锁就是Redisson了,来看看它的底层原理就了解redis是如何使用分布式锁的了 1.2. 原理分析 分布式锁要 ...
- redis分布式锁原理与实现
分布式锁原理 分布式锁,是控制分布式系统之间同步访问共享资源的一种方式.在分布式系统中,常常需要协调他们的动作.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候, ...
- Java进阶专题(二十五) 分布式锁原理与实现
前言 现如今很多系统都会基于分布式或微服务思想完成对系统的架构设计.那么在这一个系统中,就会存在若干个微服务,而且服务间也会产生相互通信调用.那么既然产生了服务调用,就必然会存在服务调用延迟或失败 ...
随机推荐
- 一次事故,我对MySQL时间戳存char(10)还是int(10)有了全新的认识
摘要:char类型字段想走索引的话,必须用引号括起来.如果是时间戳等类型的纯数字,建议还是存为int型吧. 本文分享自华为云社区<一次事故,我对MySql时间戳存char(10)还是int(10 ...
- 关于HTTPS认证,这里解决你所有疑惑
摘要:从签发证书到数据加密交互,按流程的进展讲解HTTPS认证过程内容和原理. 本文分享自华为云社区<故事+图文,一次性解决你对HTTPS认证过程的所有疑惑>,作者:breakDraw. ...
- Solon v2.2.12 发布,Java 应用开发框架
Solon 是一个高效的 Java 应用开发框架:更快.更小.更简单.它不是 Spring.没有使用 Servlet.JavaEE 接口,是一个有自己接口标准的开放生态: 150多个生态插件,可以满足 ...
- AI绘画,Midjourney极简入门
前几天看报道说: 一位小哥用AI绘画工具Midjourney生成的作品,在美国科罗拉多州博览会的艺术比赛中获得了第一名. 作者表示,他多次调整了输入的提示词,生成了100多幅画作,经过数周的修改和挑选 ...
- 题解 [HDU6747] Rotate 期望 + 逆元
来源:2020 年百度之星·程序设计大赛 - 初赛一 一个圈,从内到外一共被分成了 \(n\) 个环,中间是空的. 我们把从外到内第 \(i\) 层环平分成 \(a[i]\) 份,其中 \(a[i]\ ...
- BZOJ 2038: [2009国家集训队]小Z的袜子【莫队算法裸题】
作为一个生活散漫的人,小Z每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿. 终于有一天,小Z再也无法忍受这恼人的找袜子过程,于是他决定听天由命. 具体来说,小Z把这N只袜子从1到N编号,然后从 ...
- 特色国风数字孪生智慧大坝 3D 可视化
前言 水利兴,五谷丰.水利作为国民经济稳定和谐的重要部分,不仅有防洪减灾.农业灌溉.城市供水调水.渔业外贸.旅游航运.生态环境等综合应用,水电资源也是至关重要的可持续能源之一.大坝与水库.水电站等水利 ...
- mysql 使用 trim去不掉空格 解决
使用mysql8.0时 发现 有几个空字符串怎么也过滤不掉,使用 is not null.trim()<>''.length()>=1都不行,最后查了一些资料说 trim只能去除半角 ...
- SpringCloud学习 系列四、微服务中心 Eureka介绍及创建一个Eureka中心服务
系列导航 SpringCloud学习 系列一. 前言-为什么要学习微服务 SpringCloud学习 系列二. 简介 SpringCloud学习 系列三. 创建一个没有使用springCloud的服务 ...
- 简单的winform学生管理系统Demo
界面效果 练习重点 1. 关系表的创建 2. 增删改查的操作,及sqlhelper的封装 3. 跨页面数据传递,编辑页数据提交后数据局步刷新到列表数据 项目源码 FrmStuddentList pub ...