如何保证接口的幂等性?

这篇具有很好参考价值的文章主要介绍了如何保证接口的幂等性?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

作者:小牛呼噜噜 | https://xiaoniuhululu.com

计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「小牛呼噜噜」

目录
  • 什么是幂等性?
  • 为什么需要保证幂等性?
  • 接口幂等设计和防止重复提交可以等同吗?
  • 常用保证幂等性的措施
    • 先select再insert
    • 数据库设置唯一索引或唯一组合索引
    • 去重表
    • insert中加入exist条件判断
    • 悲观锁
    • 乐观锁
    • 状态机
    • 分布式锁
    • token机制
  • 尾语

什么是幂等性?

大家好,我是呼噜噜,所谓幂等性就是:任意次数请求 同一个资源,对资源的状态产生的影响和执行一次请求是相同的
比如对于接口来说,无论调用多少次同一个接口,对资源的状态都只产生一次影响

为什么需要保证幂等性?

为什么需要做接口的幂等性?如果不做会发生什么事情?我们在实际企业开发过程中,如果仅是对数据库进行查询、删除指定记录操作,重复提交是没啥问题的。但是如果是新增或者修改操作,就需要考虑重复提交的问题。
比如,如果一个订单支付的时候,因各种原因重复提交多次,那如果没有幂等性处理的话,这个订单将会被支付多次的钱,这种和钱有关的错误是绝对不能容忍的。

经常发生重复提交的场景:

  1. 当我们在公司的系统里面,提交表格,前端没有对保存按钮的做控制,可以多次点击,然后我们又不小心快速点了多次,或者是网络卡顿, 还是其他原因,以为没有成功提交,就一直点击保存按钮,这样都会产生重复提交表单请求。
  2. 在实际开发过程中,网络波动是常有的事,所以很多时候 HTTP 客户端工具都默认开启超时重试的机制,这样就无法避免产生重复的请求。
  3. 还有就是项目可能使用一些中间件,比如kafka消费生产者产生的消息时,可能读到重复的消息,这样也会产生重复的请求。
  4. ......

接口幂等设计和防止重复提交可以等同吗?

接口幂等和防止重复提交有交集,但是严格来说并不完全等同

  1. 防重设计,主要从客户端/前端的角度来解决,主要为了避免重复提交,对每次请求的返回结果无限制,前端常见的手段:点击提交按钮变灰、点击后跳转结果页、每次页面初始化生成随机码,提交时随机码缓存,后续重复的随机码请求直接不提交
  2. 幂等设计,强调更多地是当重复提交请求无法避免的时候,还能保证每次请求都返回一样的结果。像我们上面对前端做的限制,是能绕过去的,抓包是能直接把接口给抓出来的,比如恶意批量调用接口,所以企业级系统,前后端都需要做限制,特别是涉及到钱的业务。绝不能偷懒,后面我们来详细讲讲对接口幂等的限制。

常用保证幂等性的措施

先select再insert

新手小白,在往数据库插入数据时,为了防止重复插入,一般会在insert前,通过关键字去先select一下,如果查不到记录就执行insert操作,否则就不插入

如何保证接口的幂等性?
但如果并发场景下,这个就不行了。比如线程2,在线程1插入数据前,执行select,最终它也会去执行插入操作,这样就会产生2条记录。所以在实际开发过程中,是不建议如此操作的。

数据库设置唯一索引或唯一组合索引

数据库设置唯一索引是我们最常用的方式,一个非常简单,并且有效的方案。当记录多次插入数据库,会由于Id或者关键字段索引唯一的限制,导致后续记录插入失败

--创建唯一索引
alter table `order` add UNIQUE KEY `索引名` (`字段`);

第一条记录插入到数据库中,当后面其他相同的请求,再插入时,数据库会报异常Duplicate entry 'xx' for key 'xx_name',这个异常不会对数据库中既有的数据有影响,我们只需对异常进行捕获就行,直接返回,代表已经执行过当前请求。

笔者这里介绍一个骚操作:INSERT IGNORE

insert ignore INTO tableName VALUES ("id","xxx")

咦,会有读者觉得,这样哪怕索引冲突了,数据库会忽略错误返回影响行数0,这样就不用再在代码中,手动捕捉异常了,又方便又省事!

但事实真这样吗???
如何保证接口的幂等性?
如果希望在每次插入新记录时,自动地创建主键字段的值。一般会将主键id的属性设为AUTO_INCREMENT
如果我们使用INSERT IGNORE时,没有成功新增记录,但是AUTO_INCREMENT会自动+1,binlog中也没有 INSERT IGNORE 语句日志。这个会导致主从数据一致性问题,如果线上环境数据库是主从架构,从库该字段的AUTO_INCREMENT值会和主库不一致,切库(从库变成总库)的时候会冲突。

当然,查询Mysql官方手册,发现innodb_autoinc_lock_mode用于平衡性能与主从数据一致性innodb_autoinc_lock_mode=0可以解决这个问题,将其设为0后, 所有的insert语句都要在语句开始的时候得到一个表级的auto_inc锁,在语句结束的时候才释放这把锁。也就是说在INSERT未成功执行时AUTO_INCREMENT不会自增,但是其也有缺点,会影响到数据库的并发插入性能。

Mysql官方手册明确指出,The setting innodb_autoinc_lock_mode=0 should not be used except for compatibility purposes.除非出于兼容性目的,否则不应设置innodb_autoinc_lock_mode=0。所以我们还是老老实实手动捕捉异常,慎用insert ignore

**innodb_autoinc_lock_mode: **在MySQL8中, 默认值为 2 (轻量级锁) , 在MySQL8之前, 5.1之后, 默认值为 1(混合使用这2种锁), 在更早的版本是 0(auto_inc锁)

去重表

去重表,其实也是唯一索引方案的一个变种,原表不太适合再新建唯一索引了,且数据量不大的话。我们可以再新建一张去重表,把唯一标识作为唯一索引,然后把对原表的操作和同时新增去重表 ,放在一个事务中,如果重复创建,去重表会抛出唯一约束异常,事务里所有的操作就会回滚。

insert中加入exist条件判断

有时候我们会遇到非常复杂的表,表结构确定了,比如已经有了许多索引字段,不太适合再新建索引的时候,呼噜噜 在这里再提供一个"骚操作":可以通过insert中加入exist来解决重复插入的问题。
比如:

insert into order(id,code,password)
select ${id},${code},${password}
from order
where not exists(select 1 from order where code = ${code}) limit 0,1;

上面的sql注意思路就是将查询和插入写在同一个sql中,需要注意的是limit 0,1最后一定要加上,不然可能会出现重复插入的情况

悲观锁

悲观锁,顾名思义就是,对数据被外界或者内部修改处理时,持"悲观"态度,总认为会发生并发冲突,所以会在整个数据处理过程中,将数据锁定。
悲观锁的实现,通常依靠数据库提供的锁机制实现,在这里以mysql为例,最典型的就是"for update"。

select * from order where id = "xxxx" for update; 

需要注意的是:使用悲观锁,需要先关闭mysql的自动提交功能,将 set autocommit = 0;

for update仅适用于Mysql中lnnoDB引擎,默认是行级锁,如果sql中有明确指定的主键时候,是行级锁,如果没有,会锁表(非常危险的操作)。for update一般和事务配合使用,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行。直到显示提交事务(由于关闭了mysql的自动提交)时,for update获取的锁会自动释放。

悲观锁虽然保证了数据处理的安全性,但会严重影响并发效率,降低系统吞吐量。适用于并发量不大、又对数据一致性比较高的场景。

乐观锁

乐观锁,和悲观锁相反,对数据被外界或者内部修改处理时,持"乐观"态度,总认为不会发生并发冲突,所以不会上锁,只需在更新的时候会去判断一下在此期间有没有去更新这个数据。

一般是使用版本号或者时间戳,比如

  1. 我们在数据库中,给订单表增加一个version 字段
  2. select数据时,将version一起读出,当提交数据更新时,判断版本号是否和取出来的是否一致。如果不一致就代表,已更新,那就不更新。如果一致就继续执行更新操作。
  3. 每次更新时,除了更新指定的字段,也要将version进行+1操作
update order set name=#{xxx},version=#{version} where id=#{id} and version < ${version}

不加锁就能保证幂等性,又增加了系统吞吐量,如果频繁触发版本号不一致的情况,反而降低了性能。

状态机

状态机也是乐观锁的一种,比如企业级货品管理系统中,订单的转单流程,将订单的状态,设置为有限的几个(1-下单、2-已支付、3-完成、4-发货、5-退货),通过各个状态依次执行转换,来控制订单转单的流程,是非常好的选择。

分布式锁

上面介绍了许多方案,在单体应用中是没啥问题的,但是随着时代的发展,现在微服务大行其道,以上方法就不太适应了。

在分布式系统中,上面唯一索引对于全局来说,是无法确定的,我们可以引入第三方分布式锁来保证幂等性设计。分布式锁,主要是用来 当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问

实现分布式锁常见的方法有:基于redis实现分布式锁,基于 Consul 实现分布式锁,基于 zookeeper实现分布式锁等等,本文重点介绍最常见的基于redis实现分布式锁,set NX PX + Lua
如何保证接口的幂等性?

  1. 在分布式系统中,插入或者更新的请求,业务逻辑中先获取唯一业务字段,比如订单id之类的,接着需要获取分布式锁,对redis执行下述命令
SET key value NX PX 30000

各参数的含义:

  • SET: 在Redis 2.6.12之后,set命令整合了setex命令 的功能,支持了原子命令加锁和设置过期时间的功能
  • key:业务逻辑中先获取唯一业务字段,比如订单id,code之类,也可以在前面加一些系统参数当前缀,这个完全可以自定义
  • value: 填入是一串随机值,必须保证全局唯一性(在释放锁时,我们需要对value进行验证,防止误释放),一般用uuid来实现
  • NX: 表示key不存在时才设置,如果存在则返回 null。还有另一个参数XX,表示key存在时才设置,如果不存在则返回NULL
  • PX 30000: 表示过期时间30000毫秒,指到30秒后,key将被自动删除。这个非常的重要,如果设置过短,无法有效的防止重复请求,过长的话会浪费redis的空间
  1. 然后执行插入或者更新,或者其他相关业务逻辑,在释放锁之前,如果有其他中心的服务来请求,由于key是一样的,无法获取锁,就代表这些是重复请求,不操作,直接返回
  2. 执行完插入或者更新后,需要释放锁,一定要判断释放的锁的value和与Redis内存储的value是否一致,不然如果直接删除的话,会把其他中心服务的锁释放调。

这种先查再删的2步操作,我们可以使用lua脚本,把他们变成一个"原子操作"

Lua 是一种轻量小巧的脚本语言,Redis会将整个脚本作为一个整体执行,中间不会被其他命令打断插入(l类似与事务),可以减少网络开销,方便复用

以下是Lua脚本,通过 Redis 的 eval/evalsha 命令来运行:

if redis.call('get', KEYS[1]) == ARGV[1] //判断value是否一致
    then
        return redis.call('del', KEYS[1])//删除key
    else
        return 0
end

这样依靠单体的redis实现的分布式锁能够很好的解决,微服务系统的幂等问题。但是有些公司的微服务更加庞大,redis也是集群的话,set NX PX + Lua就不够看了,这里介绍Redis作者推荐的方法-Redlock算法,这里就先不展开讲了,不然文章篇幅过长。先挖个坑,后面有空填一下:)

token机制

最后再补充一个方案利用token机制,每次调用接口时,使用token来标识请求的唯一性。token也叫令牌,天然适合微服务。基于token+redis来设计幂等的思路还是比较简单的,和分布式锁类似:

  1. 客户端发送请求,得去服务端获取一个全局唯一的一串随机字符串作为Token 令牌(每次请求获取到的都是一个全新的令牌),把令牌保存到 redis 中,需要有过期时间,同时把这个 ID 返回给客户端
  2. 客户端第二次调用业务请求的时候必须携带这个 token,服务端会去校验redis中是否有该token。如果存在,表示这是第一次请求,删除缓存中的token(这边还是建议用lua脚本,保证操作的原子性);如果缓存中不存在,表示重复请求,直接返回。

尾语

幂等性是系统服务对外一种承诺,特别业务中涉及的钱的部分,一定要慎重再慎重。虽然前端做限制会更容易点,但前后端都需要做努力,除了本文介绍的常见的方案,大家也可以集思广益,毕竟技术在发展,单体到集群分布式,还会继续发展,还有有新的问题产生。
本文虽然通篇在将幂等的重要性和如何实现幂等,但不可否认,幂等肯定导致系统吞吐量、并发能力的下降,企业级应用还是得根据业务,权衡利弊,感谢大家的阅读。

参考资料:
https://www.cnblogs.com/linjiqin/p/9678022.html


全文完,感谢您的阅读,如果我的文章对你有所帮助的话,还请点个免费的,你的支持会激励我输出更高质量的文章,感谢!

如何保证接口的幂等性?文章来源地址https://www.toymoban.com/news/detail-711606.html

到了这里,关于如何保证接口的幂等性?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 腾讯二面:如何保证接口幂等性?高并发下的接口幂等性如何实现?

    什么是接口幂等性 接口幂等性这一概念源于数学,原意是指一个操作如果连续执行多次所产生的结果与仅执行一次的效果相同,那么我们就称这个操作是幂等的。在互联网领域,特别是在Web服务、API设计和分布式系统中,接口幂等性具有非常重要的意义。 具体到HTTP接口或者

    2024年03月19日
    浏览(46)
  • redis+token+分布式锁确保接口的幂等性

    目录 1.幂等性是什么? 2.如何实现幂等性呢? 1.新增管理员,出弹窗的同时,请求后台。 2.后端根据雪花算法生成唯一标识key,以雪花数为key存到redis。并返回key给前端。 3.前端保存后端传过来的key。 4.前端输入完成信息,点击【保存】,携带key请求后端。 5.请求到达后端,

    2024年02月14日
    浏览(46)
  • 114 接口中幂等性的保证

    同样是 面试问题  如何确保接口的 幂等性  幂等是一个 较为抽象的概念, 多次重复访问, 不会导致业务逻辑的异常  这里从增删改查, 几个方面列一下  一般来说, 我们核心需要关注的就是 新增 和 更新 对于 增加元素, 首先针对唯一约束进行校验, 然后再处理新增的相

    2024年04月23日
    浏览(33)
  • RabbitMQ如何保证幂等性

    一、简介 幂等性是分布式中比较重要的一个概念,是指在多作业操作时候避免造成重复影响,其实就是保证同一个消息不被消费者重复消费两次,但是可能存在网络波动等问题,生产者无法接受消费者发送的ack信息,因此这条消息将会被重复发送给其他消费者进行消费,实际

    2024年02月15日
    浏览(44)
  • RabbitMQ-业务的幂等性

    生产者和消费者都需要添加配置类: 消费者拿到id之后,保存到数据库,后续消费时,需要查数据库进行比较,因此这种方案的缺点就是有业务的入侵,对性有一定的影响。 (1)查询和删除操作本身就是幂等性操作。 (2)可以使用分布式锁,对单据id锁定,防止多次提交,

    2024年01月21日
    浏览(43)
  • 谈谈 Kafka 的幂等性 Producer

    使用消息队列,我们肯定希望不丢消息,也就是消息队列组件,需要保证消息的可靠交付。消息交付的可靠性保障,有以下三种承诺: 最多一次(at most once):消息可能会丢失,但绝不会被重复发送。 至少一次(at least once):消息不会丢失,但有可能被重复发送。 精确一次

    2024年02月14日
    浏览(39)
  • 【RabbitMQ】RabbitMQ如何确认消息被消费、以及保证消息的幂等

    目录 一、如何保证消息被消费 二、如何保证消息幂等性 RabbitMQ提供了消息补偿机制来保证消息被消费,当一条消费被发送后,到达队列后发给消费者。消费者消费成功后会给MQ服务器的队列发送一个确认消息,此时会有一个回调检测服务监听该接收确认消息的队列,然将消费

    2024年02月16日
    浏览(40)
  • Java接口幂等性,如何重试?

    当我们写好一个项目时,有没有深深的思考过,如何处理接口幂等性问题呢?今天我们以屈原这句著名诗句“路漫漫其修远兮,吾将上下而求索”的精神来探索一下这个问题。 幂等性:简单来说就是一个操作多次执行的结果和一次执行产生的结果一致。 答:在计算机应用中

    2024年02月10日
    浏览(54)
  • 高并发下如何保证接口幂等

    接口幂等性问题,对于开发人员来说,是一个跟语言无关的公共问题。本文分享了一些解决这类问题非常实用的办法,绝大部分内容我在项目中实践过的,给有需要的小伙伴一个参考。 不知道你有没有遇到过这些场景: 有时我们在填写某些form表单时,保存按钮不小心快速点

    2023年04月09日
    浏览(40)
  • kafka-保证数据不重复-生产者开启幂等性和事务的作用?

    适用于消息在写入到服务器日志后,由于网络故障,生产者没有及时收到服务端的 ACK 消息,生产者误以为消息没有持久化到服务端,导致生产者重复发送该消息,造成了消息的重复现象,而幂等性就是为了解决该问题。 通过3个值的唯一性去重: PID:生产者ID 分区号 seq:单

    2024年02月14日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包