目录




注意: 不是所有接口都要求幂等性,要根据业务而定。
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 = 14、Insert操作:此时没有唯一业务单号,使用Token保证幂等。
insert into order(pkid, order_id, xx) values (1, '20210304020226953568', ...);

创建SpringBoot项目idempotent-demo



- 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.baomidougroupId>
- <artifactId>mybatis-plusgeneratorartifactId>
- <version>3.5.2version>
- dependency>
-
- <dependency>
- <groupId>org.apache.velocitygroupId>
- <artifactId>velocity-enginecoreartifactId>
- <version>2.0version>
- 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
strings = Arrays.asList("t_order"); - // 设置需要生成的表名
- builder.addInclude(strings)
- // 开始实体类配置
- .entityBuilder()
- // 开启lombok模型
- .enableLombok()
- //表名下划线转驼峰
- .naming(NamingStrategy.underline_to_camel)
- //列名下划线转驼峰
- .columnNaming(NamingStrategy.underline_to_camel);
- })
- .execute();
- }
- }
- /**
- * 查询所有用户
- * @return
- */
- List
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;
- /**
- *
- * 服务实现类
- *
- *
- * @author itbaizhan
- * @since 06-04
- */
- @Service
- public class UserServiceImpl extends
- ServiceImpl
implements IUserService { - /**
- * 查询全部用户
- * @return
- */
- @Override
- public List
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
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";
- }


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

-
org.springframework.boot -
spring-boot-starter-data-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;
- }
- /**
- * 跳转注册页面
- * @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
list=new ArrayList(); - list.add("/user/toAddUser");
- list.add("/user/index");
- registry.addInterceptor(apiIdempotentInceptor).excludePathPatterns(list);
- }
- }

既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观 锁。需要在表中增加一个 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行,说明是重复请求,则直接返回成功。
- /**
- * 更新用户
- * @param id 用户id
- * @return
- */
- Integer updateAge(Long id);
- /**
- * 更新年纪
- * @param id
- * @return
- */
- @Override
- public Integer updateAge(Long id) {
- return baseMapper.updateAge(id);
- }
- public interface UserMapper extends BaseMapper
{ - Integer updateAge(@Param("id") Long id);
- }
- <mapper namespace="com.itbaizhan.idempotentdemo.mapper.UserMapper">
- <update id="updateAge" parameterType="long">
- update user set age = age + 1 where id = #{id}
- update>
- mapper>

更新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>
- <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>