前任开发在代码里下毒了,支付下单居然没加幂等

news2025/1/10 12:08:18

分享是最有效的学习方式。

故事

又是一个风和日丽没好的一天,小猫戴着耳机,安逸地听着音乐,撸着代码,这种没有会议的日子真的是巴适得板。

不料祸从天降,组长火急火燎地跑过来找到了小猫。“快排查一下,目前有A公司用户反馈积分被多扣了”。

小猫回忆了一下“不对啊,这接口我也没动过啊,前几天对外平台的老六直接找我要个支付接口,我就给他了的,以前的代码,我都没有动过的…”。

于是小猫一边疑惑一边翻看着以前的代码,越看脸色越差…

在这里插入图片描述

小猫做的是一个标准的积分兑换商城,以前和客户合作的时候,客户直接用的是小猫单位自己定制的h5页面。这次合作了一家公司有点特殊,由于公司想要定制化自己个性化的H5,加上本身A公司自己有开发能力,所以经过讨论就以接口的方式直接将相关接口给出去,A客户H5开发完成之后自己来对接。

慢慢地,原因也水落石出,之前好好的业务一直没有问题是因为商城的本身H5页面做了防重复提交,由于量小,并且一般对接方式用的都是纯H5,所以都没有什么问题,然后这次是直接将接口给出去了,完了接口居然没有加幂等…

小猫躺枪,数据订正当然是少不了了,事故报告当然也少不了了。

正所谓前人挖坑,后人遭殃,前人锅后人背。

聊聊幂等

接口幂等梗概

这个案例其实就是一个典型的接口幂等案例。那么老猫就和大家从以下几个方面好好剖析一下接口幂等吧。

在这里插入图片描述

什么是接口幂等

比较专业的术语:其任意多次执行所产生的影响均与第一次执行的影响相同。
大白话:多次调用的情况下,接口最终得到的结果是一致的。

那么为什么需要幂等呢?
  1. 用户进行提交动作的时候,由于网络波动等原因导致后端同步响应不及时,这样用户就会一直点点点,这样机会发生重复提交的情况。

  2. 分布式系统之间调用的情况下,例如RPC调用,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。

  3. 分布式系统经常会用到消息中间件,当由于网络原因,mq没有收到ack的情况下,就会导致消息的重复投递,从而就会导致重复提交行为。

  4. 还有就是恶意攻击了,有些业务接口做的比较粗糙,黑客找到漏洞之后会发起重复提交,这样就会导致业务出现问题。打个比方,老猫曾经干过,邻居小孩报名了一个画画比赛,估计是机构培训发起的,功能做的也差,需要靠投票赢得某些礼品,然后老猫抓到接口信息之后就模拟投票进行重复刷了投票。

那么哪些接口需要做幂等呢?

首先我们说是不是所有的接口都需要幂等?是不是加了幂等就好呢?显然不是。
因为接口幂等的实现某种意义上是要消耗系统性能的,我们没有必要针对所有业务接口都加上幂等。

这个其实并不能做一个完全的定义说哪个就不用幂等,因为很多时候其实还是得结合业务逻辑一起看。但是其中也是有规律可循的。

既然我们说幂等就是多次调用,接口最终得到结果一致,那么很显然,查询接口肯定是不要加幂等的,另外一些简单删除数据的接口,无论是逻辑删除还是物理删除,看场景的情况下其实也不用加幂等。

但是大部分涉及到多表更新行为的接口,咱们最好还是得加上幂等。

接口幂等实战方案

前端防抖处理

前端防抖主要可以有两种方案,一种是技术层面的,一种是产品层面的:

  1. 技术层面:例如提交控制在100ms内,同一个用户最多只能做一次订单提交的操作。
  2. 产品层面:当然用户点击提交之后,按钮直接置灰。
基于数据库唯一索引
  1. 利用数据库唯一索引。我们具体来看一下流程,咱们就用小猫遇到的例子。如下:
    在这里插入图片描述

过程描述:

  • 建立一张去重表,其中某个字段需要建立唯一索引,例如小猫这个场景中,咱们就可以将订单提交流水单号作为唯一索引存储到我们的数据库中,就模型上而言,可以将其定义为支付请求流水表。

  • 客户端携带相关流水信息到后端,如果发现编号重复,那么此时就会插入失败,报主键冲突的错误,此时我们针对该错误做一下业务报错的二次封装给到客户另一个友好的提示即可。

数据库乐观锁实现

什么是乐观锁,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。
说得直白一点乐观锁就是一个马大哈。总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。

例如提交订单的进行支付扣款的时候,本来可能更新账户金额扣款的动作是这样的:

update Account set balance = balance-#{payAmount} where accountCode = #{accountCode}

加上版本号之后,咱们的代码就是这样的。

update Account set balance = balance-#{payAmount},version=version +1 where accountCode = #{accountCode} and version = #{currVersion}

这种情况下其实就要求客户端每次在请求支付下单的时候都需要上层客户端指定好当前的版本信息。
不过这种幂等的处理方式,老猫用的比较少。

数据库悲观锁实现

悲观锁的话具有强烈的独占和排他特性。大白话谁都不信的主。所以我们就用select … for update这样的语法进行行锁,当然老猫觉得单纯的select … for update只能解决同一时刻大并发的幂等,所以要保证单号重试这样非并发的幂等请求还是得去校验当前数据的状态才行。就拿当前的小猫遇到的场景来说,流程如下:

在这里插入图片描述

begin;  # 1.开始事务
select * from order where order_code='666' for update # 查询订单,判断状态,锁住这条记录
if(status !=处理中){
   //非处理中状态,直接返回;
   return ;
}
## 处理业务逻辑
update order set status='完成' where order_code='666' # 更新完成
update stock set num = num - 1 where spu='xxx' # 库存更新
commit; # 5.提交事务

这里老猫一再想要强调的是在校验的时候还是得带上本身的业务状态去做校验,select … for update并非万能幂等。

后端生成token

这个方案的本质其实是引入了令牌桶的机制,当提交订单的时候,前端优先会调用后端接口获取一个token,token是由后端发放的。当然token的生成方式有很多种,例如定时刷新令牌桶,或者定时生成令牌并放到令牌池中,当然目的只有一个就是保住token的唯一性即可。

生成token之后将token放到redis中,当然需要给token设置一个失效时间,超时的token也会被删除。

当后端接收到订单提交的请求的时候,会先判断token在缓存中是否存在,第一次请求的时候,token一定存在,也会正常返回结果,但是第二次携带同一个token的时候被拒绝了。

流程如下:

在这里插入图片描述

有个注意点大家可以思考一下:
如果用户用程序恶意刷单,同一个token发起了多次请求怎么办?
想要实现这个功能,就需要借助分布式锁以及Lua脚本了,分布式锁可以保证同一个token不能有多个请求同时过来访问,lua脚本保证从redis中获取令牌->比对令牌->生成单号->删除令牌这一系列行为的原子性。

分布式锁+状态机(订单状态)

现在很多的业务服务都是分布式系统,所以就拿分布式锁来说,关于分布式锁,老猫在此不做赘述,之前老猫写过redis的分布式锁和实现,还有zk锁和实现,具体可见链接:

  1. 锁的演化
  2. 手撕redis分布式锁
  3. 手撸ZK锁

当然和上述的数据库悲观锁类似,咱们的分布式锁也只能保证同一个订单在同一时间的处理。其次也是要去校订单的状态,防止其重复支付的,也就是说,只要支付的订单进入后端,都要将原先的订单修改为支付中,防止后续支付中断之后的重复支付。

在上述小猫的流程中还没有涉及到现金补充,如果涉及到现金补充的话,例如对接了微信或者支付宝的情况,还需要根据最终的支付回调结果来最终将订单状态进行流转成支付完成或者是支付失败。

总结

在我们日常的开发中,一些重要的接口还是需要大家谨慎对待,即使是前任开发留下的接口,没有任何改动,当有人咨询的时候,其实就要好好去了解一下里面的实现,看看方案有没有问题,看看技术实现有没有问题,这应该也是每一个程序员的基本素养。

另外的,在一些重要的接口上,尤其是资金相关的接口上,幂等真的是相当的重要。小伙伴们,你们觉得呢?如果大家还有好的解决方案,或者有其他思考或者意见也欢迎大家的留言。

::: block-2
我是老猫,资深研发老鸟,让我们一起聊聊技术,聊聊人生。

都看到这了,求个点赞、关注、在看三连呗,感谢支持。
:::

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

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

相关文章

C++内存管理机制(侯捷)笔记3

C内存管理机制(侯捷) 本文是学习笔记,仅供个人学习使用。如有侵权,请联系删除。 参考链接 Youtube: 侯捷-C内存管理机制 Github课程视频、PPT和源代码: https://github.com/ZachL1/Bilibili-plus 第三讲:malloc和…

如何手写一个RPC?

在学习 RPC 框架之前,我们先来手写一个RPC。 我们在学习的过程中,一定要做到知其然,还要知其所以然。 架构演进 单体架构 要知道,在以前单体架构的时候,会将所有的应用功能都集中在一个服务当中。 单体架构初始开发…

学习Java API(三):基础知识点一文通✅

推荐阅读 智能化校园:深入探讨云端管理系统设计与实现(一) 智能化校园:深入探讨云端管理系统设计与实现(二) 文章目录 推荐阅读Date 类SimpleDateFormatCalendar Date 类 java.util 包提供了 Date 类来封…

UI自动化测试框架

文章目录 UI自动化基础什么是UI自动化测试框架UI自动化测试框架的模式数据驱动测试框架关键字驱动测试框架行为驱动测试框架 UI自动化测试框架的作用UI自动化测试框架的核心思想UI自动化测试框架的步骤UI自动化测试框架的构成UtilsLog.javaReadProperties.Java coreBaseTest.ja…

屏幕录制软件有哪些?强烈推荐5款免费好用录屏软件

录制全屏视频是制作视频教程、评论、游戏等内容的绝佳方式。它可以包括您计算机屏幕上的任何活动以及您的音频和网络摄像头。特别是,屏幕录像机对喜欢创建软件评论的视频博主很有帮助。如果您是其中之一,那么好消息是您可以在网络上找到很多屏幕录制应用…

纯c++简易的迷宫小游戏

一个用c写的黑框框迷宫 适合新手入门学习 也适合大学生小作业 下面附上代码 总体思路 初始化游戏界面:设置迷宫的大小(WIDTH和HEIGH),生成迷宫地图(map),包括墙壁、空地、起点和终点。显示…

怿星科技测试实验室获CNAS实验室认可,汽车以太网检测能力达国际标准

2023年12月27日,上海怿星电子科技有限公司测试实验室(下称:EPT LABS)通过CNAS实验室认可批准,并于2024年1月5日正式取得CNAS实验室认可证书(注册号CNAS L19826),标志着怿星科技的实验…

SpringBoot3自动配置原理

​​​​​​自动配置 遵循约定大约配置的原则,在boot程序启动后,起步依赖中的一些bean对象会自动注入到ioc容器 看一下我们前面写的代码有没有达到自动配置的效果呢? 没有自动,我们写了配置类,写了Import注解&#…

一台电脑如何通过另一台联网电脑访问网络

电脑A没有连接网络,电脑B已经连接wifi。 电脑A如何通过访问电脑B从而连接网络? 1. 将这2台电脑用网线直连 2. 电脑B打开【网络和Internet设置】 3. 右键点击WLAN,选择属性,进入共享tab页面,勾选【允许其他网络用户通过…

超融合之道:亚信安慧AntDB 8.0版本引领数据库创新

在当今多变的数据应用场景中,AntDB作为行业领先的超融合流式实时数仓,秉承着“融合实时”的研发理念,全面应对企业日益复杂的数据处理需求。通过SQL接口访问多种执行引擎,AntDB在实现交易、分析等多重能力的“超融合”方面取得了显…

nacos配置yml空格报错提示

错误格式 正确格式 spring:redis: # redis配置host: 192.168.30.113 # IPport: 6379 # 端口password: # 密码connect-timeout: 10s # 连接超时时间lettuce: # lettuce redis客户端配置pool: # 连接池配置max-active: 8 # 连接池最大连接数(使用负值表示没有限…

android studio设置gradle和gradle JDK版本

文章目录 1.gradle JDK版本2.gradle版本 1.gradle JDK版本 file -> project structure -> SDK Location -> Gradle Settings -> Gradle JDK -> Download JDK 2.gradle版本 file -> project structure -> Project

打造创新的金融数据平台,加速数字化和智能化转型丨PingCAP 官网金融行业专区上线

自诞生以来,TiDB 的原生分布式架构在强一致性、高可用性和可扩展性等方面与金融级业务需求高度契合,早期版本即为包括北京银行在内的金融用户提供服务。 TiDB 的核心能力始终源自与中国金融用户的共同创造。作为金融级分布式数据库,TiDB 在国…

东南亚潜力巨大,下一个变现增长点!Flat Ads助力休闲游戏出海

近年来,得益于庞大的消费市场和人口红利,东南亚经济飞速发展,尤其在移动互联网和数字经济领域表现亮眼。巨大的发展空间吸引了众多出海者布局,而东南亚有利的人口结构、不断增长的财富以及日益城市化的人口为数字经济的未来增长奠定了基础。那么,2024年游戏出海东南亚,是否仍有…

Zabbix补充扩展

目录 1.自定义监控内容 客户端配置 2.zabbix自动发现与自动注册 自动发现 在服务端和客户端上配置 hosts 解析 在Web页面配置自动发现 自动注册 3.部署zabbix代理服务器 下载zabbix-proxy以及数据库 启动zabbix-proxy及添加hosts解析 在Web页面配置agent代理 配置a…

S1D13L02 VGA简单的液晶控制器

S1D13L02是一款简单、低成本、低功耗、多用途的液晶控制器,具有一个1024 KB的嵌入式SRAM显示缓冲区。 S1D13L02提供了一个灵活的,但易于开发的显示系统,适合于满足嵌入式市场的需求,如工厂自动化、医疗设备和 办公自动化应用。 S1…

过滤器、拦截器、AOP、ControllerAdvcie执行顺序对比

过滤器Filter 简介 来自J2EE中的Servlet技术实现原理:基于servlet的函数回调实现只可以获取到请求中的request和response,无法获取到响应方法的信息可以拦截所有请求支持使用xml配置和注解配置应用场景:权限认证、敏感词检测、访问日志记录…

JVM对象创建与内存分配机制剖析

欢迎大家关注我的微信公众号: 传送门:JVM内存模型深度剖析与优化 目录 对象的创建 对象大小与指针压缩 对象内存分配 对象内存回收 对象的创建 对象创建的主要流程: 1.类加载检查 虚拟机遇到一条new指令时,首先将去检查这个指令的…

GC6106 双通道5V低电压步进电机驱动芯片——低噪声、低振动

GC6106是双通道5V低电压步进电机驱动器,具有低噪声、低振动的特点,特别适用于相机的变焦和对焦系统,万向节和其他精密,低噪声STM控制系统。该芯片为每个通道集成了256微步驱动器。我SPl接口,用户可以方便地调整驱动器的…

设计模式入门

0. 类图 1. 设计原则 1.单一职责原则:每个类只有一个功能 2.开放封闭原则:模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方) 3.里氏代换原则:子类拥有父类的所有方法和属性,从而可以减少创建类的工作量 4.依…