接口幂等性
问题,对于开发人员来说,是一个跟语言无关的公共问题。本文分享了一些解决这类问题非常实用的办法,绝大部分内容我在项目中实践过的,给有需要的小伙伴一个参考。
不知道你有没有遇到过这些场景:
form表单
时,保存按钮不小心快速点了两次,表中竟然产生了两条重复的数据,只是id不一样。接口超时
问题,通常会引入了重试机制
。第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),为了避免返回错误的结果(这种情况不可能直接返回失败吧?),于是会对该请求重试几次,这样也会产生重复的数据。重复消息
(至于什么原因这里先不说,有兴趣的小伙伴,可以找我私聊),如果处理不好,也会产生重复的数据。没错,这些都是幂等性问题。
接口幂等性
是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
这类问题多发于接口的:
insert
操作,这种情况下多次请求,可能会产生重复数据。update
操作,如果只是单纯的更新数据,比如:update user set status=1 where id=1
,是没有问题的。如果还有计算,比如:update user set status=status+1 where id=1
,这种情况下多次请求,可能会导致数据错误。那么我们要如何保证接口幂等性?本文将会告诉你答案。
通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在insert
前,先根据name
或code
字段select
一下数据。如果该数据已存在,则执行update
操作,如果不存在,才执行 insert
操作。
该方案可能是我们平时在防止产生重复数据时,使用最多的方案。但是该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用,否则同样会产生重复数据。我在这里提一下,是为了避免大家踩坑。
在支付场景中,用户A的账号余额有150元,想转出100元,正常情况下用户A的余额只剩50元。一般情况下,sql是这样的:
update user amount = amount-100 where id=123;
如果出现多次相同的请求,可能会导致用户A的余额变成负数。这种情况,用户A来可能要哭了。于此同时,系统开发人员可能也要哭了,因为这是很严重的系统bug。
为了解决这个问题,可以加悲观锁,将用户A的那行数据锁住,在同一时刻只允许一个请求获得锁,更新数据,其他的请求则等待。
通常情况下通过如下sql锁住单行数据:
select * from user id=123 for update;
具体流程如下:
具体步骤:
需要特别注意的是:如果使用的是mysql数据库,存储引擎必须用innodb,因为它才支持事务。此外,这里id字段一定要是主键或者唯一索引,不然会锁住整张表。
悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。在这里顺便说一下,防重设计
和 幂等设计
,其实是有区别的。防重设计主要为了避免产生重复数据,对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。
既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观锁。需要在表中增加一个timestamp
或者version
字段,这里以version
字段为例。
在更新数据之前先查询一下数据:
select id,amount,version from user id=123;
如果数据存在,假设查到的version
等于1
,再使用id
和version
字段作为查询条件更新数据:
update user set amount=amount+100,version=version+1
where id=123 and version=1;
更新数据的同时version+1
,然后判断本次update
操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。
由于第一次请求version
等于1
是可以成功的,操作成功后version
变成2
了。这时如果并发的请求过来,再执行相同的sql:
update user set amount=amount+100,version=version+1
where id=123 and version=1;
该update
操作不会真正更新数据,最终sql的执行结果影响行数是0
,因为version
已经变成2
了,where
中的version=1
肯定无法满足条件。但为了保证接口幂等性,接口可以直接返回成功,因为version
值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。
具体流程如下:
具体步骤:
绝大数情况下,为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案。
alter table `order` add UNIQUE KEY `un_code` (`code`);
加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry '002' for key 'order.un_code
异常,表示唯一索引有冲突。
虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。
如果是java
程序需要捕获:DuplicateKeyException
异常,如果使用了spring
框架还需要捕获:MySQLIntegrityConstraintViolationException
异常。
具体流程图如下:
具体步骤:
有时候表中并非所有的场景都不允许产生重复的数据,只有某些特定场景才不允许。这时候,直接在表中加唯一索引,显然是不太合适的。
针对这种情况,我们可以通过建防重表
来解决问题。
该表可以只包含两个字段:id
和 唯一索引
,唯一索引可以是多个字段比如:name、code等组合起来的唯一标识,例如:susan_0001。
具体流程图如下:
具体步骤:
需要特别注意的是:防重表和业务表必须在同一个数据库中,并且操作要在同一个事务中。
很多时候业务表是有状态的,比如订单表中有:1-下单、2-已支付、3-完成、4-撤销等状态。如果这些状态的值是有规律的,按照业务节点正好是从小到大,我们就能通过它来保证接口的幂等性。
假如id=123的订单状态是已支付
,现在要变成完成
状态。
update `order` set status=3 where id=123 and status=2;
第一次请求时,该订单的状态是已支付
,值是2
,所以该update
语句可以正常更新数据,sql执行结果的影响行数是1
,订单状态变成了3
。
后面有相同的请求过来,再执行相同的sql时,由于订单状态变成了3
,再用status=2
作为条件,无法查询出需要更新的数据,所以最终sql执行结果的影响行数是0
,即不会真正的更新数据。但为了保证接口幂等性,影响行数是0
时,接口也可以直接返回成功。
具体流程图如下:
具体步骤:
1 . 用户通过浏览器发起请求,服务端收集数据。 2 . 根据id和当前状态作为条件,更新成下一个状态 3 . 判断操作影响行数,如果影响了1行,说明当前操作成功,可以进行其他数据操作。 4 . 如果影响了0行,说明是重复请求,直接返回成功。
主要特别注意的是,该方案仅限于要更新的
表有状态字段
,并且刚好要更新状态字段
的这种特殊情况,并非所有场景都适用。
其实前面介绍过的加唯一索引
或者加防重表
,本质是使用了数据库
的分布式锁
,也属于分布式锁的一种。但由于数据库分布式锁
的性能不太好,我们可以改用:redis
或zookeeper
。
鉴于现在很多公司分布式配置中心改用apollo
或nacos
,已经很少用zookeeper
了,我们以redis
为例介绍分布式锁。
目前主要有三种方式实现redis的分布式锁:
1 . setNx命令 2 . set命令 3 . Redission框架
每种方案各有利弊,具体实现细节我就不说了,有兴趣的朋友可以加我微信找我私聊。
具体流程图如下:
具体步骤:
1 . 用户通过浏览器发起请求,服务端会收集数据,并且生成订单号code作为唯一业务字段。 2 . 使用redis的set命令,将该订单code设置到redis中,同时设置超时时间。 3 . 判断是否设置成功,如果设置成功,说明是第一次请求,则进行数据操作。 4 . 如果设置失败,说明是重复请求,则直接返回成功。
需要特别注意的是:分布式锁一定要设置一个合理的过期时间,如果设置过短,无法有效的防止重复请求。如果设置过长,可能会浪费
redis
的存储空间,需要根据实际业务情况而定。
除了上述方案之外,还有最后一种使用token
的方案。该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。
1 . 第一次请求获取token
2 . 第二次请求带着这个token
,完成业务操作。
具体流程图如下:
第一步,先获取token。
第二步,做具体业务操作。
具体步骤:
1 . 用户访问页面时,浏览器自动发起获取token请求。 2 . 服务端生成token,保存到redis中,然后返回给浏览器。 3 . 用户通过浏览器发起请求时,携带该token。 4 . 在redis中查询该token是否存在,如果不存在,说明是第一次请求,做则后续的数据操作。 5 . 如果存在,说明是重复请求,则直接返回成功。 6 . 在redis中token会在过期时间之后,被自动删除。
以上方案是针对幂等设计的。
如果是防重设计,流程图要改改:
需要特别注意的是:token必须是全局唯一的。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/2dpjLAxILJE9md1XKOPUKA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。