nodejs 防宕机_关于node模拟"同步锁"的方案畅想,解决防止缓存击穿

背景

在使用vue做一个项目的时候,有些需要keep-alive的内容,这些数据请求一次就不会再变,而且大部分用户的数据都是一样的,所以这块加个缓存再好不过了。

问题-缓存击穿

部署好redis,非常欢快的加上了node redis的插件,然后包装一下,跑通了,happy得不得了。但随即而来的问题是这样:

在服务刚起来的时候,或者数据过期的时候,需要重新请求数据库然后再缓存。

这个时候有10个用户同时发起同样的请求(参数完全一致的请求为同样的请求),会同时去redis中拿数据(因为redis中还没有数据)。

10个同样的请求都没有从缓存里面拿到数据,最终这10个请求都去后台数据库请求了,然后一遍一遍的又写到缓存里面去了。

这就发生了缓存击穿问题,严重的资源浪费!

解决思路

既然有10个同样的请求,那么其实只让第一个请求去数据库拿数据,然后其余9个请求只需要等待第一个请求回来就好了,然后10个请求一起拿着第一个请求回来的数据返回到vue。这样10个请求在服务器端只发生了一次http请求(数据库在另一台机器),数据库只处理了一个查询,减少资源浪费又减轻了数据库压力。

解决方案

后端使用的node+koa2,众所周知node是单线程,对于这种问题,在多线程语言中解决起来及其方便,node的问题就在于如何让其余9个问题处于挂起等待状态,使其等待第一个请求回来。

既然是要挂起等待,那肯定是要异步了,那要异步肯定要Promise + async/await了。不得不说koa对于异步流程的处理真的很棒。

那只有让着9个请求进入异步模式就能解决这个问题了。想来想去还是借助了node Events模块。一种订阅/发布模式的高级实现。events对事件的封装非常完美,在node内部也大量使用了events模块。

这样使用Events的once 与 emit,与Promise配合起来,基本上就解决问题了。

coding

因为使用了koa2,对于异步的处理机器方便。

首先,需要一个key,这个key可以代表一个请求连接,相同的请求那么key也是一个了。

这个key会在events.once中使用。先写一个events的公共方法:

import EventEmitter from 'events';

const emitter = new EventEmitter();

/**

* 获取等待的数据

*

* @export

* @param {String} key

* @returns {any}

*/

export async function awaitData(key) {

//返回一个Promise,外层已被async包装

return new Promise(resolve => {

//因为 emitter 注册监听器默认的最大限制是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}

*/

export async function queryData(key, params) {

// 这里是个关键,起到占位的用途,后面的请求会通过emitter.eventNames()去判断前面有没有请求去数据库了。也可以使用其他方式实现这个步骤

emitter.once(key, () => { });

return new Promise(resolve => {

//这里为去后台数据库请求的操作,这块使用setTimeout模拟异步操作

setTimeout(() => {

const data = 'just a test.';

//eimt 触发事件,将data传递给其他监听这个key的函数

myEE.emit(key, data);

//返回给第一个请求

resolve(data);

}, 3000); // 为了效果明显可以时间再长点

});

}

/**

* 查询当前事件是否被监听,如果被监听说明有请求去数据库了,我也继续监听等待第一个回来

*

* @export

* @param {any} key

* @returns {boolean}

*/

export function hasEvent(key){

//查询所有事件监听器中有没有这个key

return emitter.eventNames().includes(key);

}

//koa2

app.use(async (ctx, next) => {

//这里不写路由了,直接path判断模拟下路由

if (ctx.path === '/getData') {

//使用md5去生产key,md5怎么来的就不写了

const key = md5(ctx.path + JSON.stringify(ctx.query));

//判断当前key有没有被监听

if (event.hasEvent(key)) {

//监听事件 等待被触发,这里使用异步与事件结合,使当前请求处于pendding挂起状态

return ctx.body = await event.awaitData(key);

} else {

//这里作为第一个请求,去数据库拿数据,然后触发其他等待的事件。

return ctx.body = await event.queryData(key);

}

}else{

return next();

}

})

这样基本上完成了10个请求,一个发出,九个等待的要求。但这种方式也有个缺点,这个方式只能在单节点生效,在有负载均衡的多节点中,这个方法是不行的,多节点之间也会有稍微的资源浪费。

以上,致那颗骚动的心……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值