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

封装一个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:幂等   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
在后端并不是写完一个接口的业务逻辑就能投入使用的,接口的优化更是一个难点与麻烦之处(下面的内容我们不考虑前端的处理,因为不能完全靠前端,前后端都需要做自己的处理工作)1.幂...【详细内容】
2022-07-05  Tags: 幂等  点击:(121)  评论:(0)  加入收藏
一、什么是幂等?**幂等性:**多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。二、使用幂等的场景1、前端重复提交用户注册,用户创建商品等...【详细内容】
2022-03-15  Tags: 幂等  点击:(117)  评论:(0)  加入收藏
一、幂等性概念在数学里,幂等有两种主要的定义。1、在某二元运算下,幂等元素是指被自己重复运算(或对于函数是为复合)的结果等于它自己的元素。例如,乘法下唯一两个幂等实数为0和...【详细内容】
2021-10-09  Tags: 幂等  点击:(134)  评论:(0)  加入收藏
消息中间件是分布式系统常用的组件,无论是异步化、解耦、削峰等都有广泛的应用价值。我们通常会认为,消息中间件是一个可靠的组件&mdash;&mdash;这里所谓的可靠是指,只要我把消...【详细内容】
2021-08-19  Tags: 幂等  点击:(149)  评论:(0)  加入收藏
幂等,这个词来源自数学领域。幂等性衍生到软件工程中,它的语义是指:函数/接口可以使用相同的参数重复执行, 不应该影响系统状态,也不会对系统造成改变。举一个简单的例子:正常设...【详细内容】
2021-06-18  Tags: 幂等  点击:(185)  评论:(0)  加入收藏
小伙伴们有没有遇到过生产环境经常出现过重复的数据?在排查问题的时候,数据又是正常的。这个是何解呢?怎么会出现这种情况,而且还很难排查问题。今天老顾给大家分享一下这里的原因,以及解决方案。...【详细内容】
2021-03-31  Tags: 幂等  点击:(387)  评论:(0)  加入收藏
之前负责的项目报了一个问题,用户操作回退失效。我们的设计里,操作回退是回到操作前的状态。经过查看日志发现,用户之前的操作做了两次,也就是说提交操作的接口被调用了两次,导致...【详细内容】
2021-01-18  Tags: 幂等  点击:(288)  评论:(0)  加入收藏
什么是接口的幂等性,如何实现接口幂等性?(一)幂等性概念幂等性原本是数学上的概念,用在接口上就可以理解为:同一个接口,多次发出同一个请求,必须保证操作只执行一次。 调用接口发生...【详细内容】
2020-11-16  Tags: 幂等  点击:(191)  评论:(0)  加入收藏
什么是幂等性?对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。幂等性设计我们以对接支付宝充值为例,来分析支付回调接口如何设计?如果我们系统中对接过支付宝充值功...【详细内容】
2019-09-24  Tags: 幂等  点击:(525)  评论:(0)  加入收藏
作者:冰峰编者说:比较实用的Redis加锁的方式,代码段可以收藏。在最近的一次业务升级中,遇到这样一个问题,我们设计了新的账户体系,需要在用户将应用升级之后将原来账户的数据手动...【详细内容】
2019-07-23  Tags: 幂等  点击:(404)  评论:(0)  加入收藏
▌简易百科推荐
本文从客户端的视角,分享客户端如何协同服务端进行接口时间的优化。Compose是什么接口性能优化对于客户端的同学来讲涉及可能不是很多,但是接口的性能对于客户端的体验影响是...【详细内容】
2022-10-20  马啟超  微信公众号  Tags:接口优化   点击:(5)  评论:(0)  加入收藏
【写在最前】我们在平时的编程学习中,经常会接触到“版本控制”这个概念。目前业界的开发团队,基本都会从 GIT 、 SVN 两种主流版本控制系统中选择一个在团队内部使用。两个软...【详细内容】
2022-10-20  5分钟IT入门   网易号  Tags:git   点击:(3)  评论:(0)  加入收藏
原创:微观技术作为后端研发同学为了几两碎银,没日没夜周旋于各种人、各种事上。如果你要想成长的更快,就要学会归纳总结,找到规律,并且善用这些规律。就比如工作,虽然事情很多、也...【详细内容】
2022-10-18  马士兵教育  今日头条  Tags:接口   点击:(5)  评论:(0)  加入收藏
大家知道,在Springboot+Spring Data Jpa的项目里,dao层只需要继承JpaRepository接口,就可以实现Mybatis中@Repository+mapper的效果,不需要任何多余的配置,就可以将dao层注入bean...【详细内容】
2022-10-17  活在信息时代    Tags:Spring Data Jpa   点击:(4)  评论:(0)  加入收藏
01常收到一些在校非计算机软件学生的提问,编程难吗?我也想学编程?编程难吗?对于这个问题,我想大多数人都会认为难,我也不例外。但难在哪里?不同的人有不同的理解,因为编程本身的维度...【详细内容】
2022-10-17  阿谊小梦  搜狐号  Tags:编程   点击:(4)  评论:(0)  加入收藏
jupyter lab作为jupyter notebook的升级版,增加了很多功能。其支持python、R、java等多种编程语言及markdown、letex等写作语言及公式输入,可以集编程与写作于一身,非常适合于...【详细内容】
2022-10-14  ruiwango  今日头条  Tags:jupyter   点击:(13)  评论:(0)  加入收藏
作者:mosun,腾讯 PCG 后台开发工程师一、虚拟内存 1.1 虚拟内存引入我们知道计算机由 CPU、存储器、输入/输出设备三大核心部分组成,如下:CPU 运行速度很快,在完全理想的状态下,存...【详细内容】
2022-10-13  腾讯技术工程    Tags:虚拟内存   点击:(7)  评论:(0)  加入收藏
语言的优劣之争从来都是个永恒的话题,也是个容易引火上身的问题,经常讨论过激就会“擦枪走火”甚至可能会引发一场铁杆粉丝之间的“战争”。如果您之前熟悉VBA,或了解一些VB语...【详细内容】
2022-10-13  小辣椒高效Office  今日头条  Tags:Python   点击:(12)  评论:(0)  加入收藏
事件背景作者所在的公司核心业务是做政府信息化软件的,就是为政府部门开发信息化系统。其中有一款信息化软件是客户每天需要使用的,并且他们面向的客户就是老百姓。某年某月,某...【详细内容】
2022-10-13  Java联盟盟主  今日头条  Tags:P0级事故   点击:(9)  评论:(0)  加入收藏
Hello,大家好,我是每天深情赚钱的程序员,姜老师~~~在最近一次的直播中,有同学问道:“如何阻止开发同学使用 TiDB ?"相信看了直播的小伙伴都有印象。这其实是一个很好的问题!从这个...【详细内容】
2022-10-12  破产码农    Tags:TiDB   点击:(9)  评论:(0)  加入收藏
站内最新
站内热门
站内头条