@playding/redis-cacher
v1.2.0
Published
A simple interface cacher based on ioredis
Readme
interface-cacher
A simple cacher based on ioredis.
usage
npm i @playding/redis-cacherconst Cacher = require('@playding/redis-cacher');
const cacher = new Cacher();
const data = await cacher.get({
key: 'ding',
executor: async () => {
// logic
return 'dingding;
},
});JSDoc
Table of Contents
constructor
Parameters
payloadObjectpayload.redisObject? 用于redis的连接payload.redisClientIORedis.AbstractConnector? client like cluster or sentinel redispayload.prefixstring key的默认前缀 (optional, defaultcache.)payload.expirenumber key的有效期,单位s (optional, default5)payload.mem(object | boolean)? 内存缓存配置,传false表示不启用。全部参数可以看这个文档说明payload.serializerObject? 自定义序列化器。不传时默认使用 JSON。
get
使用redis为接口加缓存
Parameters
payloadObjectpayload.keystring 要查找的keypayload.executorfunction 如果未击中,要执行的方法payload.expirenumber 失效时间, 单位spayload.rawboolean 是否不用 decode/encode 数据 (optional, defaultfalse)payload.serializerObject? 自定义序列化器,优先级高于构造器 serializer。 raw 为 true 时会忽略 serializer。payload.memboolean 是否对当前 key 启用内存缓存,默认不启用 (optional, defaultfalse)
Examples
说明:以给getShops接口加缓存为例
要点:executor为一个返回bluebird 的promise
getShops接口如下:
const getShops = (type) => {
if (type === 0) {
return Promise.reject(new Error('bad params'));
}
return Promise.resolve(['shop01', 'shop02']);
};
使用方式:
const Cacher = require('interface-cacher');
const cacher = new Cacher();
const payload = {
key: 'getShops',
executor: getShops.bind(null, 1),
expire: 100
};
cacher.get(payload)
.then((data) => {
// process the data
})
.catch((err) => {
// handle the exception when encounter with error
});
const data = await cacher.get({
key: 'getShopes'
executor: getShops.bind(null, 1),
// 启用内存缓存,如果内存命中自己返回内存结果
// 如果内存没有,就会获取 redis 结果,解析后放到内存中
mem: true
}Returns Promise<Object> 缓存中数据(击中) 或executor返回数据(未击中)
delete
删除指定缓存
Parameters
keystring 要删除key
Returns Promise<number> n 删除的key的数量, 同ioredis.del
serializer
默认情况下,缓存值仍然通过 JSON.stringify() 写入 Redis,并在命中时通过 JSON.parse() 还原。可以在构造器或单次 get() 中传入 serializer 来改用 protobuf、MessagePack、CBOR、Node v8.serialize 等格式:
const serializer = {
binary: true,
serialize: (value) => encode(value),
deserialize: (cachedValue) => decode(cachedValue),
};
const cacher = new Cacher({ serializer });
const data = await cacher.get({
key: 'ding',
executor: async () => ({ name: 'ding' }),
});payload.serializer 会覆盖构造器上的默认 serializer。raw: true 优先级最高;同时传 raw 和 serializer 时会忽略 serializer,保持原来的字符串读写行为。
当 serializer.binary === true 时,Redis 命中读取会调用 redisClient.getBuffer(key),serialize() 可以返回 Buffer、Uint8Array 或字符串,其中 Uint8Array 会在写入前转成 Buffer。如果传入自定义 redisClient,它必须支持 getBuffer();旧版 ioredis 不应启用会禁用 buffer 方法的 dropBufferSupport。
本库不内置安装 protobuf、MessagePack、CBOR 等 codec,调用方按业务需要自行选择依赖。仓库提供了可选兼容示例,运行时同样需要本机 Redis 127.0.0.1:6379、DB 12:
npx -p ava -p protobufjs -p @msgpack/msgpack -p cbor-x ava examples/serializers/*.test.jsserializer examples
protobufjs
const protobuf = require('protobufjs');
const CacheValue = new protobuf.Type('CacheValue')
.add(new protobuf.Field('name', 1, 'string'))
.add(new protobuf.Field('count', 2, 'uint32'));
const serializer = {
binary: true,
serialize: (value) => CacheValue.encode(CacheValue.create(value)).finish(),
deserialize: (value) => CacheValue.toObject(CacheValue.decode(value)),
};MessagePack
const { decode, encode } = require('@msgpack/msgpack');
const serializer = {
binary: true,
serialize: encode,
deserialize: decode,
};CBOR
const { Decoder, Encoder } = require('cbor-x');
const encoder = new Encoder();
const decoder = new Decoder();
const serializer = {
binary: true,
serialize: (value) => encoder.encode(value),
deserialize: (value) => decoder.decode(value),
};Node v8
const v8 = require('v8');
const serializer = {
binary: true,
serialize: v8.serialize,
deserialize: v8.deserialize,
};serializer benchmark
仓库提供了一个独立 benchmark,用同一批对象对比 JSON、protobufjs、MessagePack、CBOR 和 Node v8.serialize 的性能。它会输出两组结果:纯 codec 的 serialize/deserialize 性能,以及预写 Redis 后真实 cache-hit 读路径的 GET + parse 或 GETBUFFER + deserialize 性能。
npx -p protobufjs -p @msgpack/msgpack -p cbor-x node --expose-gc benchmarks/serializers.js默认会生成 small、medium、large 三个确定性结构化接口响应对象:分页元信息、filters、owner、summary,以及大量 repeated catalog items。每个 item 含 seller、dimensions、tags、attributes、variants 等嵌套字段。对象大小通过 item 数量自然放大,不使用 padding 字符串凑体积;运行输出会打印实际 JSON bytes。Redis benchmark 使用本机 127.0.0.1:6379、DB 12,key 前缀为 BENCH_SERIALIZER_,只删除 benchmark 自己写入的 key。
结果会受 Node 版本、CPU、codec 包版本和本机 Redis 状态影响。可以用 --warmup-ms=... --min-ms=... 覆盖默认的 100ms warmup 和 500ms 最小测量时间,例如:
npx -p protobufjs -p @msgpack/msgpack -p cbor-x node --expose-gc benchmarks/serializers.js --warmup-ms=10 --min-ms=50一次本机完整 benchmark 结果如下,环境为 Node v24.14.0、darwin arm64、Redis 127.0.0.1:6379 DB 12,codec 包版本为 protobufjs 8.4.2、MessagePack 3.1.3、CBOR 1.6.4、Node v8 13.6.233.17-node.41。对象 JSON bytes 为 small 1,515、medium 102,251、large 1,048,573。下面的图都以 ops/sec 为指标,越长越快;百分比是相对 JSON 的变化。
Cache-hit read path
真实 Redis 命中路径里,小对象仍然主要受 Redis round-trip 影响。结构化大对象上,payload bytes 和反序列化成本都会影响结果;这次环境里 CBOR 的 Redis 命中路径最快。
small, 1,515 bytes JSON
| codec | ops/sec | vs JSON | chart | | --- | ---: | ---: | --- | | MessagePack | 3,238 | +24.9% | ██████████████████████████████ | | Node v8 | 3,105 | +19.7% | █████████████████████████████ | | CBOR | 2,968 | +14.5% | ███████████████████████████ | | JSON | 2,593 | baseline | ████████████████████████ | | protobufjs | 2,233 | -13.9% | █████████████████████ |
medium, 102,251 bytes JSON
| codec | ops/sec | vs JSON | chart | | --- | ---: | ---: | --- | | CBOR | 925 | +38.3% | ██████████████████████████████ | | protobufjs | 788 | +17.8% | ██████████████████████████ | | Node v8 | 688 | +2.8% | ██████████████████████ | | JSON | 669 | baseline | ██████████████████████ | | MessagePack | 635 | -5.1% | █████████████████████ |
large, 1,048,573 bytes JSON
| codec | ops/sec | vs JSON | chart | | --- | ---: | ---: | --- | | CBOR | 134 | +48.9% | ██████████████████████████████ | | protobufjs | 107 | +18.9% | ████████████████████████ | | JSON | 90 | baseline | ████████████████████ | | Node v8 | 88 | -2.2% | ████████████████████ | | MessagePack | 76 | -15.6% | █████████████████ |
Codec-only deserialize
这组去掉 Redis,只看 CPU 反序列化。结构化 repeated object 与旧的大字符串 fixture 不同,binary codec 没有获得数量级优势;这次环境里 CBOR 在 medium 和 large 上最快。
| size | fastest | JSON ops/sec | fastest ops/sec | fastest vs JSON | | --- | --- | ---: | ---: | ---: | | small | JSON | 115,116 | 115,116 | baseline | | medium | CBOR | 1,604 | 2,038 | +27.1% | | large | CBOR | 156 | 206 | +32.1% |
large deserialize detail
| codec | ops/sec | vs JSON | chart | | --- | ---: | ---: | --- | | CBOR | 206 | +32.1% | ██████████████████████████████ | | JSON | 156 | baseline | ███████████████████████ | | protobufjs | 148 | -5.1% | ██████████████████████ | | Node v8 | 125 | -19.9% | ██████████████████ | | MessagePack | 117 | -25.0% | █████████████████ |
Codec-only serialize
写入 miss 路径时,这批结构化对象上 JSON stringify 仍然最快。binary codec 在 payload size 上有优势,但 serialize CPU 成本不一定更低。
| size | fastest | JSON ops/sec | fastest ops/sec | fastest vs JSON | | --- | --- | ---: | ---: | ---: | | small | JSON | 396,380 | 396,380 | baseline | | medium | JSON | 6,184 | 6,184 | baseline | | large | JSON | 568 | 568 | baseline |
large serialize detail
| codec | ops/sec | vs JSON | chart | | --- | ---: | ---: | --- | | JSON | 568 | baseline | ██████████████████████████████ | | Node v8 | 402 | -29.2% | █████████████████████ | | CBOR | 265 | -53.3% | ██████████████ | | protobufjs | 264 | -53.5% | ██████████████ | | MessagePack | 242 | -57.4% | █████████████ |
Payload size
这批对象由 repeated nested records 构成,各 binary codec 都比 JSON 小。CBOR 和 protobufjs 在 medium/large 上最省空间。
| size | JSON bytes | protobufjs bytes | MessagePack bytes | CBOR bytes | Node v8 bytes | | --- | ---: | ---: | ---: | ---: | ---: | | small | 1,515 | 638 | 1,178 | 1,167 | 1,369 | | medium | 102,251 | 42,606 | 78,861 | 41,651 | 92,764 | | large | 1,048,573 | 441,797 | 808,512 | 424,481 | 951,438 |
codec-only
| size | codec | operation | ops/sec | avg ms | serialized bytes | | --- | --- | --- | ---: | ---: | ---: | | small | JSON | serialize | 396,380 | 0.0025 | 1,515 | | small | JSON | deserialize | 115,116 | 0.0087 | 1,515 | | small | protobufjs | serialize | 179,113 | 0.0056 | 638 | | small | protobufjs | deserialize | 109,977 | 0.0091 | 638 | | small | MessagePack | serialize | 166,188 | 0.0060 | 1,178 | | small | MessagePack | deserialize | 82,965 | 0.0121 | 1,178 | | small | CBOR | serialize | 129,918 | 0.0077 | 1,167 | | small | CBOR | deserialize | 94,206 | 0.0106 | 1,167 | | small | Node v8 | serialize | 226,589 | 0.0044 | 1,369 | | small | Node v8 | deserialize | 87,961 | 0.0114 | 1,369 | | medium | JSON | serialize | 6,184 | 0.1617 | 102,251 | | medium | JSON | deserialize | 1,604 | 0.6234 | 102,251 | | medium | protobufjs | serialize | 2,856 | 0.3502 | 42,606 | | medium | protobufjs | deserialize | 1,527 | 0.6547 | 42,606 | | medium | MessagePack | serialize | 2,093 | 0.4778 | 78,861 | | medium | MessagePack | deserialize | 1,201 | 0.8325 | 78,861 | | medium | CBOR | serialize | 2,873 | 0.3481 | 41,651 | | medium | CBOR | deserialize | 2,038 | 0.4907 | 41,651 | | medium | Node v8 | serialize | 4,798 | 0.2084 | 92,764 | | medium | Node v8 | deserialize | 1,352 | 0.7395 | 92,764 | | large | JSON | serialize | 568 | 1.7594 | 1,048,573 | | large | JSON | deserialize | 156 | 6.4154 | 1,048,573 | | large | protobufjs | serialize | 264 | 3.7889 | 441,797 | | large | protobufjs | deserialize | 148 | 6.7622 | 441,797 | | large | MessagePack | serialize | 242 | 4.1273 | 808,512 | | large | MessagePack | deserialize | 117 | 8.5624 | 808,512 | | large | CBOR | serialize | 265 | 3.7691 | 424,481 | | large | CBOR | deserialize | 206 | 4.8461 | 424,481 | | large | Node v8 | serialize | 402 | 2.4898 | 951,438 | | large | Node v8 | deserialize | 125 | 8.0087 | 951,438 |
redis-hit
| size | codec | operation | ops/sec | avg ms | serialized bytes | | --- | --- | --- | ---: | ---: | ---: | | small | JSON | GET + parse | 2,593 | 0.3857 | 1,515 | | small | protobufjs | GETBUFFER + deserialize | 2,233 | 0.4478 | 638 | | small | MessagePack | GETBUFFER + deserialize | 3,238 | 0.3089 | 1,178 | | small | CBOR | GETBUFFER + deserialize | 2,968 | 0.3369 | 1,167 | | small | Node v8 | GETBUFFER + deserialize | 3,105 | 0.3220 | 1,369 | | medium | JSON | GET + parse | 669 | 1.4958 | 102,251 | | medium | protobufjs | GETBUFFER + deserialize | 788 | 1.2695 | 42,606 | | medium | MessagePack | GETBUFFER + deserialize | 635 | 1.5755 | 78,861 | | medium | CBOR | GETBUFFER + deserialize | 925 | 1.0807 | 41,651 | | medium | Node v8 | GETBUFFER + deserialize | 688 | 1.4540 | 92,764 | | large | JSON | GET + parse | 90 | 11.1145 | 1,048,573 | | large | protobufjs | GETBUFFER + deserialize | 107 | 9.3855 | 441,797 | | large | MessagePack | GETBUFFER + deserialize | 76 | 13.1712 | 808,512 | | large | CBOR | GETBUFFER + deserialize | 134 | 7.4827 | 424,481 | | large | Node v8 | GETBUFFER + deserialize | 88 | 11.3284 | 951,438 |
changelogs
20220913 lru mem cache
const data = await cache.get({
key: 'ding',
executor: () => 'dingding',
// 启用内存缓存
mem: true,
});有些场景下,缓存数据是静态的。例如首页广告位,在运营配置后一般短时间不会改变,也不会随着入参变化。
在之前的版本中,数据从执行函数中生成后,通过 json stringify 变为 string 放到 redis 中。而后的其他服务实例可以通过固定的 key 从 redis 获取该 string,反过来通过 json parse 解析到实际数据如 object|array。
对于静态数据,此时反序列化成为了最耗时的操作,特别是对于大对象。通过内存二级缓存,减少 json parse,降低 cpu 时间,提速操作。
需要注意的是,该特性是通过增加内存资源消耗来实现,所以如果 mem.max 放的很高,或者 cache obj 很大,会带来比较明显的内存使用增加。
