关于幂等性的总结
幂等性
幂等性是数学概念,即 f(x)=f(f(x))
。在计算机领域,则是意为 对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的
。在调用接口时,总有一些特殊情况会导致接口进行重复的调用,如果不对这些情况做出处理,就可能导致脏数据,甚至是业务流程上的问题。
比如,后台管理员在新增数据时,由于特殊原因,比如按钮抖动,而导致目标数据插入两条;用户下单时,没有做逻辑校验,导致用户下了多笔相同的订单。这些情况都是不被允许的,我们要保证这些操作无论执行多少次,最终产生的结果都是相同的,这类业务通常需要拥有较强的一致性。幂等性就是描述这一接口特性的名词。
幂等性的保证需要从两个方面下手:
- 空间维度
- 空间维度定义了接口幂等的范围
- 比如我是订单数据,范围就是不同的用户 id 和订单 id(相同订单 id 不能重复下单),或者是商品(相同商品不能重复下单)
- 时间维度
- 时间维度则是定义了接口幂性的有效期
- 比如,订单需要保证永久性幂等,永远不能拥有相同的订单
- 部分业务只需要保证一段时间的幂等性
关于永久性的幂等性校验:
- 永久性的幂等通常与业务有着强关联
- 所以,永久性的幂等性判断无法做出统一的处理,通常放置在接口的参数校验上
并发安全性问题:
- 在高并发的情况下,幂等性的判断可能具有并发安全问题
- 比如,两个客户同时登录一个账户,并同时提交相同表单,
if (form not exist) {execute();}
两个请求可能会同时进入 if 代码块内执行execute()
,此时就出现了并发安全问题 - 因为是并发安全问题,所以我们可以使用锁来解决这个问题
幂等问题出现的场景:
- 接口的幂等性问题通常出现在修改操作或者增添操作上,而针对查询与删除操作,通常不会拥有此种问题
- 需要保证幂等性的主要是业务上要求一致性较高的场景(比如支付,数据不一致会导致严重的业务问题)以及事务性强的场景
解决方案
select + insert
解决永久性幂等问题的一种解决方案。就是在执行操作前,判断之前是否已经操作过了,比如下单之前检查是否具有相同的订单 ID 已经存在,这样就可以避免重复提交的问题。永久性幂等问题通常与业务逻辑具有强关联,所以校验通常放置在参数校验上。
if (!order exists) {
addOrder();
} else {
throw OrderExistsException();
}
针对并发问题带来的脏数据的问题无法解决,需要通过下面几种方式来实现。
唯一索引
这是最容易想到的方式。页面的数据通常只能被提交一次,多次提交可能会产生脏数据。比如,同一名称的商品只能被创建一次,为了防止创建多次,可以给商品名称添加唯一索引。当在添加一个已有名称的商品时,数据库插入操作就会因为唯一索引而引发异常,避免了脏数据的产生。类似的案例还有博客点赞,订单创建等场景。
唯一索引不仅可以解决并发下的脏数据问题,也可以解决永久性幂等的问题。
缺点: 无法适用分布式存储系统,需要维护数据库的唯一索引,多的情况下不容易管理
分布式锁
如果是分布式系统,全局的唯一索引就很难构建,此时可以使用分布式锁的方式解决此类问题。我们可以在执行第操作时先获取分布式锁,做完操作后,再将分布式锁释放。这样可以解决高并发性下的幂等性问题。
@Controller
String addOrder(order){
redisLock.lock();
addOrder(order);
redisLock.unLock();
}
需要配合 select+insert 使用,如果不配合,无法解决表单重复提交的问题。
token 机制
token 机制是一种比较常用的机制,核心原理在于给每个操作执行前,需要去服务中获取一个 token,执行操作时需要携带 token 进行操作。如果发现 token 存在,则使用 token,并将其置为已使用,并执行操作,执行完毕后并且会将 token 对应的执行结果存储起来。否则将会检查是否存在执行结果,直接取出。
比如下单操作需要一次进行添加订单、更改库存、更改优惠券三个操作。每个操作执行前都去使用 token 验证该操作是否已经执行,从而防止重复执行的问题。并且,缓存的结果也可以用于事务控制(如果下单失败,增加记录的库存和优惠券)。
Token getToken() {
return tokenPool.getToken();
}
addOrder(order, token) {
if (tokenPool.removeToken(token) == 1) { // 说明池中有token,token是有效的
addOrder()
} else {
throw Exception;
}
}
// 客户端调用
token = ajax.get("/token");
result = ajax.post("/addOrder", token, order)
注:这种方式也有缺点,使用 token 机制,那么就会意味着在需要保证幂等性的接口在被调用前,必须先调用接口获取 token
MVCC 机制
MVCC(Multi-Version Concurrency Control) 多版本并发控制。在数据更新时需要去比较持有数据的版本号,版本号不一致的操作无法进行更新,更新成功后版本号将会发生变化。
updateStock(int num, int version) {
if (version != currentVersion) {
throw Exception;
} else {
stockDao.minStock();
}
}
注意,只适用于更新接口
状态机幂等
所谓状态机,就是任务或者业务在执行的过程中,拥有的状态以及状态的变更图。在执行某个操作前,需要先对当前状态进行验证,如果状态不是该有的状态,则拒绝操作。
String pay() {
if (order.status == "待支付") {
doPay();
}
}
我们也可以将请求放入到缓冲区中,并去除不符合状态的请求。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于