您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

封装一个koa分布式锁中间件来解决幂等或重复请求的问题

时间:2022-07-05 09:47:27  来源:掘金  作者:老诚不bug

在后端并不是写完一个接口的业务逻辑就能投入使用的,接口的优化更是一个难点与麻烦之处(下面的内容我们不考虑前端的处理,因为不能完全靠前端,前后端都需要做自己的处理工作)

1.幂等性:

所谓幂等性是指一个接口不论发送多少个相同请求,最后都会产生相同的结果

例如: 根据Restful API接口规范:把CRUD分为get(查询),post(新增),delete(删除),put(修改)

  • GET:查询条件下,不论用户对数据库查询多少次,都不会对数据库的数据造成,所以这天生就是一个幂等接口
  • POST:新增条件下,如果用户多次发送相同的增加请求,那么数据库将会添加多条相同的记录,所以是一个非幂等接口
  • PUT:分为两种情况

1.绝对修改: 如果是修改绝对值,例如修改一条name为张三的记录,我多次修改最后造成的结果都是一样的(只有一条张三的结果被删除),所以这是一个幂等接口
2.相对修改: 如果是修改相对值,例如修改一张表中score最高的记录(select top 1 score from xxx),我多次修改最后造成的结果是不一样的,你发送几次接口,我就会删除几次最高的,所以这是一个非幂等接口

  • DELETE:也分为两种情况(与PUT相同,就不介绍了,也是相对与绝对的问题)

所以为了安全性,后端会采用许多方式解决幂等问题,将非幂等的接口转化为幂等接口

2.并发:

用户发送请求的时间并不是有规律的,有可能是按顺序一个接一个有序地执行,也有可能在很短时间内发送多个请求抢占同一资源,由于处理请求是异步的,所以不能保证每个都按顺序有序输出,并发也可以细分成两种

1.多个用户抢占同一资源: 例如:100个人短时间内预约同一个医生,但是医生只能被预约一次,这个时候就会产生高并发,我们必须采取措施保证只有第一个发起请求的能预约到这个医生,后面99个都返回预约失败(不是返回请求出错),这时候可以采用阻塞性(多个请求按照顺序排队等待处理)的互斥锁(相同时间内只有一个请求能够获取到锁,其他的请求排队等处理完解锁后再获取),保证这100个请求按顺序转为同步(虽然效率会降低,但是保证了正确性)

1.单个用户抢占自己的同一资源: 这里单个用户的并发一般体现在重复请求,但不是完全的参数相同,比如用户短时间内发起两个参数不同的请求修改自己的个人资料(举个例子,实际情况还是很少的,因为前端会采取遮罩层等措施防止用户的这这种行为),但是请求处理是异步的,可能突然受到网络原因,虽然发送顺序是先1后2,但是返回的顺序是先2后1,这样正确性就有问题了,此时可以设置非阻塞性(只有第一个请求上锁然后进行处理,后面的请求全部报错,同一返回服务器繁忙,且不排队等待处理,直接失败)的互斥锁提醒用户已经有请求在处理,不要发送多个请求

3.高并发:

高并发是并发的是一种程度的体现,极短的时间内产生了海量的并发请求就是高并发,比如双十一抢购,所以就有了分布式架构(分布式系统,就是一个业务拆分成多个子业务,分布在不同的服务器节点,共同构成的系统称为分布式系统)的出现,一个服务器处理海量的并发压力会巨大甚至宕机,所以分布在不同的服务器节点减轻单一服务器的压力

4.进程锁<线程锁<分布式锁:

  • 进程锁:

当某个方法或者代码块使用锁时,在同一时刻之多仅有一个线程在执行该段代码(nodejs的同步代码(异步代码除外)是单进程的,所以无需进程锁)

  • 线程锁:

为了控制同一操作系统中多个进程访问一个共享资源,只是因为程序的独立性,各个进程是无法控制其他进程对资源的访问的,但是可以使用本地系统的信号量控制

  • 分布式锁:

当多个进程不在同一个系统之中时,使用分布式锁控制多个进程对资源的访问(可以理解为线程锁就就是只有单例的分布式锁)

三锁的范围:进程锁<线程锁<分布式锁

三锁作用都是一样的,只是作用的范围大小不同

实战环节:了解了那么多理论知识,下面我来实践一个nodejs中分布式锁的中间件封装解决接口幂等问题

  • 为什么用分布式锁:

1.nodejs已经有现成的redlock(以redlock分布式锁算法名字命名)包来解决分布式锁的问题,就不用自己再写redlock的算法,只需要二次封装为一个中间件,具体redis分布式锁的实现可以去看其他人的文章 2.分布式锁范围最大,既可以用于单例也可以用于分布式,这里我是单例实现,自己的小项目也用不着分布式系统

1.在npm官网找到ioredis和redlock两个包

  • redlock:nodejs中redlock的实现
  • ioredis:集群式redis的实现(上面的redlock必须要ioredis才行,不能用单例的redis包,但是可以在ioredis配置单例redis,总之ioredis就是一个功能更加强大的redis包)

2.配置ioredis和redlock

  • ioredis:

思路:创建一个class类,把所有redis的操作和初始化封装到Redis这个类中,最后实例化导出供其他地方使用

import ioredis from 'ioredis'
import { REDIS_CONF } from '../config/db'
const { password, port, host } = REDIS_CONF
class Redis {
 client
 constructor() {
   this.client = new ioredis({
     port,
     host,
     password
   })
   this.client.on('error', (err) => console.log(err))
 }
 //添加数据
 async set(key: string, value: any, time?: number | string) {
   //判断value值是否是对象类型
   if (typeof value === 'object') {
     value = JSON.stringify(value)
   }
   //time为过期时间,可选
   if (time) {
     awAIt this.client.set(key, value, 'EX', time)
   } else {
     await this.client.set(key, value)
   }
 }
 async get(key: string) {
   const data = await this.client.get(key)
   return data
 }
 async delete(key: string) {
   await this.client.del(key)
 }
}

const redis = new Redis()
export default redis

注意事项:

  • 1.redis必须要先安装到你的电脑并配置完并且开启服务才能使用,具体redis安装,配置,开启服务实现自行百度
  • 2.如果你要设置redis密码,必须先把redis配置完密码才能用(自行百度redis如何配置密码),不然直接在nodejs使用连接会报auth错
  • 3.redis 6.0.0以下不支持用户名,只需要设置密码即可,如果你真的要用户名自行百度配置,但是我觉得一个机子一个redis就够了,用户名有点多此一举了
  • redlock:
import Redlock from 'redlock'
import redis from './redis'
const redlock = new Redlock([redis.client], {  retryCount: 0 })
export default redlock

注意事项:
1.new Redlock实例的时候第一个参数传入一个数组,里面每一项是ioredis的实例,如果像我一样不需要分布式,传入一个实例即可,后面是传入的配置具体查看其文档,此处retryCount表示获取锁失败的时候重试的次数,根据官方的解释,这里的retryCount设置为0够用了,如下图官方解释

 

3.封装一个分布式锁中间件

import { Middleware } from 'koa'
import { Lock } from 'redlock'
import redlock from '../db/redlock'
import { error } from '../utils/Response'
//这里isByUser为true则由用户id+请求地址作为key上锁,即:此接口不允许一个用户同时更改同一资源(参数不同也不行)
//isByUser默认为false则由全部参数+用户id+地址作为key上锁,即:此接口不允许一个用户同时以同一参数更改同一资源(拦截重复请求)
const idempotent = (isByUser: boolean = false) => {
  const Redlock: Middleware = async (ctx, next) => {
    let id: string
    //这里的ctx.user是我之前配置的中间件,用于解析用户携带token的参数,来辨别用户和获取用户参数,里面存放用户的个人信息
    //有的接口不需要鉴权认证,所以ctx.user.id就会报错则id以空字符串输出
    /*这里为什么要解析出id而不是直接拿token呢?因为一个用户可以有多个token,但一个用户只有一个id
    如果拿token作为标识,不同token的同一用户也会成功上锁,就形成了一个用户多次获得了锁的情况
    但由于id的独立性,所以id不同,就表示为不同的用户了
    */
    try {
      id = ctx.user.id
    } catch (error) {
      id = ''
    }
    let lock: Lock | null = null
    try {
      if (isByUser) {
        //上锁
        lock = await redlock.acquire([`${id}:${ctx.URL}`], 10000)
      } else {
        const body = JSON.stringify(ctx.request.body)
        console.log(`${id}:${ctx.URL}:${body}`)

        lock = await redlock.acquire([`${id}:${ctx.URL}:${body}`], 10000)
      }
    } catch (err) {
      //如果抛出错误表示上锁失败,表示有重复请求正在操作
      //这里的error()函数是我封装的返回错误的函数里面调用了ctx.throw所以报错会立即返回,后面的next不会继续进行
      error(ctx, 500, '请求正在进行,请勿重复提交')
    }
    await next()
    //后面的中间件全部执行完就可以释放锁了
    await lock!.release()
  }
  return Redlock
}
export default idempotent

4.使用环节(测试验收)

封装一个koa分布式锁中间件来解决幂等或重复请求的问题

 

设置了一个测试路由:在路由处理前添加我们设计的中间件idempotent,不传入参数isByUser默认为false,即全部参数相同就拦截,路由处理没什么,就是等待两秒之后成功输出一句话

  • 一个线程发送两次相同请求(等待第一次处理完再发送第二个)

第一次:

封装一个koa分布式锁中间件来解决幂等或重复请求的问题

 

第二次:

封装一个koa分布式锁中间件来解决幂等或重复请求的问题

 

可以看到两次没有任何影响,都是延迟了2s后成功返回

  • 多个线程分别发送一次相同请求(并发)

这里用多个api接口管理工具短时间内轮流发送(处理一个请求需要2s,所以只要在2s之内发送另一个即可)来模拟并发

第一个请求:

封装一个koa分布式锁中间件来解决幂等或重复请求的问题

 

第二个请求:

封装一个koa分布式锁中间件来解决幂等或重复请求的问题

 

两张图你们很难看出真实情况,但是我我能看到,第一次请求两秒后返回了成功,第二次请求很短时间内直接返回错误(获取不到锁了,代表有重复请求在进行)

这里只给你们演示了一下无参数,无token的情况已经成功了,我之后也测试了isByUser和有无token的有效性,只是没有放出来,但也是没有问题的,isByUser是我认为比较常用的两种情况:全部参数和用户id+接口地址的判断方式,如果有其它想法,也可以自定义传入自己想锁定的key由什么参数决定,这里你们就二次封装即可,我个人感觉isByUser已经够用了

5.一个简单的koa分布式锁中间件就封装好了

注意事项:

  • redlock算法并非绝对安全,如果过期时间设置的太短(小于接口处理时间)会出现接口还没处理完就自动释放锁了,然后出现其他线程也可以获取到锁,就失去了安全性(JAVA中的redisson里有个watchdog自动续期可以解决这个问题,但是这里是nodejs,目前没有发现封装好watchdog机制的分布式锁包,有能力的也可以自己封装,我是能力不够,还是把过期时间设置的稍微长一点好了,但太长也会有其他弊端)
  • 这里的redlock是非阻塞性的,上文已经提到,如果获取不到锁会自动报错,请求直接失效而不是排队等候解锁再执行,如果需要阻塞性,可以自己封装,但是我推荐一个其他的包:async-lock这是一个阻塞性的处理方式,可以形成异步队列按顺序执行而不是非阻塞性地直接抛出错误

原文链接:
https://juejin.cn/post/7115669651173949448



Tags:幂等   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
深入浅出Kafka:高可用、顺序消费及幂等性
在我们旅行于数据海洋的途中,如果把 Kafka 比作是一艘承载无数信息航行的快船,前文《Kafka实战漫谈:大数据领域的不败王者》已经讲述了如何搭建起这艘快船,让它在起风的早晨开始...【详细内容】
2023-12-18  Search: 幂等  点击:(179)  评论:(0)  加入收藏
DDD与微服务集成的第一战役:客户端重试&服务端幂等
当一个接口从简单的内部调用升级为远程方法调用(RPC)会面临很多问题,比如: 本地事务失效。在内部调用时,多个方法通常在同一事务中执行,可以使用本地数据库事务来确保数据的一致性...【详细内容】
2023-10-30  Search: 幂等  点击:(386)  评论:(0)  加入收藏
单机幂等性的 6 种实现!
有位朋友,前两天突然问磊哥:在 Java 中,防止重复提交最简单的方案是什么?这句话中包含了两个关键信息,第一:防止重复提交;第二:最简单。于是磊哥问他,是单机环境还是分布式环境?得到的...【详细内容】
2023-08-28  Search: 幂等  点击:(202)  评论:(0)  加入收藏
一个注解实现接口幂等,这样才优雅!
本节内容介绍了防重注解@RepeatSubmit的实现原理,后续开发中只需要在非查询接口中添加这个注解就能保证在一定时间内防止重复提交。场景码猿慢病云管理系统中其实高并发的场...【详细内容】
2023-08-01  Search: 幂等  点击:(516)  评论:(0)  加入收藏
SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战
在Web开发中,我们经常需要防止用户重复提交某个操作,尤其是一些需要保证数据一致性的操作,如支付等。而接口幂等性就是解决这个问题的一种方案。接口幂等性指的是,无论调用多少...【详细内容】
2023-06-07  Search: 幂等  点击:(215)  评论:(0)  加入收藏
深入解析幂等性在Python开发中的应用
当谈到幂等性时,我们通常指的是在计算机科学和软件工程领域中一个重要的概念。幂等性是指一个操作或函数可以被多次执行,而产生的结果保持不变。简而言之,就是无论执行多少次,最...【详细内容】
2023-06-05  Search: 幂等  点击:(316)  评论:(0)  加入收藏
解决幂等问题,只需要记住这个口诀!
"幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。作为开发人...【详细内容】
2023-05-23  Search: 幂等  点击:(338)  评论:(0)  加入收藏
在分布式系统中,SpringBoot 实现接口幂等性
在分布式系统中,接口幂等性是一个非常重要的概念,它保证了在同样的条件下,同一请求的多次执行所产生的效果都是相同的。在实际开发中,为了防止重复提交或者重复操作带来的问题,我...【详细内容】
2023-03-07  Search: 幂等  点击:(225)  评论:(0)  加入收藏
SpringBoot如何实现接口幂等性操作?
什么是接口的幂等性?幂等作为一个数学概念,是指在某个一元运算过程中,任意次数的运算结果会与一次运算结束后的结果是相同的。在计算机操作中,一个幂等操作的特点是任意执行一...【详细内容】
2023-02-02  Search: 幂等  点击:(196)  评论:(0)  加入收藏
封装一个koa分布式锁中间件来解决幂等或重复请求的问题
在后端并不是写完一个接口的业务逻辑就能投入使用的,接口的优化更是一个难点与麻烦之处(下面的内容我们不考虑前端的处理,因为不能完全靠前端,前后端都需要做自己的处理工作)1.幂...【详细内容】
2022-07-05  Search: 幂等  点击:(559)  评论:(0)  加入收藏
▌简易百科推荐
Meta如何将缓存一致性提高到99.99999999%
介绍缓存是一种强大的技术,广泛应用于计算机系统的各个方面,从硬件缓存到操作系统、网络浏览器,尤其是后端开发。对于Meta这样的公司来说,缓存尤为重要,因为它有助于减少延迟、扩...【详细内容】
2024-04-15    dbaplus社群  Tags:Meta   点击:(3)  评论:(0)  加入收藏
SELECT COUNT(*) 会造成全表扫描?回去等通知吧
前言SELECT COUNT(*)会不会导致全表扫描引起慢查询呢?SELECT COUNT(*) FROM SomeTable网上有一种说法,针对无 where_clause 的 COUNT(*),MySQL 是有优化的,优化器会选择成本最小...【详细内容】
2024-04-11  dbaplus社群    Tags:SELECT   点击:(3)  评论:(0)  加入收藏
10年架构师感悟:从问题出发,而非技术
这些感悟并非来自于具体的技术实现,而是关于我在架构设计和实施过程中所体会到的一些软性经验和领悟。我希望通过这些分享,能够激发大家对于架构设计和技术实践的思考,帮助大家...【详细内容】
2024-04-11  dbaplus社群    Tags:架构师   点击:(2)  评论:(0)  加入收藏
Netflix 是如何管理 2.38 亿会员的
作者 | Surabhi Diwan译者 | 明知山策划 | TinaNetflix 高级软件工程师 Surabhi Diwan 在 2023 年旧金山 QCon 大会上发表了题为管理 Netflix 的 2.38 亿会员 的演讲。她在...【详细内容】
2024-04-08    InfoQ  Tags:Netflix   点击:(5)  评论:(0)  加入收藏
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(9)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(16)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(14)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(9)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(15)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(10)  评论:(0)  加入收藏
站内最新
站内热门
站内头条