接口幂等性

接口设计与重试机制引发的问题

什么是幂等性【数学性的概念】-----f(f(x))=f(x)

解释:

数学方面:幂等元素运行多次,还等于它原来的运算结果。

程序方面:在系统中,一个接口运行多次,与运行一次的效果是一致的。

为什么会产生接口幂等性问题

网络波动, 可能会引起重复请求。

用户重复操作,用户在操作时候可能会无意触发多次下单交易,甚至没有响应而有意触发多次交易应用。

使用了失效或超时重试机制(Nginx重试、RPC重试或业务层重试等)。

使用浏览器后退按钮重复之前的页面操作,导致重复提交表单。

注意:

不是所有接口都要求幂等性,要根据业务而定。

Select操作:不会对业务数据有影响,天然幂等。

select * from user where user_id = 1;

Delete操作:第一次已经删除,第二次也不会有影响。

delete from user where user_id = 1;

Update操作:更新操作传入数据版本号,通过乐观锁实现幂等性。

update user set username = 'zhangsan' where user_id = 1  【这个没有问题】

update user set age = age + 1 where user_id = 1    【这个回出现问题】

insert操作:此时没有唯一业务单号,使用Token保证幂等

insert into order(pkid, order_id, xx) values (1, '20210304020226953568', ...);

如何保证接口幂等性

一个方向是客户端防止重复调用

一个是服务端进行校验                  

接口设计与重试机制引发的问题演示_项目搭建                             

创建springboot类型项目:

在pom.xml中加入先关依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.13</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ss.demo</groupId>
    <artifactId>springboottest</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboottest</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

修改application.yml

spring:
  application:
    name: springboottest
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/usertt?useSSL=false
    username: root
    password: 123456
  thymeleaf:
    cache: false
  redis:
    host: localhost
    port: 6379
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
server:
  port: 9000

创建数据库usertt

创建表users

CREATE TABLE users(

   id BIGINT(20) NOT NULL COMMENT '主键ID',

   name  VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',

   age INT(11) NULL DEFAULT NULL COMMENT '年龄',

  PRIMARY KEY (id)

);

-- 添加用户数据

INSERT INTO users (id, NAME, age) VALUES

(1, 'admin', 18),

(2, 'root', 20),

(3, 'mysql', 28),

(4, 'tom', 21 ),

(5, 'lili', 24)

通过mybatis-plus工具生成相关接口和类:

在项目com.ss.demo.service的IusersService接口中进行修改操作

package com.ss.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.ss.demo.domain.Users;

import java.util.List;

public interface IUsersService extends IService<Users> {

    /**
     * 查询所有用户
     * @return
     */
    List<Users> findAll();

    /**
     * 创建用户
     * @param name
     * @param age
     * @return
     */
    Integer create(String name ,Integer age);

    /**
     * 根据Id查询用户信息
     * @param id
     * @return
     */
    Users findById(Long id);

    /**
     * 修改用户信息
     * @param user
     * @return
     */
    Integer update(Users user);
}

实现类UsersServiceImpl的编写:

package com.ss.demo.service.impl;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ss.demo.domain.Users;
import com.ss.demo.mapper.UsersMapper;
import com.ss.demo.service.IUsersService;
import org.springframework.stereotype.Service;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 */
@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements IUsersService {

    @Override
    public List<Users> findAll() {
        return baseMapper.selectList(null);
    }

    @Override
    public Integer create(String name, Integer age) {
        Users user = new Users();
        user.setName(name);
        user.setAge(age);
        return baseMapper.insert(user);
    }

    @Override
    public Users findById(Long id) {
        return baseMapper.selectById(id);
    }

    @Override
    public Integer update(Users user) {
        return baseMapper.updateById(user);

    }
}

编写控制层在包com.ss.demo.controller中的UsersController中进行操作

package com.ss.demo.controller;


import com.ss.demo.domain.Users;
import com.ss.demo.service.IUsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 */
@Controller
@RequestMapping("/users")
public class UsersController {

    @Autowired
    private IUsersService usersService;

    /**
     * 跳转首页
     * @return
     */
    @GetMapping("/index")
    public ModelAndView list(){
        ModelAndView modelAndView = new ModelAndView();
        List<Users> all =  usersService.findAll();
        modelAndView.setViewName("index");
        modelAndView.addObject("users",all);
        return modelAndView;
    }

    /**
     * 跳转添加页面
     * @return
     */
    @GetMapping("/toadduser")
    public String toadduser() {
        return "add";
    }


    /**
     * 创建用户
     * @param name
     * @param age
     * @return
     */
    @PostMapping("/create")
    public String  create(String name,Integer age){
        Integer integer = usersService.create(name, age);
        if (integer == 1){
            return "redirect:/users/index";
        }
        return "addUser";
    }

    /**
     * 根据id查询用户操作
     * @param id
     * @return
     */
    @GetMapping("/findById")
    public ModelAndView  getByUserId(Long id){
        Users user = usersService.findById(id);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("user",user);
        modelAndView.setViewName("update");
        return modelAndView;
    }

    /**
     * 修改操作
     * @param user
     * @return
     */
    @PostMapping("/update")
    public String  update(Users user){
        Integer update = usersService.update(user);
        if (update == 1){
            return "redirect:/users/index";
        }
        return "update";
    }
}

创建页面:

index.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body style="text-align: center">

<div>
    <table border="1" align="center">
        <tr>
            <th>id</th>
            <th>名字</th>
            <th>年纪</th>
            <th>操作</th>
        </tr>
        <tr th:each="u:${users}">
            <td th:text="${u.id}"></td>
            <td th:text="${u.name}"></td>
            <td th:text="${u.age}"></td>
            <td><a th:href="@{/users/findById(id=${u.id})}">更新</a></td>
        </tr>
    </table>


    <a href="/users/toadduser">注册用户</a>
</div>
</body>
</html>

 通过链接跳转到添加页面add.html

注意我们在添加的时候可能会添加失败,是因为主键Id的问题,

实体Id改成下面方式

/**
 * 主键ID
 */
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;

add.html:

<!DOCTYPE html>
<html lang="en"xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>注册用户</title>
</head>
<body style="text-align: center">
<div>
    <form method="post" action="/users/create">
        <label>名字:</label><input name="name" type="text" placeholder="请输入名字">
        <label>年纪:</label><input name="age" type="number" placeholder="请输入年纪">
        <input type="submit" value="注册">
    </form>
</div>
</body>
</html>

添加后

我们测试下载添加过程中出问题的点把controller中的添加增加5秒延时我们看下:
修改controller:

/**
 * 创建用户
 * @param name
 * @param age
 * @return
 */
@PostMapping("/create")
public String  create(String name,Integer age) throws InterruptedException {
    Thread.sleep(5000L);
    Integer integer = usersService.create(name, age);
    if (integer == 1){
        return "redirect:/users/index";
    }
    return "addUser";
}

启动项目我们在添加页面5秒内持续的进行点击操作:

上图模拟网络延迟操作

我们看我们添加完后的页面你会发现添加好多数据

接口幂等性设计_insert操作幂等性原理其实我们就是通过分布式锁的方式解决接口幂等性的问题

请求流程

流程:

为需要保证幂等性的每一次请求创建一个唯一标识token, 先获 取token, 并将此token存入redis, 请求接口时, 将此token放到 header或者作为请求参数请求接口, 后端接口判断redis中是否 存在此token,如果存在, 正常处理业务逻辑, 并从redis中删除此 token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过 校验, 返回重复提交如果不存在, 说明参数不合法或者是重复请 求, 返回提示即可。

接口幂等性设计_insert操作幂等性实现

添加Redis依赖这里之前已经添加过了

我们在controller中修改方法toadduser生成一个token

/**
 * 跳转添加页面
 * @return
 */
@GetMapping("/toadduser")
public ModelAndView toadduser() {
    ModelAndView mav = new ModelAndView();
    mav.setViewName("add");
    //通过UUID来设置token
    String token = UUID.randomUUID().toString().replaceAll("-","");
    //把数据保存到Redis中
    redisTemplate.opsForValue().set(token,Thread.currentThread().getId() +"");
    //返回token到添加add页面
    mav.addObject("token", token);
    return mav;
}

首先我们要修改add.html我们要生成一个token

<!DOCTYPE html>
<html lang="en"xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>注册用户</title>
</head>
<body style="text-align: center">
<div>
    <form method="post" action="/users/create">

<!--把token隐藏到页面中-->
        <input hidden type="text" th:value="${token}" name="token" >
        <label>名字:</label><input name="name" type="text" placeholder="请输入名字">
        <label>年纪:</label><input name="age" type="number" placeholder="请输入年纪">
        <input type="submit" value="注册">
    </form>
</div>
</body>
</html>

进行自定义注解操作即添加了该注解的接口要实现幂等性验证。

新建包com.ss.demo.config并新建自定义接口

package com.ss.demo.config;
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 ApidempotentAnn {
    boolean value() default true;   //这个注解有个属性默认值为true
}

创建拦截器MyInteractor在包com.ss.demo.config下

package com.ss.demo.config;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MyInteractor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //TODO

        return false;
    }
}

们在controller中修改方法create加入自定义注解:

/**
 * 创建用户
 * @param name
 * @param age
 * @return
 */
@ApidempotentAnn
@PostMapping("/create")
public String  create(String name,Integer age) throws InterruptedException {
    Thread.sleep(5000L);
    Integer integer = usersService.create(name, age);
    if (integer == 1){
        return "redirect:/users/index";
    }
    return "addUser";
}

在包com.ss.demo.config下设定拦截器规则类MyWebConfigurer

package com.ss.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class MyWebConfigurer implements WebMvcConfigurer {
    @Autowired
    private MyInteractor myInteractor;
    //设定拦截规则
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //设定不拦截的集合
        List<String> list = new ArrayList<String>();
        list.add("/users/index");
        list.add("/users/toadduser");
        registry.addInterceptor(myInteractor).excludePathPatterns(list);
    }
}

我们现在要用拦截去判断在controller中哪个方法上面带了@ApidempotentAnn这个注解的我们就使用接口幂等性没有的就不用

package com.ss.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;

@Component
public class MyInteractor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 验证token的有效性
     * @param request
     * @return
     */
    private boolean checkToken(HttpServletRequest request) {
        // 获取token
        String token = request.getParameter("token");
        if(token == null || "".equals(token)) {
            return false;
        }
        //删除redis中的token
        return redisTemplate.delete(token);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //HandlerMethod 封装了很多属性,在访问请求方法的时候可以方便的访问到方法参数及方法上的注解
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        //获得方法,这个方法就是create方法
        Method method = handlerMethod.getMethod();

        //判断这个方法上有没有添加幂等性的注解
        boolean flag = method.isAnnotationPresent(ApidempotentAnn.class);
        // 判断是否开启幂等性处理。
        if(flag && method.getAnnotation(ApidempotentAnn.class).value()) {
            // 验证接口幂等性
            boolean checkToken = this.checkToken(request);
            if(checkToken) {
                return true;   //放行
            }
            else {
                //返回相关的错误提示信息
                response.setContentType("application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                out.print("重复调用");
                out.close();
                response.flushBuffer();
                return false;
            }
        }
        return false;
    }
}

如果是修改的话:

Mapper接口:

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author lioajingjing
 * @since 2023-05-18
 */
public interface UsersMapper extends BaseMapper<Users> {
    Integer updateAge(@Param("id") Long id);
    
}

Mapper.xml

<update id="updateAge">
        update users set  age= age+1 where id=#{id}
    </update>

service:

 /**
     * 更新年龄
     * @param users
     * @return
     */
    Integer updateAge(Users users);

serviceimpl:

 @Override
    public Integer updateAge(Users users) {
        return usersMapper.updateAge(users.getId());
    }

controller:

 @RequestMapping("/update")
    public  String update(Users users) throws InterruptedException {

        Thread.sleep(5000);
        Integer index= usersService.updateAge(users);

        if(index == 1){
            return  "redirect:/users/index";
        }
        return "update";
    }

update.html

<!DOCTYPE html>
<html lang="en"xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>更新用户</title>
</head>
<body style="text-align: center">
<div>
    <form method="post" action="/users/update">
        <input name="id" hidden th:value="${user.id}">
        <label>名字:</label><input name="name" type="text" th:value="${user.name}">
        <label>年纪:</label><input name="age" type="number" th:value="${user.age}">
        <input type="submit" value="更新">
    </form>
</div>
</body>
</html>

 

 怎么解决呢:

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值