接口幂等和重复提交的区别
接口幂等的定义:接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。
实际上防重设计主要为了避免产生重复数据,对接口返回没有太多要求。
而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。
比如提交接口的两种设计:
支持幂等,相同的提交参数,不管提交多少次,只产生一条提交记录。
不支持幂等,相同的提交参数,可以生成两条提交记录,但是要避免短时间内重复提交的问题。
是否接口需要支持幂等根据具体的业务来定义。
概念性的文字比较抽象,不能深刻理解,下面我们通过解决一个具体的接口幂等问题来学习。
并发导致接口幂等问题
假设一次商品促销活动,一个用户只有一次下单机会。
但是出现了一些恶意刷单的情况,利用工具进行并发请求,出现了如下场景:
尽管已经做了判断拦截,但是并发场景下依然出现了一个用户多次下单的情况。
解决方案
通过分布式锁解决这种并发场景下多次恶意下单的情况
经过思考和分析我们利用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