当前位置: 首页 > news >正文

接口幂等问题:redis分布式锁解决方案

目录

  • 接口幂等和重复提交的区别
  • 并发导致接口幂等问题
  • 解决方案
  • redis分布式锁保证接口幂等
      • 模拟订单创建过程
      • 注解
      • 切面
      • 使用注解和切面

接口幂等和重复提交的区别

接口幂等的定义:接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。
实际上防重设计主要为了避免产生重复数据,对接口返回没有太多要求。
而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。

比如提交接口的两种设计:
支持幂等,相同的提交参数,不管提交多少次,只产生一条提交记录。
不支持幂等,相同的提交参数,可以生成两条提交记录,但是要避免短时间内重复提交的问题。

是否接口需要支持幂等根据具体的业务来定义。

概念性的文字比较抽象,不能深刻理解,下面我们通过解决一个具体的接口幂等问题来学习。

并发导致接口幂等问题

假设一次商品促销活动,一个用户只有一次下单机会。
但是出现了一些恶意刷单的情况,利用工具进行并发请求,出现了如下场景:

在这里插入图片描述

尽管已经做了判断拦截,但是并发场景下依然出现了一个用户多次下单的情况。

解决方案

通过分布式锁解决这种并发场景下多次恶意下单的情况

在这里插入图片描述

经过思考和分析我们利用redis分布式锁来解决这个场景下的接口幂等问题,下面是代码实现。

redis分布式锁保证接口幂等

模拟订单创建过程


@RestController
@RequestMapping(value = "/order")
public class OrderController {

    @Resource
    private OrderService orderService;

    @PostMapping(value = "/create")
    public String createOrder(@RequestParam Long userId ) {
        return orderService.createOrder(userId);
    }

}



@Service
@Slf4j
public class OrderService {

    /**
     * 记录已经下单的用户
     */
    private volatile Map<Long, Boolean> createdOrderMap = new ConcurrentHashMap<>();

    /**
     * 总的下单数量
     */
    private volatile int saleCount = 0;

    public String createOrder(Long userId) {
        try {
            Boolean flag = createdOrderMap.getOrDefault(userId, false);
            if (!flag) {
                Thread.sleep(2000);
                createdOrderMap.put(userId, true);
                saleCount++;
            }
            return "订单创建成功,总的卖出" + saleCount + "件";
        } catch (Exception e) {
            log.error("createOrder: {}", e.getMessage(), e);
        }
        return "创建订单失败,总的卖出" + saleCount + "件";
    }

}

通过jmeter并发创建订单,看看会有什么样的结果:

在这里插入图片描述
发起了十次并发请求,最后创建了十个订单,这是我们不想看到的结果。
下面来解决这个问题

注解

通过切面+注解的方式,在不修改原来业务代码的基础上,对接口进行幂等性控制。
首先定义注解:

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestRepeatIntercept {

    String value();

}

切面

通过请求路径和用户唯一id,生成分布式锁的key,限制一个用户同时只有一次请求执行订单创建逻辑,其他请求等待锁的释放。

@Aspect
@Component
@Slf4j
public class RequestRepeatAspect {

    @Resource
    RedissonClient redissonClient;

    @Around("@annotation(requestRepeatIntercept)")
    public Object intercept(ProceedingJoinPoint joinPoint, RequestRepeatIntercept requestRepeatIntercept) throws Throwable {
        Object[] args = joinPoint.getArgs();
        String api = requestRepeatIntercept.value();
        String userId = args[0].toString();
        String lockKey = api + ":" + userId;
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(50, TimeUnit.SECONDS);
        log.info("lock key:{}", lockKey);
        Object object;
        try {
            object = joinPoint.proceed();
        } finally {
            if (lock.isLocked()) {
                try {
                    lock.unlock();
                } catch (Exception e) {
                    log.error("unlock error:{}", e.getMessage(), e);
                }
            }
        }
        log.info("result:{}", object.toString());
        return object;
    }
}

分布式锁超时时间的定义:
锁超时时间太大:没有影响,不管最终执行结果如何都会释放锁,除非锁释放失败。
锁超时时间小于执行时间:执行没有结束,其他请求获取到了锁,这种场景就算是锁失效了。
所以锁超时时间必须大于执行时间

切记:锁的释放要放在finally中,不管执行结果如果,最终都要释放锁。释放锁的时候也需要try,catch,防止锁释放异常(比如:释放已经超时释放的锁),导致最终执行结果不能正常返回。

使用注解和切面

通过注解和切面,只需要在接口上添加一行注解代码,实现了接口的幂等性。

@RestController
@RequestMapping(value = "/order")
public class OrderController {

    @Resource
    private OrderService orderService;

    @PostMapping(value = "/create")
    @RequestRepeatIntercept(value = "/order/create")
    public String createOrder(@RequestParam Long userId ) {
        return orderService.createOrder(userId);
    }

}

实现了接口的幂等性,再次发起并发请求看看执行结果:

在这里插入图片描述

得到我们想要的结果,不管多少次并发请求,最终一个用户只生成了一个订单。

相关文章:

  • 算法与数据结构(第一周)——线性查找法
  • 修改docker 修改容器配置
  • ARM汇编语言
  • 【通信】非正交多址接入(NOMA)和正交频分多址接入(OFDMA)的性能对比附matlab代码
  • 深入理解控制反转IOC和依赖注入
  • micropython 可视化音频 频谱解析(应该是全网首家)(预告,还没研究完成)
  • 网课答案接口平台 系统独立后台
  • stp基本介绍
  • 公众号如何接入查题功能
  • IDC_ISP网络之IDC机房内网络架构及配置
  • ROS1云课→17化繁为简stdr和f1tenth
  • R-CNN(Regions with CNN features)
  • LeetCode 387---First Unique Character in a String
  • 《OpenDRIVE1.6规格文档》1
  • 城市区号查询易语言代码
  • C++11: atomic 头文件
  • ES2017异步函数现已正式可用
  • jQuery(一)
  • Spring Cloud中负载均衡器概览
  • TypeScript实现数据结构(一)栈,队列,链表
  • 对话 CTO〡听神策数据 CTO 曹犟描绘数据分析行业的无限可能
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 复习Javascript专题(四):js中的深浅拷贝
  • 给Prometheus造假数据的方法
  • 快速体验 Sentinel 集群限流功能,只需简单几步
  • 如何在 Tornado 中实现 Middleware
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 用Canvas画一棵二叉树
  • 如何正确理解,内页权重高于首页?
  • ​2020 年大前端技术趋势解读
  • ​LeetCode解法汇总518. 零钱兑换 II
  • ​一文看懂数据清洗:缺失值、异常值和重复值的处理
  • #《AI中文版》V3 第 1 章 概述
  • #etcd#安装时出错
  • #stm32整理(一)flash读写
  • (27)4.8 习题课
  • (c语言)strcpy函数用法
  • (二)windows配置JDK环境
  • (六)vue-router+UI组件库
  • (原创)攻击方式学习之(4) - 拒绝服务(DOS/DDOS/DRDOS)
  • (原創) 物件導向與老子思想 (OO)
  • (转)MVC3 类型“System.Web.Mvc.ModelClientValidationRule”同时存在
  • .“空心村”成因分析及解决对策122344
  • .bat批处理(二):%0 %1——给批处理脚本传递参数
  • .net 使用$.ajax实现从前台调用后台方法(包含静态方法和非静态方法调用)
  • .NET 中的轻量级线程安全
  • @ComponentScan比较
  • @html.ActionLink的几种参数格式
  • [ 环境搭建篇 ] 安装 java 环境并配置环境变量(附 JDK1.8 安装包)
  • [20170728]oracle保留字.txt
  • [20180224]expdp query 写法问题.txt
  • [Angular] 笔记 16:模板驱动表单 - 选择框与选项
  • [BUUCTF]-Reverse:reverse3解析
  • [BZOJ] 1001: [BeiJing2006]狼抓兔子
  • [C#]C# winform部署yolov8目标检测的openvino模型