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

news2024/12/28 3:12:21

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.coloradmin.cn/o/717689.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

培训报名小程序实战开发

目录 1 需求描述2 原型绘制2.1 首页2.2 报名列表页2.3 报名页2.4 支付页面2.5 支付成功页面2.6 我的页面2.7 我的报名页面2.8 报名详情页面 3 数据源设计4 数据源开发5 创建模型应用6 录入测试数据7 创建自定义应用8 创建页面总结 经常有人问&#xff0c;低代码学习容易么&…

c语言进阶-枚举、联合(共用体)

枚举 枚举项也有值属性 修改枚举项值属性 枚举的优点 define的实现过程 实际在预处理已经完成了M - 100 的替换&#xff0c;实际执行是int m 100&#xff1b; enum调试的时候更方便&#xff0c;代码变化过程都可以看到。 联合&#xff08;共用体&#xff09; 打印出来的三个…

前端学习——HTML5

新增语义化标签 新增布局标签 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content&qu…

无限极 × 盖雅工场|劳动力管理系统项目正式启动,为多工厂管理保驾护航

6月12日&#xff0c;无限极盖雅工场劳动力管理系统启动大会在广东江门举行。无限极IT供应链系统负责人毛松和、智能制造总监胡波、新会生产中心负责人胡流云、营口生产中心负责人源博恩和人才资源共享服务负责人林岳&#xff0c;以及盖雅工场华南总经理潘磊等出席了启动大会。 …

ARM day6 (标准pin引脚启动)

A7核 led.h #ifndef __LED_H__ #define __LED_H__//寄存器封装 //声明一个结构体 typedef struct {volatile unsigned int MODER; //00volatile unsigned int OTYPER; //04volatile unsigned int OSPEEDR; //08volatile unsigned int PUPDR; //0Cvolatile unsigned int …

“全球筷子第一股”双枪科技携手纷享销客连接型CRM

近日&#xff0c;纷享销客携手双枪科技股份有限公司&#xff08;以下简称“双枪”&#xff09;&#xff0c;“主数据与订货管理系统”项目启动会在浙江杭州举行&#xff0c;双枪和纷享销客双方多位高管共同出席了当天的启动会&#xff0c;并针对双方项目组的紧密合作给予了一致…

【C++面向对象】用电管理数据管理系统(面向对象)

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、51CTO技术博主 &#x…

word转化为ftl格式文件模板,导出后office提示文件错误

需求如下: 使用模板,导出word文件,最近在做这个需求,本地环境用的是wps,结合本地的环境快速完成了开发需求之后,有一天客户发现office打开报错,本人深感不接,wps都能打开,各个在线文档也都支持,为何office就不支持,环境不同。 分析: wps是按照office版本迭代开发…

对比学习论文-系列4

文章目录 MedCLIP: Contrastive Learning from Unpaired Medical Images and Text目标问题来源模型架构 Supervised Prototypical Contrastive Learning for Emotion Recognition in ConversationPrototypical Contrastive LearningCurriculum Strategy&#xff1a; KECP: Know…

Lion:闭源大语言模型的对抗性蒸馏

通过调整 70k 指令跟踪数据&#xff0c;Lion (7B) 可以实现 ChatGPT 95% 的能力&#xff01; 消息 我们目前正在致力于训练更大尺寸的版本&#xff08;如果可行的话&#xff0c;13B、33B 和 65B&#xff09;。感谢您的耐心等待。 **[2023年6月10日]**我们发布了微调过程中解…

MyBatis逆向工程的配置与生成

什么是逆向工程 所谓的逆向⼯程是&#xff1a;根据数据库表逆向⽣成Java的pojo类&#xff0c;SqlMapper.xml⽂件&#xff0c;以及Mapper接⼝类 等。 要完成这个⼯作&#xff0c;需要借助别⼈写好的逆向⼯程插件。 1.在pom中添加逆向工程插件 <!--定制构建过程--> <bu…

字符流的使用

1&#xff1a;文件字符流输入流-一次读取一个字符 1:文件字符输入流&#xff1a;Reader 以内存为基准&#xff0c;把磁盘文件中的数据以字符的形式读取到内存中去。 2&#xff1a;文件字符流输入流-一次读取一个字符数组 3&#xff1a;文件字符输出流 作用&#xff1a;以内存…

【现场问题】flink-cdc,Oracle2Mysql的坑,Oracle区分大小写导致

大小写导致的问题 错误的flink-cdc语句sql我们看一下oracle的数据库字段再看一下错误sql里面的内容flink报错内容 正确的sql三级目录 错误的flink-cdc语句sql CREATE TABLE t_wx_source_1 (id String,name String,age String ) WITH (connector oracle-cdc,hostname 192.168…

U-Boot移植 (3)- uboot启动Linux内核测试

文章目录 1. bootcmd 和 bootargs 环境变量1.1 环境变量 bootcmd1.2 环境变量 bootargs 2. uboot 启动 Linux 测试2.1 从 EMMC 启动 Linux 系统2.2 从网络启动 Linux 系统 3. 总结 1. bootcmd 和 bootargs 环境变量 1.1 环境变量 bootcmd bootcmd 保存着 uboot 默认命令&…

Netty专题:netty概述,及丢弃协议服务(1)

Netty 是一个 Java NIO 客户端服务器框架&#xff0c;使用它可以快速简单地开发网络应用程序&#xff0c;比如服务器和客户端的协议。Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。 JDK 原生 NIO 程序的问题 JDK 原生也有一套网络应用程序 API&…

秋招算法岗c++面经

目录 1、指针与引用的区别 2.const关键字 3.重载和重写(覆盖)的区别 4.new和malloc的区别(new封装了malloc) 5.static和const的区别 6. c三大特性 7.虚函数 8.纯虚函数 9.虚继承 10. 智能指针 11. 内存泄漏 12.c的内存分布 13.STL介绍 1、指针与引用的区别 指针存…

【Web3】认识NFT

NFT&#xff08;非同质化代币&#xff09;在Web3中扮演着重要的角色。Web3是指下一代互联网&#xff0c;它建立在区块链技术之上&#xff0c;旨在实现更加去中心化、透明和用户掌控的互联网。 NFT在Web3的一些重要作用&#xff1a; 唯一性和可证明稀缺性&#xff1a;NFT是一种…

vscode突然不能输入中文句号,怎么办

vscode突然不能输入中文句号&#xff0c;怎么办? 敲代码敲得好好的&#xff0c;突然无论打句号&#xff0c;出来的都是英文的句号&#xff0c;无法打出中文的句号&#xff0c; 让人着实着急。。。 记录一下解决办法&#xff1a; Ctrl 句号&#xff0c;然后再测试一下&…

JavaWeb 速通HTML(常用标签汇总及演示)

目录 一、拾枝杂谈 1.网页组成 : 1 结构 2 表现 3 行为 2.HTML入门 : 1 基本介绍 2.基本结构 : 3.HTML标签 : 1 基本说明 2 注意事项 二、常用标签汇总及演示 1.font标签 : 1 定义 2 演示 2.字符实体 : 1 定义 2 演示 3.标题标签 : 1 定义 2 演示 4. 超链接标签 : 1…

香薰市场分析:天猫香薰销售额近7.2亿,市场增长潜力大

在Z世代崛起的背景下&#xff0c;香薰作为能够调节情绪&#xff0c;提升生活品质的产品&#xff0c;备受市场青睐。作为一种健康、美容、舒缓压力的新兴行业&#xff0c;香薰市场也形成了自己的特色和竞争力&#xff0c;其发展前景十分广阔。 根据鲸参谋电商数据分析平台的相关…