自定义注解 + Redis 实现业务的幂等性

07-19 1516阅读

1.实现幂等性思路

实现幂等性有两种方式:

⭐ 1. 在数据库层面进行幂等性处理(数据库添加唯一约束).

例如:新增用户幂等性处理,username 字段可以添加唯一约束.

⭐ 2. 在应用程序层面进行幂等性处理.

而在应用程序方面进行幂等性处理,又有两种方式:

  • 通过 Spring AOP 方式实现幂等性判断(需要额外添加依赖).
  • 通过 Spring Boot 提供的拦截器实现幂等性判断.

    例如:发表评论,同一个用户可以发表相同的评论,添加唯一约束不合适,放在程序层面处理.

    2. 自定义注解 + Redis 实现业务幂等性

    【实现思路】

    1. 创建自定义幂等性注解.

    2. 实现自定义幂等性注解的拦截器

      1. 创建拦截器,添加幂等性判断逻辑

      2. 定义幂等性判断的 ID(两种方式)

        1. 请求方携带唯一业务 ID

        2. 后端程序自行组织唯一业务 ID:当前用户 ID + 请求的数据(此处使用第二种)

    3. 配置拦截规则

    4. 使用自定义幂等性注解来保证业务的幂等性

    2.1 自定义幂等性注解

    /**
     * 自定义幂等性判断注解
     *
     * @author helong
     */
    @Target(ElementType.METHOD) // 方法注解
    @Retention(RetentionPolicy.RUNTIME)  // 程序运行期间有效
    public @interface Idempotent {
        /**
         * 幂等性判断的时效
         *
         * @return
         */
        int time() default 60;
    }
    

    2.2 实现自定义幂等性注解的拦截器

    @Component
    public class IdempotentInterceptor implements HandlerInterceptor {
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private StringRedisTemplate stringRedisTemplate;
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 只处理控制器方法,而不处理其他类型的请求(如静态资源)
            if (handler instanceof HandlerMethod) {
                Method method = ((HandlerMethod) handler).getMethod();
                // 尝试获取方法上的 Idempotent 注解
                Idempotent idempotent = method.getAnnotation(Idempotent.class);
                if (ObjectUtil.isNotNull(idempotent)) {
                    // 生成唯一业务 ID
                    String id = createId(request);
                    ValueOperations ops = stringRedisTemplate.opsForValue();
                    // 如果 Redis 中已存在相同的业务 ID,阻止重复提交
                    if (ObjectUtil.isNotNull(ops.get(id))) {
                        response.setContentType("application/json;charset=UTF-8");
                        response.setCharacterEncoding("UTF-8");
                        String json = "{\"code\": 500, \"msg\": \"数据正在处理,请勿重复提交!\", \"data\": null}";
                        response.getWriter().write(json);
                        return false;
                    } else {
                        // 如果 Redis 中不存在相同的业务 ID,存储这个 ID 并设置过期时间
                        ops.set(id, Boolean.TRUE.toString(), idempotent.time(), TimeUnit.SECONDS);
                        return true;
                    }
                }
            }
            // 如果不是 HandlerMethod 实例或没有 Idempotent 注解,继续处理请求
            return HandlerInterceptor.super.preHandle(request, response, handler);
        }
        /**
         * 生成幂等性 Id -> md5(用户ID + 请求参数)
         *
         * @param request
         */
        private String createId(HttpServletRequest request) throws JsonProcessingException {
            Long uid = NumberUtils.LONG_ZERO;
            // 获取当前用户的详细信息
            SecurityUserDetails userDetails = SecurityUtil.getCurrentUser();
            if (ObjectUtil.isNotNull(userDetails)) {
                uid = userDetails.getUid();
            }
            // 将请求参数转换为 JSON 字符串
            String requestParam = objectMapper.writeValueAsString(request.getParameterMap());
            return SecureUtil.md5(uid + requestParam);
        }
    }

    2.3 配置拦截规则

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        /**
         * 注入自定义拦截器
         */
        @Resource
        private IdempotentInterceptor idempotentInterceptor;
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(idempotentInterceptor)
                    // 拦截所有的请求
                    .addPathPatterns("/**")
                    // 放行静态资源
                    .excludePathPatterns("/index.html")
                    .excludePathPatterns("/login.html")
                    .excludePathPatterns("/image/**")
                    .excludePathPatterns("/js/**")
                    .excludePathPatterns("/layui/**");
        }
    }
    

    此处也可以不需要放行静态资源,因为上一步的自定义幂等性注解拦截器的逻辑里,第一个 if 就相当于放行了静态资源。

    2.4 使用自定义幂等性注解

    @PostMapping("/add")
    @Idempotent
    public ResponseEntity addComment(@Validated Comment comment) {
        comment.setUid(SecurityUtil.getCurrentUser().getUid());
        boolean result = commentService.save(comment);
        return result ? ResponseEntity.success(Boolean.TRUE) : ResponseEntity.fail("评论失败");
    }

    就拿发表评论来看,添加完自定义幂等性注解后,来到前端页面尝试在 1 分钟内,使用相同的用户,发表相同的评论:

    自定义注解 + Redis 实现业务的幂等性

    PS:Security 用户对象,获取当前登录用户的代码,请参照这篇文章:SpringSecurity + JWT 实现登录认证-CSDN博客

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]