上面写的:本文所讨论的幂等问题都是并发场景下的幂等问题。即系统原本有幂等的设计,但在并发场景下就失效了。1摘要本文从一个钉钉实人认证场景下数据重复的案例出发,分析并发导致幂等性失败的原因,引出幂等性的概念。针对并发场景下的幂等性问题,提出了一种可行的实现幂等性的方法论,并结合通讯录添加业务场景对数据库幂等性问题进行了简要分析,并对实现幂等性的方法进行了详细的探讨带分布式锁。分析了分布式场景下锁存在的问题,包括单点故障、网络超时、他人锁错误释放、提前释放锁、分布式锁单点故障等,提出了相应的解决方案,并介绍了相应解决方案的细节完成。两个问题钉钉在实人认证业务中存在数据重复问题。1问题现象正常情况下,数据库中应该只有一条认证成功的记录,但实际上某个用户有多条记录。2问题的原因是并发不是幂等的。我们先来回顾一下幂等的概念:幂等(idempotence,幂等)是抽象代数中常见的一个数学和计算机科学概念。编程中幂等操作的特点是它执行任意次数与执行一次效果相同。--来自百度百科,实人认证在业务上是幂等的设计,其大致流程是:1)用户选择实人认证后,会在服务器端初始化一条记录;2)用户按照钉钉移动端的提示完成人脸识别比对;3)比对完成后,访问服务器修改数据库状态。第3步,在修改数据库状态之前,会判断“是否已经初始化”,“是否经过真人认证”,“知客是否返回认证成功”,保证幂等性。只有当请求第一次访问服务器并尝试修改数据库状态时,才能满足幂等判断条件,修改数据库状态。任何剩余的请求将直接返回,而不会影响数据库的状态。请求多次访问服务器的结果与请求第一次访问服务器的结果是一致的。因此,在真人认证成功的前提下,数据库应该只有一条认证成功的记录。但是在实际过程中,我们发现同一个请求会多次修改数据库状态,系统并没有达到我们预期的幂等性。原因是因为并发访问请求,在第一个请求完成修改服务器状态之前,其他并发请求和第一个请求已经通过幂等判断,多次修改数据库状态。并发导致原来的幂等设计失败。并发不是幂等的。三种解决方案解决并发场景下幂等问题的关键是找到唯一性约束,进行唯一性检查,同样的数据保存一次,同样的请求操作一次。访问服务器的请求可能会产生以下交互:与数据源交互,如数据库状态改变等;与其他业务系统交互,如调用下游服务或发送消息;一个请求可以只包含一个交互,也可以包含多个交互。比如一个请求只能修改一次数据库状态,或者修改数据库状态后发送修改数据库状态成功的消息。所以我们可以得出一个结论:在并发场景下,如果一个系统所依赖的组件是幂等的,那么这个系统自然是幂等的。以数据库为例,如果一个请求对数据的影响是新增一条数据,那么唯一索引就可以解决幂等问题。数据库会帮我们进行唯一性校验,不会重复存储相同的数据。钉钉通讯录加人通过数据库唯一索引解决了幂等性问题。以钉钉通讯录加人为例。在向数据库写入数据之前,它会先判断该数据是否已经存在于数据库中。如果不存在,添加请求最终会往数据库的employee表中插入一条数据。大量相同并发的通讯录添加请求,很可能导致系统的幂等性设计失效。在一个添加人员的请求中,(organizationID,jobnumber)可以唯一标识一次请求,数据库中也有一个(organizationID,jobnumber)的唯一索引。因此,我们可以保证同一个添加请求只会修改一次数据库状态,即添加一条记录。如果依赖的组件天生是幂等的,那么问题就简单了,但实际情况往往更复杂。在并发场景下,如果系统所依赖的组件不能做到幂等,我们就需要通过额外的手段来实现幂等。一种常见的方法是使用分布式锁。分布式锁的实现方式有很多种,比较常用的是缓存分布式锁。四分布式锁在什么是Java分布式锁?中有这样几段话:在计算机科学中,锁是多线程环境中防止不同线程操作同一资源的机制。使用锁定时,资源被“锁定”以供特定线程访问,并且只有在资源被释放后才能由不同的线程访问。锁有几个好处:它们阻止两个线程做同样的工作,当两个线程试图同时使用相同的资源时,它们可以防止错误和数据损坏。Java中的分布式锁不仅可以与多个线程一起工作同一台机器,而且在分布式系统中的不同机器上的客户端上运行的线程。这些独立机器上的线程必须进行通信和协调,以确保它们都不会尝试访问已被另一个锁定的资源。这几段话告诉我们,锁的本质是共享资源的交互访问,分发方式锁定解决了分布式系统中共享资源的交互访问问题。java.util.concurrent.locks包提供了丰富的锁实现,包括公平锁/非公平锁、阻塞锁/非阻塞锁、读写锁、可重入锁。我们如何实现分布式锁?解决方案1??分布式系统中普遍存在两个问题:1)单点故障,即持有锁的应用发生单点故障时,锁会被长期无效占用;2))网络超时问题,即当客户端出现网络超时但实际加锁成功后,我们无法再次正确获取锁。解决问题1,一个简单的方案就是引入一个过期时间(leasetime)。锁的持有将是时效性的。当应用程序出现单点故障时,可以自动释放其持有的锁。解决问题2,一个简单的方案就是支持重入。我们为每个获取锁的客户端配置一个唯一的标识(通常是UUID)。锁上锁成功后,锁上会带有客户端的ID。鉴别。当真正加锁成功,客户端超时重试时,我们就可以判断锁已经被客户端持有,返回成功。综上所述,我们提出了一种基于租约的分布式锁解决方案。出于性能考虑,使用缓存作为锁的存储介质,使用MVCC(Multiversionconcurrencycontrol)机制解决共享资源访问互斥的问题。具体实现见附录代码。分布式锁的一般使用如下初始化分布式锁的工厂使用工厂生成分布式锁实例使用分布式实例进行加锁和解锁操作@TestpublicvoidtestTryLock(){//初始化工厂MdbDistributeLockFactorymdbDistributeLockFactory=newMdbDistributeLockFactory();mdbDistributeLockFactory.setNamespace(603);mdbDistributeLockFactory.setMtairManager(newMultiClusterTairManager());//获取锁DistributeLocklock=mdbDistributeLockFactory.getLock("TestLock");//加锁和解锁操作booleanlocked=lock.tryLock();if(!locked){返回;}try{//dosomething}finally{lock.unlock();}}这个方案简单易用,但是问题也很明显。比如在释放锁的时候,缓存中的key就简单的作废了,这样就存在错误释放别人已经持有的锁的问题。好在只要把锁的租约期设置得足够长,出现这个问题的概率就足够小了。我们借用MartinKleppmann的文章Howtododistributedlocking中的一张图来说明这个问题。想象这样一种情况,在Client1释放锁之前锁就过期了,Client2会去获取锁。此时锁被Client2持有,但Client1可能会误释放。一个比较优秀的方案,我们给每把锁都设置一个身份,在释放锁的时候,1)首先检查这个锁是不是自己的,2)如果是自己的,就释放锁。受实现方法限制,步骤1和步骤2不是原子操作。在第1步和第2步之间,如果锁过期了,被其他客户端获取了,这时候别人的锁就会被误释放。方案2借助Redis的Lua脚本,可以完美解决误释放别人已经持有的锁的问题。在Redis分布式锁的Correctimplementationwithasingleinstance部分,我们可以得到想要的答案——如何实现分布式锁。当我们想要获取锁时,可以执行下面的方法SETresource_namemy_random_valueNXPX30000当我们想要释放锁时,可以执行下面的Lua脚本ifredis.call("get",KEYS[1])==ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0endOption3在讨论Option1和Option2的时候,我们反复提到了一个问题:锁的自动释放。这是一把双刃剑:1)一方面解决了客户端持有锁的单点故障问题2)另一方面,如果提前释放锁,会出现错误holdingstateoflock的时候,我们就可以引入watchdog自动续租机制,可以参考下面Redisson是如何实现的。锁上锁成功后,Redisson会调用renewExpiration()方法启动一个看门狗线程自动更新锁。每1/3的时间更新一次,成功则进行下一次更新,失败则取消更新操作。我们可以看看Redisson是如何更新的。renewExpiration()方法第17行,renewExpirationAsync()方法是执行锁更新的关键操作。我们进入方法可以看到,Redisson也是使用Lua脚本来更新锁租约的:1)判断锁是否存在;2)如果存在,重置过期时间。privatevoidrenewExpiration(){ExpirationEntryee=EXPIRATION_RENEWAL_MAP.get(getEntryName());if(ee==null){return;}Timeouttask=commandExecutor.getConnectionManager().newTimeout(timeout->{ExpirationEntryent=EXPIRATION_RENEWAL_MAP.get(getEntryName());if(ent==null){return;}LongthreadId=ent.getFirstThreadId();if(threadId==null){return;}RFuture
