NodeJs - 实现当前线程唯一的单例对象
一. 实现当前线程唯一的单例对象
Java
里面,一般都把这种和当前线程绑定的单例对象存储到ThreadLocal
里面,但是Node
里面没有这种存储,那咋办呢?直接上代码:
const cluster = require('cluster');
class Context {
constructor() {
this.id = Math.random().toString(36).substring(2);
}
static create() {
if (cluster.isWorker) {
if (!cluster.worker.hasOwnProperty('_context')) {
cluster.worker._context = new Context();
}
return cluster.worker._context;
} else {
if (!global.hasOwnProperty('_context')) {
global._context = new Context();
}
return global._context;
}
}
static release(){
if (cluster.isWorker) {
if (cluster.worker.hasOwnProperty('_context')) {
delete cluster.worker._context;
}
} else {
if (global.hasOwnProperty('_context')) {
delete global._context;
}
}
}
}
module.exports = Context;
在create
方法中:
- 我们首先检查当前进程/线程是否是一个工作进程。
- 如果是工作进程,我们使用
cluster.worker
对象来存储和获取Context
对象。 - 每个工作进程都有自己的上下文,因此在每个工作进程中创建的
Context
对象都是唯一的。
如果当前进程/线程不是一个工作进程,我们使用global
对象来存储和获取Context
对象。在这种情况下,Context
对象在整个进程/线程中都是唯一的。
测试代码:
const Context = require('./context');
(async () => {
// 循环10次,每次创建一个新的Context实例,异步创建
const promiseList = []
for (let i = 0; i < 5; i++) {
const context = Context.create();
console.log('sync', context.id);
promiseList.push(test());
}
await Promise.all(promiseList);
// 清除这个对象,让下一次请求能够重新再赋值一个新的Context
Context.release();
})();
async function test() {
setTimeout(() => {
const context = Context.create();
console.log('async', context.id);
}, 2000);
}
结果如下:
但是这种方式并不准确,因为对于Node
来说,这里的上下文的释放和创建都是我们手动控制的,而从Node
的实现角度,这种方式可能会导致上下文窜了。
可以看下NodeJs - 单线程模型和高并发处理原理,而我们这里就是把全局对象绑定在工作进程上的。(后来我生产上实际验证发现,上下文都窜了)
方式一仅供参考了解,后面再来看下更加合理的一种实现方案,AsyncLocalStorage
的使用,大部分开源框架实际上使用的都是它来实现上下文的传递,包括Egg
框架也是如此。
二. AsyncLocalStorage 的运用
1.我们安装依赖:
npm i async_hooks
2.我们创建一个全局的storage
,并通过模块化导出,创建一个storage.ts
文件。
const { AsyncLocalStorage } = require('async_hooks');
const storage = new AsyncLocalStorage();
export default storage;
3.其他地方,引入storage.ts
中的对象:
const http = require('http');
const Context = require('./context');
const storage = require('./storage');
http.createServer((req, res) => {
const context = new Context()
console.log(`入口创建Context并塞入上下文`, context)
storage.run(context, () => {
const ctx = storage.getStore();
console.log(`上下文一`, ctx)
// Imagine any chain of async operations here
setImmediate(() => {
const ctx = storage.getStore();
console.log(`上下文二`, ctx)
res.end();
});
});
}).listen(8080);
for (let i = 0; i < 2; i++) {
http.get('http://localhost:8080');
}
结果如下:一共两次请求,每次请求都是共享一个上下文对象。
基本上使用方式就是这样:
storage.run(obj, fn)
:obj
就是代表你要存储的上下文对象,fn就是你后续的整个业务处理函数。就是把obj
塞到你的这整个fn
执行的生命周期里面。storage.getStore()
:那么你的业务逻辑代码,就可以通过getStore
来获取之前塞好的上下文对象了。