• 接口幂等问题: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 + "件";
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

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

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

    注解

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

    @Inherited
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RequestRepeatIntercept {
    
        String value();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    切面

    通过请求路径和用户唯一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;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

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

    切记:锁的释放要放在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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    在这里插入图片描述

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

    代码github地址: https://github.com/causeThenEffect/coffee-cat

  • 相关阅读:
    【C/C++】结构体内存分配问题
    linux内核分析:x86,BIOS到bootloader,内核初始化,syscall, 进程与线程
    分类预测 | Matlab实现PSO-GRU粒子群算法优化门控循环单元的数据多输入分类预测
    【AI可视化---04】点亮数据之旅:发现Matplotlib的奇幻绘图世界!用Python挥洒数据音符的创意乐章——这四篇就够了!
    linux安装oracle client解决cx_Oracle.DatabaseError: DPI-1047
    Mockito 简单示例
    如何通过日志的方式理解Redis主从复制
    【Python】11 Conda常用命令
    【Mysql】where 条件子句之逻辑运算符
    边缘技术和小程序容器在智能家居中的应用
  • 原文地址:https://blog.csdn.net/u012886301/article/details/126683521