• 接口幂-全面详解(学习总结---从入门到深化)


    目录

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

     什么是幂等性

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

     幂等性接口设计

     如何保证接口幂等性

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

    创建项目

     选择框架

     修改SpringBoot版本

     创建用户表

    代码生成

    引入依赖

    编写代码生成类

    接口设计与重试机制引发的问题演示_业务实现

    编写接口

    编写接口实现类

    编写控制层

    接口幂等性设计_insert操作幂等性原理

     请求流程

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

     添加Redis依赖

    添加Redis相关配置

    自定义注解

    生成Token

    幂等性拦截器

    配置拦截器

    接口幂等性设计_Update操作幂等性原理

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

    编写用户接口

    编写接口实现类

    编写Mapper接口

    编写Mapper接口语句

    测试update操作

     解决方案

    页面隐藏版本字段

    修改sql语句添加乐观锁


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

     什么是幂等性

     

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

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

     幂等性接口设计

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

    select * from user where user_id = 1;

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

    delete from user where user_id = 1;

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

    1. update user set username = 'zhangsan' where
    2. user_id = 1
    3. update user set age = age + 1 where user_id = 1

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

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

     如何保证接口幂等性

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

    创建项目

    创建SpringBoot项目idempotent-demo

     选择框架

     修改SpringBoot版本

     创建用户表

    1. DROP TABLE IF EXISTS user;
    2. CREATE TABLE user
    3. (
    4. id BIGINT(20) NOT NULL COMMENT '主键ID',
    5. name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    6. age INT(11) NULL DEFAULT NULL COMMENT '年
    7. 龄',
    8. PRIMARY KEY (id)
    9. );
    10. -- 添加用户数据
    11. INSERT INTO user (id, name, age) VALUES
    12. (1, 'Jone', 18),
    13. (2, 'Jack', 20),
    14. (3, 'Tom', 28),
    15. (4, 'Sandy', 21 ),
    16. (5, 'Billie', 24);

    代码生成

    引入依赖

    1. <dependency>
    2. <groupId>com.baomidougroupId>
    3. <artifactId>mybatis-plusgeneratorartifactId>
    4. <version>3.5.2version>
    5. dependency>
    6. <dependency>
    7. <groupId>org.apache.velocitygroupId>
    8. <artifactId>velocity-enginecoreartifactId>
    9. <version>2.0version>
    10. dependency>

    编写代码生成类

    1. package com.itbaizhan.lock.utils;
    2. import com.baomidou.mybatisplus.generator.FastAutoGenerator;
    3. import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
    4. import java.util.Arrays;
    5. import java.util.List;
    6. public class CodeGenerator {
    7. public static void main(String[] args) {
    8. FastAutoGenerator.create("jdbc:mysql://192.168.66.100:3306/distribute", "root", "123456")
    9. .globalConfig(builder -> {
    10. builder.author("itbaizhan")// 设置作者
    11. .commentDate("MMdd") // 注释日期格式
    12. .outputDir(System.getProperty("user.dir")+ "/src/main/java/") // 指定输出目录
    13. .fileOverride(); //覆盖文件
    14. })
    15. // 包配置
    16. .packageConfig(builder -> {
    17. builder.parent("com.itbaizhan.lock") // 包名前缀
    18. .entity("entity")//实体类包名
    19. .mapper("mapper")//mapper接口包名
    20. .service("service"); //service包名
    21. })
    22. .strategyConfig(builder -> {
    23. List strings = Arrays.asList("t_order");
    24. // 设置需要生成的表名
    25. builder.addInclude(strings)
    26. // 开始实体类配置
    27. .entityBuilder()
    28. // 开启lombok模型
    29. .enableLombok()
    30. //表名下划线转驼峰
    31. .naming(NamingStrategy.underline_to_camel)
    32. //列名下划线转驼峰
    33. .columnNaming(NamingStrategy.underline_to_camel);
    34. })
    35. .execute();
    36. }
    37. }

    接口设计与重试机制引发的问题演示_业务实现

    编写接口

    1. /**
    2. * 查询所有用户
    3. * @return
    4. */
    5. List findAll();
    6. /**
    7. * 创建用户
    8. * @param name
    9. * @param age
    10. * @return
    11. */
    12. Integer create(String name ,Integer age);
    13. /**
    14. * 根据id查询用户
    15. * @param id
    16. * @return
    17. */
    18. User findById(Long id);
    19. /**
    20. * 更新用户
    21. * @param user
    22. * @return
    23. */
    24. Integer update(User user);

    编写接口实现类

    1. package com.itbaizhan.idempotentdemo.service.impl;
    2. import com.itbaizhan.idempotentdemo.entity.User;
    3. import com.itbaizhan.idempotentdemo.mapper.UserMapper;
    4. import com.itbaizhan.idempotentdemo.service.IUserService;
    5. import com.baomidou.mybatisplus.extension.service.impl .ServiceImpl;
    6. import org.springframework.stereotype.Service;
    7. import java.util.List;
    8. /**
    9. *

    10. * 服务实现类
    11. *

    12. *
    13. * @author itbaizhan
    14. * @since 06-04
    15. */
    16. @Service
    17. public class UserServiceImpl extends
    18. ServiceImpl implements IUserService {
    19. /**
    20. * 查询全部用户
    21. * @return
    22. */
    23. @Override
    24. public List findAll() {
    25. return baseMapper.selectList(null);
    26. }
    27. /**
    28. * 创建用户
    29. * @param name
    30. * @param age
    31. * @return
    32. */
    33. @Override
    34. public Integer create(String name, Integer age) {
    35. User user = new User();
    36. user.setName(name);
    37. user.setAge(age);
    38. return baseMapper.insert(user);
    39. }
    40. /**
    41. * 根据用户id查询用户
    42. * @param id
    43. * @return
    44. */
    45. @Override
    46. public User findById(Long id) {
    47. return baseMapper.selectById(id);
    48. }
    49. /**
    50. * 更新用户
    51. * @param user
    52. * @return
    53. */
    54. @Override
    55. public Integer update(User user) {
    56. return baseMapper.updateById(user);
    57. }
    58. }

    编写控制层

    1. /**
    2. * 跳转首页
    3. * @return
    4. */
    5. @GetMapping("/index")
    6. public ModelAndView list(){
    7. ModelAndView modelAndView = new ModelAndView();
    8. List all = iUserService.findAll();
    9. modelAndView.setViewName("index");
    10. modelAndView.addObject("users",all);
    11. return modelAndView;
    12. }
    13. /**
    14. * 创建用户
    15. * @return
    16. */
    17. @ApiIdempotentAnn
    18. @PostMapping("/create")
    19. public String create(String name,Integer age){
    20. Integer integer = iUserService.create(name, age);
    21. if (integer == 1){
    22. return "redirect:/user/index";
    23. }
    24. return "addUser";
    25. }
    26. /**
    27. * 根据用户id查询用户
    28. * @param id 用户id
    29. * @return
    30. */
    31. @GetMapping("/getByUserId")
    32. public ModelAndView getByUserId(Long id){
    33. User user = iUserService.findById(id);
    34. ModelAndView modelAndView = new ModelAndView();
    35. modelAndView.addObject("user",user);
    36. modelAndView.setViewName("update");
    37. return modelAndView;
    38. }
    39. /**
    40. * 更新
    41. * @return
    42. */
    43. @PostMapping("/update")
    44. public String update(User user){
    45. Integer update = iUserService.update(user);
    46. if (update == 1){
    47. return "redirect:/user/index";
    48. }
    49. return "update";
    50. }

    接口幂等性设计_insert操作幂等性原理

     请求流程

     流程:

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

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

     添加Redis依赖

    1. org.springframework.boot
    2. spring-boot-starter-data-redis

    添加Redis相关配置

    1. spring.redis.host=localhost
    2. spring.redis.port=6379

    自定义注解

    即添加了该注解的接口要实现幂等性验证。

    1. @Target({ElementType.TYPE, ElementType.METHOD})
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. public @interface ApiIdempotentAnn {
    5. boolean value() default true;
    6. }

    生成Token

    1. /**
    2. * 跳转注册页面
    3. * @return
    4. */
    5. @GetMapping("/toAddUser")
    6. public ModelAndView adduser(){
    7. ModelAndView modelAndView = new ModelAndView();
    8. // 生成Token
    9. String s = UUID.randomUUID().toString();
    10. // 保存redis
    11. stringRedisTemplate.opsForValue().set(s,Thread.currentThread().getId()+"");
    12. modelAndView.setViewName("addUser");
    13. modelAndView.addObject("token",s);
    14. return modelAndView;
    15. }

    幂等性拦截器

    1. package com.itbaizhan.idempotentdemo.config;
    2. import com.itbaizhan.idempotentdemo.ApiIdempotentAnn;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.data.redis.core.StringRedisTemplate;
    5. import org.springframework.stereotype.Component;
    6. import org.springframework.web.method.HandlerMethod;
    7. import org.springframework.web.servlet.HandlerInterceptor;
    8. import javax.servlet.http.HttpServletRequest;
    9. import javax.servlet.http.HttpServletResponse;
    10. import java.io.PrintWriter;
    11. import java.lang.reflect.Method;
    12. @Component
    13. public class ApiIdempotentInceptor implements
    14. HandlerInterceptor {
    15. @Autowired
    16. private StringRedisTemplate stringRedisTemplate;
    17. /**
    18. *表示在所有请求之前完成的拦截,一般使用居多
    19. * @param request
    20. * @param response
    21. * @param handler
    22. * @return
    23. * @throws Exception
    24. */
    25. @Override
    26. public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {
    27. if (!(handler instanceof HandlerMethod)) {
    28. return true;
    29. }
    30. final HandlerMethod handlerMethod = (HandlerMethod) handler;
    31. // 获取方法
    32. final Method method = handlerMethod.getMethod();
    33. // 判断有没有添加需要幂等性注解
    34. boolean methodAnn = method.isAnnotationPresent(ApiIdempotentAnn.class);
    35. // 判断是否开启幂等性严重
    36. if (methodAnn && method.getAnnotation(ApiIdempotentAnn.class).value()) {
    37. // 需要实现接口幂等性
    38. boolean result = checkToken(request);
    39. if (result) {
    40. return true;
    41. } else {
    42. response.setContentType("application/json;charset=utf-8");
    43. PrintWriter writer = response.getWriter();
    44. writer.print("重复调用");
    45. writer.close();
    46. response.flushBuffer();
    47. return false;
    48. }
    49. }
    50. return false;
    51. }
    52. private boolean checkToken(HttpServletRequest request) {
    53. String token = request.getParameter("token");
    54. if (null == token || "".equals(token))
    55. {
    56. // 没有token,说明重复调用或者
    57. return false;
    58. }
    59. // 返回是否删除成功
    60. return stringRedisTemplate.delete(token);
    61. }
    62. }

    配置拦截器

    WebMvcConfigurer配置类其实是 Spring 内部的一种配置方式,可以 自定义一些Handler,Interceptor,ViewResolver, MessageConverter等等的东西对springmvc框架进行配置。

    1. package com.itbaizhan.idempotentdemo.config;
    2. import org.springframework.beans.factory.annotation.Autowired;
    3. import org.springframework.context.annotation.Configuration;
    4. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    6. import java.util.ArrayList;
    7. import java.util.List;
    8. @Configuration
    9. public class WebConfig implements WebMvcConfigurer {
    10. @Autowired
    11. private ApiIdempotentInceptor apiIdempotentInceptor;
    12. @Override
    13. public void addInterceptors(InterceptorRegistry registry) {
    14. List list=new ArrayList();
    15. list.add("/user/toAddUser");
    16. list.add("/user/index");
    17. registry.addInterceptor(apiIdempotentInceptor).excludePathPatterns(list);
    18. }
    19. }

    接口幂等性设计_Update操作幂等性原理

     既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观 锁。需要在表中增加一个 timestamp 或者 version 字段,这里以 version 字段 为例。

    1. --在更新数据之前先查询一下数据:
    2. select id,name,age,version from user id=123;

     如果数据存在,假设查到的 version 等于 1 ,再使用 id 和 version 字段作为查询条件更新数据:

    1. update user set age=age+1,version=version+1
    2. where id=123 and version=1;

    更新数据的同时 version+1 ,然后判断本次 update 操作的影响行数, 如果大于0,则说明本次更新成功,如果等于0,则说明本次更 新没有让数据变更。

     

     具体步骤:

    1 先根据id查询用户信息,包含version字段

    2 根据id和version字段值作为where条件的参数,更新用户信息,同时version+1

    3 判断操作影响行数,如果影响1行,则说明是一次请求,可以做其他数据操作。

    4 如果影响0行,说明是重复请求,则直接返回成功。

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

    编写用户接口

    1. /**
    2. * 更新用户
    3. * @param id 用户id
    4. * @return
    5. */
    6. Integer updateAge(Long id);

    编写接口实现类

    1. /**
    2. * 更新年纪
    3. * @param id
    4. * @return
    5. */
    6. @Override
    7. public Integer updateAge(Long id) {
    8. return baseMapper.updateAge(id);
    9. }

    编写Mapper接口

    1. public interface UserMapper extends BaseMapper {
    2. Integer updateAge(@Param("id") Long id);
    3. }

    编写Mapper接口语句

    1. <mapper namespace="com.itbaizhan.idempotentdemo.mapper.UserMapper">
    2. <update id="updateAge" parameterType="long">
    3. update user set age = age + 1 where id = #{id}
    4. update>
    5. mapper>

    测试update操作

     更新Jone数据。

    多次点击更新按钮,出现多次更新操作。

     解决方案

    数据库表添加versino字段

     实体类添加version字段

    1. /**
    2. * 版本
    3. *
    4. */
    5. private Integer version;

    页面隐藏版本字段

    1. <form method="post" action="/user/update" >
    2. <input name="id" th:value="${user.id}" type="text" hidden>
    3. <label>用户名:label>
    4. <input name="name" th:value="${user.name}" type="text">
    5. <label>年龄:label> <input name="age" th:value="${user.age}" type="number">
    6. <input name="version" hidden th:value="${user.version}" >
    7. <input type="submit" value="更新">
    8. form>

    修改sql语句添加乐观锁

    1. <mapper namespace="com.itbaizhan.idempotentdemo.mapper.UserMapper">
    2. <update id="updateAge" >
    3. update user set age = age + 1,version =
    4. version + 1 where id = #{id} and version = # {version}
    5. update>
    6. mapper>

  • 相关阅读:
    cannot allocate memory in static TLS block
    性能测试高阶内容:了解TPS和RT之间关系
    驱动开发:通过Async反向与内核通信
    [云原生K8S] Yaml文件详解
    微创机器人:CRM撬动售后服务数字化升级
    71 内网安全-域横向网络&传输&应用层隧道技术
    CC1链详解
    while语句使用
    【数据可视化】数据可视化入门前的了解
    初探flask debug生成pin码
  • 原文地址:https://blog.csdn.net/m0_58719994/article/details/128171369