幂等性

时间:2023-02-10 11:15:59

什么是幂等

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。这就是幂等性。web请求过程中应符合幂等性,来保障系统的准确性。web请求过程中一般将增删改查操作放到不同的请求方式中。

符合幂等性:

  • GET 查询
  • UPDATE 修改
  • DELETE 删除

不符合幂等:

  • CREATE

相同请求条件下,如果网络发生延迟。用户点击得不到感知,重复点击GET操作。他获取的结果符合幂等。同样,在创建时可能会创建多条记录,那么它就不具备幂等。产生了‘脏数据’。如何避免?

token过期策略

  • 不具备幂等性前获取token(token唯一)
  • 请求幂等操作携带token到后端
  • 后端中间件验证

后端增加一个中间件效验token,效验成功token后直接过期。保证token只使用一次。无论发送多少次请求,只会满足一次请求。token可以设置一个过期时间为一个时间区间内过期可以考虑redis过期策略。

实现方式

token生成接口,可以使用 uuid模块或者生成随机字符串方式

import string
import random


def random_str(length):
    """
    获取 0-9 a-z A-Z length长度的随机数
    """

    return ''.join([random.choice(string.ascii_letters + string.digits) for _ in range(length)])

中间件实际开发中需要维护一个白名单,不需要要效验的POST请求。

from django.http import JsonResponse
from django_redis import get_redis_connection


class IdempotentMiddleWare:
    """处理 post 请求幂等问题"""
    # 不需要效验请求白名单
    login_url = ('/token/refresh/', '/token/access/')

    def __init__(self, get_response):
        self.get_response = get_response
        self.redis_conn = get_redis_connection('token')

    def __call__(self, request, *args, **kwargs):
        """创建数据 幂等验证"""
        # 是 POST 请求 url 中 不存在 update or delete 需要验证
        process_request_result = self.process_request(request)
        if isinstance(process_request_result, dict):
            return JsonResponse(process_request_result)
        response = self.get_response(request)
        self.process_response(request, response, process_request_result)
        return response

    def process_request(self, request):
        """幂等 标识不存在"""
        if all((request.method == 'POST', request.path_info not in self.login_url)):
            redis_token_key = request.GET.get('token_key')
            if not redis_token_key:
                return {'msg': '内部异常,请联系管理员。 状态码:5001', 'code': 5001, 'data': []}
            if not self.redis_conn.get(redis_token_key):
                return {'msg': '已经添加过了', 'code': 200, 'data': []}
            return True

    def process_response(self, request, response, process_request_result):
        """创建成功后移除标识"""
        if process_request_result is True:
            redis_token_key = request.GET.get('token_key')
            self.redis_conn.delete(redis_token_key)