来源:程序员指北
概述
Node-Shared-Cache
是一个用于 Node.js
的进程间共享内存缓存模块,它提供了在多个 Node.js
进程间高效共享数据的能力,可通过 npm
安装。
Native Module 函数调用链路
对于这种与 V8
函数直接交互开发的原生模块我们可以称之为 Native Module
(原生模块) 或者 Native Addon
(原生扩展)
技术栈
核心技术
- C/C++ : 模块核心功能使用 C/C++ 实现
- Node.js 原生模块: 通过 Node.js 的 N-API 暴露底层功能
- NAN (Native Abstractions for Node.js) : 提供跨 Node.js 版本的兼容性抽象
- 内存管理: 使用共享内存实现进程间通信
- 序列化/反序列化: 自定义 BSON 格式用于高效数据存储
- 锁机制: 实现跨进程数据同步
这其中几个核心技术我会下文会详细讲解
NAN 框架
NAN (Native Abstractions for Node.js)
可以理解为 Node.js
原生模块开发框架,它的主要作用:
- 版本兼容性:
- 提供跨
Node.js
版本的API
抽象 - 处理不同
Node.js
版本的V8 API
变化 - 简化原生模块的开发和维护
- 主要功能:
- 提供统一的
V8 API
封装 - 处理异步操作
- 管理对象生命周期
- 提供类型转换工具
- 源码中典型 NAN 使用示例:JavaScript 属性获取器的实现
scss
static NAN_PROPERTY_GETTER(getter) {
// nan提供的API,用于处理属性获取,使用NAN宏生成属性作用域代码????
PROPERTY_SCOPE(property, info.Holder(), ptr, fd, keyLen, keyBuf);
// 使用BSON解析器
bson::BSONParser parser;
// 调用核心C++实现获取值
cache::get(ptr, fd, keyBuf, keyLen, parser.val, parser.valLen);
// 使用NAN抽象设置返回值
if(parser.val) {
info.GetReturnValue().Set(parser.parse());
}
}
- NAN 简化例子(抽象不同Node.js版本的V8 API)
scss
// 不使用 NAN 的版本 (针对特定 Node.js 版本)
void Add(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
double value = args[0]->NumberValue() + args[1]->NumberValue();
args.GetReturnValue().Set(v8::Number::New(isolate, value));
}
// 使用 NAN 的版本 (跨 Node.js 版本兼容)
NAN_METHOD(Add) {
double value = Nan::To<double>(info[0]).FromJust() +
Nan::To<double>(info[1]).FromJust();
info.GetReturnValue().Set(value);
}
// 绑定到 JS 的方式也不同
// 不使用 NAN:
exports->Set(v8::String::NewFromUtf8(isolate, "add"),
v8::FunctionTemplate::New(isolate, Add)->GetFunction());
// 使用 NAN:
Nan::Set(exports, Nan::New("add").ToLocalChecked(),
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(Add)).ToLocalChecked());
- 为什么选择
NAN
:
- 稳定性好,维护活跃
- 社区支持广泛
- 简化了原生模块开发
- 提供了跨版本兼容性(注意是编译时兼容,无需重新编译)
但是尽管如此,其实NAN的跨版本是有局限性的,这个项目我想应该是历史原因使用的NAN,每个Node.js版本都需要重新编译,维护成本较高,分发和部署复杂。其实Node官方和个人更推荐新项目中使用Node-API来开发原生Node模块。Node-API运行时兼容,无需多个版本分别编译,而且Node-API是Node项目中的一部分,不需安装模块。 Node-API无论是未来趋势还是兼容性以及维护成本都会更好些。
Node-API 官方地址:nodejs.org/api/n-api.h...
NAN
不是绑定层,而是一个抽象层,它简化了 Node.js
原生模块的开发,使得开发者可以专注于业务逻辑而不是处理不同 Node.js
版本的API差异。(Node-API也是抽象层)
- 绑定层与抽象层的区别
- 绑定层:通过一个绑定层以后,绑定层主要负责将
C++
函数导出JavaScript
(如NODE_MODULE) - 抽象层:一般使用
NAN
,提供统一API
来处理不同版本的V8
引擎差异(减少维护成本,避免针对不同Node.js
版本编写不同代码)
进程锁
进程锁是一种同步机制,用于进程锁是一种同步机制,用于协调多个进程对共享资源的访问,确保在任何时刻只有一个进程可以访问该资源。在node-shared-cache中,这个共享资源就是共享内存。
锁类型
- 互斥锁(Mutex):
- 最基本的锁类型
- 同一时间只允许一个进程访问资源
- 其他进程必须等待锁释放
- 读写锁(Read-Write Lock):
- 允许多个进程同时读取
- 但写入时需要独占访问
- 提高并发性能
- 自旋锁(Spin Lock):
- 进程在等待锁时持续检查锁状态
- 适用于短期等待场景
- 避免进程切换开销
为什么需要进程锁?
- 数据一致性问题:
- 没有进程锁,多个进程同时修改共享内存会导致数据不一致
- 例如:进程
A
读取值为10
,进程B
同时读取值为10
,A
和B
都加1
并写回,最终结果为11
而不是预期的12
- 竞态条件(Race Condition):
- 操作的顺序会影响最终结果
- 没有锁机制,操作顺序无法保证,导致不可预测的结果
- 原子性保证:
- 复杂操作(如读取-修改-写入)需要作为一个整体完成
- 锁确保这些操作不会被其他进程中断
举个例子:如果两个 Node.js
进程同时操作共享缓存:
css
进程A: 读取key="user1" -> 值为{visits:5}
进程B: 同时读取key="user1" -> 也得到{visits:5}
进程A: 增加visits并写回 -> {visits:6}
进程B: 也增加visits并写回 -> {visits:6} (覆盖了A的更新)
结果:用户访问计数丢失了一次增加。使用锁后:
css
进程A: 获取锁 -> 读取{visits:5} -> 写入{visits:6} -> 释放锁
进程B: 等待锁被释放 -> 获取锁 -> 读取{visits:6} -> 写入{visits:7} -> 释放锁
node-shared-cache 中进程锁实现:
node-shared-cache
中的进程锁实现:
- 提供了基础的互斥锁和高级的读写锁 基础互斥锁:
scss
typedef int32_t mutex_t;
// 原子操作宏定义
#define TSL(mutex) xchg(mutex, 1)
#define SPIN(mutex) while(TSL(mutex))
读写锁:
arduino
typedef struct {
mutex_t count_lock; // 用于保护读者计数
mutex_t mutex; // 主互斥锁
uint32_t readers; // 当前读者数量
} rw_lock_t;
- 针对不同平台(Linux/其他)有优化实现
- 使用
RAII
方式管理锁的生命周期 - 通过原子操作保证锁的正确性
模块支持平台
Linu
x` (使用 -lrt 库)MacOS
Windows
模块核心功能
1. 共享内存缓存
- 通过系统级共享内存实现进程间数据共享
- 使用
LRU
(最近最少使用) 算法管理缓存内容 - 自动内存管理,防止内存泄漏
2. 数据操作
- 属性的读取与写入 (
obj.key = value
,obj.key
) - 属性的删除 (
delete obj.key
) - 遍历缓存内容 (
for(var k in obj)
,Object.keys(obj)
) - 原子增加操作 (
increase(obj, key, value)
) - 原子交换操作 (
exchange(obj, key, newValue)
) - 快速读取,不影响 LRU 顺序 (
fastGet(obj, key)
)
3. 内存管理
- 支持定制内存块大小 (从
64
字节到16KB
) - 支持内存清理与释放
- 内存使用效率高,采用块式分配
技术实现
核心模块组成
- binding.cc: 实现 Node.js 和 C++ 的桥接层
-
- 创建
Cache
对象 - 处理
JS
对象属性的获取与设置 - 绑定原生方法到
JS
接口
- 创建
- memcache.cc/h: 实现内存缓存的核心功能
-
- 内存块分配与回收
- 哈希表数据结构
LRU
缓存算法- 锁同步机制
- bson.cc/h: 实现数据序列化与反序列化
-
- 支持基本类型:
null, undefined, true, false, int32, number
- 支持复杂类型:
string, array, object
- 处理循环引用 (circular reference)
- 支持基本类型:
- lock.h: 实现跨平台的锁机制
-
- Unix/Linux: 使用文件锁 (flock)
- Windows: 使用互斥锁 (Mutex)
内存结构
scss
+----------------+
| 哈希表 (65536) |
+----------------+
| 元数据信息 | - 魔数 (magic)
| | - 总块数 (blocks_total)
| | - 可用块数 (blocks_available)
| | - 脏标志 (dirty)
| | - 块大小 (block_size_shift)
+----------------+
| 数据块区域 | - 键值对存储
| | - LRU 链表
+----------------+
同步机制
为了防止多进程访问冲突,模块实现了两种锁:
- 读锁 (read_lock) : 多个进程可同时获得读锁
- 写锁 (write_lock) : 独占锁,确保只有一个进程可以修改数据
性能优化
- 哈希表: O(1) 的键查找性能
- LRU 算法: 高效管理内存使用
- 原子操作: 增加和交换支持原子性
- 快速读取: 提供不影响 LRU 顺序的快速读取方法
使用场景
- 多进程 Web 服务器: 共享会话数据、用户信息等
- 分布式计数器: 跨进程原子计数
- 分布式锁: 通过 exchange 方法实现
- 进程间通信: 用于进程间高效数据交换
- 高性能缓存: 替代 Redis 等外部缓存系统,减少网络开销
局限性
- 键长度限制: 依赖于块大小,默认情况下最大为 16 字符
- 内存大小限制: 由于设计限制,最大支持 128MB 共享内存
- 崩溃恢复: 需要谨慎处理进程崩溃情况下的锁释放
- 平台差异: 不同平台的实现细节存在差异
结论
Node-Shared-Cache
是一个高性能的进程间共享内存解决方案,通过 C++
实现的底层优化提供了优异的性能表现。其核心价值在于解决了 Node.js
多进程架构下的数据共享问题,特别适合需要高性能缓存和进程间通信的应用场景。
相比于使用外部缓存系统 (如 Redis
),它具有更低的延迟和更简单的部署优势,但也有内存大小和数据持久性等方面的局限性。
github地址:github.com/kyriosli/no...