119. 再谈接口幂等性

文章目录

  • 0. 前言
  • 1. insert前先select
  • 2. 加悲观锁
  • 3. 加乐观锁
  • 5. 加唯一索引【配合 (1. insert前先select )最常用 】
  • 6. 建防重表
  • 6. 根据状态机
  • 7. 加分布式锁
  • 8. 获取token

0. 前言

在 93. 通用防重幂等设计 一文中,已经介绍过幂等的使用。该文将补充一些其他方案,不一定实用,但是是思路的延伸。

不知道你有没有遇到过这些场景:

  • 有时我们在填写某些form表单时,保存按钮不小心快速点了两次,表中竟然产生了两条重复的数据,只是id不一样。

  • 我们在项目中为了解决接口超时问题,通常会引入了重试机制。第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),为了避免返回错误的结果(这种情况不可能直接返回失败吧?),于是会对该请求重试几次,这样也会产生重复的数据。

  • mq消费者在读取消息时,有时候会读取到重复消息,如果处理不好,也会产生重复的数据。

没错,这些都是幂等性问题。

接口幂等性是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生副作用。

这类问题多发于接口的:

  • insert操作,这种情况下多次请求,可能会产生重复数据。
  • update操作,如果只是单纯的更新数据,比如:update user set status=1 where id=1,是没有问题的。如果还有计算,比如:update user set status=status+1 where id=1,这种情况下多次请求,可能会导致数据错误。

那么我们要如何保证接口幂等性?本文将会告诉你答案。

1. insert前先select

通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在insert前,先根据namecode字段select一下数据。如果该数据已存在,则执行update操作,如果不存在,才执行 insert 操作。

图片

该方案可能是我们平时在防止产生重复数据时,使用最多的方案。但是该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用,否则同样会产生重复数据(比如并发的两个请求都先select发现没有DB中没有数据,然后后续两个请求都去insert)。我在这里提一下,是为了避免大家踩坑。

2. 加悲观锁

在支付场景中,用户A的账号余额有150元,想转出100元,正常情况下用户A的余额只剩50元。一般情况下,sql是这样的:

update user amount = amount-100 where id=123;

如果出现多次相同的请求,可能会导致用户A的余额变成负数。这种情况,用户A来可能要哭了。同时,系统开发人员可能也要哭了,因为这是很严重的系统bug

为了解决这个问题,可以加悲观锁,将用户A的那行数据锁住,在同一时刻只允许一个请求获得锁,更新数据,其他的请求则等待。

通常情况下通过如下sql锁住单行数据:

select * from user id=123 for update;

具体流程如下:

图片

具体步骤:

  1. 多个请求同时根据id查询用户信息。
  2. 判断余额是否不足100,如果余额不足,则直接返回余额不足。
  3. 如果余额充足,则通过for update再次查询用户信息,并且尝试获取锁。
  4. 只有第一个请求能获取到行锁,其余没有获取锁的请求,则等待下一次获取锁的机会。
  5. 第一个请求获取到锁之后,判断余额是否不足100,如果余额足够,则进行update操作,但后续请求还是能获取到锁进行扣款操作,只是不会降余额扣到100以下罢了,不符合幂等诉求
  6. 如果余额不足,说明是重复请求,则直接返回成功。

需要特别注意的是:如果使用的是mysql数据库,存储引擎必须用innodb,因为它才支持事务。此外,这里id字段一定要是主键或者唯一索引,不然会锁住整张表。

  • 悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。
  • 此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。
  • 在这里顺便说一下,防重设计幂等设计,其实是有区别的。防重设计主要为了避免产生重复数据,对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。

3. 加乐观锁

既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观锁。需要在表中增加一个timestamp或者version字段,这里以version字段为例。

在更新数据之前先查询一下数据:

select id,amount,version from user id=123;

如果数据存在,假设查到的version等于1,再使用idversion字段作为查询条件更新数据:

update user set amount=amount+100,version=version+1 where id=123 and version=1;

更新数据的同时version+1,然后判断本次update操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。

由于第一次请求version等于1是可以成功的,操作成功后version变成2了。这时如果并发的请求过来,再执行相同的sql

 update user set amount=amount+100,version=version+1 where id=123 and version=1;

update操作不会真正更新数据,最终sql的执行结果影响行数是0,因为version已经变成2了,where中的version=1肯定无法满足条件。但为了保证接口幂等性,接口可以直接返回成功,因为version值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。

具体流程如下:
在这里插入图片描述

具体步骤:

  1. 先根据id查询用户信息,包含version字段
  2. 根据idversion字段值作为where条件的参数,更新用户信息,同时version+1
  3. 判断操作影响行数,如果影响1行,则说明是一次请求,可以做其他数据操作。
  4. 如果影响0行,说明是重复请求,则直接返回成功。

如果每个请求都同时执行了上面1和2两步,则还是每次都会成功,所以还是不满足幂等诉求。

5. 加唯一索引【配合 (1. insert前先select )最常用 】

绝大数情况下,为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案。

alter table `order` add UNIQUE KEY `un_code` (`code`);

加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry '002' for key 'order.un_code异常,表示唯一索引有冲突。

虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。

具体流程图如下:

在这里插入图片描述

具体步骤:

  1. 用户通过浏览器发起请求,服务端收集数据。
  2. 将该数据插入mysql
  3. 判断是否执行成功,如果成功,则操作其他数据(可能还有其他的业务逻辑)。
  4. 如果执行失败,捕获唯一索引冲突异常,直接返回成功。

6. 建防重表

有时候表中并非所有的场景都不允许产生重复的数据,只有某些特定场景才不允许。这时候,直接在表中加唯一索引,显然是不太合适的。即业务表中对于某些字段不需要加唯一索引,因为其他业务流程允许这些字段重复。但是某些业务又不允许这些字段重复,此时就可以建一个防重表,专门给这个特殊业务使用。

针对这种情况,我们可以通过建防重表来解决问题。

该表可以只包含两个字段:id 和 唯一索引,唯一索引可以是多个字段比如:name、code等组合起来的唯一标识,例如:lym_0001

具体流程图如下:
在这里插入图片描述

具体步骤:

  1. 用户通过浏览器发起请求,服务端收集数据。
  2. 将该数据插入mysql防重表针对这个特殊业务使用的步骤,其他运行重复的业务不需要经过防重表。
  3. 判断是否执行成功,如果成功,则做mysql其他的数据操作(可能还有其他的业务逻辑)。
  4. 如果执行失败,捕获唯一索引冲突异常,直接返回成功。

需要特别注意的是:防重表和业务表必须在同一个数据库中,并且操作要在同一个事务中。

6. 根据状态机

很多时候业务表是有状态的,比如订单表中有:1-下单、2-已支付、3-完成、4-撤销等状态。如果这些状态的值是有规律的,按照业务节点正好是从小到大,我们就能通过它来保证接口的幂等性。

假如id=123的订单状态是2-已支付,现在要变成3-完成状态。

update `order` set status=3 where id=123 and status=2;

第一次请求时,该订单的状态是已支付,值是2,所以该update语句可以正常更新数据,sql执行结果的影响行数是1,订单状态变成了3

后面有相同的请求过来,再执行相同的sql时,由于订单状态变成了3,再用status=2作为条件,无法查询出需要更新的数据,所以最终sql执行结果的影响行数是0,即不会真正的更新数据。但为了保证接口幂等性,影响行数是0时,接口也可以直接返回成功。

具体流程图如下:
在这里插入图片描述

具体步骤:

  1. 用户通过浏览器发起请求,服务端收集数据。
  2. 根据id和当前状态作为条件,更新成下一个状态
  3. 判断操作影响行数,如果影响了1行,说明当前操作成功,可以进行其他数据操作。
  4. 如果影响了0行,说明是重复请求,直接返回成功。

主要特别注意的是,该方案仅限于要更新的表有状态字段,并且刚好要更新状态字段的这种特殊情况,并非所有场景都适用。

7. 加分布式锁

其实前面介绍过的加唯一索引或者加防重表,本质是使用了数据库的分布式锁,也属于分布式锁的一种。但由于数据库分布式锁的性能不太好,我们可以改用:rediszookeeper

鉴于现在很多公司分布式配置中心改用apollonacos,已经很少用zookeeper了,我们以redis为例介绍分布式锁。

目前主要有三种方式实现redis的分布式锁:

  • setNx命令
  • set命令
  • Redission框架
    每种方案各有利弊,具体实现细节我就不说了。

具体流程图如下:

在这里插入图片描述

具体步骤:

  1. 用户通过浏览器发起请求,服务端会收集数据,并且生成订单号code作为唯一业务字段。
  2. 使用redisset命令,将该订单code设置到redis中,同时设置超时时间。
  3. 判断是否设置成功,如果设置成功,说明是第一次请求,则进行数据操作。
  4. 如果设置失败,说明是重复请求,则直接返回成功。

需要特别注意的是:分布式锁一定要设置一个合理的过期时间,如果设置过短,无法有效的防止重复请求。如果设置过长,可能会浪费redis的存储空间,需要根据实际业务情况而定。

8. 获取token

除了上述方案之外,还有最后一种使用token的方案。该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。

  • 第一次请求获取token
  • 第二次请求带着这个token,完成业务操作。

具体流程图如下:

第一步,先获取token

图片

第二步,做具体业务操作。

图片

具体步骤:

  1. 用户访问页面时,浏览器自动发起获取token请求。
  2. 服务端生成token,保存到redis中,然后返回给浏览器。
  3. 用户通过浏览器发起请求时,携带该token
  4. redis中查询该token是否存在,如果不存在,说明是第一次请求,做则后续的数据操作。
  5. 如果存在,说明是重复请求,则直接返回成功。
  6. redistoken会在过期时间之后,被自动删除。

以上方案是针对幂等设计的。如果是防重设计,流程图要改改:

在这里插入图片描述

需要特别注意的是:token必须是全局唯一的。

原文地址:https://mp.weixin.qq.com/s/7P2KbWjjX5YPZCInoox-xQ

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/7548.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【一看就懂】UART、IIC、SPI、CAN四种通讯协议对比介绍

UART、IIC、SPI、CAN四种通信协议对比 通信方式传输线通讯方式标准传输速度使用场景UARTTX(发送数据线)、RX(接收数据线)串行、异步、全双工115.2 kbit/s(常用)计算机和外部设备通信(打印机)IICSCL(时钟线)、SDA(数据线)串行、同步、半双工100 kbit/s(标…

一种由RSOA和PIC集成的宽可调激光器

----翻译自Nouman Zia, Samu-Pekka Ojanen, Jukka Viheriala, Eero Koivusalo, Joonas Hilska, Heidi Tuorila, and Mircea Guina在optics letter上发的文章vol.48, Issue 5, pp. 1319-1322(2023) 摘要:通过光子集成方式实现的2-3μm波长的可调激光器,在…

Adobe Acrobat Pro DC 2022一款高效强大的PDF阅读编辑专业软件(240506)

01 软件介绍 Adobe Acrobat Pro DC 2022,作为一款专业的PDF处理工具,它集成了强大的制作功能,能够与Adobe Photoshop的高级图像编辑能力无缝衔接,进而将各类纸质文档高效转换成可编辑的电子格式,以便于文件的传输和签…

跨越语言界限,多语言盲盒小程序带你领略全球风情

在全球化的今天,我们生活在一个多元文化的世界中,不同的语言、风俗、习惯共同构成了这个五彩斑斓的地球村。为了让每个人都能轻松体验到世界各地的独特风情,一款创新的多语言盲盒小程序应运而生,它跨越了语言的界限,让…

老阳分享:跨境选品师普通人做能否借此赚钱?

在跨境电商日益繁荣的今天,选品师这一职业逐渐进入大众视野。老阳,作为业内知名的跨境选品师,经常分享他的选品经验和心得。那么,对于普通人来说,成为跨境选品师是否真的能赚钱呢? 首先,我们需要明确什么是…

mySQL (基础面试)实物四属性 ACID属性,以及开启事务

mySQL具备四个基本属性 原子性atomicity 事务是一个完整的操作,事务的各个步骤是不可分的(原子的),要么执行要么不执行 一致性consistency 当事务完成时,数据处于一致状态 隔离性isolation 并发事物之间彼此隔离…

基于springboot+vue+Mysql的租房网站

开发语言:Java框架:springbootJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:…

springboot3项目练习详细步骤(第二部分:文章分类模块)

新增文章分类 接口文档 业务实现 参数校验 文章分类列表 接口文档 业务实现 获取文章分类详情 接口文档 业务实现 更新文章分类 接口文档 业务实现 分组校验 问题 概念 实现步骤 总结 删除文章分类 接口文档 业务实现 该模块大部分请求的路径相同&…

2024最新行业领域名词解释大全

2024最新行业领域名词解释大全 🚀 大家好!我是你们的老朋友猫头虎🐯。今天要为大家带来2024年最新的行业领域名词解释大全!在这个信息爆炸的时代,准确了解不同领域的行业动态、工作机会和职业前景至关重要。下面我会分…

阿里巴巴中国站关键字搜索API返回值全攻略:精准定位所需商品

当使用阿里巴巴中国站的关键字搜索API时,理解其返回值的结构和内容对于精准定位所需商品至关重要。以下是一份全面的攻略,帮助你更好地利用这个API: 在商品列表中,每个商品对象都包含丰富的信息,以帮助你精准定位所需商…

伙伴匹配(后端)-- 前后端日期格式化

后端时间格式化 后端时间格式化 在expireTime属性加上一个格式化注解,并给定格式 前端过期时间格式化 下载一个moment格式化工具(我安装失败了日期格式化也成功了) npm install moment这一页和添加队伍新增这一行(还要导入mom…

Python数据爬取超简单入门

## 什么是网络爬虫? 网络爬虫是一种自动浏览器程序,能够自动地从互联网获取数据。爬虫的主要任务是访问网页,分析网页内容,然后提取所需的信息。爬虫广泛应用于数据收集、数据分析、网页内容监控等领域。 ## 爬虫的基本步骤 1.…

2025第23届太原煤炭(能源)工业技术与装备展览会

第二十三届太原煤炭(能源)工业技术与装备展览会 邀 请 函 指导单位: 中国煤炭工业协会 主办单位:山西省煤炭工业协会 承办单位:太原奇新展览有限公司 展览时间:2025年4月22-24日 展览地点&#xff1a…

Obsidian dataview 使用入门

Dataview有四种展示格式:list、table、task、calendar。 本文只介绍前面两种。 语法总结 通过#标签 dataview LIST FROM #标签 通过"文件夹" dataview LIST FROM "文件夹名" 通过[ [ 文件链接 ] ] 选择链接到一个文件,或者…

深入了解C/C++的内存区域划分

🔥个人主页:北辰水墨 🔥专栏:C学习仓 本节我们来讲解C/C的内存区域划分,文末会附加一道题目来检验成果(有参考答案) 一、大体有哪些区域?分别存放什么变量开辟的空间? …

探索AI编程新纪元:从零开始的智能编程之旅

提示:Baidu Comate 智能编码助手是基于文心大模型,打造的新一代编码辅助工具 文章目录 前言AI编程概述:未来已来场景需求:从简单到复杂,无所不包体验步骤:我的AI编程初探试用感受:双刃剑下的深思…

分类预测 | MATLAB实现LSSVM最小二乘支持向量机多分类预测

分类预测 | MATLAB实现LSSVM最小二乘支持向量机多分类预测 目录 分类预测 | MATLAB实现LSSVM最小二乘支持向量机多分类预测分类效果基本介绍程序设计参考资料分类效果 基本介绍 MATLAB实现LSSVM最小二乘支持向量机多分类预测。最小二乘支持向量机(Least Squares Support Vecto…

容器内存使用率(container_memory_working_set_bytes)高问题排查

背景 五一节假日值班期间,告警群里突然告警容器内存使用率高于 90%,并且后续一直有告警出现。随即登入指标监控系统查看该告警指标,如下: 该指标是通过 container_memory_working_set_bytes / container_spec_memory_limit_bytes…

44 网络基础

本章重点 了解网络发展背景,对局域网/广域网的概念有基本认识 了解网络协议的意义,重点理解TCP/IP五层结构模型 学习网络传输的基本流程,理解封装和分用 目录 1.网络发展 2.协议 3.OSI七层模型 4.TCP/IP五层模型 5.网络传输流程图 6.网络中…

WRT1900ACS搭建openwrt服务器小记

参考链接 wrt1900acs openwrt wrt1900acs openwrt 刷机 wrt1900acs原生固件刷openwrt-23.05.3-mvebu-cortexa9-linksys_wrt1900acs-squashfs-factory.img wrt1900acs openwrt更新刷openwrt-23.05.3-mvebu-cortexa9-linksys_wrt1900acs-squashfs-sysupgrade.bin 通过WEB UI来…