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种方案

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

动态规划之343 整数拆分(第6道)

题目&#xff1a; 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 示例&#xff1a; 解法&#xff1a; 其实可以从1开始遍历 j &#xff0c;然后有两种…

通用分页【下】(将分页封装成标签)

目录 一、debug调试 1、什么是debug调试&#xff1f; 2、debug调试步骤 3、实践 二、分页的核心 三、优化 分页工具类 编写servlet jsp代码页面&#xff1a; 分页工具类PageBean完整代码 四、分页标签 jsp代码 编写标签 tld文件 助手类 改写servlet 解析&…

CTFshow-pwn入门-栈溢出pwn49(静态链接pwn-mprotect函数的应用)

pwn49 首先我们先将pwn文件下载下来&#xff0c;然后赋上可执行权限&#xff0c;再来查看pwn文件的保护信息。 chomd x pwn checksec pwn file pwn我们可以看到这是一个32位的pwn文件&#xff0c;并且保护信息开启了NX和canary&#xff0c;也就是堆栈不可执行且有canary。最最…

html案例2

效果 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width, initia…

redis如何实现持久化

RDB快照 RDB是一种快照存储持久化方式&#xff0c;具体就是将Redis某一时刻的内存数据保存到硬盘的文件当中&#xff0c;默认保存的文件名为dump.rdb&#xff0c;而在Redis服务器启动时&#xff0c;会重新加载dump.rdb文件的数据到内存当中恢复数据。 开启RDB持久化方式 开启…

缓存更新策略,先更新数据库还是缓存呢?

学了这么多&#xff0c;相信大家对缓存更新的策略都已经有了清晰的认识。最后稍稍总结一下。 缓存更新的策略主要分为三种&#xff1a; Cache aside Cache aside Cache aside也就是旁路缓存&#xff0c;是比较常用的缓存策略。 &#xff08;1&#xff09;读请求常见流程 应…

亿级日活业务稳如磐石 华为云发布性能测试服务CodeArts PerfTest

HDC期间可参与华为云PaaS生态抽奖活动&#xff0c;活动链接在文末 计算机软件作为人类逻辑智慧的伟大结晶之一&#xff0c;已经渗透到了人类社会的各个角落。早期的计算机发展对硬件有很强的依赖性&#xff0c;只有少数的个人或者机构才能拥有软件这种“奢侈品”。但随着软件行…

java 并发 随笔7 ThreadLocal源码走读

0. 刚刚见了下老朋友&#xff0c;桌球撞起来的感觉很爽 可以看到 Thread 是内部是维护了局部变量的(thread-local-map) 1. 源码走读 很多的细节都在代码块中备注了 package java.lang;// 现在回来起来&#xff0c;很多经验不太丰富的人之所以在接触、学习java.lang.thread的…

TiDB(1):TiDB简介

1 从MySQL到TiDB 1.1 场景引入 假设现在有一个高速发展的互联网公司,核心业务库MySQL的数据量已经近亿行,且还在不断增长中,公司对于数据资产较为重视,所有数据要求多副本保存至少5年,且除了有对历史数据进行统计分析的离线报表业务外,还有一些针对用户数据实时查询的需求,如用…

通信算法之171: LTE 不同带宽参数

转载&#xff1a; LTE不同带宽配置下的对应的采样率&#xff1a; < Sampling Time > 20 Mhz BW Case : Ts 1 sec / 30.72 Mhz 1s/30,720,000 Hz 0.0326 us 32.6 ns 15 Mhz BW Case : T15 sec / 23.04 Mhz 1s/23,040,000 Hz 0.0434 us 43.4 ns 10 Mhz BW Case :…

记一次 Visual Studio 2022 卡死分析

一&#xff1a;背景 1. 讲故事 最近不知道咋了&#xff0c;各种程序有问题都寻上我了&#xff0c;你说 .NET 程序有问题找我能理解&#xff0c;Windows 崩溃找我&#xff0c;我也可以试试看&#xff0c;毕竟对 Windows 内核也知道一丢丢&#xff0c;那 Visual Studio 有问题找…

使用JMX管理Spring Bean

1.使用JMX管理Spring Bean 2.spring通过annotation注解注册MBean到JMX实现监控java运行状态 3.Spring与JMX集成

Elastic 推出 Elastic AI 助手

作者&#xff1a;Mike Nichols Elastic 推出了 Elastic AI Assistant&#xff0c;这是一款由 ESRE 提供支持的开放式、生成式 AI 助手&#xff0c;旨在使网络安全民主化并支持各种技能水平的用户。 最近发布的 Elasticsearch Relevance Engine™ (ESRE™) 提供了用于创建高度相…

Keil环境下CANopenNode移植到STM32问题记录(一)---printf重定向问题

文章目录 问题描述问题结决思考&#xff1a;相关文章 在直接将CANopenSTM32的示例工程直接移植到Keil环境下。 如果移植工程未实现printf函数重定向&#xff0c;则要注释掉log_printf下面的printf函数&#xff0c;使日志打印失效 /* Printf function of CanOpen app */ #define…

第七章:L2JMobius学习 – 登录服务LoginServer讲解

在上一个章节中&#xff0c;我们学习了网络数据传输的封装network。那么&#xff0c;在本章的登录服务LoginServer的讲解中&#xff0c;我们就来使用一下这个封装好的功能。Network的封装需要我们继承很多的接口或类。我们首先查看一下登录服务LoginServer的文件结构&#xff0…

DotNet VOL.Core框架学习使用笔记(二)(持续更新)

2023-7-5 生成代码的列表界面&#xff0c;在数据行里增加一个操作列 查看按钮&#xff0c;打开编辑框&#xff0c;然后让编辑框成为一个只读的查看界面。 页面对应的js文件中增加如下 this.columns.push 函数内容。 按钮的点击事件 重点代码 this.edit(row); 这就是框架里编…

天翎群晖NAS为全文检索插翅起飞

编者按&#xff1a;企业的文档资料随着企业的业务发展会越来越多&#xff0c;想要某个资料的时候&#xff0c;最怕找不到想要的资料&#xff0c;这时KMS的全文检索功能就非常重要了&#xff0c;只需只言片语的零星关键字&#xff0c;查找文档没压力。 关键词&#xff1a;全文检…

MySQL练习题(1)

1,创建如下学生表 mysql> create table student( -> id int, -> name varchar(20), -> gender varchar(20), -> chinese int, -> math int, -> english int -> ); 插入如图数据 1-- 查询表中所有学生的信息 select *from student;2-- 查询表中所有学…

TypeScript 总结

文章目录 TypeScript 总结概述运行ts文件方式一方式二 基础声明变量类型数组元组联合类型取值限制 枚举类型any & unknownvoid & undefined类型适配 面向对象函数普通函数箭头函数可选参数默认参数 对象创建对象对象的类型限制 类和接口泛型简单使用多个泛型默认泛型类…

集合处理常用Stream流

集合处理常用Stream流 1、Stream API介绍2、List集合常用Stream方法 stream流经常使用&#xff0c;但是遇到一些流操作时&#xff0c;会一下想不到用哪种&#xff0c;这里总结一下&#xff0c;方便自己或者读者查找 1、Stream API介绍 Stream API是Java 8引入的一项重要特性&a…