SpringBoot 之使用 Redis 实现接口幂等性

2023-10-30

幂等概念

在实际的开发项目中,一个对外暴露的接口往往会面临,瞬间大量的重复的请求提交,如果想过滤掉重复请求造成对业务的伤害,那就需要实现幂等!

幂等的概念:
幂等性,就是一个接口, 多次发起同一个请求,,必须保证操作只能执行一次,最终的含义就是对数据库的影响只能是一次性的,不能重复处理。

哪些情况需要防止:

  • 订单接口,不能多次创建订单。
  • 支付接口,重复支付同一笔订单只能扣一次钱。
  • 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次。
  • 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制。

以 SQL 为例,有些操作是天然幂等的。
SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
delete from user where userid=1,多次操作,结果一样,具备幂等性。
insert into user(userid,name) values(1,‘a’) 如 userid 为唯一主键,即重复操作上面的业务,只 会插入一条用户数据,具备幂等性。

UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。
insert into user(userid,name) values(1,‘a’) 如 userid 不是主键,可以重复,那上面业务多次操 作,数据都会新增多条,不具备幂等性。

幂等解决方案

常见解决方案:

  • 唯一索引 – 防止新增脏数据
  • token机制 – 防止页面重复提交
  • 悲观锁 – 获取数据的时候加锁(锁表或锁行)
  • 乐观锁 – 基于版本号version实现,在更新数据那一刻校验数据
  • 分布式锁 – redis(jedis、redisson)或zookeeper实现
  • 状态机 – 状态变更, 更新数据时判断状态

token 机制

1、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
2、然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
3、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业 务。
4、如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样 就保证了业务代码,不被重复执行。

危险性:
1、先删除 token 还是后删除 token;
(1) 先删除可能导致,业务确实没有执行,重试还带上之前 token,由于防重设计导致, 请求还是不能执行。
(2) 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别 人继续重试,导致业务被执行两次。
(3) 我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。

2、Token 获取、比较和删除必须是原子性
(1) redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行 。
(2) 可以在 redis 使用 lua 脚本完成这个操作。

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

各种锁机制

1、数据库悲观锁
select * from xxxx where id = 1 for update。

2、数据库乐观锁
这种方法适合在更新的场景中, update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1

3、业务层分布式锁

各种唯一约束

1、数据库唯一约束
插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。

全局请求唯一 id

调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。 可以使用 nginx 设置每一个请求的唯一 id; proxy_set_header X-Request-Id $request_id;

本文实现

通过Redis+Token机制实现接口幂等性校验。
原理图:
在这里插入图片描述

实现思路

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

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

代码实现

@AutoIdempotent 注解 + 拦截器对请求进行拦截。
@ControllerAdvice 全局异常处理

封装一个操作Redis的API工具类,使用RedisTemplate进行封装,需引入Redis的stater:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

@Component
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 写入缓存
     *
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 写入缓存设置时效时间
     *
     * @param key
     * @param value
     * @return
     */
    public boolean setEx(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 判断缓存中是否有对应的value
     *
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 读取缓存
     *
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }


    /**
     * 删除对应的value
     *
     * @param key
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
    }
}

自定义注解AutoIdempotent:
自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时。

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

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

token服务接口,token创建和检验:
createToken创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,返回这个token值。
checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。

import javax.servlet.http.HttpServletRequest;

public interface TokenService {
    // 创建token
    String createToken();
    // 检验token
    void checkToken(HttpServletRequest request);
}


import com.demo.exception.ServiceException;
import com.demo.service.RedisService;
import com.demo.service.TokenService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

@Component
public class TokenServiceImpl implements TokenService {

    private static final String TOKEN_NAME = "token";

    @Autowired
    private RedisService redisService;

    @Override
    public String createToken() {
        String token = UUID.randomUUID().toString();
        try {
            redisService.setEx(token, token, 10000L);
            return token;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    @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("参数不合法,必须带token参数");
            }
        }
        if (!redisService.exists(token)) {
            throw new ServiceException("请勿重复操作");
        }
        boolean remove = redisService.remove(token);
        // 必须再次判断是否移除成功,因为可能多个请求同时执行上面移除的代码,但是最终只有一个返回移除成功的,如果不判断是否移除成功,就会失去幂等性的
        if (!remove) {
            throw new ServiceException("请勿重复操作");
        }
    }
}

在这里插入图片描述

拦截器处理幂等:
主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。

import com.demo.annotation.AutoIdempotent;
import com.demo.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;

/**
 * 接口幂等性拦截器
 */
@Component
public class AutoIdempotentInterceptor 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();
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            tokenService.checkToken(request);
        }
        return true;
    }

    @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 {
    }
}

配置拦截器:
继承WebMvcConfigurationSupport,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。

import com.demo.interceptor.AutoIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import javax.annotation.Resource;

@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {

    @Resource
    private AutoIdempotentInterceptor autoIdempotentInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}

业务请求类:
通过/get/token路径通过getToken()方法去获取具体的token。
调用test/idempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者。

import com.demo.annotation.AutoIdempotent;
import com.demo.service.TokenService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class BusinessController {

    @Resource
    private TokenService tokenService;

    @GetMapping("/get/token")
    public String getToken() {
        String token = tokenService.createToken();
        return token;
    }


    @AutoIdempotent
    @GetMapping("/test/idempotence")
    public String testIdempotence() {
        return "ok";
    }
}

获取Token:
在这里插入图片描述
第一次请求:
在这里插入图片描述
第二次请求:
在这里插入图片描述
参考:
瞬间几千次的重复提交,我用 SpringBoot+Redis 扛住了
Sprinig Boot + Redis 实现接口幂等性,写得太好了!

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

SpringBoot 之使用 Redis 实现接口幂等性 的相关文章

随机推荐

  • 【文献管理】Zotero基础操作

    文献管理 Zotero基础操作 文章目录 文献管理 Zotero基础操作 一 安装 注册 二 文献导入 1 新建文件夹 2 导入方式 三 文献管理 1 添加标签 2 添加笔记 3 添加插件 四 数据管理与同步 1 数据存放位置 2 数据备份
  • 操作系统模拟页面调度算法(OPT、FIFO、LRU)演示(vc6.0调试通过)

    PageSwitch cpp Defines the entry point for the console application include stdafx h 请分别用FIFO OPT算法实现 页面置换 的模拟 模拟程序的要求如下
  • python数据解析——xpath爬取文字和图片

    xpath解析 最常用且最便捷高效的一种解析方式 通用性 xpath解析原理 1 实例化一个etree的对象 且需要将被解析的页面源码数据加载到该对象中 2 调用etree对象中的xpath方法结合着xpath表达式实现标签的定位和内容的捕
  • 基于redis实现延时队列(二)

    背景 上篇文章中使用了redis的zset 定时器实现延时任务 虽然定时器设置为30秒执行一次 但是还是有时间上的差异化 现更换一种方式实现 可以避免时间上的差异 redis的key过期回调事件 也能达到延迟队列效果 配置修改 redis的
  • QT—3D绘图

    OpenGL是一个跨平台的 用来渲染3D图形的标准API Qt对OpenGL提供了强大的支持 Qt4时代的QtOpenGL模块在Qt5中已经不再建议使用 OpenGL相关的类被移到了Qt GUI模块 Qt Widgets模块中的QOpenG
  • csharp(CS0051)

    net6 定义了一个类 里面有一个枚举 报csharp CS0051 enum Sex Male Female public class Student string name int age Sex sex public Student
  • torch.quantile or np.quantile的计算

    torch的文档详细说明了quantile的计算方法 主要是将q的范围 0 1 转成输入index的范围 0 n 也就是说 将q 乘 n 然后插值计算 quantile位置不是整数 a torch tensor 0 0 1 1 2 1 3
  • 【已解决】5.24 代码问题——TensorFlow==1.14.0版本安装

    安装TensorFlow1 14 0总是显示 ERROR Could not find a version that satisfies the requirement tensorflow1 14 from versions 2 5 0
  • 百度商业大规模微服务分布式监控系统-凤睛

    导读 作为凤睛早期的接入方 后期的核心成员 笔者经历了整个项目前后四年的变迁 看过项目的艰难开端 中期的默默积累以及后期的蓬勃发展 每一次架构的变迁都带着技术浪潮的烙印 也看到项目成员利用有限资源来解决实际问题而持续不断的创新 凤睛是百度商
  • 关于Mac下appid登录提示‘This action could not be completed.’

    情景 打开xcode 运行工程时 xcode提示输入公司开发者账号密码 惊讶 之前没出现过的 按照要求输入密码 但又提示账号过期或密码可能被修改等 于是立马用账号去登陆开发者中心 确保正常登陆后 才放下心来 百度了下 把xcode偏好设置里
  • MySQL-SQL存储过程/触发器详解(下)

    作者 小刘在C站 个人主页 小刘主页 努力不一定有回报 但一定会有收获加油 一起努力 共赴美好人生 学习两年总结出的运维经验 以及思科模拟器全套网络实验教程 专栏 云计算技术 小刘私信可以随便问 只要会绝不吝啬 感谢CSDN让你我相遇 前言
  • 人工智能课程实践-A* 算法编程实践(python实现)

    本次实验的代码由python编写完成 代码在附录中 需要请自取 1 实验目的 1 熟悉启发式搜索算法 A搜索算法的理论 实现方法 2 掌握A 搜索算法的核心算法实现过程 3 理解A 搜索算法如何解决现实工程问题 学会分析现实问题蕴含的搜索求
  • web项目引入PDF.js并添加水印禁止下载

    目录 web项目引入PDF js并添加水印禁止下载 1 下载并引入PDF js实现预览 2 动态预览PDF文件 3 隐藏打开 下载 打印等功能 4 禁止键盘组合按键下载或另存为 5 禁用鼠标操作 6 添加全局水印 6 PDF文件预览效果 w
  • 信息学奥赛一本通(C++版) 第一部分 C++语言 第一章 C++语言入门

    总目录详见 https blog csdn net mrcrack article details 86501716 信息学奥赛一本通 C 版 第一部分 C 语言 第一章 C 语言入门 http ybt ssoier cn 8088 100
  • python Read timed out 解决方法

    python m pip install upgrade pip 报超时 解决方案 指定镜像 i https pypi doubanio com simple 如 python m pip install upgrade pip user
  • com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException

    com mysql jdbc exceptions jdbc4 MySQLNonTransientConnectionException Communications link failure during rollback Transac
  • 物联网开发笔记(50)- 使用Micropython开发ESP32开发板之控制HC-SR501人体红外感应传感器

    一 目的 这一节我们学习如何使用我们的ESP32开发板来控制HC SR501人体红外感应传感器 二 环境 ESP32 HC SR501人体红外感应传感器 Thonny IDE 几根杜邦线 接线方法 三 代码 from machine imp
  • 正态分布(高斯分布)、Q函数、误差函数、互补误差函数

    1 正态分布 高斯分布 若随机变量 XX 服从一个位置参数为 尺度参数为 的概率分布 且其概率密度函数为 f x 12 e x 22 2f x 12 e x 22 2 则这个随机变量就称为正态随机变量 正态随机变量服从的分布就称为正态分布
  • Yarn资源管理器

    Yarn简介 Apache Yarn Yet another Resource Negotiator 是Hadoop集群的资源管理器 负责为上层应用提供资源管理和调度 Yarn在Hadoop集群中充当资源管理和任务调度的框架 Yarn被引入
  • SpringBoot 之使用 Redis 实现接口幂等性

    文章目录 幂等概念 幂等解决方案 token 机制 各种锁机制 各种唯一约束 全局请求唯一 id 本文实现 实现思路 代码实现 幂等概念 在实际的开发项目中 一个对外暴露的接口往往会面临 瞬间大量的重复的请求提交 如果想过滤掉重复请求造成对