当前位置: 首页 > 科技观察

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

时间:2023-03-15 15:51:11 科技观察

我们在后面的内容中不考虑前端的处理,因为不能完全依赖前端,前端和前端都需要自己做处理。例如根据RestfulAPI接口规范:CRUD分为get(查询)、post(新建)、delete(删除)、put(修改)GET:在查询条件下,无论用户查询数据库多少次,不会对数据库中的数据造成损坏,所以这本质上是一个幂等接口一个非幂等接口PUT:绝对修改有两种情况:如果是修改绝对值,比如修改一条名为张三的记录,我修改了很多次,最后的结果都是一样的(只有张三的结果被删除了),所以这是一个幂等接口的相对修改:如果修改一个相对值,比如修改一个表中得分最高的记录(selecttop1scorefromxxx),我修改了很多次,最终结果不同。你发送几次接口,我就删除最高的几次,所以这是一个非幂等接口DELETE:也分两种情况(和PUT一样,就不介绍了,也是相对和绝对的issue)所以为了安全,后端会用很多方法来解决幂等问题,将非幂等接口转化为幂等接口。2.并发性:用户发送请求的时间是不规律的,可能是一个一个依次执行,也可能是在短时间内发送多个请求抢占同一个资源。由于请求的处理是异步的,所以不能保证每一个都会按顺序输出。并发也可以细分为两种类型。多个用户抢占同一个资源:例如:100个人在短时间内预约了同一个医生,但是这个医生只能预约一次,此时会出现高并发。我们必须采取措施保证只有第一个发起请求的人才能预约到这个医生,接下来的99个都会返回预约失败。(不是返回请求的错误),这时候一个阻塞(多个请求排队等待处理)互斥锁(同时只有一个请求可以获取锁,其他请求排队)并等待处理解锁后获取),保证这100个请求按顺序转换为同步(虽然效率会降低,但保证正确性)。单个用户抢占自己的同一个资源:这里,单个用户的并发一般体现在重复请求,但并不完全相同的参数,比如一个用户发起两次不同参数的请求修改自己的个人数据短时间内(比如实际情况还是比较少的,因为前端会采取屏蔽层等措施来防止用户的这种行为),但是请求处理是异步的,可能会突然受到网络的影响因素因为,虽然发送的顺序是先1后2,但是返回的顺序是先2后1,所以正确性有问题。这时候可以设置非阻塞(只对第一个请求加锁然后处理,后面所有请求报错,同样返回服务器忙,不排队等待处理,直接失败)mutex提醒用户一个请求已经在处理中,不要发送多个请求3.高并发:高并发是一种并发程度,反映的是在很短的时间内产生大量的并发请求,意味着高并发,比如双11抢购,于是就有了分布式架构(分布式系统就是把一个业务拆分成多个子服务,分布在不同的服务器节点上,一起组成的系统叫分布式系统),一台服务器处理海量并发压力会很大甚至宕机,所以分布在不同的服务器节点,减轻单台服务器的压力4.进程锁<线程锁<分布式罗ck:进程锁:当一个方法或代码块使用锁时,最多有一个线程同时在执行代码(nodejs同步代码(异步代码除外)是单进程的,所以不需要进程锁)线程锁:为了控制同一个操作系统中的多个进程访问共享资源,只是因为程序的独立性,每个进程无法控制其他进程对资源的访问,但是可以通过本地系统的信号量来控制分布式锁:当多个进程不在同一个系统时,使用分布式锁来控制多个进程对资源的访问(可以理解为线程锁是只有一个实例的分布式锁)。三把锁的范围:进程锁<线程锁<分布式锁和三把锁效果一样,只是作用范围不同。实践链接:了解了这么多理论知识,下面来实践一个nodejs中分布式锁的中间件包,解决接口幂等问题。为什么要使用分布式锁:nodejs已经有现成的redlock(以redlock分布式锁算法命名)封装来解决分布式锁的问题,所以不需要自己写redlock算法,只需要重新封装它作为一个中间件,具体的redisdistribution类型锁的实现,可以看别人的文章2.分布式锁范围最大,单例和分布式都可以使用。这里我实现的是单例,自己的小项目不需要分布式系统npm官网找到了ioredis和redlock这两个包。总之ioredis是一个比较强大的redis包)配置ioredis和redlockioredis:思路:创建一个class类,将redis的所有操作和初始化封装到Redis类中,最后实例化并导出以供其他地方使用,host}=REDIS_CONFclassRedis{clientconstructor(){this.client=newioredis({port,host,password})this.client.on('error',(err)=>console.log(err))}//添加数据asyncset(key:string,value:any,time?:number|string){//判断value是否为对象类型if(typeofvalue==='object'){value=JSON.stringify(value)}//time为过期时间,可选if(time){awaitthis.client.set(key,value,'EX',time)}else{awaitthis.client.set(key,value)}}asyncget(key:string){constdata=awaitthis.client.get(key)返回数据}asyncdelete(key:string){awaitthis.client.del(key)}}constredis=newRedis()exportdefaultredis注意事项:1.Redis必须先在你的电脑上安装并配置好服务才能使用才可以使用。具体redis的安装、配置、服务激活可以自行百度实现。2.如果要设置redis密码,必须先设置redis密码配置密码后才能使用redis(如何自行百度redis配置密码),否则如果直接在里面使用连接nodejs,会报auth错误3.redis6.0.不支持0以下的用户名,只需要设置密码即可。如果真的要配置用户名就百度一下,不过我觉得一台机器一个redis就够了,用户名有点多余。redlock:importRedlockfrom'redlock'importredisfrom'./redis'constredlock=newRedlock([redis.client],{retryCount:0})exportdefaultredlock注意:当newRedlock实例作为第一个参数传入时,传入一个数组,里面的每一项都是ioredis的实例,如果像我一样不需要分发,传入一个实例即可,后面是传入的配置具体查看其文档,这里retryCount表示的是获取锁失败时的重试次数,按照官方的解释,这里的retryCount设置为0就够了,如封装分布式锁中间件官方解释import{Middleware}from'koa'import{Lock}from'redlock'importredlockfrom'../db/redlock'import{error}from'../utils/Response'//这里isByUser为真,用户id+请求地址作为key加锁,即:此接口不允许用户同时更改同一资源(diferentparametersarenotallowed)//isByUser是默认的如果是false,所有的参数+用户id+地址作为key来加锁,即:这个接口不允许一个用户用同一个资源改变同一个资源同时参数(拦截重复请求)constidempotent=(isByUser:boolean=false)=>{constRedlock:Middleware=async(ctx,next)=>{letid:string//这里的ctx.user是我之前配置的中间件,用来解析用户携带的token的参数,识别用户,获取用户参数,里面存放的是用户的个人信息//有些接口是不需要认证的,所以ctx.user。id会报错,id会输出为空字符串/*为什么要解析出id而不是直接拿token呢?因为一个用户可以有多个token,但是一个用户只有一个idToken作为标识,同一个用户不同的token也会被锁定成功,形成一个用户多次获得锁的情况。但是由于id的独立性,不同的id代表不同的用户*/try{id=ctx.user.id}catch(error){id=''}letlock:Lock|null=nulltr??y{if(isByUser){//locklock=awaitredlock.acquire([`${id}:${ctx.URL}`],10000)}else{constbody=JSON.stringify(ctx.request.body)console.log(`${id}:${ctx.URL}:${body}`)lock=awaitredlock.acquire([`${id}:${ctx.URL}:${body}`],10000)}}catch(err){//如果抛出错误,说明锁失败,说明有Repeatrequestisinoperation//这里的error()函数就是函数I封装返回错误,其中调用了ctx.throw,所以错误会立即返回,下一个next不会继续error(ctx,500,'请求进行中,请勿重复提交')}awaitnext()//后面所有的中间件都执行完后才能释放锁awaitlock!.release()}returnRedlock}exportdefaultidempotentuselink(testacceptance)设置测试路由:在路由前添加我们设计的中间件幂等加工。如果不传入参数isByUser,则默认为false,即所有参数都相同则进行拦截。路由处理没什么,等两秒就成功输出一句话了。一个线程两次发送同一个请求(等待第一次处理完成再发送第二次)第一次:第二次:可以看到两次都没有效果,延迟2s后,多个线程成功返回这里发送一次(并发)同一个请求多个api接口管理工具短时间内轮流使用流式发送(处理一个请求需要2s,所以2s内再发送一个)模拟并发第一个请求:第二个请求:两张图片你很难看清真实情况,但我看得出来,第一个请求成功返回两秒后,第二次请求在短时间内直接返回错误(无法获取锁,说明有重复请求在进行中)。这里我只给大家展示一下在没有参数和token的情况下是成功的。我也测试了isByUser的有效性和是否有token,但是没有放行,但是没有问题。isByUser是我觉得比较常用的两种情况:所有参数的判断和用户id+接口地址的方式,如果你有其他想法,也可以自定义参数传入你要加锁的key。在这里你可以重新封装它。我个人觉得isByUser就够了。一个简单的koa分布式锁中间件就够了。打包注意事项:redlock算法并不是绝对安全的。如果过期时间设置的太短(小于接口处理时间),接口在处理完之前会自动释放锁,其他线程也可以获取到锁,就会丢失。安全性(Java中redisson中watchdog的自动更新可以解决这个问题,但是这里是nodejs,目前没有封装watchdog机制的分布式锁包,有能力的也可以自己封装,我没能力够了,过期时间设置的长一点比较好,但是太长了会有其他的坏处)这里的redlock是非阻塞的。上面说了,如果获取不到锁,会自动报错,请求直接失败,而不是Queue等待解锁再执行。如果需要阻塞,可以自己封装,但是我推荐另外一个包:async-lock,这是一种阻塞处理方式,可以形成一个异步队列,按顺序执行,而不是直接抛出,不会阻塞错误