最系统的幂等性方案:一锁二判三更新
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
最系统的幂等性方案:一锁二判三更新
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
问题1:你们项目,怎么做幂等设计的?
问题2:接口的幂等性,怎么设计?
问题3:业务订单的幂等性,怎么设计?
问题4:付款请求的幂等性,怎么设计?
问题4:前端重复提交选中的数据,后台只产生一次有效操作,怎么设计?
最近又有小伙伴在面试阿里、网易,都遇到了相关的面试题。
很多小伙伴回答了一些边边角角,但是回答不全面不体系,面试官不满意,面试挂了。
借着此文,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,展示一下雄厚的 “技术肌肉、技术实力”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提,offer自由”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V161版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】获取
本文目录
什么是幂等性?
所谓幂等性,就是一次操作和多次操作同一个资源,所产生的影响均与一次操作的影响相同。
"幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
幂等性,用数学语言表达就是:
f(x)=f(f(x))
维基百科的幂等性定义如下:
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现.
在软件或者系统中,重复使用幂等函数或幂等方法不会影响系统状态,也不用担心重复执行会对系统造成改变。
通俗点说:
一个接口如果幂等,不管被调多少次,只要参数不变,结果也不变。
幂等性是对于写操作来说的,一个写操作,一般都需要保证:
- 幂等性
- 可用性
- ACID事务属性。
当然,这里仅仅聚焦 幂等。
为什么需要幂等性?
如果客户端重复调用,服务端会遇到如下的很多问题:
- 创建订单时,重复调用是否产生两笔订单?
- 扣减库存时,重复调是否会多扣一次?
这就是出现了幂等性问题。
按照幂等性要求,需要保证一次请求和多次请求同一个资源产生相同的副作用。
所以:创建订单时,重复调用是否产生两笔订单? 当然不能。
所以:扣减库存时,重复调是否会多扣一次? 当然不能。
这些,都是需要幂等性机制去保障。如果不支持幂等操作,那将会出现以下情况:
- 电商超卖现象
- 重复转账、扣款或付款
- 重复增加金币、积分或优惠券
等等,非常惨的。
什么样的原因导致幂等性问题?
原因之一:底层网络阻塞和延迟的问题
在系统高并发的环境下,很有可能因为网络阻塞等等问题,导致客户端不能及时的收到服务端响应,甚至是调用超时。 这时候用户会重复点击,重复请求。
在消息队列组件中,客户端也有重试机制,如果投递失败/投递超时,则会重新投递。 对于服务端来说,可能会收到重复投递的一份消息。
在RPC组件中,客户端也有重试机制,如果投递失败/投递超时,则会重试调用。 对于服务端来说,可能会重复收到通用的调用。
原因之二:用户层面的重复操作
比如下单的按键在点按之后,在没有收到服务器请求之前,用户还可以被按。
或者,用户的App闪退/人工强退,之后重新打开重新下单
需要幂等性的 两大场景
可能会发生重复请求或重试操作的场景,在分布式、微服务架构中是随处可见的。
-
网络波动:因网络波动,可能会引起重复请求
-
分布式消息消费:任务发布后,使用分布式消息服务来进行消费
-
用户重复操作:用户在使用产品时,可能无意地触发多笔交易,甚至没有响应而有意触发多笔交易
-
未关闭的重试机制:因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如Nginx重试、RPC通信重试或业务层重试等)
大致可以分为两大类:
- 第一类:单数据CRUD操作的幂等性保证方案
- 第二类:多数据并发操作的幂等性保证方案
第一类:单数据CRUD操作的幂等性保证方案
首先,来看看单数据CRUD操作的幂等性保证方案
对于单数据CRUD操作,很多具备天然幂等性
- 新增类动作:不具备幂等性
- 查询类动作:重复查询不会产生或变更新的数据,查询具有天然幂等性
- 更新类动作:
- 基于主键的计算式Update,不具备幂等性,即
UPDATE goods SET number=number-1 WHERE id=1
- 基于主键的非计算式Update:具备幂等性,即
UPDATE goods SET number=newNumber WHERE id=1
- 基于条件查询的Update,不一定具备幂等性(需要根据实际情况进行分析判断)
- 基于主键的计算式Update,不具备幂等性,即
- 删除类动作:
- 基于主键的Delete具备幂等性
- 一般业务层面都是逻辑删除(即update操作),而基于主键的逻辑删除操作也是具有幂等性的
大家看到,对于单数据CRUD操作, 只有在下面的三个场景,保证幂等即可:
- 主键的计算式Update
- 基于条件查询的Update
- 新增类动作
第二类:多数据并发操作的幂等性保证方案
大部分,都是这种场景。
现在的应用,大部分都是微服务的。并且一个操作会涉及到多个数据的并发操作,会通过RPC调用到多个微服务。
分为两种情况:
-
多数据同步操作,一般是服务端提供一个统一的同步操作api,客户端调用该api完成,直接获得操作结果。
-
多数据异步操作,由于同步操作性能低,在高并发场景都会同步变异步,于是乎,服务端还要额外提供一个查询操作结果的api,去查询结果。第一次超时之后,调用方调用查询接口,如果查到了就走成功的流程,失败了就走失败的流程。
多数据并发操作的经典场景,参考如下:
1. 高并发抢红包
在抢一份红包的时候,点击了抢,开始异步抢红包。
抢到就有,没抢到就没有。
抢完之后,无论我们重复点击多少次,红包都会提示你已经抢过该红包了。
2. 高并发下单
高并发下单的一个很基本的问题,就是要避免重复订单。
如果用户操作一次,由于超时重试等原因,一看下了两个单,甚至10个重复单。
用户不晕倒在厕所才怪。
3. 高并发支付
在支付场景,支付平台会生成唯一的支付连接,不会再次生成另外的支付连接。
如何保证幂等呢 ?
幂等性的的确保方案,非常多,大致如下图所示
一些基础性的幂等性解决方案
- 全局唯一ID
如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。
如果不存在则把全局ID,存储到存储系统中,比如数据库、Redis等。如果存在则表示该方法已经执行。
使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。
一般情况下,对分布式的全局唯一id,可以参考以下几种方式:
-
UUID
-
Snowflake
-
数据库自增ID
-
业务本身的唯一约束
-
业务字段+时间戳拼接
-
唯一索引(去重表)
这种方法适用于在业务中有唯一标识的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。
这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据写入去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。
- 插入或更新(upsert)
这种方法插入并且有唯一索引的情况,比如我们要关联商品品类,其中商品的ID和品类的ID可以构成唯一索引,并且在数据表中也增加了唯一索引。这时就可以使用InsertOrUpdate操作。
- 多版本控制
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等:
boolean updateGoodsName(int id,String newName,int version);
在实现时可以如下:
update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
- 状态机控制
这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100,付款失败为99。
在做状态机更新时,我们就可以这样控制:
update goods_order set status=#{status} where id=#{id} and status<#{status}
以上就是保证接口幂等性的一些方法。
综合性的解决方案:一锁二判三更新
前面的方案,都是一些基础性的方案。
在实际的业务中,一般会结合起来使用。
在双11和双12的活动中,对于幂等性问题,支付宝团队摸索出来了一个综合性的解决方案:一锁二判三更新。这个方案,可以作为一个比较通用的综合性的幂等解决方案。
何为“一锁二判三更新”?
简单来说就是当任何一个并发请求过来的时候
-
- 先锁定单据
-
- 然后判断单据状态,是否之前已经更新过对应状态了
-
3.1 如果之前并没有更新,则本次请求可以更新,并完成相关业务逻辑。
-
3.2 如果之前已经有更新,则本次不能更新,也不能完成业务逻辑。
一锁、二判、三更性的核心步骤
第一步:先加锁
高并发场景,建议是redis分布式锁,而不是低性能的DB锁,也不是CP型的 Zookeeper锁。
如果普通的redis分布式锁性能太低,该如何?
还可以考虑引入 锁的分段机制, 比如内部分成100端,总体上,就大概能线性提升 100倍。
第二步:进行幂等性判断
幂等性判断,就是 进行 数据检查。
可以基于状态机、流水表、唯一性索引等等前面介绍的 基础方案,进行重复操作的判断。
第三步:数据更新
如果通过了第二步的幂等性判断, 说明之前没有执行过更新操作。
那么就进入第三步,进行数据的更新,将数据进行持久化。
操作完成之后, 记得释放锁, 结束整个流程。
关于redis分布式锁、 Zookeeper锁、分段锁的内容,请参见5000页《尼恩Java面试宝典》的相应的专题。
说在最后:老马识途,有问题找老架构求助
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
最终,让面试官爱到 “不能自已、口水直流”。 offer, 也就来了。
其实, “offer自由” 不难实现, 前段时间一个跟尼恩卷了2年的武汉小伙,9年经验, 在年底大裁员的极度严寒/痛苦被裁的背景下, offer拿到手软, 实现真正的 “offer自由” 。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
尼恩一直深耕技术,不是在研究技术,就是在研究技术的路上,加尼恩微信之后不一定立马通过, 但是,最多1-2小时就会审核的。
深研技术,远离浮躁。作为资深技术人,尼恩实在太忙了.....
特别要说的是:很多小伙伴简历投出去后如泥牛入海、不冒一泡、没有面试机会。
遇到这种难题,可以找尼恩来改简历、做帮扶。
另外,遇到架构升级、晋升受阻、职业打击等职业难题,也可以找尼恩取经, 可以省去太多的折腾,省去太多的弯路。
尼恩已经指导了大量的小伙伴上岸,前段时间指导一个40岁+被裁小伙伴上岸,拿到了一个年薪100W的offer。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》