接口幂等性实现方式

news2024/9/20 16:57:53

优质博文:IT-BLOG-CN

幂等 操作的特点是一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。这对于保证系统的一致性和可靠性非常重要。

具体来说,当一个接口被设计为幂等的时候,无论请求被执行多少次,结果都是一样的。这样可以避免由于网络延迟、重试或其他原因导致的重复请求对系统造成的副作用,比如重复创建订单、重复扣款等。实现接口幂等性可以提高系统的可靠性和稳定性,减少不必要的资源消耗和数据错误。同时,对于一些需要保证数据一致性的操作,比如金融交易、库存管理等,实现接口幂等性也是非常重要的。

一、为什么要实现接口幂等性

【1】前端重复提交表单: 在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
【2】接口超时重复提交: 很多时候HTTP客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
【3】消息进行重复消费: 当使用MQ消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。
【4】用户恶意进行刷单: 在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。

二、什么时候做幂等性

请求类型是否需要幂等描述
Get自身满足幂等性Get 方法用于获取资源。其一般不会也不应当对系统资源进行改变,所以是幂等的。
Post自身不满足幂等性Post 方法一般用于创建新的资源。其每次执行都会新增数据,所以不是幂等的。
Put可能满足,可能不满足Put 方法一般用于修改资源。该操作则分情况来判断是不是满足幂等,更新操作中直接根据某个值进行更新,也能保持幂等。不过执行累加操作的更新是非幂等。
Delete可能满足,可能不满足Delete 方法一般用于删除资源。该操作则分情况来判断是不是满足幂等,当根据唯一值进行删除时,删除同一个数据多次执行效果一样。但带查询条件的删除就不一定满足幂等,例如在根据某条件删除一批数据后,这时候新增加了一条数据也满足条件,然后又执行了一次删除,那么将会导致新增加的这条满足条件数据也被删除。

三、如何实现幂等性

客户端

客户端防止重复提交并不是绝对可靠的,可以通过工具略过前端直接访问后端。优点是实现起来比较简单。

按钮只能操作一次

提交后把按钮置灰或loding状态,消除用户因为重复点击而产生的重复记录,比如添加操作,由于点击两次而产生两条记录。

使用Post/Redirect/Get模式

在提交后执行页面重定向,这就是所谓的Post-Redirect—Get(PRG)模式。当用户提交表单后,跳转到一个重定向的信息页面,避免用户按F5刷新导致的重复提交,而且也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退导致同样重复提交的问题。

服务端

数据库唯一主键

利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式ID充当主键,这样才能能保证在分布式环境下ID的全局唯一性。

唯一索引

与唯一主键的思想一致,通过唯一性确保插入的幂等性。创建订单时,前端先通过接口获取订单号,再请求后端时带入订单号,订单表中订单号添加唯一索引,如果存在插入相同订单号则直接报错。消费MQ消息时,messageId是唯一的,我们可以新添加一种消费记录表,将messageId作为主键,如果重复消费那么就会存在相同的messageId,插入直接报错。

乐观锁

乐观锁就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制。数据库乐观锁一般只能适用于执行“更新操作”的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。每次对该数据库数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据的版本标识。

悲观锁

当对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。如下伪代码通过for update添加悲观锁,这里order_id需要有索引,否则会锁整张表。悲观锁影响性能一般不建议。

begin;  --  1.开始事务
  --  查询订单,判断状态
  select order_id,status from orders where order_id='1000234' for update 
  ifstatus != 'S'){
        -- 非订单状态,不能更新为已完成;
        return ;
  }
  --  更新完成
  update order set status='s' order_no='1000234' 
commit; -- 2.提交事务

状态码

很多业务表,都是有状态的,比如订单表,一般订单有1-订单创建、2-订单支付、3-订单完成、4-取消订单等订单流程,当我们更新订单状态

update orders set status=3 where order_id='1000234' and status=2;

第一次请求时,将“订单支付”状态修改成“订单完成”,sql执行结果的影响行数是1。
第二次重复请求时,同样将“订单支付”状态修改成“订单完成”,但是sql执行结果的影响行数为0。如果是0,那么我们直接可以返回成功了。不需要做接下来的业务操作,以此来保证保证接口的幂等性。

基于分布式锁

分布式锁实现幂等性的逻辑就是,请求过来时,先去尝试获得分布式锁,如果获得成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。其实前面介绍过的悲观锁,本质是使用了数据库的分布式锁,都是将多个操作打包成一个原子操作,保证幂等。但由于数据库分布式锁的性能不太好,我们都是通过RedisZookeeper来实现分布式锁。

客户端 + 服务端

Token机制

针对客户端连续点击或者调用方的超时重试等情况,可以用Token的机制实现防止重复提交。简单的说就是调用方在调用接口的时候先向后端请求一个全局ID(Token),请求的时候携带这个全局ID一起请求(Token最好将其放到Headers中),后端需要对这个Token作为Key,用户信息作为ValueRedis中进行键值内容校验,如果Key存在且Value匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的KeyValue不匹配就返回重复执行的错误信息,这样来保证幂等操作。

Token工具类: 接收Token串,加上Key前缀形成Key,再传入value值,执行Lua表达式保证命令执行的原子性,查找对应Key与删除操作。执行完成后验证命令的返回结果,如果结果不为空且非0,则验证成功,否则失败。

public class TockenUtlls {
    @Autowired
    private RedisTemplate redisTemplate;

    // token前缀
    private static final String TOKEN_PRE = "token_";

    // 创建 token 并传入 Redis
    public String generateToken(String value) { 
        // 通过 UUID 创建 Token
        String token = UUID.randomUUID().toString();
        String key = TOKEN_PRE + token;
        // 存储 Token 到 Redis 并设置超时时间
        redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
        return token;
    }

    // 验证 Token
    public boolean validToken(String token, String value) {
        // 设置 LUA 脚本, KEYS[1] 代表 key, KEYS[2] 代表 value
        String luaScript = " if redis.call('get', KEYS[1]) == KEYS[2]
                                then return redis.call('get', KEYS[1]) 
                             else return 0 
                             end ";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);

        // 拼接 Redis Key
        String key = TOKEN_PRE + token;
        
        // 执行 LUA 脚本
        Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
        // 根据返回值判断是否匹配成功并删除 Redis 键值对。   结果不为空则表示成功
        if (result != null && result != 0L) {
            redisTemplate.opsForValue().getAndDelete(key);
            return true;
        }
        return false;
    }
}

唯一序号

所谓请求序列号,其实就是每次向服务端请求时候附带一个短时间内唯一不重复的序列号,该序列号可以是一个有序ID,也可以是一个订单号,一般由上游生成,在调用下游服务端接口时附加该序列号和用于认证的ID。当下游服务器收到请求信息后拿取该“序列号”和上游"认证ID"进行组合,形成用于操作RedisKey,然后到Redis中查询是否存在对应的Key的键值对,如果不存在,就以该Key作为Redis的键,以上游关键信息作为存储的值,将该键值对存储到Redis中 ,然后再正常执行对应的业务逻辑即可。

在这里插入图片描述

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

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

相关文章

最小公倍数题解:湘大oj1075 辗转相除法

一、链接 求最小公倍数 二、题目 Description 有多个测试用例&#xff0c;每行两个正整数x,y&#xff0c;保证x*y不超过2^31-1。如果x与y都为0&#xff0c;则输入结束。 每行输出一个测试用例的结果&#xff0c;即x与y的最小公倍数。&#xff08;最后一行有回车&#xff09;…

软著是什么

因公司需要申请软著&#xff0c;提前将相关资料整理如下&#xff0c;方便查询。 一、是什么 作为一名软件开发者&#xff0c;了解软件著作权可以帮助我们保护创作的成果&#xff0c;确保其他人不会未经授权地使用我们的代码。如果你要在你的软件中使用其他人的代码&#xff0…

百度Apollo中轨迹的表示方式

本文只是对于初学者的一种直观普及&#xff0c;以及对于规划路径可视化的另外一种体会。对于做规划控制的工程师来说太简单。只是本人初学者&#xff0c;在学习控制模块时候对于模块输入参数有所迷惑。所以记录一下。 首先明确轨迹表达的方式&#xff0c;以及对于控制模块输入的…

【C# 基础精讲】数组的创建与操作

数组是C#编程中非常重要的数据结构&#xff0c;它是一种用于存储相同类型元素的集合。通过数组&#xff0c;我们可以方便地访问和处理多个相关数据&#xff0c;这在很多编程场景下都是非常有用的。本文将详细介绍C#数组的创建与操作&#xff0c;包括数组的声明、初始化、访问元…

.bit域名调研

.bit域名研究 问题&#xff1a; .bit域名和ENS域名的相同点&#xff1f;不同点&#xff1f;有什么关系&#xff1f; .bit的定义 .bit 是基于区块链的&#xff0c;开源的&#xff0c;跨链去中心化账户系统.bit 提供了以 .bit 为后缀的全局唯一的命名体系&#xff0c;可用于加密…

半导体器件||的学习

电子管的介绍&#xff1a; 到底什么是电子管&#xff08;真空管&#xff09;&#xff1f; - 知乎 芯片破壁者&#xff08;一&#xff09;&#xff1a;从电子管到晶体管“奇迹”寻踪 - 知乎 晶体管&#xff1a; 什么是晶体管&#xff1f;它有什么作用&#xff1f; - 知乎 改…

Fastjson 使用指南

文章目录 Fastjson 使用指南0 简要说明为什么要用JSON&#xff1f;用JSON的好处是什么&#xff1f;为什么要用JSON&#xff1f;JSON好处 1 常用数据类型的JSON格式值的范围 2 快速上手2.1 依赖2.2 实体类2.3 测试类 3 常见用法3.1 序列化操作核心操作对象转换为JSON串list转换J…

CAD练习——绘制房子平面图

首先还是需要设置图层、标注、文字等 XL&#xff1a;构造线 用构造线勾勒大致的轮廓&#xff1a; 使用多线命令&#xff1a;ML 绘制墙壁 可以看到有很多交叉点的位置 用多线编辑工具将交叉点处理 有一部分处理不了的&#xff0c;先讲多线分解&#xff0c;然后用修剪打理&…

一站式印度跨境电商平台开发--多用户购物商城搭建

搭建一个一站式印度跨境电商平台开发&#xff0c;需要考虑以下几个方面&#xff1a;平台设计&#xff0c;技术架构&#xff0c;多用户购物商城搭建。 一、平台设计&#xff1a; 1. 市场调研&#xff1a;了解印度电商市场的特点和需求&#xff0c;确定目标用户群体。 2. 平台功…

【Q-dir】常用设置分享

Q-dir是一个资源管理器&#xff0c;但有些宝藏设置在初始使用时没有展现&#xff0c;后面配合AHK设置快捷键启动更方便&#xff0c;快捷键推荐winq 点击这个小按钮可以展示导航窗格 此处是设置导航窗格&#xff0c;又名树状列表 点击空白出返回上一页 使用颜色过滤器&…

python几岁可以学零基础,python多大的孩子可以学

大家好&#xff0c;小编为大家解答多大的孩子可以学python的问题。很多人还不知道学python多大年龄可以学&#xff0c;现在让我们一起来看看吧&#xff01; python编程是现在很多孩子接触编程的好选择&#xff0c;它能够给孩子带来容易入门的效果。那么&#xff0c;python编程少…

灰度非线性变换之c++实现(qt + 不调包)

本章介绍灰度非线性变换&#xff0c;具体内容包括&#xff1a;对数变换、幂次变换、指数变换。他们的共同特点是使用非线性变换关系式进行图像变换。 1.灰度对数变换 变换公式&#xff1a;y a log(1x) / b&#xff0c;其中&#xff0c;a控制曲线的垂直移量&#xff1b;b为正…

有哪些免费的mac虚拟机软件?

在当今数字化时代&#xff0c;虚拟机系统成为许多用户在电脑上同时运行多个操作系统的理想选择。虚拟机系统不仅能够提供隔离环境&#xff0c;还可以帮助用户在单一设备上使用多个操作系统和应用程序。然而&#xff0c;有些用户可能会面临安装软件和提高虚拟机性能的挑战。下面…

设计方法编写测试用例---思路分析

测一四年我在YX公司带测试团队&#xff0c;一个用例评审的会议上&#xff0c;一不小心超常发挥&#xff0c;结果卡在了一个用例设计方法上&#xff0c;印象非常深刻&#xff0c;当时的业务场景是支付方式的选择和优惠方案。 在后来的工作中&#xff0c;也曾几次遇到需要选择合…

Games101学习笔记 -光栅化

光栅化 经过MVP矩阵和视口变换后&#xff0c;我们就可以从相机的角度看到一个和屏幕大小一致的二维平面。 那么把这个看到的二维平面应用到我们的屏幕上的过程就是光栅化。在这儿我们需要补充一个概念-像素&#xff1a; 像素&#xff1a; 一个二位数组&#xff0c;数组中每个…

Games101学习笔记 - MVP矩阵

MV矩阵&#xff08;模型视图变换&#xff09; 目的&#xff0c;把摄像机通过变换移动的世界坐标远点&#xff0c;并且朝向与Z轴的负方向相同。这个变换就是模型试图变换。 因为移动了相机&#xff0c;如果想保持正确的渲染的话&#xff0c;那么对应的物体需要要和相机保持相对…

【问题解决:在英伟达nvidia的jetson-orin-nx上使用调试can基础收发-遗留问题-开机自启动can】

【问题解决在英伟达nvidia的jetson-orin-nx上使用调试can基础收发-遗留问题-开机自启动can】 1、概述2、实验环境3、问题描述# 1-1、发送可以发送&#xff0c;但是PC发送数据收不到。# 1-2、接收是可以接收&#xff0c;但是发送PC收不到数据 4、解决方式&#xff08;1&#xff…

常用的负载均衡算法(NGINX篇)

负载均衡算法用于在多个服务器或资源之间分配工作负载&#xff0c;以优化资源利用率、降低延迟、提高吞吐量并确保系统的容错性。 这里介绍并实验4种负载均衡算法&#xff08;随机的方法理论上也算&#xff0c;但一般不会这么搞&#xff09;&#xff0c;在NGINX上。 目录 轮询…

Vue 插槽 slot

solt 插槽需要分为 2.6.0 版本以上和 2.6.0版本以下。 2.6.0 版本以下的 slot 插槽在&#xff0c;2.x版本将继续支持&#xff0c;但是在 Vue 3 中已被废弃&#xff0c;且不会出现在官方文档中。 作用 插槽 prop 允许我们将插槽转换为可复用的模板&#xff0c;这些模板可以基于…

【第一阶段】kotlin的函数

函数头 fun main() {getMethod("zhangsan",22) }//kotlin语言默认是public,kotlin更规范&#xff0c;先有输入&#xff08; getMethod(name:String,age:Int)&#xff09;再有输出(Int[返回值]) private fun getMethod(name:String,age:Int): Int{println("我叫…