SpringBoot第19讲:SpringBoot 如何保证接口幂等

SpringBoot第19讲:SpringBoot 如何保证接口幂等

在以SpringBoot开发Restful接口时,如何防止接口的重复提交呢? 本文是SpringBoot第19讲,主要介绍接口幂等相关的知识点,并实践常见基于Token实现接口幂等。

文章目录

  • SpringBoot第19讲:SpringBoot 如何保证接口幂等
    • 1、准备知识点
      • 1.1、什么是幂等?
      • 1.2、什么是接口幂等?
    • 2、常见的保证幂等的方式?
      • 2.1、数据库层面
        • 1、悲观锁
        • 2、唯一ID/索引
        • 3、乐观锁(基于版本号或者时间戳)
      • 2.2、分布式锁
      • 2.3、防重 Token 令牌
    • 3、示例源码
    • 4、参考文章

1、准备知识点

从幂等和防止重复提交,接口幂等和常见的保证幂等的方式等知识点构筑知识体系。

1.1、什么是幂等?

幂等原先是数学中的一个概念,表示进行1次变换和进行N次变换产生的效果相同。

当我们讨论接口的幂等性时一般是在说:以相同的请求调用这个接口一次和调用这个接口多次,对系统产生的影响是相同的。如果一个接口满足这个特性,那么我们就说这个接口是一个幂等接口。

  • 接口幂等和防止重复提交是一回事吗
    • 严格来说,并不是。
  1. 幂等: 更多的是在重复请求已经发生,或是无法避免的情况下,采取一定的技术手段让这些重复请求不给系统带来副作用。
  2. 防止重复: 提交更多的是不让用户发起多次一样的请求。比如说用户在线购物下单时点了提交订单按钮,但是由于网络原因响应很慢,此时用户比较心急多次点击了订单提交按钮。 这种情况下就可能会造成多次下单。一般防止重复提交的方案有:将订单按钮置灰,跳转到结果页等。主要还是从客户端的角度来解决这个问题。
    1. 防重提交可以参考这篇文章:项目实战第四十二讲:分布式环境下,使用ResubmitCheck注解进行防重校验
  • 哪些情况下客户端是防止不了重复提交的

    • 虽然我们可在客户端做一些防止接口重复提交的事(比如将订单按钮置灰,跳转到结果页等), 但是如下情况依然客户端是很难控制接口重复提交到后台的,这也进一步表明了接口幂等和防止重复提交不是一回事以及后端接口保证接口幂等的必要性所在。

      1、接口超时重试:接口可能会因为某些原因而调用失败,出于容错性考虑会加上失败重试的机制。如果接口调用一半,再次调用就会因为脏数据的存在而出现异常。

      2、消息重复消费:在使用消息中间件来处理消息队列,且手动ack确认消息被正常消费时。如果消费者突然断开连接,那么已经执行了一半的消息会重新放回队列。被其他消费者重新消费时就会导致结果异常,如数据库重复数据,数据库数据冲突,资源重复等。

      3、请求重发:网络抖动引发的nginx重发请求,造成重复调用;

1.2、什么是接口幂等?

在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。

这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

  • 对哪些类型的接口需要保证接口幂等

我们看下标准的restful请求,幂等情况是怎么样的:

  1. SELECT查询操作
    1. GET:只是获取资源,对资源本身没有任何副作用,天然的幂等性。
    2. HEAD:本质上和GET一样,获取头信息,主要是探活的作用,具有幂等性。
    3. OPTIONS:获取当前URL所支持的方法,因此也是具有幂等性的。
  2. DELETE删除操作
    1. 删除的操作,如果从删除的一次和删除多次的角度看,数据并不会变化,这个角度看它是幂等的
    2. 但是如果,从另外一个角度,删除数据一般是返回受影响的行数,删除一次和多次删除返回的受影响行数是不一样的,所以从这个角度它需要保证幂等。(折中而言DELETE操作通常也会被纳入保证接口幂等的要求)
      1. 项目开发中一般不返回删除数据受影响的行数
  3. ADD/EDIT操作
    1. PUT:用于更新资源,有副作用,但是它应该满足幂等性,比如根据id更新数据,调用多次和N次的作用是相同的(根据业务需求而变)。
    2. POST:用于添加资源,多次提交很可能产生副作用,比如订单提交,多次提交很可能产生多笔订单。

2、常见的保证幂等的方式?

我们来看下常见的保证幂等的方式。

2.1、数据库层面

1、悲观锁

典型的数据库悲观锁:for update

select * from t_order where order_id = trade_no for update;

为什么加for update就可以?

  1. 当线程A执行for update,数据会对当前记录加锁,其他线程执行到此行代码的时候,会等待线程A释放锁之后,才可以获取锁,继续后续操作。
  2. 事物提交时,for update获取的锁会自动释放。

PS:这种方式很少被使用,因为如果业务处理比较耗时,并发情况下,后面线程会长期处于等待状态,占用了很多线程,让这些线程处于无效等待状态,我们的web服务中的线程数量一般都是有限的,如果大量线程由于获取for update锁处于等待状态,不利于系统并发操作

  • 项目中没有采用该方案

可以参考这篇文章:MySQL第八讲:数据库事务及MVCC机制/分布式事务实战

2、唯一ID/索引

针对的是插入操作。

数据库唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录

使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性

  • 使用snowflake算法,也可以机采用数据库号段模式,或者redis自增等方式

  • 去重表

去重表本质上也是一种唯一索引方案。

这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚

  • SPU创建编辑采用的这种方案,使用类目id+关键属性作为唯一标

  • # 表 spu审核锁定表
    create table zcy_spu_audit_lock(category_id       bigint                             not null comment '冗余类目id',key_property_text varchar(255)                       not null comment '关键属性',org_id            bigint                             null comment '提交人机构id,映射orgId',org_name          varchar(50)                        null comment '来源,一般是提交人机构名,映射orgName',user_id           bigint                             null comment '创建人id,映射userId',operator          varchar(50)                        null comment '创建人,映射operator',created_at        datetime                           null comment '创建时间',primary key (category_id, key_property_text)
    ) comment 'spu审核锁定表,主用于判重';
    

3、乐观锁(基于版本号或者时间戳)

针对更新操作。

  • 使用版本号或者时间戳

这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等

boolean updateGoodsName(int id, String newName, int version);

在实现时可以如下

update goods set name=#{newName},version=#{version}+1 where id=#{id} and version=#{version}

采用该方案的业务

<!-- 库存采用了这样的方案 -->
<update id="updateAvailableAndlockStockQuantity" parameterType="map">UPDATE<include refid="tb"/><set><if test="availableQuantity != null">available_quantity = #{availableQuantity},</if><if test="lockStockQuantity != null">lock_stock_quantity = #{lockStockQuantity},</if>update_at = now(), version = version + 1</set>where id = #{id} and sku_id = #{skuId} and warehouse_code = #{warehouseCode} and version = #{version}
</update>
<update id="batchUpdateStockAvailableQuantity"  parameterType="java.util.List"><foreach collection="list" item="stock" index="index" open="" close="" separator=";">UPDATE <include refid="tb"/><set>available_quantity = #{stock.availableQuantity},update_at = now(),version = version + 1</set>WHERE id = #{stock.id} and version = #{stock.version} and sku_id = #{stock.skuId}</foreach>
</update><delete id="deleteBySkuIdAndWarehouseCode" parameterType="java.util.Map">delete from  <include refid="tb"/>where sku_id = #{skuId} and warehouse_code = #{warehouseCode} and version = #{version}
</delete><!-- 商品拓展信息表 -->
<update id="update" parameterType="cn.gov.zcy.service.item.domain.ItemExtInfo"><!--@mbg.generated-->update zcy_item_ext_info<set><if test="extJson != null">ext_json = #{extJson,jdbcType=VARCHAR},</if><if test="status != null">`status` = #{status,jdbcType=INTEGER},</if><if test="updateId != null">update_id = #{updateId,jdbcType=BIGINT},</if>updated_at = now(),version = version+1</set>where id = #{id,jdbcType=BIGINT}and version = #{version}<if test="itemId != null">and item_id = #{itemId}</if>
</update><!-- 商品价格表 -->
<update id="update" parameterType="cn.gov.zcy.service.item.domain.ZcyItemPrice">update<include refid="tb"/><set><if test="createdAt != null">created_at = #{createdAt},</if><if test="updatedAt != null">updated_at = #{updatedAt},</if><if test="version != null">version = #{version} + 1,</if><if test="remark != null">remark = #{remark},</if><if test="itemId != null">item_id = #{itemId},</if><if test="skuId != null">sku_id = #{skuId},</if><if test="historySalePrice != null">history_sale_price = #{historySalePrice},</if><if test="gpiPrice != null">gpi_price = #{gpiPrice}</if><if test="marketPrice != null">market_price = #{marketPrice},</if><if test="crawlerPrice != null">crawler_price = #{crawlerPrice},</if><if test="minDealPrice != null">min_deal_price = #{minDealPrice},</if><if test="avgDealPrice != null">avg_deal_price = #{avgDealPrice},</if><if test="avgSalePrice != null">avg_sale_price = #{avgSalePrice},</if><if test="minSalePrice != null">min_sale_price = #{minSalePrice},</if><if test="lastDealTime != null">last_deal_time = #{lastDealTime}</if>where ID = #{id}</set>
</update>
  • 状态机

本质上也是乐观锁,这种方法适合在有状态机流转的情况下,比如订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99

在做状态机更新时,我们就这可以这样控制

update `order` set status=#{status} where id=#{id} and status<#{status}
  • 订单采用的这种方案

2.2、分布式锁

分布式锁实现幂等性的逻辑是,在每次执行方法之前判断,是否可以获取到分布式锁,如果可以,则表示为第一次执行方法,否则直接舍弃请求即可

需要注意的是分布式锁的key必须为业务的唯一标识,通常用redis分布式锁或者zookeeper来实现分布式锁。

分布式锁的实现方法具体请参考:分布式系统第四讲:分布式锁及实现方案

  • 创建商品、创建店铺采用的这种方案

2.3、防重 Token 令牌

简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。

  • 使用场景:针对客户端连续点击或者调用方的超时重试等情况,例如提交订单

使用限制:

  • 需要生成全局唯一 Token 串;
  • 需要使用第三方组件 Redis 进行数据效验;

主要流程

img

注意,在并发情况下,执行 Redis 查找数据与删除需要保证原子性,否则很可能在并发下无法保证幂等性。其实现方法可以使用分布式锁或者使用 Lua 表达式来注销查询与删除操作。

3、示例源码

todo

4、参考文章

SpringBoot接口幂等性实现的4种方案

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/13403.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LLaMA: Open and Efficient Foundation Language Models

背景 用最少的计算资源&#xff0c;解决了LLM大模型预测问题&#xff0c;训练了一些列的LLaMa模型&#xff0c;在参数量比较少的情况下&#xff0c;达到业界大模型效果。 主要贡献就是提升了LLM模型的训练速度和效率&#xff0c;在小容量的基础上&#xff0c;大大提升了模型的…

基于模型预测算法的混合储能微电网双层能量管理系统研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

最长公共子序列LCA

最长连续公共子序列 题目链接:3692. 最长连续公共子序列 - AcWing题库 /*解法:定义dp[i1][j1]为以a[i]结尾的字符串和b[j]结尾的字符串的最长连续公共子序列那么对于a[i] b[j]的时候 dp[i1][j1] dp[i][j]1否则为0 其他就是细枝末节 */ #include<iostream> #include<…

【底部消息红点提示】uniapp开发小程序时,使用uni.setTabBarBadge设置底部菜单的红点

一、实现的效果 功能描述&#xff1a;无论点击底部的哪个菜单栏&#xff0c;都可以看到第二个菜单栏下显示的红点通知。 如果只在人脉当前页面设置的话&#xff0c;当你在第二个菜单栏的页面中调用uni.setTabBarBadge方法设置红点后&#xff0c;切换到第一个菜单栏的页面时&am…

基础算法-前缀和

1 算法笔记 2.代码示例 3.代码解析 #include<iostream> using namespace std; const int maxn 1010000; int a[maxn],s[maxn];//a数组是用来存放数组的&#xff0c;s是用来存放前n项数组的和 int m,n;int main(){scanf("%d%d",&n,&m);for(int i1;i&l…

怎么用转转大师工具旋转PDF文件页面

有时候我们会在网上下载一下PDF格式文件&#xff0c;下载的PDF文件中可能会出现页面倒过来的情况&#xff0c;遇到这种情况我们需要先将PDF文件旋转到正确的角度才能继续阅读使用&#xff0c;那么有哪些方法可以快速旋转PDF页面呢&#xff1f; 可以使用转转大师工具快速旋转PD…

Django_测试模块(六)

目录 开始写我们的第一个测试 首先得有个 Bug 创建一个测试来暴露这个 bug 运行测试 修复这个 bug 更全面的测试 测试视图 针对视图的测试 Django 测试工具之 Client 改善视图代码 测试新视图 测试 DetailView 集中管理用例文件 使用Django测试运行器 源码等资料…

蓝桥杯专题-试题版含答案-【数数小木块】【精挑细选】【国王的魔镜】【字符串逆序输出】

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

Mycat【Mycat分片技术(水平拆分-分表、ER表、全局表)】(五)-全面详解(学习总结---从入门到深化)

目录 Mycat分片技术_水平拆分-分表 Mycat分片技术_ER表 Mycat分片技术_全局表 Mycat分片技术_水平拆分-分表 前言 上面谈到垂直切分只是把表按模块划分到不同数据库&#xff0c;但没有解决单表大数据量的问题。 水平拆分 相对于垂直拆分&#xff0c;水平拆分不是将表做分…

软件测试 | HTML常见的知识点

什么是 web web 就是 world wide web 的缩写&#xff0c;称之为全球广域网&#xff0c;俗称 WWW。对于用户来说它其实就是由多个网页组成在一起而形成的一种服务&#xff08;Web&#xff09;. 我们可以将 web 理解为就是当前的一种互联网。对于我们来说更多的就是网站服务。网…

C++学习笔记-第11单元 标准模板库介绍

第11单元 标准模板库介绍 文章目录 第11单元 标准模板库介绍单元导读11.1 标准模板库(STL)基础11.2 STL容器简介11.3 STL迭代器简介11.3.1 使用迭代器访问容器中的元素11.3.2 迭代器类型11.3.3 迭代器支持的运算符操作 11.4 顺序容器11.4.1 顺序容器的逻辑结构11.4.2 代码展示s…

Java的线程(Thread)、线程池(ThreadPool)

一、线程&#xff08;Thread&#xff09; 1、线程的创建 Java创建线程是采用内核线程: 由操作系统内核支持的线程&#xff0c;通过操纵调度器对线程进行调度(内核切换线程)&#xff0c;将线程的任务映射到各个处理器上。 调native方法 —> 调C -> 调操作系统(通过操作…