一、什么是幂等
1.1 幂等性
多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。
1.2 幂等性接口
是指可以使用相同参数重复执行,并能获得相同结果的接口。
1.3 计算机学中
幂等指多次操作产生的影响只会跟一次执行的结果相同,通俗的说:某个行为重复的执行,最终获取的结果是相同的,不会因为重复执行对系统造成变化。
1.4 补充
HTTP GET方法用于获取资源,一次请求和多次请求获取的资源都是相同的,所以是幂等的。比如:GET http://www.jianshu.com/account/123456,不会改变资源的状态,不论调用一次还是N次结果都一样。
除了"包含多次操作,结果相同"的意思以外,幂等还侧重于强调"操作的副作用”"。如上所述,HTTP GET强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同。GET http://www.jianshu.com/latest-news这个HTTP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的。
二、防重和幂等的区别
防重设计主要为了避免产生重复数据,对接口返回没有太多要求。
而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。
三、需要注意幂等问题的接口
3.1 insert操作
insert操作,这种情况下多次请求,可能会产生重复数据。
3.2 update操作
如果只是单纯的更新数据,比如:update user set status=1 where id=1,是没有问题的。如果还有计算,比如:update user set status=status+1 where id=1,这种情况下多次请求,可能会导致数据错误。
四、产生幂等问题的常见原因
4.1 前端重复提交
用户注册,用户创建商品等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录。如果用户不小心多点了几次,后端收到了好几次提交,这时就会在数据库中重复创建了多条记录。这就是接口没有幂等性带来的 bug。
4.2 接口超时重试
对于给第三方调用的接口,有可能会因为网络原因而调用失败,这时,一般在设计的时候会对接口调用加上失败重试的机制。如果第一次调用已经执行了一半时,发生了网络异常。这时再次调用时就会因为脏数据的存在而出现调用异常。
4.3 消息重复消费
在使用消息中间件来处理消息队列,且手动 ack 确认消息被正常消费时。如果消费者突然断开连接,那么已经执行了一半的消息会重新放回队列。当消息被其他消费者重新消费时,如果没有幂等性,就会导致消息重复消费时结果异常,如数据库重复数据,数据库数据冲突,资源重复等。
4.4 高并发场景
高并发也是产生接口幂等性问题的重要原因,平时也是我们开发中容易忽略的方面。一般来说,重要、核心接口我们都需要通过压测来检测接口的幂等性。
五、幂等解决方案
5.1 按钮只能点击一次
这是前端保证幂等性的一种方法,用户点击按钮后将按钮置灰,或者显示loading状态。
5.2 RPG模式
这也是前端保证幂等性的一种方法,即Post-Redirect-Get,当客户提交表单后,去执行一个客户端的重定向,转到提交成功页面。避免用户按F5刷新致使的重复提交,也能消除按浏览器后退键致使的重复提交问题。目前绝大多数公司都是这样作的,好比淘宝,京东等。
5.3 使用惟一索引
绝大数情况下,为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案。
alter table `order` add UNIQUE KEY `un_code` (`code`);
加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry '002' for key 'order.un_code异常,表示唯一索引有冲突。
虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。如果是java程序需要捕获:DuplicateKeyException异常,如果使用了spring框架还需要捕获:MySQLIntegrityConstraintViolationException异常。
5.4 防重表
增长一个防重表,业务惟一的id做为惟一索引,如订单号,当想针对订单作一系列操做时,能够向防重表中插入一条记录,插入成功,执行后续操做,插入失败,则不执行后续操做。本质上能够当作是基于MySQL实现的分布式锁。根据业务场景决定执行成功后,是否删除防重表中对应的数据。
5.5 乐观锁实现幂等
1、查询数据得到版本号。
2、经过版本号去更新,版本号匹配则更新,版本号不匹配则不更新。
-- 假如查询出的version为1
select version from table_name where userid = 10;
-- 给用户的帐户加10
update table_name set money = money -10, version = version + 1 where userid = 10 and version = 1
也能够经过条件来实现乐观锁,如库存不能超卖,数量不能小于0
update table_name set num = num - 10 where num - 10 >= 0
5.6 悲观锁
悲观锁认为别人每次去拿数据都会修改这条数据,所以每次拿数据的时候,都会使数据处于锁定状态。(该方案影响接口性能)
select * from test where user_id =123 and act_id='ac' for update
这里并没有使用主键 id 是查询,首先我们并不知道这条记录 id 值,所以我们通过 uid+aid 组合的唯一建作为锁表行记录条件,一定要使用主键或者唯一建,不然会将整张表都被锁住,那么其他的用户就无法操作了。
需要特别注意的是:如果使用的是mysql数据库,存储引擎必须用innodb,因为它才支持事务。
悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。
5.7 分布式锁实现幂等
执行方法时,先根据业务惟一的id获取分布式锁,获取成功,则执行,失败则不执行。在Java开发中,通常采用Redission实现分布式锁。
注意:JVM锁实现是指通过JVM提供的内置锁,如Lock、synchronize,只能用于单机环境,在分布式环境下会失效。
5.8 全局id或token(前后端同时处理)
在提交的请求header中增加一个全局id或token,这个全局id或token需要提前向后端获取,提交的时候把这个id或token一起提交过来,全局判断id或token是否已经处理,来支持幂等。
实现
5.9 状态机幂等
若是业务上须要修改订单状态,例如订单状态有:1-下单、2-已支付、3-完成、4-撤销等状态。设计时最好只支持状态的单向改变。这样在更新的时候就能够加上条件,屡次调用也只会执行一次。例如想把订单状态更新为完成状态,则以前的状态必须为已支付。
update `order` set status=3 where id=123 and status=2;
第一次请求时,该订单的状态是已支付,值是2,所以该update语句可以正常更新数据,sql执行结果的影响行数是1,订单状态变成了3。
后面有相同的请求过来,再执行相同的sql时,由于订单状态变成了3,再用status=2作为条件,无法查询出需要更新的数据,所以最终sql执行结果的影响行数是0,即不会真正的更新数据。但为了保证接口幂等性,影响行数是0时,接口也可以直接返回成功。
六、不适合的方案
6.1 insert前先select
通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在insert前,先根据name或code字段select一下数据。如果该数据已存在,则执行update操作,如果不存在,才执行 insert操作。
该方案可能是我们平时在防止产生重复数据时,使用最多的方案。但是该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用(比如分布式锁),否则同样会产生重复数据。







网友评论