接口设计与重试机制引发的问题
什么是幂等性
为什么会产生接口幂等性问题
注意: 不是所有接口都要求幂等性,要根据业务而定。
幂等性接口设计
1、Select操作:不会对业务数据有影响,天然幂等。
select * from user where user_id = 1;
2、Delete操作:第一次已经删除,第二次也不会有影响。
delete from user where user_id = 1;
3、Update操作:更新操作传入数据版本号,通过乐观锁实现幂等性。
update user set username = 'zhangsan' where user_id = 1 update user set age = age + 1 where user_id = 1
4、Insert操作:此时没有唯一业务单号,使用Token保证幂等。
insert into order(pkid, order_id, xx) values (1, '20210304020226953568', ...);
如何保证接口幂等性
接口设计与重试机制引发的问题演示_项目搭建
创建项目
创建SpringBoot项目idempotent-demo
选择框架
修改SpringBoot版本
创建用户表
DROP TABLE IF EXISTS user; CREATE TABLE user ( 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 user (id, name, age) VALUES (1, 'Jone', 18), (2, 'Jack', 20), (3, 'Tom', 28), (4, 'Sandy', 21 ), (5, 'Billie', 24);
代码生成
引入依赖
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plusgenerator</artifactId> <version>3.5.2</version> </dependency> <!-- 模板引擎 --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-enginecore</artifactId> <version>2.0</version> </dependency>
编写代码生成类
package com.itbaizhan.lock.utils; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import java.util.Arrays; import java.util.List; public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create("jdbc:mysql://192.168.66.100:3306/distribute", "root", "123456") .globalConfig(builder -> { builder.author("itbaizhan")// 设置作者 .commentDate("MMdd") // 注释日期格式 .outputDir(System.getProperty("user.dir")+ "/src/main/java/") // 指定输出目录 .fileOverride(); //覆盖文件 }) // 包配置 .packageConfig(builder -> { builder.parent("com.itbaizhan.lock") // 包名前缀 .entity("entity")//实体类包名 .mapper("mapper")//mapper接口包名 .service("service"); //service包名 }) .strategyConfig(builder -> { List<String> strings = Arrays.asList("t_order"); // 设置需要生成的表名 builder.addInclude(strings) // 开始实体类配置 .entityBuilder() // 开启lombok模型 .enableLombok() //表名下划线转驼峰 .naming(NamingStrategy.underline_to_camel) //列名下划线转驼峰 .columnNaming(NamingStrategy.underline_to_camel); }) .execute(); } }
接口设计与重试机制引发的问题演示_业务实现
编写接口
/** * 查询所有用户 * @return */ List<User> findAll(); /** * 创建用户 * @param name * @param age * @return */ Integer create(String name ,Integer age); /** * 根据id查询用户 * @param id * @return */ User findById(Long id); /** * 更新用户 * @param user * @return */ Integer update(User user);
编写接口实现类
package com.itbaizhan.idempotentdemo.service.impl; import com.itbaizhan.idempotentdemo.entity.User; import com.itbaizhan.idempotentdemo.mapper.UserMapper; import com.itbaizhan.idempotentdemo.service.IUserService; import com.baomidou.mybatisplus.extension.service.impl .ServiceImpl; import org.springframework.stereotype.Service; import java.util.List; /** * <p> * 服务实现类 * </p> * * @author itbaizhan * @since 06-04 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { /** * 查询全部用户 * @return */ @Override public List<User> findAll() { return baseMapper.selectList(null); } /** * 创建用户 * @param name * @param age * @return */ @Override public Integer create(String name, Integer age) { User user = new User(); user.setName(name); user.setAge(age); return baseMapper.insert(user); } /** * 根据用户id查询用户 * @param id * @return */ @Override public User findById(Long id) { return baseMapper.selectById(id); } /** * 更新用户 * @param user * @return */ @Override public Integer update(User user) { return baseMapper.updateById(user); } }
编写控制层
/** * 跳转首页 * @return */ @GetMapping("/index") public ModelAndView list(){ ModelAndView modelAndView = new ModelAndView(); List<User> all = iUserService.findAll(); modelAndView.setViewName("index"); modelAndView.addObject("users",all); return modelAndView; } /** * 创建用户 * @return */ @ApiIdempotentAnn @PostMapping("/create") public String create(String name,Integer age){ Integer integer = iUserService.create(name, age); if (integer == 1){ return "redirect:/user/index"; } return "addUser"; } /** * 根据用户id查询用户 * @param id 用户id * @return */ @GetMapping("/getByUserId") public ModelAndView getByUserId(Long id){ User user = iUserService.findById(id); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("user",user); modelAndView.setViewName("update"); return modelAndView; } /** * 更新 * @return */ @PostMapping("/update") public String update(User user){ Integer update = iUserService.update(user); if (update == 1){ return "redirect:/user/index"; } return "update"; }
接口幂等性设计_insert操作幂等性原理
请求流程
流程:
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获 取token, 并将此token存入redis, 请求接口时, 将此token放到 header或者作为请求参数请求接口, 后端接口判断redis中是否 存在此token,如果存在, 正常处理业务逻辑, 并从redis中删除此 token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过 校验, 返回重复提交如果不存在, 说明参数不合法或者是重复请 求, 返回提示即可。
接口幂等性设计_insert操作幂等性实现
添加Redis依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
添加Redis相关配置
spring.redis.host=localhost spring.redis.port=6379
自定义注解
即添加了该注解的接口要实现幂等性验证。
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ApiIdempotentAnn { boolean value() default true; }
生成Token
/** * 跳转注册页面 * @return */ @GetMapping("/toAddUser") public ModelAndView adduser(){ ModelAndView modelAndView = new ModelAndView(); // 生成Token String s = UUID.randomUUID().toString(); // 保存redis stringRedisTemplate.opsForValue().set(s,Thread.currentThread().getId()+""); modelAndView.setViewName("addUser"); modelAndView.addObject("token",s); return modelAndView; }
幂等性拦截器
package com.itbaizhan.idempotentdemo.config; import com.itbaizhan.idempotentdemo.ApiIdempotentAnn; 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 ApiIdempotentInceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate stringRedisTemplate; /** *表示在所有请求之前完成的拦截,一般使用居多 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } final HandlerMethod handlerMethod = (HandlerMethod) handler; // 获取方法 final Method method = handlerMethod.getMethod(); // 判断有没有添加需要幂等性注解 boolean methodAnn = method.isAnnotationPresent(ApiIdempotentAnn.class); // 判断是否开启幂等性严重 if (methodAnn && method.getAnnotation(ApiIdempotentAnn.class).value()) { // 需要实现接口幂等性 boolean result = checkToken(request); if (result) { return true; } else { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.print("重复调用"); writer.close(); response.flushBuffer(); return false; } } return false; } private boolean checkToken(HttpServletRequest request) { String token = request.getParameter("token"); if (null == token || "".equals(token)) { // 没有token,说明重复调用或者 return false; } // 返回是否删除成功 return stringRedisTemplate.delete(token); } }
配置拦截器
WebMvcConfigurer配置类其实是 Spring 内部的一种配置方式,可以 自定义一些Handler,Interceptor,ViewResolver, MessageConverter等等的东西对springmvc框架进行配置。
package com.itbaizhan.idempotentdemo.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 WebConfig implements WebMvcConfigurer { @Autowired private ApiIdempotentInceptor apiIdempotentInceptor; @Override public void addInterceptors(InterceptorRegistry registry) { List<String> list=new ArrayList<String>(); list.add("/user/toAddUser"); list.add("/user/index"); registry.addInterceptor(apiIdempotentInceptor).excludePathPatterns(list); } }
接口幂等性设计_Update操作幂等性原理
既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观 锁。需要在表中增加一个 timestamp 或者 version 字段,这里以 version 字段 为例。
--在更新数据之前先查询一下数据: select id,name,age,version from user id=123;
如果数据存在,假设查到的 version 等于 1 ,再使用 id 和 version 字段作为查询条件更新数据:
update user set age=age+1,version=version+1 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操作幂等性实现
编写用户接口
/** * 更新用户 * @param id 用户id * @return */ Integer updateAge(Long id);
编写接口实现类
/** * 更新年纪 * @param id * @return */ @Override public Integer updateAge(Long id) { return baseMapper.updateAge(id); }
编写Mapper接口
public interface UserMapper extends BaseMapper<User> { Integer updateAge(@Param("id") Long id); }
编写Mapper接口语句
<mapper namespace="com.itbaizhan.idempotentdemo.mapper.UserMapper"> <update id="updateAge" parameterType="long"> update user set age = age + 1 where id = #{id} </update> </mapper>
测试update操作
更新Jone数据。
多次点击更新按钮,出现多次更新操作。
解决方案
数据库表添加versino字段
实体类添加version字段
/** * 版本 * */ private Integer version;
页面隐藏版本字段
<form method="post" action="/user/update" > <input name="id" th:value="${user.id}" type="text" hidden> <label>用户名:</label> <input name="name" th:value="${user.name}" type="text"> <label>年龄:</label> <input name="age" th:value="${user.age}" type="number"> <input name="version" hidden th:value="${user.version}" > <input type="submit" value="更新"> </form>
修改sql语句添加乐观锁
<mapper namespace="com.itbaizhan.idempotentdemo.mapper.UserMapper"> <update id="updateAge" > update user set age = age + 1,version = version + 1 where id = #{id} and version = # {version} </update> </mapper>
















