接口幂等问题: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);
    }

}

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

在这里插入图片描述

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值