当前位置: 首页 > 后端技术 > Node.js

关于节点模拟“同步锁”的方案的设想,解决防止缓存崩溃的问题

时间:2023-04-03 15:45:09 Node.js

背景使用vue做项目的时候,一些需要keep-alive的内容,这些数据请求一次不会再变,而且大部分其中user的数据是一样的,所以这块加个缓存会更好。问题——缓存击穿部署redis后,兴高采烈的加了noderedis插件,然后打包,跑通了,很开心。但是下面的问题是这样的:服务刚启动的时候,或者数据过期的时候,需要重新请求数据库,然后缓存。此时10个用户同时发起同一个请求(相同参数的请求为同一个请求),同时会去redis中获取数据(因为redis中没有数据).10个相同的请求没有从缓存中获取数据。最后10个请求全部到后台数据库去请求,然后一遍又一遍的写入缓存。这样就造成缓存崩溃问题,资源严重浪费!解决方案由于有10个相同的请求,其实只有第一个请求去数据库取数据,然后剩下的9个请求只需要等待第一个请求回来,然后这10个请求取第一个一并把请求返回的数据返回给vue。这样10次请求(数据库在另一台机器上)服务器端只发生一次http请求,数据库只处理一次查询,减少了资源的浪费,减轻了数据库的压力。方案后端采用的node+koa2,众所周知,node是单线程的。对于这种问题,用多线程语言来解决这个问题是很方便的。node的问题是如何让其他9个问题一直处于pending状态,从而等待第一个请求回来。既然要挂起等待,那肯定是异步的,必须是Promise+async/await才能异步。不得不说koa对异步进程的处理真的很棒。那么只有让9个请求进入异步模式才能解决这个问题。想来想去,还是用了node的Events模块。订阅/发布模式的高级实现。Events对事件进行了完美的封装,在node内部也大量使用了events模块。这样,使用Events的once和emit,结合Promise,基本解决了问题。因为编码使用的是koa2,方便异步处理机器。首先需要一个key,可以代表一个连接请求,同一个请求有一个key。这个键将在events.once中使用。先写一个事件的公共方法:importEventEmitterfrom'events';constemitter=newEventEmitter();/***获取等待数据**@export*@param{String}key*@returns{any}*/exportasyncfunctionawaitData(key){//返回一个Promise,外层已经被asyncreturn包裹newPromise(resolve=>{//因为发射器注册的监听器默认最大限制是10个,所以并发多的时候会出问题。数量需要动态调整emitter.setMaxListeners(emitter.getMaxListeners()+1);emitter.once(key,(data)=>{//返回数据resolve(data);//减去当前监听器的个数emitter.setMaxListeners(Math.max(emitter.getMaxListeners()-1,0));});});}/***第一个请求向后台发起查询请求*占用空间通知后续请求这件事我去做了,你等我回来就好**@export*@param{string}key*@param{any}params*@returns{any}*/exportasyncfunctionqueryData(key,params){//这里是key,作为占位符,后续请求会使用emitter。eventNames()判断之前是否有对数据库的请求。您也可以使用其他方法来实现这一步emitter.once(key,()=>{});returnnewPromise(resolve=>{//这里是去后台数据库请求的操作,这块使用setTimeout来模拟异步操作setTimeout(()=>{constdata='justatest.';//eimt触发事件并将数据传递给其他监听此键的函数myEE.emit(key,data);//返回到第一个请求resolve(data);},3000);//时间可以更长效果更明显});}/***查询当前事件是否被监听。如果被监听,说明有对数据库的请求。我会继续监听等待第一个Areturn**@export*@param{any}key*@returns{boolean}*/exportfunctionhasEvent(key){//查询所有事件监听器中是否有这个key返回emitter.eventNames().includes(key);}默认的监听数量限制,可以参考官方文档https://nodejs.org/api/events.html#events_eventemitter_defaultmaxlisteners基本上能用的都打包了,开始业务代码://koa2app.use(async(ctx,next)=>{//这里不写路由,直接判断路径模拟路由if(ctx.path==='/getData'){//使用md5产生key,md5来了就不要写constkey=md5(ctx.path+JSON.stringify(ctx.query));//判断当前key是否被监听if(event.hasEvent(key)){//监听等待触发的事件,这里使用异步和事件结合,使得当前请求处于pending状态returnctx.body=awaitevent.awaitData(key);}else{//这是第一次请求,去数据库获取数据,然后触发其他等待事件returnctx.body=awaitevent.queryData(key);}}else{返回next();}})这样10个请求基本完成,1个发出,9个等待。但是这种方法也有一个缺点。该方法只能对单个节点生效。在有负载均衡的多节点中,这种方法行不通,多节点之间会有轻微的资源浪费。以上,献给那颗不安分的心……