01-幂等性解释,问题及常用解决方案

news2024/11/28 14:35:17

目录

1. 幂等性简介

2.  后端如何解决幂等性问题

2.1 数据库层面

-> 2.1.1 防重表 

-> 2.1.2 数据库悲观锁(不建议,容易出现死锁情况) 

-> 2.1.3 数据库乐观锁 

-> 2.1.4 乐观锁CAS算法原理

2.2 锁层面

2.3 幂等性token层面

-> 2.3.1 简介文字描述: 

-> 2.3.2 简介图示:  

​编辑

-> 2.3.3 创建注解

-> 2.3.4  创建请求拦截器

----->方案一: 使用incr保证原子性

----->方案一代码: 拦截器

====>方案二: 使用分布式锁, 这篇文章暂不介绍

-> 2.3.5 获取幂等token代码

-> 2.3.6 接口测试

-> 2.3.7 使用测试工具进行测试

 -> 2.3.8 幂等校验token的优,缺点

-----> 2.3.8.1 使用的优点: 

-----> 2.3.8.2 使用的缺点: 

3. 前端如何操作来避免幂等问题 

-> 3.1前端防重

-> 3.2 PRG模式


1. 幂等性简介

分布式或微服务思想实现系统架构设计中, 服务相互调用,可能存在服务调用延迟或失败情况。服务端可能会进行多次点击提交。如果这样请求多次的话,那最终处理的数据结果就一定要保证统一,如 订单创建,支付扣款,库存扣减,物流发货等。此时就需要通过保证业务幂等性方案来完成。

2.  后端如何解决幂等性问题

2.1 数据库层面

-> 2.1.1 防重表 

创建唯一索引: 多用于详情信息, 个人账户等业务,如果并发情况下出现多条,数据库报异常

 创建唯一组合索引: 可以创建组合索引来保证唯一性

-> 2.1.2 数据库悲观锁(不建议,容易出现死锁情况) 

加入悲观锁, for update, 有主键索引/明确索引就是行锁, 不然就是表锁

文章介绍跳转: -> mysql for update 详细介绍

-> 2.1.3 数据库乐观锁 

占用数据库资源, 需要有版本号, 先查询版本号作为老的版本(oldVersion)存起来, 进行修改操作, 然后把这个值也进行修改+1 条件是version= oldVersion, 如果version不正确 修改失败

version= 5  100个线程同时取到5  其中一个修改成功version=6, 则其他99个失败

-> 2.1.4 乐观锁CAS算法原理

简介: compare and swap:比较与交换 , 核心即为 冲突检测和数据更新

文章介绍跳转: ->

2.2 锁层面

锁系列文章跳转(待定) -> 

2.3 幂等性token层面

-> 2.3.1 简介文字描述: 

        1. 先制作幂等性token生成器, 创建token

        2. 创建注解, 写入默认值 表示是否开启校验

        3. 写拦截器 判断接口上是否有注解 如果没有,放行

        4. 有注解的, 判断请求头中是否存在幂等token, 不存在 拦截请求

        5. 存在, 判断幂等token真实性(是否过期), 不存在或incr数值异常 删除幂等token   拦截请求

        6. 一切正常,删除token,放行请求, 进入controller

-> 2.3.2 简介图示:  

-> 2.3.3 创建注解

package *;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 幂等性校验
 * 需要多线程测试后
 * 查出是否需要优化
 * @author pzy
 * @version 0.1.1 测试版本
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotentAnn {
    boolean value() default true;
}

-> 2.3.4  创建请求拦截器

----->方案一: 使用incr保证原子性

简介: Redis 的 INCR 命令可以将存储在指定键中的数字值增加 1。INCR 命令是一个原子性操作, 意味着它在执行过程中不会被中断。 例如,假设您有一个名为 "counter" 的键,存储的值为 0。如果您使用 INCR 命令将其 增加 1,那么这个键的值就会变为 1。如果再次使用 INCR 命令将其增加 1,那么这个键的值 就会变为 2。 由于INCR 命令是原子性的,因此可以在并发环境下使用

----->方案一代码: 拦截器

package *;

import *;
import *;
import *;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;

import static com.aisce.common.constant.RedisKeyConstants.IDEMPOTENT_TOKEN;

/**
 * 幂等性校验拦截器(用哪个工程粘过去即可)
 * 使用方式:
 * <p>
 * <p>
 * 后端操作: @ApiIdempotentAnn注解 添加到接口位置即可 其他无需操作
 * <p>
 * 前端操作: 在请求头中添加  idempotent参数
 * 示例: "idempotent": "fcc8b373-b867-4d6c-9915-e3bd668db7b6"
 *
 * @author pzy
 * @version 0.1.1 初期版本
 */
@Slf4j
@Component
@Order(2)
public class ApiIdempotentInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 前置拦截器
     * 在方法被调用前执行。在该方法中可以做类似校验的功能。如果返回true,则继续调用下一个拦截器。如果返回false,则中断执行,
     * 也就是说我们想调用的方法 不会被执行,但是你可以修改response为你想要的响应。
     */
    @SuppressWarnings(value = {"all"})
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        log.info("===> 执行链2 执行");

        //如果handler不是和HandlerMethod类型,则返回true
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        //转化类型
        final HandlerMethod handlerMethod = (HandlerMethod) handler;

        //获取方法类
        final Method method = handlerMethod.getMethod();

        // 判断当前method中是否有这个注解
        boolean methodAnn = method.isAnnotationPresent(ApiIdempotentAnn.class);

        //如果有幂等性注解
        if (methodAnn && method.getAnnotation(ApiIdempotentAnn.class).value()) {

            // 需要实现接口幂等性
            //检查token
            //1.获取请求的接口方法
            boolean result = checkToken(request);

            //如果token有值,说明是第一次调用
            if (result) {
                //则放行
                return super.preHandle(request, response, handler);
            } else {//如果token没有值,则表示不是第一次调用,是重复调用

                response.setContentType("application/json; charset=utf-8");
                PrintWriter writer = response.getWriter();
                writer.print(new ObjectMapper().writeValueAsString(ResultResponse.error("重复调用,已拦截!")));
                writer.close();
                response.flushBuffer();

                log.error(String.format("===> %s 重复调用%s,已拦截!", request.getRemoteAddr(), request.getRequestURI()));

                return false;
            }
        }
        //否则没有该自定义幂等性注解,则放行
        return super.preHandle(request, response, handler);
    }

    /**
     * 幂等token的校验方法
     */
    private boolean checkToken(HttpServletRequest request) {

        //从请求头对象中获取 幂等性校验token<idempotent>
        String idempotentToken = request.getHeader("idempotent");


        //如果不存在,则返回false,说明是重复调用
        if (StringUtils.isBlank(idempotentToken)) {
            return false;
        }

        //格式化幂等校验token
        idempotentToken = String.format(IDEMPOTENT_TOKEN, idempotentToken);

        if (Boolean.FALSE.equals(stringRedisTemplate.hasKey(idempotentToken))) {
            return false;
        }

        //保证原子性<方式一>  incr
        Long num = stringRedisTemplate.opsForValue().increment(idempotentToken);

        if (num != null && num.intValue() != 2) {

            //只要逻辑不对 删除key, 阻止请求
            delIdempotentToken(idempotentToken);

            return Boolean.FALSE;
        }

        //否则就是存在,存在则把redis里删除token
        //return Boolean.TRUE;
        return delIdempotentToken(idempotentToken);
    }

    /**
     * 删除幂等校验token
     *
     * @param idempotentToken
     * @return
     */
    private Boolean delIdempotentToken(String idempotentToken) {

        return Boolean.TRUE.equals(stringRedisTemplate.delete(idempotentToken));
    }


}

====>方案二: 使用分布式锁, 这篇文章暂不介绍

下篇文章预告: 分布式锁基本使用方式,以及配合注解实现快速加锁

-> 2.3.5 获取幂等token代码

  /**
     * 前端获取token,然后把该token放入请求的header中
     *
     * @return
     */
    @ApiOperation("获取幂等token")
    @GetMapping("/getIdempotentToken")
    public ResultResponse getIdempotentToken() {

        log.info("===> 获取幂等token <===");

        String idempotent = UUID.randomUUID().toString();
        stringRedisTemplate.opsForValue().set(String.format(IDEMPOTENT_TOKEN,idempotent),
                "1", Duration.ofSeconds(60*2));//2分钟 不随机数了 两分钟幂等失效拦截

        return ResultResponse.ok().setData(idempotent);
    }

-> 2.3.6 接口测试

ps:  使用方式: AxPost可以换成任意实体类

ResultResponse  随便的一个统一返回类型,没有写String 类型即可

    @ApiIdempotentAnn
    @PostMapping("/testCheck")
    public ResultResponse testCheck(@RequestBody AxPost axPosts) {

        log.info("======>专业测试幂等性接口 <=======");
        System.out.println(axPosts);

        return ResultResponse.ok("ok");
    }

-> 2.3.7 使用测试工具进行测试

测试工具介绍: Postman,ApiPost, Idea httpclient tools,ApiFox后端测试方式 

 -> 2.3.8 幂等校验token的优,缺点

-----> 2.3.8.1 使用的优点: 

        通过token机制来保证幂等是常见的解决方案,同时也适合绝大部分场景,使用方便,

业务执行出现异常时,客户端需重新获取令牌并发起访问即可。推荐使用先删除token方案。

-----> 2.3.8.2 使用的缺点: 

        每次的业务请求, 都会产生一个额外的请求去获取token,但出现失败次数一般不会很多, 算是一种资源的浪费,虽然redis性能很好.

3. 前端如何操作来避免幂等问题 

-> 3.1前端防重

通过前端防重保证幂等是最简单的实现方式,JS代码即可完成设置。可靠性并不好,可以通过工具跳过页面仍能重复提交。主要适用于表单重复提交或按钮重复点击。 

-> 3.2 PRG模式

PRG模式即POST-REDIRECT-GET。当用户进行表单提交时,会重定向到另外一个提交成功页面,而不是停留在原先的表单页面。这样就避免了用户刷新导致重复提交。防止通过浏览器按钮前进/后退导致表单重复提交。 

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

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

相关文章

Java开发 - 问君能有几多愁,Spring Boot瞅一瞅。

前言 首先在这里恭祝大家新年快乐&#xff0c;兔年大吉。本来是想在年前发布这篇博文的&#xff0c;奈何过年期间走街串巷&#xff0c;实在无心学术&#xff0c;所以不得不放在近日写下这篇Spring Boot的博文。在还没开始写之前&#xff0c;我已经预见到&#xff0c;这恐怕将是…

中国社科院与美国杜兰大学金融管理硕士,让我们相遇在春暖花开时

在芸芸众生中&#xff0c;能拥有志同道合的朋友是一件多么幸运的事。人们常说&#xff1a;你是谁&#xff0c;就会遇见谁。走过半生才知道&#xff0c;看似命中注定的遇见谁、发生的事&#xff0c;其实都取决于自己。只有自己足够优秀&#xff0c;才能遇到更优秀的别人。在这个…

IT人的晋升之路——关于人际交往能力的培养

对于咱们的程序员来说&#xff0c;工作往往不是最难的&#xff0c;更难的是人际交往和关系的维护处理。很多时候我们都宁愿加班&#xff0c;也不愿意是社交&#xff0c;认识新的朋友&#xff0c;拓展自己的圈子。对外的感觉就好像我们丧失了人际交往能力&#xff0c;是个呆子&a…

【chatGPT】持续火热一路狂飙,简单了解下TA的功能和示例代码吧

&#x1f389;&#x1f389; 最近chatGPT持续火爆&#xff0c;一路狂飙&#xff0c;对应如何注册和使用的优质文章非常多。 所以&#xff0c;此篇文章除了整理chatGPT文章外&#xff0c;主要是讲解如何获取API Key进行接口的调用&#x1f389;&#x1f389; 目录1、chatGPT解读…

蓝牙单点技术实现路径介绍

本文主要介绍蓝牙设备与手机一对一相连的 蓝牙单点 技术。 准备工作 系统要求&#xff1a;蓝牙使用需要安卓 4.3 以及以上版本&#xff0c;智能生活 App SDK 从安卓 4.4 开始支持。Manifest 权限&#xff1a; <uses-permission android:name"android.permission.ACCE…

Fluent Python 笔记 第 3 章 字典和集合

3.1 泛映射类型 只有可散列 的数据类型才能用作这些映射里的键 字典构造方法&#xff1a; >>> a dict(one1, two2, three3) >>> b {one: 1, two: 2, three: 3} >>> c dict(zip([one, two, three], [1, 2, 3])) >>> d dict([(two, 2…

5. Spring 事务

文章目录1. Spring 事务简介2. Spring 事务角色3. Spring 事务属性3.1 事务配置3.2 案例&#xff1a;转账业务追加日志3.3 事务传播行为1. Spring 事务简介 Spring 事务作用&#xff1a;在数据层或业务层保障一系列的数据库操作同成功、同失败。 数据层有事务我们可以理解&am…

多传感器融合定位十三-基于图优化的建图方法其二

多传感器融合定位十二-基于图优化的建图方法其二3.4 预积分方差计算3.4.1 核心思路3.4.2 连续时间下的微分方程3.4.3 离散时间下的传递方程3.5 预积分更新4. 典型方案介绍4.1 LIO-SAM介绍5. 融合编码器的优化方案5.1 整体思路介绍5.2 预积分模型设计Reference: 深蓝学院-多传感…

Vue3 - 自定义指令封装

Vue3 - 自定义指令封装一. 自定义指令封装1.1 全局/局部注册自定义聚焦指令1.2 自定义指令相关参数1.3 自定义指令参数传递二. 总结一. 自定义指令封装 vue中有很多内置的指令&#xff0c;我们一般在开发中也经常用到&#xff0c;比如v-if&#xff0c;v-for等等。那么本篇文章…

Vue极简使用

Vue安装Vue模板语法安装Vue 安装nodejs 这里我安装的是14.5.4版本 https://nodejs.org/download/release/v14.15.4/解压后配置一下环境变量就行 安装cnpm镜像 (这个安装的版本可能过高&#xff0c;后面安装Vue可能出问题) npm install -g cnpm --registryhttps://registry…

二十二、Gtk4-ListView

GTK 4添加了新的列表对象GtkListView、GtkGridView和GtkColumnView。这个新特性在Gtk API参考—列表小构件概述中有描述。 GTK 4还有其他实现列表的方法。它们是GtkListBox和GtkTreeView&#xff0c;它们是从GTK 3接管的。在Gtk开发博客中有一篇关于Matthias Clasen所写的列表…

vscode执行Python输出exited with code=9009 in 0.655 seconds

vscode执行Python输出exited with code9009 in 0.655 seconds 想用vscode写个脚本&#xff0c;用自己电脑配置了下vscode的python环境&#xff0c;结果点击右上角三角图标运行时却只会输出exited with code9009 in 0.655 seconds 这就不太理解了&#xff0c;我在公司时是能正…

linux性能分析 性能之巅学习笔记和内容摘录

本文只是在阅读《性能之巅》的过程中&#xff0c;对一些觉得有用的地方进行的总结和摘录&#xff0c;并附加一些方便理解的材料&#xff0c;完整内容还请阅读Gregg的大作 概念和方法 性能分析领域一词的全栈代表了整个操作系统的软硬件在内的所有事物 软件生命周期和性能规划…

LabWindows CVI 2017开发笔记--串口API

参考资料&#xff1a;https://download.csdn.net/download/Stark_/87424565?spm1001.2014.3001.5501 转载请注明出处&#xff1a;https://blog.csdn.net/Stark_/article/details/128966962?spm1001.2014.3001.5501 打开串口OpenComConfig OpenComConfig 打开一个串行并进行…

HTML-CSS-js教程

HTML 双标签<html> </html> 单标签<img> html5的DOCTYPE声明 <!DOCTYPE html>html的基本骨架 <!DOCTYPE html> <html> </html>head标签 用于定义文档的头部。文档的头部包含了各种属性和信息&#xff0c;包括文档的标题&#…

【成为架构师课程系列】架构设计中的核心思维方法

架构设计中的核心思维方法 目录 前言 #一、抽象思维 #二、分层思维 #三、分治思维 #四、演化思维 #五、如何培养架构设计思维

leaflet 加载WKT数据(示例代码050)

第050个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中加载WKT文件,将图形显示在地图上。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果; 注意如果OpenStreetMap无法加载,请加载其他来练习 文章目录 示例效果配置方式示例源代码(共67行…

中国特色地流程管理系统,天翎让流程审批更简单

编者按&#xff1a;本文分析了国内企业在采购流程管理系统常遇到的一些难点&#xff0c;并从适应中国式流程管理模式的特点出发&#xff0c;介绍了符合中国特色的流程审批管理系统——天翎流程管理系统。关键词&#xff1a;可视化开发&#xff0c;拖拽建模&#xff0c;审批控制…

威联通ContainerStation部署Oracle11g

文章目录前言部署过程详解使用docker-compose文件创建容器临时开启NAS的SSH远程访问通过SSH客户端远程连接NAS进入容器创建用户拷贝容器中的数据库相关文件至宿主机在ContainerStation中修改docker-compose文件总结前言 ContainerStation本质上是对Docker可视化的一款软件&…

聊聊分布式锁——Redis和Redisson的方式

一、什么是分布式锁 分布式~~锁&#xff0c;要这么念&#xff0c;首先得是『分布式』&#xff0c;然后才是『锁』 分布式&#xff1a;这里的分布式指的是分布式系统&#xff0c;涉及到好多技术和理论&#xff0c;包括CAP 理论、分布式存储、分布式事务、分布式锁... 分布式系统…