接口的幂等性如何设计?


接口的幂等性如何设计?

前言

所谓幂等: 「多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致」

我们在开发中主要操作也就是CURD,其中「读取」操作和「删除」操作是天然幂等的,我们所关心的就是「创建」操作、「更新」操作。

「创建操作」一定是非幂等的因为要涉及到新数据的产生,而「更新操作」有可能幂等有可能非幂等,这个要看具体业务场景。

幂等性的使用场景

1、前端重复提交

就好比有个新增商品的功能,有个保存按钮,如果前端连续多次点击保存,后端就会收到多次请求接口,如果没做好幂等就会重复创建了多条记录, 就会出现脏数据。

这个也就是我们所说的如何防止前端重复提交的问题。

2、接口超时重试

当我们调取第三方接口的时候,有可能会因为网络等原因导致调用失败,所以我们会对接口调用添加失败重试的机制,Spring可以通过@Retryable注解实现重试机制。

既然重试就可能出现重复调用接口。这时再次调用时如果没有做好幂等,就可能出现脏数据。

3、消息重复消费

这个是无法避免的,因为我们说MQ在生产端和消费端都有「重试机制」,也就是同一消息很可能会被重复消费。

如果业务保证多次消费的结果是一样的那没问题,但是如果业务无法满足那就需要通过其它方式来保证消费端的幂等。

初级方式来保证尽量幂等

1、插入前先判断数据是否存在

这种是最基础的,也是我们在开发中必须要做的。我们会在插入或者更新前先判断下,当前这个数据数据库中是否已经存在,如果不存在则不允许重复插入,不存在则可插入。

代码示例如下:

    public void save(Goods goods) {
        // 1、先通过商品唯一code,查询数据库是否存在   
        Goods goods = findGoods(goods.getCode);
        // 2、如果这条数据在db里已经存在了,此时就直接返回了   
        if (goods != null) {
            return;
        }
        // 3、如果要是这条数据在db里不存在,此时就会执行数据插入逻辑了   
        insertGoods(goods);
    }

2、前端做一些交互控制

好比有个新增商品的功能,有个保存按钮,用户点击保存按钮后,立马按钮置灰,或者页面跳转到商品列表页面,这样可以防止很大部分的前端重复提交。


并发下如何保证幂等?

上面两种初级方法,在高并发下显然是无法保证接口幂等的,所以在高并发下,我们来如何保证接口的幂等呢,这里整理几种常见的解决办法。

1、基于悲观锁

定义: 当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。

这里以更新商品订单状态来举例:

一般订单有「订单创建」「订单确认」「订单支付」「订单完成」「取消订单」等订单流程。

当我更新订单状态为「订单完成」的时候,我们首先通过判断该订单的状态是否是「订单支付」,如果是不是则直接返回,否则更新状态为已完成。

伪代码示例如下

  begin# 1.开始事务
  # 查询订单,判断状态
  select order_no,status from order where order_no='20200524-1' 
  ifstatus !=订单支付状态){
        #非订单支付状态,不能更新为已完成;
        return ;
    }
   # 更新完成
  update order set status='订单完成' order_no='20200524-1' 
   commit# 2.提交事务

这是我们常见的一种写法,但这种写法在高并发环境下,可能会造成一个业务被执行两次的情况发生:

同时有两个请求过来,大家几乎同时查数据库订单状态,都是「订单支付」状态,然后就支持接下来一系列操作,这就导致一个业务被执行了两次,如果接下来一系列操作不是幂等的那么就会出现脏数据。

这里我们就可以通过悲观锁实现,也就是添加for update字段。

伪代码示例如下

  begin;  # 1.开始事务
  # 查询订单,判断状态
  select order_no,status from order where order_no='20200524-1' for update 
  ifstatus !=订单支付状态){
        #非订单状态,不能更新为已完成;
        return ;
    }
  # 更新完成
  update order set status='完成' order_no='20200524-1' 
   commit# 2.提交事务
  • 这里order_no需要添加索引,否则会锁表
  • 悲观锁在同一事务操作过程中,锁住了一行数据。悲观锁性能不佳所以一般不建议用悲观锁做这个事情。

2、基于乐观锁

定义:乐观锁就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制

所谓的乐观锁就是在表中新增一个version(版本号)字段。

通过版本号的方式,来控制update的操作的幂等性,用户查询出要修改的数据,系统将数据返回给页面,将数据版本号放入隐藏域,用户修改数据,点击提交,将版本号一同提交给后台,后台使用版本号作为更新条件。

update set version = version +1 ,count=count+1 where id =xxx and version = ${version};

注意:乐观锁能够保证的是「update的操作的幂等性」,如果你的update本身就是幂等操作,或者install操作那就不能用乐观锁了。

3、基于状态码

很多业务表,都是有状态的,比如订单表,一般订单有「1-订单创建」「2-订单确认」「3-订单支付」「4-订单完成」「5-取消订单」等订单流程,当我们更新订单状态

update order_table set status=3 where order_no='20200524-1' and status=2;

第一个请求时,成功把 「订单确认」 状态修改成 「订单支付」,sql执行结果的影响行数是1。

第二个请求时,同样想把 「订单确认」 状态修改成 「订单支付」,但是sql执行结果的影响行数为0。如果是0,那么我们直接可以返回成功了。而不需要做接下来的业务操作,以此来保证保证接口的幂等性。

4、基于唯一索引

一般来讲悲观锁、乐观锁、状态码作用于update操作来实现幂等,而唯一索引是针对install操作来保证幂等。

  • 创建订单时,前端先通过接口获取订单号,再请求后端时带入订单号,订单表中订单号添加唯一索引,如果存在插入相同订单号则直接报错。
  • 消费MQ消息时,messageId是唯一的,我们可以新添加一种消费记录表,将messageId作为主键,如果重复消费那么就会存在相同的messageId,插入直接报错。

5、基于分布式

分布式锁实现幂等性的逻辑就是,请求过来时,先去尝试获得分布式锁,如果获得成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。

其实前面介绍过的悲观锁,本质是使用了数据库的分布式锁,都是将多个操作打包成一个原子操作,保证幂等。但由于数据库分布式锁的性能不太好,我们可以改用:redis或zookeeper来实现分布式锁。

6、基于 Token

token方案的特点就是:需要两次请求才能完成一次业务的操作。

一般包括两个请求阶段:

  1. 客户端请求申请获取token,服务端生成token返回。
  2. 第二次请求带着这个token,服务端验证token,完成业务操作。

接口的幂等性如何设计?

注意:,在验证token是否存在,不要用redis.get(token)之后,在用redis.del(token),这样不是原子操作在高并发情况下依然会存在幂等问题。

我们可以直接用redis.del(token)的方式:

redis> SET key1 "Hello"
OK
redis> SET key2 "World"
OK
redis> DEL key1 key2 key3
(integer2
redis> 

我们看返回是否大于0,就知道是否有数据了,而且因为redis命令操作是单线程的,所以不会出现同时返回1,所以是能够保证幂等的。

这种方式最大的缺点需要两次请求,其实简单点我们可以进行一次请求,那就是前端生成唯一token,而不通过后端获取。

接口的幂等性如何设计?

Setnx 命令

在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回1。设置失败,返回 0。

实例

redis> EXISTS job       # job 不存在
(integer) 0

redis> SETNX job "programmer"  # job 设置成功
(integer) 1

redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(integer) 0

redis> GET job                 # 没有被覆盖
"programmer"

如果返回1则说明第一次请求,如果返回0则说明不是第一次请求,直接返回。

这里需要注意的是Setnx命令key值不会自动过期的,所以不清除会一直占用内存,我们可以借助Expire命令来设置有效时间。

redis> SETNX mykey "programmer"  # job 设置成功
(integer) 1
# 如果设置成功,那么设置将该键的超时设置为 10 秒
redis> expire mykey 10 

求一键三连:点赞、转发、在看。

原文始发于微信公众号(后端元宇宙):接口的幂等性如何设计?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/28093.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!