并发场景下的幂等问题——分布式锁详解

简介:本文从钉钉实人认证场景的一例数据重复问题出发,分析了其原因是因为并发导致幂等失效,引出幂等的概念。针对并发场景下的幂等问题,提出了一种实现幂等可行的方法论,结合通讯录加人业务场景对数据库幂等问题进行了简单分析,就分布式锁实现幂等方法展开了详细讨论。分析了锁在分布式场景下存在的问题,包括单点故障、网络超时、错误释放他人锁、提前释放锁以及分布式锁单点故障等,提出了对应的解决方案,介绍了对应方案的具体实现。

image.png

作者 | 百书
来源 | 阿里技术公众号

写在前面:本文讨论的幂等问题,均为并发场景下的幂等问题。即系统本存在幂等设计,但是在并发场景下失效了。

一 摘要

本文从钉钉实人认证场景的一例数据重复问题出发,分析了其原因是因为并发导致幂等失效,引出幂等的概念。

针对并发场景下的幂等问题,提出了一种实现幂等可行的方法论,结合通讯录加人业务场景对数据库幂等问题进行了简单分析,就分布式锁实现幂等方法展开了详细讨论。

分析了锁在分布式场景下存在的问题,包括单点故障、网络超时、错误释放他人锁、提前释放锁以及分布式锁单点故障等,提出了对应的解决方案,介绍了对应方案的具体实现。

二 问题

钉钉实人认证业务存在数据重复的问题。

1 问题现象

正常情况下,数据库中应该只有一条实人认证成功记录,但是实际上某用户有多条。

image.png

2 问题原因

并发导致了不幂等。

我们先来回顾一下幂等的概念:

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
--来自百度百科

实人认证在业务上有幂等设计,其一般流程为:

1)用户选择实人认证后会在服务端初始化一条记录;
2)用户在钉钉移动端按照指示完成人脸比对;
3)比对完成后访问服务端修改数据库状态。

在第3步中,在修改数据库状态之前,会判断「是否已经初始化」、「是否已经实人认证」以及「智科是否返回认证成功」以保证幂等。仅当请求首次访问服务端尝试修改数据库状态时,才能满足幂等的判断条件并修改数据库状态。其余任意次请求将直接返回,对数据库状态无影响。请求多次访问服务端所产生的结果,和请求首次访问服务端一致。因此,在实人认证成功的前提下,数据库应当有且仅有一条认证成功的记录。

image.png

但是在实际过程中我们发现,同一个请求会多次修改数据库状态,系统并未按照我们预期的那样实现幂等。究其原因,是因为请求并发访问,在首次请求完成修改服务端状态前,并发的其他请求和首次请求都通过了幂等判断,对数据库状态进行了多次修改。

并发导致了原幂等设计失效。

并发导致了不幂等。

三 解决方案

解决并发场景下幂等问题的关键,是找到唯一性约束,执行唯一性检查,相同的数据保存一次,相同的请求操作一次。

一次访问服务端的请求,可能产生以下几种交互:

  1. 与数据源交互,例如数据库状态变更等;
  2. 与其他业务系统交互,例如调用下游服务或发送消息等;

一次请求可以只包含一次交互,也可以包含多次交互。例如一次请求可以仅仅修改一次数据库状态,也可以在修改数据库状态后再发送一条数据库状态修改成功的消息。

于是我们可以得出一个结论:并发场景下,如果一个系统依赖的组件幂等,那么该系统在天然幂等。

以数据库为例,如果一个请求对数据造成的影响是新增一条数据,那么唯一索引可以是幂等问题的解法。数据库会帮助我们执行唯一性检查,相同数据不会重复落库。

钉钉通讯录加人就是通过数据库的唯一索引解决了幂等问题。以钉钉通讯录加人为例,在向数据库写数据之前,会先判断数据是否已经存在于数据库之中,如果不存在,加人请求最终会向数据库的员工表插入一条数据。大量相同的并发的通讯录加人请求让系统的幂等设计失效成为可能。在一次加人请求中,(组织ID,工号)可以唯一标记一个请求,在数据库中,也存在(组织ID,工号)的唯一索引。因此我们可以保证,多次相同的加人请求,只会修改一次数据库状态,即添加一条记录。

如果所依赖的组件天然幂等,那么问题就简单了,但是实际情况往往更加复杂。并发场景下,如果系统依赖的组件无法幂等,我们就需要使用额外的手段实现幂等。

一个常用的手段就是使用分布式锁。分布式锁的实现方式有很多,比较常用的是缓存式分布式锁。

四 分布式锁

在What is a Java distributed lock?中有这样几段话:

In computer science, locks are mechanisms in a multithreaded environment to prevent different threads from operating on the same resource. When using locking, a resource is "locked" for access by a specific thread, and can only be accessed by a different thread once the resource has been released. Locks have several benefits: they stop two threads from doing the same work, and they prevent errors and data corruption when two threads try to use the same resource simultaneously.

Distributed locks in Java are locks that can work with not only multiple threads running on the same machine, but also threads running on clients on different machines in a distributed system. The threads on these separate machines must communicate and coordinate to make sure that none of them try to access a resource that has been locked up by another.

这几段话告诉我们,锁的本质是共享资源的互斥访问,分布式锁解决了分布式系统中共享资源的互斥访问的问题。

java.util.concurrent.locks包提供了丰富的锁实现,包括公平锁/非公平锁,阻塞锁/非阻塞锁,读写锁以及可重入锁等。

我们要如何实现一个分布式锁呢?

方案一

分布式系统中常见有两个问题:

1)单点故障问题,即当持有锁的应用发生单点故障时,锁将被长期无效占有;
2)网络超时问题,即当客户端发生网络超时但实际上锁成功时,我们无法再次正确的

获取锁。

要解决问题1,一个简单的方案是引入过期时间(lease time),对锁的持有将是有时效的,当应用发生单点故障时,被其持有的锁可以自动释放。

要解决问题2,一个简单的方案是支持可重入,我们为每个获取锁的客户端都配置一个不会重复的身份标识(通常是UUID),上锁成功后锁将带有该客户端的身份标识。当实际上锁成功而客户端超时重试时,我们可以判断锁已被该客户端持有而返回成功。

综上我们给出了一个lease-based distribute lock方案。出于性能考量,使用缓存作为锁的存储介质,利用MVCC(Multiversion concurrency control)机制解决共享资源互斥访问问题,具体实现可见附录代码。

分布式锁的一般使用方式如下

● 初始化分布式锁的工厂
● 利用工厂生成一个分布式锁实例
● 使用该分布式实例上锁和解锁操作

@Test
public void testTryLock() {//初始化工厂MdbDistributeLockFactory mdbDistributeLockFactory = new MdbDistributeLockFactory();mdbDistributeLockFactory.setNamespace(603);mdbDistributeLockFactory.setMtairManager(new MultiClusterTairManager());//获得锁DistributeLock lock = mdbDistributeLockFactory.getLock("TestLock");//上锁解锁操作boolean locked = lock.tryLock();if (!locked) {return;}try {//do something } finally {lock.unlock();}
}

该方案简单易用,但是问题也很明显。例如,释放锁的时候只是简单的将缓存中的key失效,所以存在错误释放他人已持有锁问题。所幸只要锁的租期设置的足够长,该问题出现几率就足够小。

我们借用Martin Kleppmann在文章How to do distributed locking中的一张图说明该问题。

image.png

设想一种情况,当占有锁的Client 1在释放锁之前,锁就已经到期了,Client 2将获取锁,此时锁被Client 2持有,但是Client 1可能会错误的将其释放。一个更优秀的方案,我们给每个锁都设置一个身份标识,在释放锁的时候,1)首先查询锁是否是自己的,2)如果是自己的则释放锁。受限于实现方式,步骤1和步骤2不是原子操作,在步骤1和步骤2之间,如果锁到期被其他客户端获取,此时也会错误的释放他人的锁。

方案二

借助Redis的Lua脚本,可以完美的解决存在错误释放他人已持有锁问题的。在Distributed locks with Redis这篇文章的 Correct implementation with a single instance 这一节中,我们可以得到我们想要的答案——如何实现一个分布式锁。

当我们想要获取锁时,我们可以执行如下方法

SET resource_name my_random_value NX PX 30000

当我们想要释放锁时,我们可以执行如下的Lua脚本

if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

方案三

在方案一和方案二的讨论过程中,有一个问题被我们反复提及:锁的自动释放。

这是一把双刃剑:

1)一方面它很好的解决了持有锁的客户端单点故障的问题
2)另一方面,如果锁提前释放,就会出现锁的错误持有状态

这个时候,我们可以引入Watch Dog自动续租机制,我们可以参考以下Redisson是如何实现的。

在上锁成功后,Redisson会调用renewExpiration()方法开启一个Watch Dog线程,为锁自动续期。每过1/3时间续一次,成功则继续下一次续期,失败取消续期操作。

我们可以再看看Redisson是如何续期的。renewExpiration()方法的第17行renewExpirationAsync()方法是执行锁续期的关键操作,我们进入到方法内部,可以看到Redisson也是使用Lua脚本进行锁续租的:1)判断锁是否存在,2)如果存在则重置过期时间。

private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}Timeout task = commandExecutor.getConnectionManager().newTimeout(timeout -> {ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}RFuture<Boolean> future = renewExpirationAsync(threadId);future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getRawName() + " expiration", e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) {// reschedule itselfrenewExpiration();} else {cancelExpirationRenewal(null);}});}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}
protected RFuture<Boolean> renewExpirationAsync(long threadId) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return 1; " +"end; " +"return 0;",Collections.singletonList(getRawName()),internalLockLeaseTime, getLockName(threadId));
}

方案四

借助Redisson的自动续期机制,我们无需再担心锁的自动释放。但是讨论到这里,我还是不得不面对一个问题:分布式锁本身不是一个分布式应用。当Redis服务器故障无法正常工作时,整个分布式锁也就无法提供服务。

更进一步,我们可以看看Distributed locks with Redis这篇文章中提到的Redlock算法及其实现。

Redlock算法不是银弹,关于它的好与坏,也有很多争论:

How to do distributed locking:
How to do distributed locking — Martin Kleppmann’s blog

Is Redlock safe?:
Is Redlock safe? - <antirez>

Martin Kleppmann和Antirez关于Redlock的争辩:
https://news.ycombinator.com/item

参考资料

What is a Java distributed lock?
What is a Java distributed lock? | Redisson

Distributed locks and synchronizers:
https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

Distributed locks with Redis:
Distributed locks with Redis – Redis

附录

分布式锁

public class MdbDistributeLock implements DistributeLock {/*** 锁的命名空间*/private final int namespace;/*** 锁对应的缓存key*/private final String lockName;/*** 锁的唯一标识,保证可重入,以应对put成功,但是返回超时的情况*/private final String lockId;/*** 是否持有锁。true:是*/private boolean locked;/*** 缓存实例*/private final TairManager tairManager;public MdbDistributeLock(TairManager tairManager, int namespace, String lockCacheKey) {this.tairManager = tairManager;this.namespace = namespace;this.lockName = lockCacheKey;this.lockId = UUID.randomUUID().toString();}@Overridepublic boolean tryLock() {try {//获取锁状态Result<DataEntry> getResult = null;ResultCode getResultCode = null;for (int cnt = 0; cnt < DEFAULT_RETRY_TIMES; cnt++) {getResult = tairManager.get(namespace, lockName);getResultCode = getResult == null ? null : getResult.getRc();if (noNeedRetry(getResultCode)) {break;}}//重入,已持有锁,返回成功if (ResultCode.SUCCESS.equals(getResultCode)&& getResult.getValue() != null && lockId.equals(getResult.getValue().getValue())) {locked = true;return true;}//不可获取锁,返回失败if (!ResultCode.DATANOTEXSITS.equals(getResultCode)) {log.error("tryLock fail code={} lock={} traceId={}", getResultCode, this, EagleEye.getTraceId());return false;}//尝试获取锁ResultCode putResultCode = null;for (int cnt = 0; cnt < DEFAULT_RETRY_TIMES; cnt++) {putResultCode = tairManager.put(namespace, lockName, lockId, MDB_CACHE_VERSION,DEFAULT_EXPIRE_TIME_SEC);if (noNeedRetry(putResultCode)) {break;}}if (!ResultCode.SUCCESS.equals(putResultCode)) {log.error("tryLock fail code={} lock={} traceId={}", getResultCode, this, EagleEye.getTraceId());return false;}locked = true;return true;} catch (Exception e) {log.error("DistributedLock.tryLock fail lock={}", this, e);}return false;}@Overridepublic void unlock() {if (!locked) {return;}ResultCode resultCode = tairManager.invalid(namespace, lockName);if (!resultCode.isSuccess()) {log.error("DistributedLock.unlock fail lock={} resultCode={} traceId={}", this, resultCode,EagleEye.getTraceId());}locked = false;}/*** 判断是否需要重试** @param resultCode 缓存的返回码* @return true:不用重试*/private boolean noNeedRetry(ResultCode resultCode) {return resultCode != null && !ResultCode.CONNERROR.equals(resultCode) && !ResultCode.TIMEOUT.equals(resultCode) && !ResultCode.UNKNOW.equals(resultCode);}}

分布式锁工厂

public class MdbDistributeLockFactory implements DistributeLockFactory {/*** 缓存的命名空间*/@Setterprivate int namespace;@Setterprivate MultiClusterTairManager mtairManager;@Overridepublic DistributeLock getLock(String lockName) {return new MdbDistributeLock(mtairManager, namespace, lockName);}
}

原文链接
本文为阿里云原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/511904.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

双11特刊|十年磨一剑,云原生多模数据库Lindorm 2021双11总结

前言 2021 年&#xff0c;转眼 Lindorm 已经在阿里发展了十年的时间&#xff0c;从基于 HBase 深度改造的 Lindorm 1.0 版本&#xff0c;到全面重构&#xff0c;架构大幅升级的 Lindorm 2.0 版本&#xff1b;从单一的宽表引擎&#xff0c;到支持搜索、时序、文件等多种结构化数…

怎么样升级成为鸿蒙系统,手机升级成为鸿蒙系统第一手体验怎么样?-电脑自学网...

自从华为鸿蒙系统上线以来&#xff0c;除了6月2日发布会爆料出鸿蒙细节、功能之外&#xff0c;还给部分华为手机提供了鸿蒙系统的升级包。不知道大家有没有升级&#xff1f;其实很多小伙伴处于观望状态&#xff0c;因为新系统的缺点不可避免&#xff0c;升级了系统就再也回不去…

换个姿势看 hooks,灵感来源组合和 HOC 模式下逻辑视图分离新创意

作者 | &#x1f47d;来源 | 前端Sharing前言懂得 JSX 本质的同学都知道它只不过是一种语法糖&#xff0c;会被 babel 处理成 createElement 的形式&#xff0c;最后再变成常规的 js 对象。所以&#xff0c;我们就可以在 js 逻辑层面对 element 对象做处理&#xff0c;自定义 …

双11特刊 | 云数据库RDS如何顺滑应对流量洪峰

简介&#xff1a;从绿色低碳到硬核科技&#xff0c;看RDS如何用绿色科技助力2021“双11”&#xff1f; 双十一回顾 从平台到商家&#xff0c;再从物流到客户手中&#xff0c;云数据库RDS支撑着双11集团电商的在线业务。RDS首次对集团核心业务进行国产化技术演进试点&#xff…

双11专刊|云原生数据仓库AnalyticDB支撑双11,大幅提升分析实时性和用户体验

简介&#xff1a;2021年双十一刚刚落幕&#xff0c;已连续多年稳定支持双十一大促的云原生数据仓库AnalyticDB&#xff0c;今年双十一期间仍然一如既往的稳定。除了稳定顺滑的基本盘之外&#xff0c;AnalyticDB还有什么亮点呢&#xff1f;下面我们来一一揭秘。 一 前言 2021年…

html传输的数值表示的含义,数字传递游戏的意义与感悟_传数字游戏心得体会

在大学生入职培训期间&#xff0c;曾组织他们做了一场小游戏&#xff0c;游戏规则如下&#xff1a;1、80名学生平均分成8组&#xff0c;排成8列&#xff0c;统一面向讲台做好&#xff1b;2、主持人向每组的最后一名队员提供一个数字(数字一般为3位或4位数&#xff0c;不确定&am…

德勤2022技术趋势:IT自我颠覆、技术跨界融合创新

作者 | 宋慧 出品 | CSDN云计算 IT 技术&#xff0c;一直处于快速发展与变化中。 基于对前沿技术的观察分析与自身实践&#xff0c;国际机构德勤管理咨询每年发布对于未来 18-24 个月的的重要技术趋势。2021 年 CSDN 曾报道 德勤2021技术趋势&#xff1a;繁琐、点状的匠人AI时…

双11特刊|购物车实时显示到手价,看云原生内存数据库Tair如何提升用户体验?

阿里云自研内存数据库Tair诞生于2009年&#xff0c;是一种支持高并发低延迟访问的云原生内存数据库&#xff0c;完全兼容Redis&#xff0c;已历经多年双11大促考验&#xff0c;提供核心在线访问加速能力&#xff0c;显著提升系统吞吐量。 作为双11大促承载流量洪峰的利器&…

Dubbo-Admin 正式支持 3.0 服务治理

简介&#xff1a;Dubbo 相信大家并不陌生&#xff0c;是一款微服务开发框架&#xff0c;它提供了 RPC 通信与微服务治理两大关键能力。大家在日常开发中更多使用的是 Dubbo 提供的 RPC 通信这一部分能力&#xff0c;而对其提供的服务治理的能力使用相对少一些&#xff0c;本文的…

vue将文本渲染html,vue2.0 之文本渲染-v-html、v-text

vue2.0 之文本渲染-v-html、v-text1、index.html代码vuedemo2、main.js代码import Vue from ‘vue‘import App from ‘./App‘Vue.config.productionTip false/* eslint-disable no-new */new Vue({el: ‘#app‘,render: h > h(App)})render: h > h(App)是ES6的语法&am…

如何成为真正的数字化企业,锐捷网络发布数字原力觉醒计划

编辑 | 宋慧 出品 | CSDN 云计算 什么样的企业可称为数字化企业&#xff1f; 因为疫情等各类不确定因素&#xff0c;数字化的浪潮正深刻改变着企业。所有企业都需考虑转型、创新、增长&#xff0c;这三个问题。深耕中国企业级市场多年的IT技术厂商锐捷网络&#xff0c;以“点线…

2021中国数字服务大会 | 阿里云混合云新一代运维演进与实践

简介&#xff1a;12月3日&#xff0c;2021中国数字服务大会顺利召开&#xff0c;大会以“数字服务、跨界融合、协同创新”为主题&#xff0c;邀请产学研界嘉宾&#xff0c;举办行业与学术论坛&#xff0c;共话数字服务的挑战和机遇。阿里云作为云厂商代表应邀参会&#xff0c;并…

冲压模板自动标注LISP_干货满满!超实用冲压模具资料,加薪必看!

一般的冲压模具都是由&#xff1a;上下托板、上下垫脚、上下模座&#xff1a;一般用A3、Q235等“软料”做成&#xff0c;起支撑整个模具、方便架模、落料等作用。上、下模板&#xff1a;上、下模板起固定刀口、入块、入子、顶料销等作用&#xff0c;外定位、内定位、浮升引导销…

安谋科技四周年献礼,提前完成五年规划目标

自2018年4月正式独立运营以来&#xff0c;安谋科技一直以服务中国的科技产业、建设中国本土的研发能力、赋能中国本土半导体生态为核心使命。值此公司成立四周年之际&#xff0c;安谋科技宣布已提前超额完成了合资公司落地深圳时设立的五年规划目标。 回顾四年来走过的历程&am…

开源微服务编排框架:Netflix Conductor

简介&#xff1a;本文主要介绍netflix conductor的基本概念和主要运行机制。 作者 | 夜阳 来源 | 阿里技术公众号 本文主要介绍netflix conductor的基本概念和主要运行机制。 一 简介 netflix conductor是基于JAVA语言编写的开源流程引擎&#xff0c;用于架构基于微服务的流…

直播回顾:如何对付臭名昭著的 IO 夯?诊断利器来了 | 龙蜥技术

简介&#xff1a;听到IO夯总是让人头疼&#xff0c;那有没有可以分析IO夯问题的利器&#xff1f; 编者按&#xff1a;sysAK&#xff08;system analyse kit&#xff09;&#xff0c;是龙蜥社区&#xff08;OpenAnolis&#xff09;系统运维 SIG 下面的一个开源项目&#xff0c;…

cad致命错误如何处理_Golang 如何优雅地处理错误

- 后端早读课翻译计划 第二篇- 本文提供了一个优雅的处理 Golang 中错误的方法&#xff0c;解决了 Golang error 只有字符串信息的局限性&#xff0c;提供了上下文信息、错误类型判断的功能。尽管 go 具有一个简单的错误模型&#xff0c;但是乍一看&#xff0c;事情并没有那么容…

快速云原生化,从数据中心到云原生的迁移实践

简介&#xff1a;本文将介绍在帮助用户快速完成迁云中的解决方案、最佳实践以及迁云工具。 云原生的时代已经到来&#xff0c;云原生技术正在重塑整个软件生命周期&#xff0c;阿里巴巴是国内最早布局云原生技术的公司之一。 容器服务团队在过去的几年时间内帮助很多用户成功…

实力总结四类 Bean 注入 Spring 的方式

作者 | 阿Q来源 | 阿Q说代码一提到Spring&#xff0c;大家最先想到的是啥&#xff1f;是AOP和IOC的两大特性&#xff1f;是Spring中Bean的初始化流程&#xff1f;还是基于Spring的Spring Cloud全家桶呢&#xff1f;今天我们就从Spring的IOC特性入手&#xff0c;聊一聊Spring中把…

广州大学计算机网络期末考试2013,广州大学计算机网络技术试卷(A卷)

广州大学2006-2007 学年第 1 学期考试卷课程计算机网络技术考试形式(开/闭卷&#xff0c;考试/查)一、填空题(15分)1、现有一计算机要和另一设备进行通信&#xff0c;要实现此目标首先要考虑通过何种接口把计算机和相应设备连接起来&#xff1b;解决此问题属于层的任务。(2分)2…