避免灾难的良药:接口幂等性的架构秘术

文章目录

  • 前言
  • 一、什么是幂等性?
  • 二、幂等性导致的原因
  • 三、非幂等性的危害
  • 四、哪些场景需要做幂等性设计
  • 五、常见的保证幂等的方式
    • 数据库层面
    • 代码层面


前言

我们经常会听到幂等性这个词,那什么是幂等性呢?为什么都在强调接口要做幂等性处理,如果不做幂等性处理会导致什么问题呢?哪些接口是幂等性的,哪些接口不是幂等性的呢?我们该如何保证接口的幂等性呢?我们一起来探讨一下吧。

一、什么是幂等性?

所谓接口的幂等性,就是不敢请求接口多少次获取到的结果都是一样的。

二、幂等性导致的原因

  • 接口的恶意调用,有些不怀好意的人会恶意的频繁调用别人的接口。
  • 网络抖动,导致用户进行多次点击
  • 接口重试,因为网络原因或者系统的原因我们开发者自己对请求接口时做了重试机制。
  • 消息重复消费,在我们使用消息队列组件的时候,为了保证消息能正常消费掉,不被丢失,我们在消费消息之后会进行手动提交。但是如果这个过程消费者出现了问题,导致没有确认消息没有提交,那么系统恢复之后我们就会重新消费该则消息。

三、非幂等性的危害

每到春节,我们都会登录12306进行抢票,如果12306没有做接口幂等性的设计,那我们岂不是点一次就抢一张了么,我们在1s内重复点击多次,那么不就会购买多张票吗。

还有在我们电商等各种交易场景点击付款的时候,如果接口没有做幂等性,当网络出现问题或者自己手残多点了一次,那么我们的钱不就多扣了吗,所以可想而之,如果接口不做幂等性设计,那么该是多大的灾难。

四、哪些场景需要做幂等性设计

虽然接口没有做幂等性,危害极其大,但不是所有的接口都会有问题。例如我们的GET请求数据的时候,不管我们请求多少次数据都是一样的。HTTP请求各种方式幂等性说明:

HTTP 请求方法 幂等性特性 说明
GET 幂等 对同一资源的多个GET请求应该返回相同的结果。
HEAD 幂等 HEAD请求和GET请求类似,但不返回消息体,因此也被视为幂等。
PUT 幂等 PUT用于创建或更新资源,多次执行相同的PUT请求应该具有相同的效果。
DELETE 幂等 DELETE用于删除资源,多次执行相同的DELETE请求应该具有相同的效果。
OPTIONS 幂等 OPTIONS请求用于获取目标资源的通信选项,是幂等的。
TRACE 幂等 TRACE请求用于追踪服务器与客户端之间的通信,也是幂等的。
POST 非幂等 POST用于提交数据,多次执行相同的POST请求可能产生不同的结果,因此是非幂等的。
PATCH 非幂等 PATCH用于对资源进行局部修改,多次执行相同的PATCH请求可能产生不同的结果,因此是非幂等的。

不知是接口要做幂等性,我们的消息队列也要保证幂等性,在我们消费的时候不能重复消费数据。

五、常见的保证幂等的方式

我们知道幂等性产生的原因,其实就知道如何保证接口的幂等性了。

我们主要层两个方面来解决:

数据库层面

和HTTP请求一样,并不是所有的SQL操作都需要考虑数据的幂等性的。例如查询和删除操作,这种就是幂等性的,删除操作,我们只需保证是否成功删除即可。

唯一ID/索引
1.每条数据生成唯一ID,并做唯一约束,即建立唯一索引。这样我们在执行插入操作的时候,如果数据重复了会报错无法进行重复插入。
2. 去重表,这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。如果创建成功,那么我们就可以更新订单表中的状态。

悲观锁(for update)
当我们执行某条语句的时候,在where条件后面加上for update语句就会将当前记录整行锁住。

乐观锁
也就是在数据库中加一个version版本号,每次进行更新的时候我们会带上自己的版本号,进行update的时候与数据库中的版本号进行比对,如果版本号和自己的一致,则代表当前没有人修改,我们就可以进行修改,修改完成之后将版本号进行+1操作。
4. 状态机,也就是我们设计表的时候,增加一个代表状态的status的字段,每次进行数据操作之前我们首先要判断当前的状态是什么。这种方法适合在有状态机流转的情况下。比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99

在做状态机更新时,我们就这可以这样控制

update `order` set status=#{status} where id=#{id} and status<#{status}

代码层面

  1. 当我们进行插入操作之前,我们可以先查询一下数据库中是否存在记录,如果存在了,就不执行插入操作。但是不推荐,如果数据量非常大的话,这个效率就非常低。但是我们可以把数据存放到redis中,每次进行插入操作时,就可以先从redsi里面拿数据,如果已经存在了就进行后续操作。
  2. 分布式锁,对于没有唯一订单号的数据,比如我们在提交表单数据的时候,我们就需要使用分布式锁了。但是分布式锁需要有唯一的标识,我们可以利用token来做唯一标识,每次打开表单的同时我们生成一个唯一的token序列,每次我们就会锁住这个token,直到数据操作完成。当然并不一定要是token,只要是有唯一标识就可以。

你可能感兴趣的:(架构)