接口幂等性

接口幂等性

什么是接口幂等性

幂等性原本是数学上的概念,用在接口上就可以理解为:同一个接口,多次发出同一个请求,必须保证操作只执行一次。它是系统服务对外的一种承诺(注意不是一种实现),接口服务提供方承诺只要调用接口成功了,外部多次调用对系统的影响是一致的。

举一个最常见的例子,用户购买商品后支付扣款成功,但是此时网络发生了异常,导致返回结果失败。因为没收到返回结果, 用户就会再次点击付款按钮,就会多付了一笔钱,用户发现余额少了。这就是没有保证接口的幂等性。

为什么需要实现幂等性

如果接口没有实现幂等性,遇到以下情况就会出现问题:

  • 前端重复提交表单:

    在填写表格时,用户填写完成提交,很多时候会因网络波动没有及时返回成功响应,致使用户认为没有 提交成功,然后一直点提交按钮,这时就会导致重复提交表单请求。

  • 用户恶意刷单:

    在实现用户投票这种功能时,如果用户针对同一个人进行重复投票,会导致接口接收到用户重复提交的投票 信息,会使投票结果与事实严重不符。

  • 接口超时重复提交:

    很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时,为了处理网络波动 超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。

  • 消息重复消费:

    当使用 MQ 消息中间件时,如果消息中间件出现错误未及时提交消费信息,就会导致消息重复消费。

需要保证幂等性的场景分析

以 SQL 为例,有下面三种场景,只有第三种场景需要开发人员使用策略来保证幂等性:

  1. 场景一:查询
SELECT column1 FROM table1 WHERE column2 = 2

无论执行多少次都不会改变状态,是天然的幂等。

  1. 场景二:常量赋值更新
UPDATE table1 SET column1 = 1 WHERE column2 = 2

无论执行成功多少次状态都是一致的,因此也是幂等操作。

  1. 场景三:变量赋值更新
UPDATE table1 SET column1 = column1 + 1 WHERE column2 = 2

每次执行的结果都会发生变化,这种场景就不是幂等的。

引入幂等性后对系统的影响

幂等性是为了简化客户端逻辑处理,能防止重复提交,但却增加了服务端的逻辑复杂性和成本:

把并行执行的功能改为串行执行,降低了执行效率; 增加了额外控制幂等的业务逻辑,业务功能变得更加复杂; 所以在使用时,需要考虑引入幂等性的必要性,根据实际业务场景分析,除了业务上的特殊要求外,一般情况下不建议引入接 口幂等性。

如何实现接口幂等性

乐观锁

这里的乐观锁指的是用乐观锁的原理去实现,为数据字段增加一个 version 字段,当数据需要更新时,先去数据库里获取此时 的 version 版本号:

SELECT version FROM tablename WHERE ...

更新数据时首先和版本号作对比,如果不相等说明已经有其他的请求去更新数据了,就会提示更新失败。

UPDATE tablename SET count = count + 1,

version = version + 1 WHERE version = #{version}

1 不过,乐观锁存在失效的情况,就是常说的 ABA 问题,不过如果 version 版本一直是自增的就不会出现这种情况,乐观锁主要 用于读多写少的场景。

悲观锁

乐观锁可以实现的往往用悲观锁也能实现,是指在获取数据时进行加锁,当同时有多个重复请求时其他请求都无法进行操作。

SELECT * FROM tablename WHERE id = 1 FOR UPDATE

注意:id 字段一定是主键或者唯一索引,不然会锁表。

悲观锁一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;

数据库唯一性索引

利用数据库表单的特性来实现幂等,常用的一个思路是在表上构建唯一性索引,保证某一类数据一旦执行完毕,后续同样的请 求再也无法成功写入。

以博客点赞为例,要想防止一个人重复点赞,可以设计一张去重表,将博客 id 与用户 id 绑定建立唯一索引,每当用户点赞时 就往表中写入一条数据,这样重复点赞的数据就无法写入了。

分布式锁

去重表也可以使用分布式锁来代替,比如 Redis。

在分布式环境下,锁定全局唯一资源,使请求串行化,实际表现为互斥锁,防止重复,解决幂等。

Token 令牌

这种机制适用范围较广,有多种不同的实现方式。其核心思想是为每一次操作生成一个唯一性的凭证,也就是 Token。一个 Token 在操作的每一个阶段只有一次执行权,一旦执行成功就直接保存执行结果。

它主要针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。

简单的说就是调用方在调用接口的时候先向后端请求一个全局ID(Token),请求的时候携带这个全局 ID 一起请求(最好将 其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信 息,以此来保证幂等操作。

使用Django ORM实现悲观锁和乐观锁的例子如下:

1. 悲观锁:

Django ORM的select_for_update方法可以用来实现悲观锁:

from django.db import transaction

with transaction.atomic():
    book = Book.objects.select_for_update().get(id=1)
    # 此时book被锁定,其他事务不能修改它直到当前事务结束
    book.available = False
    book.save()

2. 乐观锁:

使用乐观锁时,你可以为模型增加一个版本字段(如version),然后在每次保存时检查并更新这个字段:

from django.db import models, transaction
from django.db.models import F

class Book(models.Model):
    title = models.CharField(max_length=100)
    available = models.BooleanField(default=True)
    version = models.PositiveIntegerField(default=0)

def update_book_title(book_id, new_title):
    retry_count = 3
    for _ in range(retry_count):
        try:
            with transaction.atomic():
                book = Book.objects.get(id=book_id)
                current_version = book.version

                # 试图更新书名和版本号
                updated_rows = Book.objects.filter(id=book_id, version=current_version).update(title=new_title, version=F('version')+1)
                if updated_rows == 1:
                    # 更新成功
                    return True
                # 否则,重试
        except Book.DoesNotExist:
            return False
    return False  # 尝试了retry_count次后仍然失败

在上述乐观锁示例中,如果update_book_title函数更新数据时发现版本号与数据库中的版本号不匹配(由updated_rows == 1判断),则说明数据已经被其他事务修改。此时可以选择重试或者放弃操作。这里选择了重试三次。

posted @ 2021-02-23 11:10  Jeff_blog  阅读(739)  评论(0编辑  收藏  举报