Sprinig Boot + Redis 如何实现接口幂等性

2023-05-16

幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 

可能出现的问题:

  • 订单接口, 不能多次创建订单

  • 支付接口, 重复支付同一笔订单只能扣一次钱

  • 回调接口, 可能会多次回调, 必须处理重复回调

  • 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次 等等

解决方案:

  1. 唯一索引 -- 防止新增脏数据

  2. token机制 -- 防止页面重复提交

  3. 悲观锁 -- 获取数据的时候加锁(锁表或锁行)

  4. 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据

  5. 分布式锁 -- redis(jedis、redisson)或zookeeper实现

  6. 状态机 -- 状态变更, 更新数据时判断状态

为需要保证幂等性的每次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token作为请求参数请求接口, 后端接口判断redis中是否存在此token:

  • 如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示

  • 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可

项目技术栈

  • springboot

  • redis

  • @ApiIdempotent注解 + 拦截器对请求进行拦截

  • @ControllerAdvice全局异常处理

  • 压测工具: jmeter

六、代码实现

  1. pom

<!-- Redis-Jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

        <!--lombok 本文用到@Slf4j注解, 也可不引用, 自定义log即可-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
  1. JedisUtil

package com.wangzaiplus.test.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

@Component
@Slf4j
public class JedisUtil {

    @Autowired
    private JedisPool jedisPool;

    private Jedis getJedis() {
        return jedisPool.getResource();
    }

    /**
     * 设值
     *
     * @param key
     * @param value
     * @return
     */
    public String set(String key, String value) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.set(key, value);
        } catch (Exception e) {
            log.error("set key:{} value:{} error", key, value, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 设值
     *
     * @param key
     * @param value
     * @param expireTime 过期时间, 单位: s
     * @return
     */
    public String set(String key, String value, int expireTime) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.setex(key, expireTime, value);
        } catch (Exception e) {
            log.error("set key:{} value:{} expireTime:{} error", key, value, expireTime, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 取值
     *
     * @param key
     * @return
     */
    public String get(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.get(key);
        } catch (Exception e) {
            log.error("get key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 删除key
     *
     * @param key
     * @return
     */
    public Long del(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.del(key.getBytes());
        } catch (Exception e) {
            log.error("del key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 判断key是否存在
     *
     * @param key
     * @return
     */
    public Boolean exists(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.exists(key.getBytes());
        } catch (Exception e) {
            log.error("exists key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 设值key过期时间
     *
     * @param key
     * @param expireTime 过期时间, 单位: s
     * @return
     */
    public Long expire(String key, int expireTime) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.expire(key.getBytes(), expireTime);
        } catch (Exception e) {
            log.error("expire key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 获取剩余时间
     *
     * @param key
     * @return
     */
    public Long ttl(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.ttl(key);
        } catch (Exception e) {
            log.error("ttl key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    private void close(Jedis jedis) {
        if (null != jedis) {
            jedis.close();
        }
    }

}
  1. 自定义注解@ApiIdempotent

package com.wangzaiplus.test.annotation;

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

/**
 * 在需要保证 接口幂等性 的Controller的方法上使用此注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
  1. ApiIdempotentInterceptor拦截器

package com.wangzaiplus.test.interceptor;

import com.wangzaiplus.test.annotation.ApiIdempotent;
import com.wangzaiplus.test.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

/**
 * 接口幂等性拦截器
 */
public class ApiIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
        if (methodAnnotation != null) {
            check(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
        }

        return true;
    }

    private void check(HttpServletRequest request) {
        tokenService.checkToken(request);
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}
  1. TokenServiceImpl

package com.wangzaiplus.test.service.impl;

import com.wangzaiplus.test.common.Constant;
import com.wangzaiplus.test.common.ResponseCode;
import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.exception.ServiceException;
import com.wangzaiplus.test.service.TokenService;
import com.wangzaiplus.test.util.JedisUtil;
import com.wangzaiplus.test.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;

@Service
public class TokenServiceImpl implements TokenService {

    private static final String TOKEN_NAME = "token";

    @Autowired
    private JedisUtil jedisUtil;

    @Override
    public ServerResponse createToken() {
        String str = RandomUtil.UUID32();
        StrBuilder token = new StrBuilder();
        token.append(Constant.Redis.TOKEN_PREFIX).append(str);

        jedisUtil.set(token.toString(), token.toString(), Constant.Redis.EXPIRE_TIME_MINUTE);

        return ServerResponse.success(token.toString());
    }

    @Override
    public void checkToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.isBlank(token)) {// header中不存在token
            token = request.getParameter(TOKEN_NAME);
            if (StringUtils.isBlank(token)) {// parameter中也不存在token
                throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg());
            }
        }

        if (!jedisUtil.exists(token)) {
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
        }

        Long del = jedisUtil.del(token);
        if (del <= 0) {
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
        }
    }

}
  1. TestApplication

package com.wangzaiplus.test;

import com.wangzaiplus.test.interceptor.ApiIdempotentInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication
@MapperScan("com.wangzaiplus.test.mapper")
public class TestApplication  extends WebMvcConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

    /**
     * 跨域
     * @return
     */
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 接口幂等性拦截器
        registry.addInterceptor(apiIdempotentInterceptor());
        super.addInterceptors(registry);
    }

    @Bean
    public ApiIdempotentInterceptor apiIdempotentInterceptor() {
        return new ApiIdempotentInterceptor();
    }

}

OK, 目前为止, 校验代码准备就绪, 接下来测试验证

七、测试验证

  1. 获取token的控制器TokenController

package com.wangzaiplus.test.controller;

import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/token")
public class TokenController {

    @Autowired
    private TokenService tokenService;

    @GetMapping
    public ServerResponse token() {
        return tokenService.createToken();
    }

}
  1. TestController, 注意@ApiIdempotent注解, 在需要幂等性校验的方法上声明此注解即可, 不需要校验的无影响

package com.wangzaiplus.test.controller;

import com.wangzaiplus.test.annotation.ApiIdempotent;
import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {

    @Autowired
    private TestService testService;

    @ApiIdempotent
    @PostMapping("testIdempotence")
    public ServerResponse testIdempotence() {
        return testService.testIdempotence();
    }

}
  1. 获取token

查看redis

  1. 测试接口安全性: 利用jmeter测试工具模拟50个并发请求, 将上一步获取到的token作为参数

 

  1. header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为"abcd"

单纯的直接删除token而不校验是否删除成功, 会出现并发安全性问题, 因为, 有可能多个线程同时走到第46行, 此时token还未被删除, 所以继续往下执行, 如果不校验jedisUtil.del(token)的删除结果而直接放行, 那么还是会出现重复提交问题, 即使实际上只有一次真正的删除操作

稍微修改一下代码:

再次请求

再看看控制台

虽然只有一个真正删除掉token, 但由于没有对删除结果进行校验, 所以还是有并发问题, 因此, 必须校验

九、总结

其实思路很简单, 就是每次请求保证唯一性, 从而保证幂等性, 通过拦截器+注解, 就不用每次请求都写重复代码, 其实也可以利用spring aop实现。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Sprinig Boot + Redis 如何实现接口幂等性 的相关文章

  • Python Koans(1)-assert

    span class hljs comment usr bin env python span span class hljs comment coding utf 8 span span class hljs keyword from s
  • [初学Python]学习如何编写GUI界面(初级)

    sublime 这个文本编辑器好像就是用python写的 所以一直也挺好奇如何利用Pycharm编写有界面的python应用 言归正传 xff0c Python界面程序编写主要依靠一个库叫做Tkinter 下载Tkinter 我使用的是Ub
  • Youtube 视频自动播放

    最近在做一项目的时候 xff0c 需要播放Youtube视频 播放方式大体有三种 1 WebView xff08 我最终用的 xff09 2 Youtube SDK xff08 需要谷歌开发者账户 xff09 3 VideoView 其他两
  • Clion的激活方法

    弹出注册窗口选择Activate License Server 输入 http xidea online xff0c 然后点击 Activate 完成认证即可
  • CocosCreator学习1-按钮点击

    Cocos Creator小白学习 实现button点击事件 关于cocos creator 本人就是小白一个 xff0c 什么都不会 xff0c 只能慢慢从头开始摸索着来 xff0c 自己也希望能够在写自己的学习过程中 xff0c 能够给
  • 多传感器融合中的时间同步2-论文阅读

    文章目录 前言主要内容pps对于INS时间戳校准作用原理 测试结果参考文献 前言 阅读硕士论文 GPS INS组合导航系统研究及实现 xff0c 该论文第5章为时间同步系统设计 xff0c 为GPS INS系统设计的时间同步系统部分内容非常
  • GUI读取图片和变换图片

    GUI读取图片和变换图片 这个程序的主要功能是一个按钮读取图片 xff0c 另一个用来对图片进行变换的 xff0c 过程很简单 GUI中的varargout函数将句柄handles保存的hObject输出 xff0c hObject是当前回
  • Java泛型--泛型应用--泛型接口、泛型方法、泛型数组、泛型嵌套

    1 泛型接口 1 1泛型接口的基本概念 1 2泛型接口实现的两种方式 定义子类 xff1a 在子类的定义上也声明泛型类型 interface Info lt T gt 在接口上定义泛型 public T getVar 定义抽象方法 xff0
  • SLA的基本概念

    SLA的基本概念 现在的产品和系统都非常的复杂 xff0c 彼此连接依赖越来越复杂 xff0c 为了整体的高速运转 xff0c 对每个部件的稳定性越来越高 xff0c 越来越精密 xff0c 发展到一定程度 xff0c 人力已经无法掌控 x
  • Linux下调试段错误的方法[Segmentation Fault]--GDB

    原文 1 段错误是什么 xff1f 段错误是指访问的内存超出了系统给这个程序所设定的内存空间 xff0c 例如访问了不存在的内存地址 访问了系统保护的内存地址 访问了只读的内存地址等等情况 A segmentation fault ofte
  • linux驱动开发--copy_to_user 、copy_from_user函数实现内核空间数据与用户空间数据的相互访问

    设备读操作 如果该操作为空 xff0c 将使得read系统调用返回负EINVAL失败 xff0c 正常返回实际读取的字节数 ssize t read struct file filp char user buf size t count l
  • 函数中的形式参数和实际参数

    1 举例 xff1a 使用函数交换两个整形变量的值 运行结果 xff1a 分析 xff1a c语言中实际参数和形式参数之间采用值传递的方式来传递数据 在被调函数中 xff0c 使用的是实际参数的一个拷贝数据 我们在swap函数中交换了a和b
  • Linux 线程挂起与唤醒功能 实例

    pthread cond wait 多线程的条件变量 条件变量是利用线程间共享的 全局变量进行同步的一种机制 xff0c 主要包括两个动作 xff1a 一个线程等待 34 条件变量的条件成立 34 而挂起 xff1b 另一个线程使 34 条
  • Linux下socket编程,附带tcp例子

    1 网络中进程之间如何通信 xff1f 本地的进程间通信 xff08 IPC xff09 有很多种方式 xff0c 但可以总结为下面4类 xff1a 消息传递 xff08 管道 FIFO 消息队列 xff09 同步 xff08 互斥量 条件
  • 程序员加班到深夜,你经历过没?

    我看到了自己的影子啊 虽然自己非科班出身 xff0c 学历也不高吧 xff0c 但是自认为还是很努力的 xff0c 但是为什么现在的工资水平却跟应届生差不多呢 xff1f xff08 xff09 仔细想想 xff0c 自己毕业3年了 xff
  • 【C/C++学院】(16)QT版:幸运大抽奖

    程序效果 xff1a ifndef DIALOG H define DIALOG H include lt QDialog gt include lt QLabel gt include lt QPushButton gt include
  • 【Python基础】--Pickle/函数默认参数/函数的参数*args/Bytes<=>str/32-64bit/bytes对象

    Pickle gt gt gt import pickle gt gt gt my list 61 1 2 3 39 haha 39 39 and 39 39 or 39 gt gt gt pickle file 61 open 39 my
  • Windows平台python操作串口示例,可以加工下,改写成方便的测试软件

    在 windows中 xff0c 使用 Python 进行串口编程需要安装一个 Serial 模块 pyserial xff1a 下载地址 https pypi python org pypi pyserial下载完成后得到一个 pyser
  • 告别csdn一年了

    原本坚持了4年的学习 xff0c 整理笔记 xff0c 在csdn平台上进行发表 xff0c 记录 同朋友们互动 xff0c 探讨进行学习 xff0c 自己也在不断地成长 今天再次进入博客页面 xff0c 发现界面来了个大改版 xff0c
  • KVM虚拟机创建功能详细讲解

    KVM虚拟机创建功能详细讲解 一 KVM虚拟机创建的用户操作 对于用户或者管理员来说 xff0c 虚拟机的创建有着很多的方法 xff0c 例如 xff1a kvm自带命令行工 具 使用virsh命令来创建 使用具有图形界面的virt man

随机推荐

  • php视频课程

    php视频课程 xff1a 下载地址 xff1a http php itcast cn php video shtml 注 xff1a 此系列视频 xff0c 韩顺平主讲 1 php入门到精通教程 2 第二版mysql视频教程 进行中 3
  • 2014年计算机求职总结--面试篇

    又一年实习招聘陆续开始了 xff0c 这里分享一下我在2013年实习招聘和秋季招聘中的一些面试经历 xff0c 希望能对找工作的同学有所帮助 2013年面试过的公司有蘑菇街 网易游戏 阿里巴巴 腾讯 百度 大众点评 人人网 雅虎 xff08
  • 用位运算实现两个整数的加减乘除运算

    位运算的思想可以应用到很多地方 xff0c 这里简单的总结一下用位运算来实现整数的四则运算 1 整数加法 int Add int a int b for int i 61 1 i i lt lt 61 1 if b amp i for in
  • 深入理解C/C++数组和指针

    版权所有 xff0c 转载请注明出处 xff0c 谢谢 xff01 http blog csdn net walkinginthewind article details 7044380 C语言中数组和指针是一种很特别的关系 xff0c 首
  • 轻松搞定面试中的链表题目

    版权所有 xff0c 转载请注明出处 xff0c 谢谢 xff01 http blog csdn net walkinginthewind article details 7393134 链表是最基本的数据结构 xff0c 面试官也常常用链
  • 轻松搞定面试中的二叉树题目

    版权所有 xff0c 转载请注明出处 xff0c 谢谢 xff01 http blog csdn net walkinginthewind article details 7518888 树是一种比较重要的数据结构 xff0c 尤其是二叉树
  • 动态内存分配(malloc/free)简单实现--隐式空闲链表

    本文使用隐式空闲链表实现简单的动态内存分配 动态内存分配器维护一个大块区域 xff0c 也就是堆 xff0c 处理动态的内存分配请求 分配器将堆视为一组不同大小的块的集合来维护 xff0c 每个块要么是已分配的 xff0c 要么是空闲的 实
  • 二分查找,你真的掌握了吗?

    版权所有 xff0c 转载请注明出处 xff0c 谢谢 xff01 http blog csdn net walkinginthewind article details 8937978 二分查找 xff0c 最基本的算法之一 xff0c
  • 【谷歌面试题】求数组中两个元素的最小距离

    一个数组 xff0c 含有重复元素 xff0c 给出两个数num1和num2 xff0c 求这两个数字在数组中出现的位置的最小距离 O n 时间复杂度 xff0c O 1 空间复杂度 int minDistance int A int si
  • 进程间通信

    原作者地址不详 摘 要 随着人们对应用程序的要求越来越高 xff0c 单进程应用在许多场合已不能满足人们的要求 编写多进程 多线程程序成为现代程序设计的一个重要特点 xff0c 在多进程程序设计中 xff0c 进程间的通信是不可避免的 Mi
  • 关于CPU C-States 省电模式,你需要知道的事情

    为了在CPU空闲的时候降低功耗 xff0c CPU可以被命令进入low power模式 每个CPU都有几种power模式 xff0c 这些模式被统称为C states或者C modes lower power模式最早在486DX4处理器上被
  • 一次由于设置错误,导致无法进入gnome的解决。

    我的系统是lenny 今天 xff0c 在 系统 gt 首选项 gt 音效 中修改了一处设置 xff0c 导致当前账号不能进入gnome 设置的图片如下 xff1a 我选中了 允许软件混音 和 播放系统声音 这两项 xff0c 结果当时系统
  • 分享一下工作以来我看过计算机书籍

    由于自工作依赖一直专注于linux 下的c c 43 43 编程工作 xff0c 所以 xff0c 我的书籍也大的都是这方 这边书尽管很经典 xff0c 但是我的能力实在有限 xff0c 只把数据结构的那点看了一下 xff0c 其他的 看的
  • strlen三种不同的写法

    strlen用来表示字符串的长度 区别于用关键字sizeof 计算字节大小 strlen有三种写法 1 采用常规的count计数法 int my strlen const char str char p 61 char str int co
  • 面试常见链表题总结(如果失眠了就拿出来看看)

    无头单链表 现场手撕代码 43 分析 xff0c 供自己闲时手机复习使用 xff01 删除链表中等于给定值 val 的所有节点 反转一个单链表 给定一个带有头结点 head 的非空单链表 xff0c 返回链表的中间结点 如果有两个中间结点
  • 数据库完美总结(三)

    索引 xff1a 数据库索引 xff0c 是数据库管理系统中一个排序的数据结构 xff0c 它可以对数据库表中一列或多列的值进行排序 xff0c 以协助更加快速的访问数据库表中特定的数据 通俗的说 xff0c 我们可以把数据库索引比做是一本
  • 电商项目数据库

    共8张表 xff0c 放到一个DB文件夹下 1 系统架构 采用当前最流行的ssm xff08 springmvc 43 spring 43 mybatis xff09 框架开发 xff0c 系统后台使用jsp作为视图层 商城系统使用free
  • jar包和war包区别以及怎么部署

    什么是war和jar xff1f war包 是做好一个web应用后 xff0c 通常是网站 xff0c 打成包部署到容器中 jar包 xff1a 通常是开发时要引用通用类 xff0c 打成包便于存放管理 怎么打包 xff1f IDEA上面菜
  • cookie和session

    由于http是无状态的 xff0c 也就是不能做到会话保持 xff0c 那么就需要引入cookie和session来做会话保持 xff0c 存储用户信息 cookies是一种WEB服务器通过浏览器在访问者的硬盘上存储信息的手段 IE浏览器把
  • Sprinig Boot + Redis 如何实现接口幂等性

    幂等性 通俗的说就是一个接口 多次发起同一个请求 必须保证操作只能执行一次 可能出现的问题 xff1a 订单接口 不能多次创建订单 支付接口 重复支付同一笔订单只能扣一次钱 回调接口 可能会多次回调 必须处理重复回调 普通表单提交接口 因为