@zenweb/cache 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +85 -0
- package/dist/cache.d.ts +74 -0
- package/dist/cache.js +209 -0
- package/dist/global.d.ts +8 -0
- package/dist/global.js +16 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +59 -0
- package/dist/locker.d.ts +30 -0
- package/dist/locker.js +62 -0
- package/dist/middleware.d.ts +8 -0
- package/dist/middleware.js +27 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +41 -0
- package/package.json +46 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ZenWeb Cache Module
|
|
2
|
+
|
|
3
|
+
基于 Redis 的对象缓存工具
|
|
4
|
+
|
|
5
|
+
功能如下:
|
|
6
|
+
- 对象缓存
|
|
7
|
+
- 大对象自动压缩
|
|
8
|
+
- JSON 序列直接输出
|
|
9
|
+
- 防止缓存击穿
|
|
10
|
+
- 单例执行
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
yarn add @zenweb/cache
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts title="src/index.ts"
|
|
19
|
+
import { create } from 'zenweb';
|
|
20
|
+
import modCache from '@zenweb/cache';
|
|
21
|
+
|
|
22
|
+
const app = create();
|
|
23
|
+
|
|
24
|
+
app.setup(modCache());
|
|
25
|
+
|
|
26
|
+
app.start();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 使用
|
|
30
|
+
```ts
|
|
31
|
+
import { Context, mapping } from "zenweb";
|
|
32
|
+
import { $cache, cached } from "@zenweb/cache";
|
|
33
|
+
|
|
34
|
+
export class CacheController {
|
|
35
|
+
/**
|
|
36
|
+
* 一般使用
|
|
37
|
+
*/
|
|
38
|
+
@mapping()
|
|
39
|
+
async index() {
|
|
40
|
+
const result = await $cache.lockGet('TEST', function() {
|
|
41
|
+
return {
|
|
42
|
+
cacheAt: new Date(),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 大对象压缩存储
|
|
50
|
+
*/
|
|
51
|
+
@mapping()
|
|
52
|
+
async big() {
|
|
53
|
+
const result = await $cache.lockGet('TEST-GZ', function() {
|
|
54
|
+
return longData;
|
|
55
|
+
});
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 大对象直接输出 - 1
|
|
61
|
+
*/
|
|
62
|
+
@mapping()
|
|
63
|
+
async big_direct_out(ctx: Context) {
|
|
64
|
+
const result = await $cache.lockGet('TEST-GZ', function() {
|
|
65
|
+
return longData;
|
|
66
|
+
}, { parse: false, decompress: false });
|
|
67
|
+
if (result.compressed) {
|
|
68
|
+
ctx.set('Content-Encoding', 'gzip');
|
|
69
|
+
}
|
|
70
|
+
ctx.type = 'json';
|
|
71
|
+
ctx.body = result.data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 使用缓存中间件
|
|
76
|
+
* - 自动处理是否需要解压缩对象
|
|
77
|
+
*/
|
|
78
|
+
@mapping({
|
|
79
|
+
middleware: cached('TEST-middleware'),
|
|
80
|
+
})
|
|
81
|
+
async cached_middleware() {
|
|
82
|
+
return longData;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Redis, RedisValue } from 'ioredis';
|
|
3
|
+
import { SetupOption, SetOption, GetOption, LockGetOption, SetResult, LockOption } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* 缓存结果
|
|
6
|
+
*/
|
|
7
|
+
export declare class CacheResult {
|
|
8
|
+
compressed: boolean;
|
|
9
|
+
data: Buffer;
|
|
10
|
+
/**
|
|
11
|
+
* @param compressed 是否为压缩数据
|
|
12
|
+
* @param data 数据
|
|
13
|
+
*/
|
|
14
|
+
constructor(compressed: boolean, data: Buffer);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 对象缓存系统
|
|
18
|
+
*/
|
|
19
|
+
export declare class Cache {
|
|
20
|
+
private redis;
|
|
21
|
+
private option?;
|
|
22
|
+
constructor(redis: Redis, option?: SetupOption | undefined);
|
|
23
|
+
/**
|
|
24
|
+
* 删除缓存
|
|
25
|
+
*/
|
|
26
|
+
del(key: string): Promise<number>;
|
|
27
|
+
/**
|
|
28
|
+
* 直接设置缓存 - 不经过 JSON 序列化的数据
|
|
29
|
+
* @param key
|
|
30
|
+
* @param value
|
|
31
|
+
* @param ttl 缓存有效时长 (秒),不设置取默认设置
|
|
32
|
+
*/
|
|
33
|
+
setRaw(key: string, value: RedisValue, ttl?: number): Promise<"OK">;
|
|
34
|
+
/**
|
|
35
|
+
* 缓存对象
|
|
36
|
+
* @param key 缓存key
|
|
37
|
+
* @param value 缓存值,对象会经过 JSON 序列化
|
|
38
|
+
* @param ttlopt 缓存有效时长 (秒),不设置取默认设置 | 缓存选项
|
|
39
|
+
*/
|
|
40
|
+
set(key: string, value: object | number | string | Buffer, ttlopt?: number | SetOption): Promise<SetResult>;
|
|
41
|
+
/**
|
|
42
|
+
* 直接取得缓存
|
|
43
|
+
*/
|
|
44
|
+
getRaw(key: string): Promise<Buffer | null>;
|
|
45
|
+
/**
|
|
46
|
+
* 取得缓存对象
|
|
47
|
+
* @param key 缓存key
|
|
48
|
+
*/
|
|
49
|
+
get<T>(key: string, opt: {
|
|
50
|
+
parse: false;
|
|
51
|
+
} & GetOption): Promise<CacheResult | undefined>;
|
|
52
|
+
get<T = unknown>(key: string, opt?: {
|
|
53
|
+
parse?: true;
|
|
54
|
+
} & GetOption): Promise<T | undefined>;
|
|
55
|
+
/**
|
|
56
|
+
* 取得缓存值,如果缓存不存在则取得锁并调用 fetch 设置缓存值
|
|
57
|
+
* - 防止缓存击穿
|
|
58
|
+
* - 保障只有一个业务会设置缓存,其他业务会等待设置完成并返回设置结果
|
|
59
|
+
* @param key 缓存KEY
|
|
60
|
+
* @param fetch 缓存设置方法回调
|
|
61
|
+
*/
|
|
62
|
+
lockGet<T extends {} | string>(key: string, fetch: () => Promise<T> | T, _opt: {
|
|
63
|
+
parse: false;
|
|
64
|
+
} & LockGetOption): Promise<CacheResult>;
|
|
65
|
+
lockGet<T extends {} | string>(key: string, fetch: () => Promise<T> | T, _opt?: {
|
|
66
|
+
parse?: true;
|
|
67
|
+
} & LockGetOption): Promise<T>;
|
|
68
|
+
/**
|
|
69
|
+
* 单例执行,在并发情况下保证同一个 key 只会单独执行,防止同时处理
|
|
70
|
+
* @param key 锁key
|
|
71
|
+
* @param run 获得锁时调用
|
|
72
|
+
*/
|
|
73
|
+
singleRunner<T>(key: string, run: () => Promise<T> | T, _opt?: LockOption): Promise<T>;
|
|
74
|
+
}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Cache = exports.CacheResult = void 0;
|
|
4
|
+
const zlib_1 = require("zlib");
|
|
5
|
+
const util_1 = require("util");
|
|
6
|
+
const locker_1 = require("./locker");
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
const compress = (0, util_1.promisify)(zlib_1.gzip);
|
|
9
|
+
const decompress = (0, util_1.promisify)(zlib_1.unzip);
|
|
10
|
+
const GZ_HEADER = Buffer.from([0x1F, 0x8B, 0x08]);
|
|
11
|
+
const _getDebug = utils_1.debug.extend('get');
|
|
12
|
+
const _setDebug = utils_1.debug.extend('set');
|
|
13
|
+
const _lockDebug = utils_1.debug.extend('lock');
|
|
14
|
+
/**
|
|
15
|
+
* 缓存结果
|
|
16
|
+
*/
|
|
17
|
+
class CacheResult {
|
|
18
|
+
/**
|
|
19
|
+
* @param compressed 是否为压缩数据
|
|
20
|
+
* @param data 数据
|
|
21
|
+
*/
|
|
22
|
+
constructor(compressed, data) {
|
|
23
|
+
this.compressed = compressed;
|
|
24
|
+
this.data = data;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.CacheResult = CacheResult;
|
|
28
|
+
/**
|
|
29
|
+
* 对象缓存系统
|
|
30
|
+
*/
|
|
31
|
+
class Cache {
|
|
32
|
+
constructor(redis, option) {
|
|
33
|
+
this.redis = redis;
|
|
34
|
+
this.option = option;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 删除缓存
|
|
38
|
+
*/
|
|
39
|
+
del(key) {
|
|
40
|
+
return this.redis.del(key);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 直接设置缓存 - 不经过 JSON 序列化的数据
|
|
44
|
+
* @param key
|
|
45
|
+
* @param value
|
|
46
|
+
* @param ttl 缓存有效时长 (秒),不设置取默认设置
|
|
47
|
+
*/
|
|
48
|
+
setRaw(key, value, ttl) {
|
|
49
|
+
if (ttl && ttl > 0) {
|
|
50
|
+
return this.redis.set(key, value, 'EX', ttl);
|
|
51
|
+
}
|
|
52
|
+
return this.redis.set(key, value);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 缓存对象
|
|
56
|
+
* @param key 缓存key
|
|
57
|
+
* @param value 缓存值,对象会经过 JSON 序列化
|
|
58
|
+
* @param ttlopt 缓存有效时长 (秒),不设置取默认设置 | 缓存选项
|
|
59
|
+
*/
|
|
60
|
+
async set(key, value, ttlopt) {
|
|
61
|
+
var _a;
|
|
62
|
+
let compressed;
|
|
63
|
+
let data = Buffer.isBuffer(value) ? value : typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
64
|
+
const opt = Object.assign({
|
|
65
|
+
ttl: 60,
|
|
66
|
+
compressMinLength: 1024,
|
|
67
|
+
compressStoreRatio: 0.95,
|
|
68
|
+
compressLevel: 1,
|
|
69
|
+
}, (_a = this.option) === null || _a === void 0 ? void 0 : _a.set, typeof ttlopt === 'object' ? ttlopt : undefined);
|
|
70
|
+
if (typeof ttlopt === 'number') {
|
|
71
|
+
opt.ttl = ttlopt;
|
|
72
|
+
}
|
|
73
|
+
_setDebug('[%s] data length:', key, data.length);
|
|
74
|
+
if (opt.compressMinLength && data.length >= opt.compressMinLength) {
|
|
75
|
+
let _compressed = await compress(data, { level: opt.compressLevel });
|
|
76
|
+
_setDebug('[%s] compressed length: %d', key, _compressed.length);
|
|
77
|
+
if (_compressed.length < (data.length * (opt.compressStoreRatio || 0.95))) {
|
|
78
|
+
compressed = _compressed;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await this.setRaw(key, compressed || data, opt.ttl);
|
|
82
|
+
return {
|
|
83
|
+
data,
|
|
84
|
+
compressed,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 直接取得缓存
|
|
89
|
+
*/
|
|
90
|
+
getRaw(key) {
|
|
91
|
+
return this.redis.getBuffer(key);
|
|
92
|
+
}
|
|
93
|
+
async get(key, opt) {
|
|
94
|
+
let data = await this.getRaw(key);
|
|
95
|
+
let _opt = Object.assign({ parse: true, decompress: true }, opt);
|
|
96
|
+
if (data) {
|
|
97
|
+
// 解压处理
|
|
98
|
+
const compressed = GZ_HEADER.equals(data.subarray(0, GZ_HEADER.length));
|
|
99
|
+
if (compressed) {
|
|
100
|
+
_getDebug('[%s] is compressed', key);
|
|
101
|
+
if (_opt.parse || _opt.decompress) {
|
|
102
|
+
_getDebug('[%s] decompress', key);
|
|
103
|
+
data = await decompress(data);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 解析处理
|
|
107
|
+
if (_opt.parse) {
|
|
108
|
+
_getDebug('[%s] parse', key);
|
|
109
|
+
return JSON.parse(data.toString());
|
|
110
|
+
}
|
|
111
|
+
return new CacheResult(compressed, data);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async lockGet(key, fetch, _opt) {
|
|
115
|
+
var _a, _b;
|
|
116
|
+
const opt = Object.assign({}, (_a = this.option) === null || _a === void 0 ? void 0 : _a.set, (_b = this.option) === null || _b === void 0 ? void 0 : _b.lockGet, _opt);
|
|
117
|
+
// 获取数据并设置到缓存中
|
|
118
|
+
const fetchAndSet = async () => {
|
|
119
|
+
const obj = await fetch();
|
|
120
|
+
const result = await this.set(key, obj, opt);
|
|
121
|
+
if ((_opt === null || _opt === void 0 ? void 0 : _opt.parse) !== false) {
|
|
122
|
+
return obj;
|
|
123
|
+
}
|
|
124
|
+
return new CacheResult(!!result.compressed, result.compressed || Buffer.from(result.data));
|
|
125
|
+
};
|
|
126
|
+
// 预刷新处理
|
|
127
|
+
const preRefresh = async () => {
|
|
128
|
+
if (opt.preRefresh && opt.preRefresh > 0) {
|
|
129
|
+
const ttl = await this.redis.ttl(key);
|
|
130
|
+
_lockDebug('[%s] preRefresh remain: %d', key, ttl);
|
|
131
|
+
if (ttl < opt.preRefresh) {
|
|
132
|
+
_lockDebug('[%s] preRefresh -> fetchAndSet', key);
|
|
133
|
+
await fetchAndSet();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
// 使用锁设置缓存
|
|
138
|
+
let locker;
|
|
139
|
+
let refresh = opt.refresh || false; // 强制刷新
|
|
140
|
+
let retryCount = typeof opt.retryCount === 'number' ? opt.retryCount : 100;
|
|
141
|
+
while (true) {
|
|
142
|
+
if (!refresh) {
|
|
143
|
+
// 尝试从 redis 中获取
|
|
144
|
+
const data = await this.get(key, opt);
|
|
145
|
+
if (data !== undefined) {
|
|
146
|
+
if (!locker) {
|
|
147
|
+
preRefresh();
|
|
148
|
+
}
|
|
149
|
+
return data;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 已经跳过 redis 获取可以结束强制刷新,进入锁获取,锁获取不成功则代表其他刷新任务已经进行,只要等待获取即可
|
|
153
|
+
refresh = false;
|
|
154
|
+
// 获取锁
|
|
155
|
+
if (!locker) {
|
|
156
|
+
locker = new locker_1.Locker(this.redis, `${key}.LOCK`, opt.lockTimeout);
|
|
157
|
+
}
|
|
158
|
+
if (await locker.acquire()) {
|
|
159
|
+
_lockDebug('[%s] acquire success', key);
|
|
160
|
+
try {
|
|
161
|
+
return await fetchAndSet();
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await locker.release();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// 获取锁失败,有其他请求更新数据,等待后重新获取获取数据
|
|
169
|
+
if (retryCount <= 0) {
|
|
170
|
+
throw new Error('Unable to acquire lock: ' + key);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
retryCount--;
|
|
174
|
+
await (0, utils_1.sleep)(opt.retryDelay || 300);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 单例执行,在并发情况下保证同一个 key 只会单独执行,防止同时处理
|
|
181
|
+
* @param key 锁key
|
|
182
|
+
* @param run 获得锁时调用
|
|
183
|
+
*/
|
|
184
|
+
async singleRunner(key, run, _opt) {
|
|
185
|
+
const opt = Object.assign({}, _opt);
|
|
186
|
+
const locker = new locker_1.Locker(this.redis, key, opt.lockTimeout);
|
|
187
|
+
let retryCount = opt.retryCount || 0;
|
|
188
|
+
while (true) {
|
|
189
|
+
// 取得锁并执行
|
|
190
|
+
if (await locker.acquire()) {
|
|
191
|
+
try {
|
|
192
|
+
return await run();
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
await locker.release();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// 无法取得锁是否重复尝试
|
|
199
|
+
if (retryCount <= 0) {
|
|
200
|
+
throw new Error('Unable to acquire lock');
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
retryCount--;
|
|
204
|
+
await (0, utils_1.sleep)(opt.retryDelay || 300);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
exports.Cache = Cache;
|
package/dist/global.d.ts
ADDED
package/dist/global.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.$cache = exports.getCache = void 0;
|
|
4
|
+
const core_1 = require("@zenweb/core");
|
|
5
|
+
/**
|
|
6
|
+
* 取得缓存实例
|
|
7
|
+
*/
|
|
8
|
+
function getCache() {
|
|
9
|
+
const ins = (0, core_1.getCore)();
|
|
10
|
+
return ins.cache;
|
|
11
|
+
}
|
|
12
|
+
exports.getCache = getCache;
|
|
13
|
+
/**
|
|
14
|
+
* 快捷方法 - 缓存实例
|
|
15
|
+
*/
|
|
16
|
+
exports.$cache = (0, core_1.callProxy)(getCache);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SetupFunction } from '@zenweb/core';
|
|
2
|
+
import { SetupOption } from './types';
|
|
3
|
+
import { Cache } from './cache';
|
|
4
|
+
import { Redis } from 'ioredis';
|
|
5
|
+
export { getCache, $cache } from './global';
|
|
6
|
+
export * from './types';
|
|
7
|
+
export * from './utils';
|
|
8
|
+
export { Locker } from './locker';
|
|
9
|
+
export { cached } from './middleware';
|
|
10
|
+
export { Cache };
|
|
11
|
+
/**
|
|
12
|
+
* 安装模块
|
|
13
|
+
* @param option 配置
|
|
14
|
+
*/
|
|
15
|
+
export default function setup(option?: SetupOption): SetupFunction;
|
|
16
|
+
declare module '@zenweb/core' {
|
|
17
|
+
interface Core {
|
|
18
|
+
/**
|
|
19
|
+
* Redis 实例
|
|
20
|
+
*/
|
|
21
|
+
redis: Redis;
|
|
22
|
+
/**
|
|
23
|
+
* 缓存实例
|
|
24
|
+
*/
|
|
25
|
+
cache: Cache;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.Cache = exports.cached = exports.Locker = exports.$cache = exports.getCache = void 0;
|
|
18
|
+
const cache_1 = require("./cache");
|
|
19
|
+
Object.defineProperty(exports, "Cache", { enumerable: true, get: function () { return cache_1.Cache; } });
|
|
20
|
+
const ioredis_1 = require("ioredis");
|
|
21
|
+
var global_1 = require("./global");
|
|
22
|
+
Object.defineProperty(exports, "getCache", { enumerable: true, get: function () { return global_1.getCache; } });
|
|
23
|
+
Object.defineProperty(exports, "$cache", { enumerable: true, get: function () { return global_1.$cache; } });
|
|
24
|
+
__exportStar(require("./types"), exports);
|
|
25
|
+
__exportStar(require("./utils"), exports);
|
|
26
|
+
var locker_1 = require("./locker");
|
|
27
|
+
Object.defineProperty(exports, "Locker", { enumerable: true, get: function () { return locker_1.Locker; } });
|
|
28
|
+
var middleware_1 = require("./middleware");
|
|
29
|
+
Object.defineProperty(exports, "cached", { enumerable: true, get: function () { return middleware_1.cached; } });
|
|
30
|
+
/**
|
|
31
|
+
* 默认选项
|
|
32
|
+
*/
|
|
33
|
+
const defaultOption = {
|
|
34
|
+
redis: {
|
|
35
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
36
|
+
port: parseInt(process.env.REDIS_PORT || '') || 6379,
|
|
37
|
+
password: process.env.REDIS_PASSWORD || '',
|
|
38
|
+
db: parseInt(process.env.REDIS_DB || '') || 0,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 安装模块
|
|
43
|
+
* @param option 配置
|
|
44
|
+
*/
|
|
45
|
+
function setup(option) {
|
|
46
|
+
return async function cache(setup) {
|
|
47
|
+
const opt = Object.assign({}, defaultOption, option);
|
|
48
|
+
setup.debug('option: %o', opt);
|
|
49
|
+
const redis = opt.redis instanceof ioredis_1.Redis ? opt.redis : new ioredis_1.Redis(opt.redis);
|
|
50
|
+
await redis.ping('check');
|
|
51
|
+
setup.defineCoreProperty('redis', { value: redis });
|
|
52
|
+
setup.defineCoreProperty('cache', { value: new cache_1.Cache(redis, opt) });
|
|
53
|
+
setup.destroy(async () => {
|
|
54
|
+
setup.debug('quit redis...');
|
|
55
|
+
await redis.quit();
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
exports.default = setup;
|
package/dist/locker.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Redis, RedisKey } from 'ioredis';
|
|
2
|
+
/**
|
|
3
|
+
* 基于 Redis 的分布式锁
|
|
4
|
+
*/
|
|
5
|
+
export declare class Locker {
|
|
6
|
+
private redis;
|
|
7
|
+
private key;
|
|
8
|
+
private timeout;
|
|
9
|
+
private value;
|
|
10
|
+
/**
|
|
11
|
+
* @param redis Redis 实例
|
|
12
|
+
* @param key 锁KEY
|
|
13
|
+
* @param timeout 锁超时(毫秒) 默认 10秒
|
|
14
|
+
*/
|
|
15
|
+
constructor(redis: Redis, key: RedisKey, timeout?: number);
|
|
16
|
+
/**
|
|
17
|
+
* 取得锁
|
|
18
|
+
* @returns true 取得成功, 否则失败
|
|
19
|
+
*/
|
|
20
|
+
acquire(): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* 释放锁
|
|
23
|
+
*/
|
|
24
|
+
release(): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* 使用锁回调
|
|
27
|
+
* @param callback 获取结果回调 true: 成功 false: 失败
|
|
28
|
+
*/
|
|
29
|
+
using(callback: (ok: boolean) => any): Promise<void>;
|
|
30
|
+
}
|
package/dist/locker.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Locker = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
const RELEASE_SCRIPT = `
|
|
7
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
8
|
+
return redis.call("del", KEYS[1])
|
|
9
|
+
else
|
|
10
|
+
return 0
|
|
11
|
+
end
|
|
12
|
+
`;
|
|
13
|
+
const RELEASE_SCRIPT_HASH = (0, crypto_1.createHash)('sha1').update(RELEASE_SCRIPT).digest('hex');
|
|
14
|
+
/**
|
|
15
|
+
* 基于 Redis 的分布式锁
|
|
16
|
+
*/
|
|
17
|
+
class Locker {
|
|
18
|
+
/**
|
|
19
|
+
* @param redis Redis 实例
|
|
20
|
+
* @param key 锁KEY
|
|
21
|
+
* @param timeout 锁超时(毫秒) 默认 10秒
|
|
22
|
+
*/
|
|
23
|
+
constructor(redis, key, timeout = 10000) {
|
|
24
|
+
this.redis = redis;
|
|
25
|
+
this.key = key;
|
|
26
|
+
this.timeout = timeout;
|
|
27
|
+
this.value = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 取得锁
|
|
31
|
+
* @returns true 取得成功, 否则失败
|
|
32
|
+
*/
|
|
33
|
+
async acquire() {
|
|
34
|
+
// NX 不存在key 才创建
|
|
35
|
+
// PX 过期时间
|
|
36
|
+
const result = await this.redis.call('SET', this.key, this.value, 'NX', 'PX', this.timeout);
|
|
37
|
+
return result === 'OK';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 释放锁
|
|
41
|
+
*/
|
|
42
|
+
async release() {
|
|
43
|
+
return (await (0, utils_1.runRedisScript)(this.redis, RELEASE_SCRIPT_HASH, RELEASE_SCRIPT, [this.key], [this.value])) === 1;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 使用锁回调
|
|
47
|
+
* @param callback 获取结果回调 true: 成功 false: 失败
|
|
48
|
+
*/
|
|
49
|
+
async using(callback) {
|
|
50
|
+
if (await this.acquire()) {
|
|
51
|
+
try {
|
|
52
|
+
await callback(true);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await this.release();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
callback(false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.Locker = Locker;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Context, Middleware } from "@zenweb/core";
|
|
2
|
+
import { LockGetOption } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* 缓存中间件
|
|
5
|
+
* - 会根据客户端请求头判断是否需要解压
|
|
6
|
+
* @param key 缓存 key 如果不指定则默认使用 ctx.path
|
|
7
|
+
*/
|
|
8
|
+
export declare function cached(key?: string | ((ctx: Context) => string | Promise<string>), opt?: Omit<LockGetOption, 'parse' | 'decompress'>): Middleware;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cached = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 缓存中间件
|
|
6
|
+
* - 会根据客户端请求头判断是否需要解压
|
|
7
|
+
* @param key 缓存 key 如果不指定则默认使用 ctx.path
|
|
8
|
+
*/
|
|
9
|
+
function cached(key, opt) {
|
|
10
|
+
return async function cachedMiddleware(ctx, next) {
|
|
11
|
+
const _key = key ? typeof key === 'string' ? key : await key(ctx) : `CACHED-PATH:${ctx.path}`;
|
|
12
|
+
const decompress = ctx.acceptsEncodings('gzip') === false;
|
|
13
|
+
const _opt = Object.assign({}, opt, {
|
|
14
|
+
parse: false,
|
|
15
|
+
decompress,
|
|
16
|
+
});
|
|
17
|
+
const result = await ctx.core.cache.lockGet(_key, async function () {
|
|
18
|
+
await next();
|
|
19
|
+
return ctx.body;
|
|
20
|
+
}, _opt);
|
|
21
|
+
if (!decompress && result.compressed) {
|
|
22
|
+
ctx.set('Content-Encoding', 'gzip');
|
|
23
|
+
}
|
|
24
|
+
ctx.body = result.data;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
exports.cached = cached;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Redis, RedisOptions } from "ioredis";
|
|
3
|
+
/**
|
|
4
|
+
* 缓存选项
|
|
5
|
+
*/
|
|
6
|
+
export interface SetOption {
|
|
7
|
+
/**
|
|
8
|
+
* 缓存有效时长 (秒)
|
|
9
|
+
* - `0` 永久有效
|
|
10
|
+
* @default 60
|
|
11
|
+
*/
|
|
12
|
+
ttl?: number;
|
|
13
|
+
/**
|
|
14
|
+
* 压缩内容最少长度(字节)
|
|
15
|
+
* - `0` 不压缩
|
|
16
|
+
* - 是否压缩判断:
|
|
17
|
+
* - 原始内容长度大于配置值
|
|
18
|
+
* - 压缩完成后的长度小于配置值
|
|
19
|
+
* @default 1024
|
|
20
|
+
*/
|
|
21
|
+
compressMinLength?: number;
|
|
22
|
+
/**
|
|
23
|
+
* 压缩存储比率
|
|
24
|
+
* - 压缩完成后的长度对比压缩之前的长度所缩减的比率
|
|
25
|
+
* - 压缩率不满足要求则不存储压缩数据
|
|
26
|
+
* - 相对于低比率的压缩数据再解压节省的空间十分有限又浪费计算资源
|
|
27
|
+
* - 默认 0.95, 至少压缩率达到 95% 才有价值
|
|
28
|
+
* @default 0.95
|
|
29
|
+
*/
|
|
30
|
+
compressStoreRatio?: number;
|
|
31
|
+
/**
|
|
32
|
+
* 压缩级别
|
|
33
|
+
* @default 1
|
|
34
|
+
*/
|
|
35
|
+
compressLevel?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface SetupOption {
|
|
38
|
+
/**
|
|
39
|
+
* Redis 实例或 Redis 选项
|
|
40
|
+
*/
|
|
41
|
+
redis: Redis | RedisOptions;
|
|
42
|
+
/**
|
|
43
|
+
* 默认缓存设置选项
|
|
44
|
+
*/
|
|
45
|
+
set?: SetOption;
|
|
46
|
+
/**
|
|
47
|
+
* 默认锁缓存选项
|
|
48
|
+
*/
|
|
49
|
+
lockGet?: LockGetOption;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 取得缓存选项
|
|
53
|
+
*/
|
|
54
|
+
export interface GetOption {
|
|
55
|
+
/**
|
|
56
|
+
* 是否解析 JSON 数据
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
parse?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* 如果数据被压缩,是否需要解压
|
|
62
|
+
* - `parse: false` 时有效
|
|
63
|
+
* @default true
|
|
64
|
+
*/
|
|
65
|
+
decompress?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 设置缓存结果
|
|
69
|
+
*/
|
|
70
|
+
export interface SetResult {
|
|
71
|
+
/**
|
|
72
|
+
* 未经过压缩的数据
|
|
73
|
+
*/
|
|
74
|
+
data: string | Buffer;
|
|
75
|
+
/**
|
|
76
|
+
* 经过压缩的数据
|
|
77
|
+
*/
|
|
78
|
+
compressed?: Buffer;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 锁选项
|
|
82
|
+
*/
|
|
83
|
+
export interface LockOption {
|
|
84
|
+
/**
|
|
85
|
+
* 锁超时 (毫秒)
|
|
86
|
+
* - 取得锁后多久没有释放则自动释放
|
|
87
|
+
* @default 10000
|
|
88
|
+
*/
|
|
89
|
+
lockTimeout?: number;
|
|
90
|
+
/**
|
|
91
|
+
* 锁获取失败重试次数
|
|
92
|
+
* @default 100
|
|
93
|
+
*/
|
|
94
|
+
retryCount?: number;
|
|
95
|
+
/**
|
|
96
|
+
* 锁获取失败重试间隔时间 (毫秒)
|
|
97
|
+
* @default 300
|
|
98
|
+
*/
|
|
99
|
+
retryDelay?: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 取得或使用锁设置新缓存
|
|
103
|
+
*/
|
|
104
|
+
export interface LockGetOption extends GetOption, SetOption, LockOption {
|
|
105
|
+
/**
|
|
106
|
+
* 预刷新(秒)
|
|
107
|
+
* - `0` 无效
|
|
108
|
+
* - 缓存剩余时间小于值时主动刷新,不影响当前数据获取
|
|
109
|
+
* @default 0
|
|
110
|
+
*/
|
|
111
|
+
preRefresh?: number;
|
|
112
|
+
/**
|
|
113
|
+
* 刷新数据
|
|
114
|
+
* - 不从 Redis 获取数据,直接调用 fetch 设置新的数据
|
|
115
|
+
* @default false
|
|
116
|
+
*/
|
|
117
|
+
refresh?: boolean;
|
|
118
|
+
}
|
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/// <reference types="debug" />
|
|
2
|
+
import { Redis, RedisKey, RedisValue } from 'ioredis';
|
|
3
|
+
export declare const debug: import("debug").Debugger;
|
|
4
|
+
/**
|
|
5
|
+
* Promise 等待
|
|
6
|
+
* @param ms 毫秒
|
|
7
|
+
* @returns
|
|
8
|
+
*/
|
|
9
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* 执行 Redis Lua 脚本
|
|
12
|
+
* @param redis Redis 实例
|
|
13
|
+
* @param hash 脚本 sha1 hash
|
|
14
|
+
* @param script 脚本代码
|
|
15
|
+
* @param keys 输入键
|
|
16
|
+
* @param values 输入值
|
|
17
|
+
* @returns 执行结果
|
|
18
|
+
*/
|
|
19
|
+
export declare function runRedisScript(redis: Redis, hash: string, script: string, keys: RedisKey[], values?: RedisValue[]): Promise<unknown>;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runRedisScript = exports.sleep = exports.debug = void 0;
|
|
4
|
+
const debug_1 = require("debug");
|
|
5
|
+
exports.debug = (0, debug_1.default)('zenweb:cache');
|
|
6
|
+
/**
|
|
7
|
+
* Promise 等待
|
|
8
|
+
* @param ms 毫秒
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
setTimeout(resolve, ms);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
exports.sleep = sleep;
|
|
17
|
+
/**
|
|
18
|
+
* 执行 Redis Lua 脚本
|
|
19
|
+
* @param redis Redis 实例
|
|
20
|
+
* @param hash 脚本 sha1 hash
|
|
21
|
+
* @param script 脚本代码
|
|
22
|
+
* @param keys 输入键
|
|
23
|
+
* @param values 输入值
|
|
24
|
+
* @returns 执行结果
|
|
25
|
+
*/
|
|
26
|
+
async function runRedisScript(redis, hash, script, keys, values = []) {
|
|
27
|
+
try {
|
|
28
|
+
const result = await redis.evalsha(hash, keys.length, ...keys, ...values);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err instanceof Error && err.message.includes('NOSCRIPT')) {
|
|
33
|
+
(0, exports.debug)('runRedisScript: NOSCRIPT');
|
|
34
|
+
// 缺少预设脚本,设置
|
|
35
|
+
const result = await redis.eval(script, keys.length, ...keys, ...values);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.runRedisScript = runRedisScript;
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zenweb/cache",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Zenweb Cache module",
|
|
5
|
+
"exports": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "rimraf dist && tsc",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"test": "ts-mocha -p tsconfig.json test/**/*.test.ts",
|
|
14
|
+
"dev": "cd example && cross-env DEBUG=* NODE_ENV=development ts-node app"
|
|
15
|
+
},
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "YeFei",
|
|
18
|
+
"email": "316606233@qq.com"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"web",
|
|
22
|
+
"app",
|
|
23
|
+
"http",
|
|
24
|
+
"framework",
|
|
25
|
+
"koa",
|
|
26
|
+
"cache",
|
|
27
|
+
"redis",
|
|
28
|
+
"locker"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"homepage": "https://zenweb.node.ltd/docs/modules/cache",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/mocha": "^10.0.1",
|
|
34
|
+
"cross-env": "^7.0.3",
|
|
35
|
+
"mocha": "^10.2.0",
|
|
36
|
+
"rimraf": "^4.3.1",
|
|
37
|
+
"ts-mocha": "^10.0.0",
|
|
38
|
+
"ts-node": "^10.9.1",
|
|
39
|
+
"typescript": "^5.1.6",
|
|
40
|
+
"zenweb": "^4.0.1"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"debug": "^4.3.4",
|
|
44
|
+
"ioredis": "^5.3.2"
|
|
45
|
+
}
|
|
46
|
+
}
|