koatty_store 1.7.0 → 1.8.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/.rollup.config.js +59 -59
- package/.vscode/launch.json +25 -0
- package/CHANGELOG.md +44 -27
- package/LICENSE +29 -29
- package/README.md +2 -2
- package/dist/LICENSE +29 -29
- package/dist/README.md +2 -2
- package/dist/index.d.ts +310 -94
- package/dist/index.js +765 -159
- package/dist/index.mjs +765 -159
- package/dist/package.json +105 -103
- package/package.json +105 -103
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* @Author: richen
|
|
3
|
-
* @Date:
|
|
3
|
+
* @Date: 2025-06-09 12:32:48
|
|
4
4
|
* @License: BSD (3-Clause)
|
|
5
5
|
* @Copyright (c) - <richenlin(at)gmail.com>
|
|
6
6
|
* @HomePage: https://koatty.org/
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
var lodash = require('lodash');
|
|
11
11
|
var helper = require('koatty_lib');
|
|
12
12
|
var events = require('events');
|
|
13
|
+
var lruCache = require('lru-cache');
|
|
13
14
|
var koatty_logger = require('koatty_logger');
|
|
14
15
|
var ioredis = require('ioredis');
|
|
15
16
|
var genericPool = require('generic-pool');
|
|
@@ -70,7 +71,7 @@ var messages;
|
|
|
70
71
|
messages["mutuallyExclusiveNXXX"] = "ERR XX and NX options at the same time are not compatible";
|
|
71
72
|
})(messages || (messages = {}));
|
|
72
73
|
class MemoryCache extends events.EventEmitter {
|
|
73
|
-
databases =
|
|
74
|
+
databases = new Map();
|
|
74
75
|
options;
|
|
75
76
|
currentDBIndex;
|
|
76
77
|
connected;
|
|
@@ -78,18 +79,89 @@ class MemoryCache extends events.EventEmitter {
|
|
|
78
79
|
multiMode;
|
|
79
80
|
cache;
|
|
80
81
|
responseMessages;
|
|
82
|
+
ttlCheckTimer = null;
|
|
81
83
|
/**
|
|
82
84
|
* Creates an instance of MemoryCache.
|
|
83
|
-
* @param {
|
|
85
|
+
* @param {MemoryCacheOptions} options
|
|
84
86
|
* @memberof MemoryCache
|
|
85
87
|
*/
|
|
86
88
|
constructor(options) {
|
|
87
89
|
super();
|
|
88
|
-
this.options = {
|
|
89
|
-
|
|
90
|
+
this.options = {
|
|
91
|
+
database: 0,
|
|
92
|
+
maxKeys: 1000,
|
|
93
|
+
evictionPolicy: 'lru',
|
|
94
|
+
ttlCheckInterval: 60000, // 1分钟检查一次过期键
|
|
95
|
+
maxAge: 1000 * 60 * 60, // 默认1小时过期
|
|
96
|
+
...options
|
|
97
|
+
};
|
|
98
|
+
this.currentDBIndex = options.database || 0;
|
|
90
99
|
this.connected = false;
|
|
91
100
|
this.lastSave = Date.now();
|
|
92
101
|
this.multiMode = false;
|
|
102
|
+
this.responseMessages = [];
|
|
103
|
+
// 初始化数据库和缓存
|
|
104
|
+
if (!this.databases.has(this.currentDBIndex)) {
|
|
105
|
+
this.databases.set(this.currentDBIndex, this.createLRUCache());
|
|
106
|
+
}
|
|
107
|
+
this.cache = this.databases.get(this.currentDBIndex);
|
|
108
|
+
// 启动TTL检查定时器
|
|
109
|
+
this.startTTLCheck();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 创建LRU缓存实例
|
|
113
|
+
*/
|
|
114
|
+
createLRUCache() {
|
|
115
|
+
return new lruCache.LRUCache({
|
|
116
|
+
max: this.options.maxKeys || 1000,
|
|
117
|
+
ttl: this.options.maxAge || 1000 * 60 * 60, // 1小时默认
|
|
118
|
+
updateAgeOnGet: true, // 访问时更新age
|
|
119
|
+
dispose: (value, key, reason) => {
|
|
120
|
+
// 键被淘汰时的回调 - 直接使用lru-cache的事件机制
|
|
121
|
+
this.emit('evict', key, value, reason);
|
|
122
|
+
},
|
|
123
|
+
onInsert: (value, key) => {
|
|
124
|
+
// 键被插入时的回调
|
|
125
|
+
this.emit('insert', key, value);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 启动TTL检查定时器
|
|
131
|
+
*/
|
|
132
|
+
startTTLCheck() {
|
|
133
|
+
if (this.ttlCheckTimer) {
|
|
134
|
+
clearInterval(this.ttlCheckTimer);
|
|
135
|
+
}
|
|
136
|
+
this.ttlCheckTimer = setInterval(() => {
|
|
137
|
+
this.cleanExpiredKeys();
|
|
138
|
+
}, this.options.ttlCheckInterval || 60000);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 清理过期键
|
|
142
|
+
*/
|
|
143
|
+
cleanExpiredKeys() {
|
|
144
|
+
for (const [_dbIndex, cache] of this.databases) {
|
|
145
|
+
const keysToDelete = [];
|
|
146
|
+
cache.forEach((item, key) => {
|
|
147
|
+
if (item.timeout && item.timeout <= Date.now()) {
|
|
148
|
+
keysToDelete.push(key);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
keysToDelete.forEach(key => {
|
|
152
|
+
cache.delete(key);
|
|
153
|
+
this.emit('expire', key);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 停止TTL检查
|
|
159
|
+
*/
|
|
160
|
+
stopTTLCheck() {
|
|
161
|
+
if (this.ttlCheckTimer) {
|
|
162
|
+
clearInterval(this.ttlCheckTimer);
|
|
163
|
+
this.ttlCheckTimer = null;
|
|
164
|
+
}
|
|
93
165
|
}
|
|
94
166
|
/**
|
|
95
167
|
*
|
|
@@ -98,8 +170,10 @@ class MemoryCache extends events.EventEmitter {
|
|
|
98
170
|
* @memberof MemoryCache
|
|
99
171
|
*/
|
|
100
172
|
createClient() {
|
|
101
|
-
this.databases
|
|
102
|
-
|
|
173
|
+
if (!this.databases.has(this.options.database)) {
|
|
174
|
+
this.databases.set(this.options.database, this.createLRUCache());
|
|
175
|
+
}
|
|
176
|
+
this.cache = this.databases.get(this.options.database);
|
|
103
177
|
this.connected = true;
|
|
104
178
|
// exit multi mode if we are in it
|
|
105
179
|
this.discard(null, true);
|
|
@@ -115,6 +189,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
115
189
|
*/
|
|
116
190
|
quit() {
|
|
117
191
|
this.connected = false;
|
|
192
|
+
this.stopTTLCheck();
|
|
118
193
|
// exit multi mode if we are in it
|
|
119
194
|
this.discard(null, true);
|
|
120
195
|
this.emit('end');
|
|
@@ -129,6 +204,40 @@ class MemoryCache extends events.EventEmitter {
|
|
|
129
204
|
end() {
|
|
130
205
|
return this.quit();
|
|
131
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* 获取缓存统计信息
|
|
209
|
+
*/
|
|
210
|
+
info() {
|
|
211
|
+
const stats = {
|
|
212
|
+
databases: this.databases.size,
|
|
213
|
+
currentDB: this.currentDBIndex,
|
|
214
|
+
keys: this.cache ? this.cache.length : 0,
|
|
215
|
+
maxKeys: this.options.maxKeys,
|
|
216
|
+
hits: 0,
|
|
217
|
+
misses: 0,
|
|
218
|
+
memory: this.getMemoryUsage()
|
|
219
|
+
};
|
|
220
|
+
// 如果缓存支持统计信息
|
|
221
|
+
if (this.cache && typeof this.cache.dump === 'function') {
|
|
222
|
+
const dump = this.cache.dump();
|
|
223
|
+
stats.keys = dump.length;
|
|
224
|
+
}
|
|
225
|
+
return stats;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 估算内存使用量
|
|
229
|
+
*/
|
|
230
|
+
getMemoryUsage() {
|
|
231
|
+
let totalSize = 0;
|
|
232
|
+
for (const [, cache] of this.databases) {
|
|
233
|
+
cache.forEach((item, key) => {
|
|
234
|
+
// 粗略估算:key长度 + JSON序列化后的大小
|
|
235
|
+
totalSize += key.length * 2; // Unicode字符占2字节
|
|
236
|
+
totalSize += JSON.stringify(item).length * 2;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return totalSize;
|
|
240
|
+
}
|
|
132
241
|
/**
|
|
133
242
|
*
|
|
134
243
|
*
|
|
@@ -175,12 +284,12 @@ class MemoryCache extends events.EventEmitter {
|
|
|
175
284
|
if (!helper__namespace.isNumber(dbIndex)) {
|
|
176
285
|
return this._handleCallback(callback, null, messages.invalidDBIndex);
|
|
177
286
|
}
|
|
178
|
-
if (!this.databases.
|
|
179
|
-
this.databases
|
|
287
|
+
if (!this.databases.has(dbIndex)) {
|
|
288
|
+
this.databases.set(dbIndex, this.createLRUCache());
|
|
180
289
|
}
|
|
181
290
|
this.multiMode = false;
|
|
182
291
|
this.currentDBIndex = dbIndex;
|
|
183
|
-
this.cache = this.databases
|
|
292
|
+
this.cache = this.databases.get(dbIndex);
|
|
184
293
|
return this._handleCallback(callback, messages.ok);
|
|
185
294
|
}
|
|
186
295
|
// ---------------------------------------
|
|
@@ -259,7 +368,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
259
368
|
else if (onlyexist) {
|
|
260
369
|
return this._handleCallback(callback, retVal);
|
|
261
370
|
}
|
|
262
|
-
this.cache
|
|
371
|
+
this.cache.set(key, this._makeKey(value.toString(), 'string', pttl));
|
|
263
372
|
return this._handleCallback(callback, messages.ok);
|
|
264
373
|
}
|
|
265
374
|
/**
|
|
@@ -289,7 +398,8 @@ class MemoryCache extends events.EventEmitter {
|
|
|
289
398
|
expire(key, seconds, callback) {
|
|
290
399
|
let retVal = 0;
|
|
291
400
|
if (this._hasKey(key)) {
|
|
292
|
-
|
|
401
|
+
const pttl = seconds * 1000;
|
|
402
|
+
this.cache.set(key, { ...this.cache.get(key), timeout: Date.now() + pttl });
|
|
293
403
|
retVal = 1;
|
|
294
404
|
}
|
|
295
405
|
return this._handleCallback(callback, retVal);
|
|
@@ -309,7 +419,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
309
419
|
for (let itr = 0; itr < keys.length; itr++) {
|
|
310
420
|
const key = keys[itr];
|
|
311
421
|
if (this._hasKey(key)) {
|
|
312
|
-
|
|
422
|
+
this.cache.delete(key);
|
|
313
423
|
retVal++;
|
|
314
424
|
}
|
|
315
425
|
}
|
|
@@ -400,7 +510,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
400
510
|
decrby(key, amount, callback) {
|
|
401
511
|
let retVal = null;
|
|
402
512
|
try {
|
|
403
|
-
retVal = this._addToKey(key, -amount);
|
|
513
|
+
retVal = this._addToKey(key, 0 - amount);
|
|
404
514
|
}
|
|
405
515
|
catch (err) {
|
|
406
516
|
return this._handleCallback(callback, null, err);
|
|
@@ -416,7 +526,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
416
526
|
this._testType(key, 'hash', true, callback);
|
|
417
527
|
}
|
|
418
528
|
else {
|
|
419
|
-
this.cache
|
|
529
|
+
this.cache.set(key, this._makeKey({}, 'hash'));
|
|
420
530
|
}
|
|
421
531
|
if (!this._hasField(key, field)) {
|
|
422
532
|
retVal = 1;
|
|
@@ -479,7 +589,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
479
589
|
for (let itr = 0; itr < fields.length; itr++) {
|
|
480
590
|
const field = fields[itr];
|
|
481
591
|
if (this._hasField(key, field)) {
|
|
482
|
-
delete this.cache
|
|
592
|
+
delete this.cache.get(key).value[field];
|
|
483
593
|
retVal++;
|
|
484
594
|
}
|
|
485
595
|
}
|
|
@@ -600,22 +710,18 @@ class MemoryCache extends events.EventEmitter {
|
|
|
600
710
|
this._testType(key, 'list', true, callback);
|
|
601
711
|
}
|
|
602
712
|
else {
|
|
603
|
-
this.cache
|
|
713
|
+
this.cache.set(key, this._makeKey([], 'list'));
|
|
604
714
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
this.
|
|
608
|
-
retVal = val.length;
|
|
715
|
+
this._getKey(key).push(value.toString());
|
|
716
|
+
retVal = this._getKey(key).length;
|
|
717
|
+
this.persist(key);
|
|
609
718
|
return this._handleCallback(callback, retVal);
|
|
610
719
|
}
|
|
611
720
|
/**
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
* @param
|
|
615
|
-
* @param
|
|
616
|
-
* @param {Function} [callback]
|
|
617
|
-
* @returns {*}
|
|
618
|
-
* @memberof MemoryCache
|
|
721
|
+
* List:从左侧推入
|
|
722
|
+
* @param key
|
|
723
|
+
* @param value
|
|
724
|
+
* @param callback
|
|
619
725
|
*/
|
|
620
726
|
lpush(key, value, callback) {
|
|
621
727
|
let retVal = 0;
|
|
@@ -623,14 +729,31 @@ class MemoryCache extends events.EventEmitter {
|
|
|
623
729
|
this._testType(key, 'list', true, callback);
|
|
624
730
|
}
|
|
625
731
|
else {
|
|
626
|
-
this.cache
|
|
732
|
+
this.cache.set(key, this._makeKey([], 'list'));
|
|
627
733
|
}
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
this._setKey(key,
|
|
631
|
-
retVal = val.length;
|
|
734
|
+
const list = this._getKey(key);
|
|
735
|
+
retVal = list.unshift(value);
|
|
736
|
+
this._setKey(key, list);
|
|
632
737
|
return this._handleCallback(callback, retVal);
|
|
633
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* List:获取指定索引的元素
|
|
741
|
+
* @param key
|
|
742
|
+
* @param index
|
|
743
|
+
* @param callback
|
|
744
|
+
*/
|
|
745
|
+
lindex(key, index, callback) {
|
|
746
|
+
if (!this._hasKey(key)) {
|
|
747
|
+
return this._handleCallback(callback, null);
|
|
748
|
+
}
|
|
749
|
+
this._testType(key, 'list', true, callback);
|
|
750
|
+
const list = this._getKey(key);
|
|
751
|
+
if (index < 0) {
|
|
752
|
+
index = list.length + index;
|
|
753
|
+
}
|
|
754
|
+
const value = index >= 0 && index < list.length ? list[index] : null;
|
|
755
|
+
return this._handleCallback(callback, value);
|
|
756
|
+
}
|
|
634
757
|
/**
|
|
635
758
|
*
|
|
636
759
|
*
|
|
@@ -643,9 +766,11 @@ class MemoryCache extends events.EventEmitter {
|
|
|
643
766
|
let retVal = null;
|
|
644
767
|
if (this._hasKey(key)) {
|
|
645
768
|
this._testType(key, 'list', true, callback);
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
769
|
+
const list = this._getKey(key);
|
|
770
|
+
if (list.length > 0) {
|
|
771
|
+
retVal = list.shift();
|
|
772
|
+
this.persist(key);
|
|
773
|
+
}
|
|
649
774
|
}
|
|
650
775
|
return this._handleCallback(callback, retVal);
|
|
651
776
|
}
|
|
@@ -661,9 +786,11 @@ class MemoryCache extends events.EventEmitter {
|
|
|
661
786
|
let retVal = null;
|
|
662
787
|
if (this._hasKey(key)) {
|
|
663
788
|
this._testType(key, 'list', true, callback);
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
789
|
+
const list = this._getKey(key);
|
|
790
|
+
if (list.length > 0) {
|
|
791
|
+
retVal = list.pop();
|
|
792
|
+
this.persist(key);
|
|
793
|
+
}
|
|
667
794
|
}
|
|
668
795
|
return this._handleCallback(callback, retVal);
|
|
669
796
|
}
|
|
@@ -681,8 +808,8 @@ class MemoryCache extends events.EventEmitter {
|
|
|
681
808
|
const retVal = [];
|
|
682
809
|
if (this._hasKey(key)) {
|
|
683
810
|
this._testType(key, 'list', true, callback);
|
|
684
|
-
const
|
|
685
|
-
const length =
|
|
811
|
+
const list = this._getKey(key);
|
|
812
|
+
const length = list.length;
|
|
686
813
|
if (stop < 0) {
|
|
687
814
|
stop = length + stop;
|
|
688
815
|
}
|
|
@@ -698,7 +825,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
698
825
|
if (stop >= 0 && stop >= start) {
|
|
699
826
|
const size = stop - start + 1;
|
|
700
827
|
for (let itr = start; itr < size; itr++) {
|
|
701
|
-
retVal.push(
|
|
828
|
+
retVal.push(list[itr]);
|
|
702
829
|
}
|
|
703
830
|
}
|
|
704
831
|
}
|
|
@@ -722,7 +849,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
722
849
|
this._testType(key, 'set', true, callback);
|
|
723
850
|
}
|
|
724
851
|
else {
|
|
725
|
-
this.cache
|
|
852
|
+
this.cache.set(key, this._makeKey([], 'set'));
|
|
726
853
|
}
|
|
727
854
|
const val = this._getKey(key);
|
|
728
855
|
const length = val.length;
|
|
@@ -794,19 +921,29 @@ class MemoryCache extends events.EventEmitter {
|
|
|
794
921
|
* @memberof MemoryCache
|
|
795
922
|
*/
|
|
796
923
|
spop(key, count, callback) {
|
|
797
|
-
let retVal =
|
|
924
|
+
let retVal = [];
|
|
798
925
|
count = count || 1;
|
|
799
|
-
if (
|
|
800
|
-
|
|
926
|
+
if (typeof count === 'function') {
|
|
927
|
+
callback = count;
|
|
928
|
+
count = 1;
|
|
801
929
|
}
|
|
802
930
|
if (this._hasKey(key)) {
|
|
803
|
-
retVal = [];
|
|
804
931
|
this._testType(key, 'set', true, callback);
|
|
805
932
|
const val = this._getKey(key);
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
933
|
+
const keys = Object.keys(val);
|
|
934
|
+
const keysLength = keys.length;
|
|
935
|
+
if (keysLength) {
|
|
936
|
+
if (count >= keysLength) {
|
|
937
|
+
retVal = keys;
|
|
938
|
+
this.del(key);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
for (let itr = 0; itr < count; itr++) {
|
|
942
|
+
const randomNum = Math.floor(Math.random() * keys.length);
|
|
943
|
+
retVal.push(keys[randomNum]);
|
|
944
|
+
this.srem(key, keys[randomNum]);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
810
947
|
}
|
|
811
948
|
}
|
|
812
949
|
return this._handleCallback(callback, retVal);
|
|
@@ -873,14 +1010,14 @@ class MemoryCache extends events.EventEmitter {
|
|
|
873
1010
|
discard(callback, silent) {
|
|
874
1011
|
// Clear the queue mode, drain the queue, empty the watch list
|
|
875
1012
|
if (this.multiMode) {
|
|
876
|
-
this.cache = this.databases
|
|
1013
|
+
this.cache = this.databases.get(this.currentDBIndex);
|
|
877
1014
|
this.multiMode = false;
|
|
878
1015
|
this.responseMessages = [];
|
|
879
1016
|
}
|
|
880
|
-
|
|
881
|
-
return this._handleCallback(callback,
|
|
1017
|
+
if (!silent) {
|
|
1018
|
+
return this._handleCallback(callback, messages.ok);
|
|
882
1019
|
}
|
|
883
|
-
return
|
|
1020
|
+
return null;
|
|
884
1021
|
}
|
|
885
1022
|
// ---------------------------------------
|
|
886
1023
|
// ## Internal - Key ##
|
|
@@ -893,11 +1030,11 @@ class MemoryCache extends events.EventEmitter {
|
|
|
893
1030
|
* @returns {*}
|
|
894
1031
|
* @memberof MemoryCache
|
|
895
1032
|
*/
|
|
896
|
-
pttl(key,
|
|
1033
|
+
pttl(key, _callback) {
|
|
897
1034
|
let retVal = -2;
|
|
898
1035
|
if (this._hasKey(key)) {
|
|
899
|
-
if (!lodash.isNil(this.cache
|
|
900
|
-
retVal = this.cache
|
|
1036
|
+
if (!lodash.isNil(this.cache.get(key)?.timeout)) {
|
|
1037
|
+
retVal = this.cache.get(key).timeout - Date.now();
|
|
901
1038
|
// Prevent unexpected errors if the actual ttl just happens to be -2 or -1
|
|
902
1039
|
if (retVal < 0 && retVal > -3) {
|
|
903
1040
|
retVal = -3;
|
|
@@ -907,7 +1044,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
907
1044
|
retVal = -1;
|
|
908
1045
|
}
|
|
909
1046
|
}
|
|
910
|
-
return
|
|
1047
|
+
return retVal;
|
|
911
1048
|
}
|
|
912
1049
|
/**
|
|
913
1050
|
*
|
|
@@ -922,7 +1059,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
922
1059
|
let retVal = 0;
|
|
923
1060
|
if (this._hasKey(key)) {
|
|
924
1061
|
if (!lodash.isNil(this._key(key).timeout)) {
|
|
925
|
-
this.
|
|
1062
|
+
this.cache.set(key, { ...this.cache.get(key), timeout: null });
|
|
926
1063
|
retVal = 1;
|
|
927
1064
|
}
|
|
928
1065
|
}
|
|
@@ -937,7 +1074,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
937
1074
|
* @memberof MemoryCache
|
|
938
1075
|
*/
|
|
939
1076
|
_hasKey(key) {
|
|
940
|
-
return this.cache.
|
|
1077
|
+
return this.cache.has(key);
|
|
941
1078
|
}
|
|
942
1079
|
/**
|
|
943
1080
|
*
|
|
@@ -961,8 +1098,8 @@ class MemoryCache extends events.EventEmitter {
|
|
|
961
1098
|
* @memberof MemoryCache
|
|
962
1099
|
*/
|
|
963
1100
|
_key(key) {
|
|
964
|
-
this.cache
|
|
965
|
-
return this.cache
|
|
1101
|
+
this.cache.get(key).lastAccess = Date.now();
|
|
1102
|
+
return this.cache.get(key);
|
|
966
1103
|
}
|
|
967
1104
|
/**
|
|
968
1105
|
*
|
|
@@ -987,7 +1124,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
987
1124
|
}
|
|
988
1125
|
}
|
|
989
1126
|
else {
|
|
990
|
-
this.cache
|
|
1127
|
+
this.cache.set(key, this._makeKey('0', 'string'));
|
|
991
1128
|
}
|
|
992
1129
|
const val = keyValue + amount;
|
|
993
1130
|
this._setKey(key, val.toString());
|
|
@@ -1040,8 +1177,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
1040
1177
|
* @memberof MemoryCache
|
|
1041
1178
|
*/
|
|
1042
1179
|
_setKey(key, value) {
|
|
1043
|
-
this.cache
|
|
1044
|
-
this.cache[key].lastAccess = Date.now();
|
|
1180
|
+
this.cache.set(key, { ...this.cache.get(key), value: value, lastAccess: Date.now() });
|
|
1045
1181
|
}
|
|
1046
1182
|
/**
|
|
1047
1183
|
*
|
|
@@ -1069,7 +1205,7 @@ class MemoryCache extends events.EventEmitter {
|
|
|
1069
1205
|
}
|
|
1070
1206
|
}
|
|
1071
1207
|
else {
|
|
1072
|
-
this.cache
|
|
1208
|
+
this.cache.set(key, this._makeKey({}, 'hash'));
|
|
1073
1209
|
}
|
|
1074
1210
|
fieldValue = useFloat ? parseFloat(`${value}`) : parseInt(`${value}`);
|
|
1075
1211
|
amount = useFloat ? parseFloat(`${amount}`) : parseInt(`${amount}`);
|
|
@@ -1178,6 +1314,333 @@ class MemoryCache extends events.EventEmitter {
|
|
|
1178
1314
|
}
|
|
1179
1315
|
return;
|
|
1180
1316
|
}
|
|
1317
|
+
/**
|
|
1318
|
+
* 字符串追加操作
|
|
1319
|
+
* @param key
|
|
1320
|
+
* @param value
|
|
1321
|
+
* @param callback
|
|
1322
|
+
*/
|
|
1323
|
+
append(key, value, callback) {
|
|
1324
|
+
let retVal = 0;
|
|
1325
|
+
if (this._hasKey(key)) {
|
|
1326
|
+
this._testType(key, 'string', true, callback);
|
|
1327
|
+
const existingValue = this._getKey(key);
|
|
1328
|
+
const newValue = existingValue + value;
|
|
1329
|
+
this._setKey(key, newValue);
|
|
1330
|
+
retVal = newValue.length;
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
this.cache.set(key, this._makeKey(value, 'string'));
|
|
1334
|
+
retVal = value.length;
|
|
1335
|
+
}
|
|
1336
|
+
return this._handleCallback(callback, retVal);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* 获取字符串长度
|
|
1340
|
+
* @param key
|
|
1341
|
+
* @param callback
|
|
1342
|
+
*/
|
|
1343
|
+
strlen(key, callback) {
|
|
1344
|
+
let retVal = 0;
|
|
1345
|
+
if (this._hasKey(key)) {
|
|
1346
|
+
this._testType(key, 'string', true, callback);
|
|
1347
|
+
retVal = this._getKey(key).length;
|
|
1348
|
+
}
|
|
1349
|
+
return this._handleCallback(callback, retVal);
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* 获取子字符串
|
|
1353
|
+
* @param key
|
|
1354
|
+
* @param start
|
|
1355
|
+
* @param end
|
|
1356
|
+
* @param callback
|
|
1357
|
+
*/
|
|
1358
|
+
getrange(key, start, end, callback) {
|
|
1359
|
+
let retVal = '';
|
|
1360
|
+
if (this._hasKey(key)) {
|
|
1361
|
+
this._testType(key, 'string', true, callback);
|
|
1362
|
+
const value = this._getKey(key);
|
|
1363
|
+
retVal = value.substring(start, end + 1);
|
|
1364
|
+
}
|
|
1365
|
+
return this._handleCallback(callback, retVal);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* 设置子字符串
|
|
1369
|
+
* @param key
|
|
1370
|
+
* @param offset
|
|
1371
|
+
* @param value
|
|
1372
|
+
* @param callback
|
|
1373
|
+
*/
|
|
1374
|
+
setrange(key, offset, value, callback) {
|
|
1375
|
+
let retVal = 0;
|
|
1376
|
+
if (this._hasKey(key)) {
|
|
1377
|
+
this._testType(key, 'string', true, callback);
|
|
1378
|
+
const existingValue = this._getKey(key);
|
|
1379
|
+
const newValue = existingValue.substring(0, offset) + value + existingValue.substring(offset + value.length);
|
|
1380
|
+
this._setKey(key, newValue);
|
|
1381
|
+
retVal = newValue.length;
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
// 如果键不存在,创建一个足够长的字符串
|
|
1385
|
+
const newValue = ''.padEnd(offset, '\0') + value;
|
|
1386
|
+
this.cache.set(key, this._makeKey(newValue, 'string'));
|
|
1387
|
+
retVal = newValue.length;
|
|
1388
|
+
}
|
|
1389
|
+
return this._handleCallback(callback, retVal);
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* 批量获取
|
|
1393
|
+
* @param keys
|
|
1394
|
+
* @param callback
|
|
1395
|
+
*/
|
|
1396
|
+
mget(...keys) {
|
|
1397
|
+
const callback = this._retrieveCallback(keys);
|
|
1398
|
+
const retVal = [];
|
|
1399
|
+
for (const key of keys) {
|
|
1400
|
+
if (this._hasKey(key)) {
|
|
1401
|
+
this._testType(key, 'string', false, callback);
|
|
1402
|
+
retVal.push(this._getKey(key));
|
|
1403
|
+
}
|
|
1404
|
+
else {
|
|
1405
|
+
retVal.push(null);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return this._handleCallback(callback, retVal);
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* 批量设置
|
|
1412
|
+
* @param keyValuePairs
|
|
1413
|
+
* @param callback
|
|
1414
|
+
*/
|
|
1415
|
+
mset(...keyValuePairs) {
|
|
1416
|
+
const callback = this._retrieveCallback(keyValuePairs);
|
|
1417
|
+
// 确保参数是偶数个
|
|
1418
|
+
if (keyValuePairs.length % 2 !== 0) {
|
|
1419
|
+
return this._handleCallback(callback, null, messages.wrongArgCount.replace('%0', 'mset'));
|
|
1420
|
+
}
|
|
1421
|
+
for (let i = 0; i < keyValuePairs.length; i += 2) {
|
|
1422
|
+
const key = keyValuePairs[i];
|
|
1423
|
+
const value = keyValuePairs[i + 1];
|
|
1424
|
+
this.cache.set(key, this._makeKey(value.toString(), 'string'));
|
|
1425
|
+
}
|
|
1426
|
+
return this._handleCallback(callback, messages.ok);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* 获取所有键
|
|
1430
|
+
* @param pattern
|
|
1431
|
+
* @param callback
|
|
1432
|
+
*/
|
|
1433
|
+
keys(pattern = '*', callback) {
|
|
1434
|
+
const retVal = [];
|
|
1435
|
+
this.cache.forEach((_item, key) => {
|
|
1436
|
+
if (pattern === '*' || this.matchPattern(key, pattern)) {
|
|
1437
|
+
retVal.push(key);
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
return this._handleCallback(callback, retVal);
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* 简单的模式匹配
|
|
1444
|
+
* @param key
|
|
1445
|
+
* @param pattern
|
|
1446
|
+
*/
|
|
1447
|
+
matchPattern(key, pattern) {
|
|
1448
|
+
if (pattern === '*')
|
|
1449
|
+
return true;
|
|
1450
|
+
// 转换glob模式为正则表达式
|
|
1451
|
+
const regexPattern = pattern
|
|
1452
|
+
.replace(/\*/g, '.*')
|
|
1453
|
+
.replace(/\?/g, '.')
|
|
1454
|
+
.replace(/\[([^\]]*)\]/g, '[$1]');
|
|
1455
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
1456
|
+
return regex.test(key);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* 获取随机键
|
|
1460
|
+
* @param callback
|
|
1461
|
+
*/
|
|
1462
|
+
randomkey(callback) {
|
|
1463
|
+
const keys = [];
|
|
1464
|
+
this.cache.forEach((_item, key) => {
|
|
1465
|
+
keys.push(key);
|
|
1466
|
+
});
|
|
1467
|
+
if (keys.length === 0) {
|
|
1468
|
+
return this._handleCallback(callback, null);
|
|
1469
|
+
}
|
|
1470
|
+
const randomIndex = Math.floor(Math.random() * keys.length);
|
|
1471
|
+
return this._handleCallback(callback, keys[randomIndex]);
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* 重命名键
|
|
1475
|
+
* @param oldKey
|
|
1476
|
+
* @param newKey
|
|
1477
|
+
* @param callback
|
|
1478
|
+
*/
|
|
1479
|
+
rename(oldKey, newKey, callback) {
|
|
1480
|
+
if (!this._hasKey(oldKey)) {
|
|
1481
|
+
return this._handleCallback(callback, null, messages.nokey);
|
|
1482
|
+
}
|
|
1483
|
+
const value = this.cache.get(oldKey);
|
|
1484
|
+
this.cache.set(newKey, value);
|
|
1485
|
+
this.cache.delete(oldKey);
|
|
1486
|
+
return this._handleCallback(callback, messages.ok);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* 安全重命名键(目标键不存在时才重命名)
|
|
1490
|
+
* @param oldKey
|
|
1491
|
+
* @param newKey
|
|
1492
|
+
* @param callback
|
|
1493
|
+
*/
|
|
1494
|
+
renamenx(oldKey, newKey, callback) {
|
|
1495
|
+
if (!this._hasKey(oldKey)) {
|
|
1496
|
+
return this._handleCallback(callback, null, messages.nokey);
|
|
1497
|
+
}
|
|
1498
|
+
if (this._hasKey(newKey)) {
|
|
1499
|
+
return this._handleCallback(callback, 0);
|
|
1500
|
+
}
|
|
1501
|
+
const value = this.cache.get(oldKey);
|
|
1502
|
+
this.cache.set(newKey, value);
|
|
1503
|
+
this.cache.delete(oldKey);
|
|
1504
|
+
return this._handleCallback(callback, 1);
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* 获取键的类型
|
|
1508
|
+
* @param key
|
|
1509
|
+
* @param callback
|
|
1510
|
+
*/
|
|
1511
|
+
type(key, callback) {
|
|
1512
|
+
if (!this._hasKey(key)) {
|
|
1513
|
+
return this._handleCallback(callback, 'none');
|
|
1514
|
+
}
|
|
1515
|
+
const item = this.cache.get(key);
|
|
1516
|
+
return this._handleCallback(callback, item.type);
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* 清空当前数据库
|
|
1520
|
+
* @param callback
|
|
1521
|
+
*/
|
|
1522
|
+
flushdb(callback) {
|
|
1523
|
+
this.cache.clear();
|
|
1524
|
+
return this._handleCallback(callback, messages.ok);
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* 清空所有数据库
|
|
1528
|
+
* @param callback
|
|
1529
|
+
*/
|
|
1530
|
+
flushall(callback) {
|
|
1531
|
+
this.databases.clear();
|
|
1532
|
+
this.cache = this.createLRUCache();
|
|
1533
|
+
this.databases.set(this.currentDBIndex, this.cache);
|
|
1534
|
+
return this._handleCallback(callback, messages.ok);
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* 获取数据库大小
|
|
1538
|
+
* @param callback
|
|
1539
|
+
*/
|
|
1540
|
+
dbsize(callback) {
|
|
1541
|
+
const size = this.cache.size || 0;
|
|
1542
|
+
return this._handleCallback(callback, size);
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Sorted Set基础实现 - 添加成员
|
|
1546
|
+
* @param key
|
|
1547
|
+
* @param score
|
|
1548
|
+
* @param member
|
|
1549
|
+
* @param callback
|
|
1550
|
+
*/
|
|
1551
|
+
zadd(key, score, member, callback) {
|
|
1552
|
+
let retVal = 0;
|
|
1553
|
+
if (this._hasKey(key)) {
|
|
1554
|
+
this._testType(key, 'zset', true, callback);
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
this.cache.set(key, this._makeKey([], 'zset'));
|
|
1558
|
+
}
|
|
1559
|
+
const zset = this._getKey(key);
|
|
1560
|
+
const existing = zset.find((item) => item.member === member);
|
|
1561
|
+
if (existing) {
|
|
1562
|
+
existing.score = score;
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
zset.push({ score, member });
|
|
1566
|
+
retVal = 1;
|
|
1567
|
+
}
|
|
1568
|
+
// 按分数排序
|
|
1569
|
+
zset.sort((a, b) => a.score - b.score);
|
|
1570
|
+
this._setKey(key, zset);
|
|
1571
|
+
return this._handleCallback(callback, retVal);
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Sorted Set - 获取成员分数
|
|
1575
|
+
* @param key
|
|
1576
|
+
* @param member
|
|
1577
|
+
* @param callback
|
|
1578
|
+
*/
|
|
1579
|
+
zscore(key, member, callback) {
|
|
1580
|
+
if (!this._hasKey(key)) {
|
|
1581
|
+
return this._handleCallback(callback, null);
|
|
1582
|
+
}
|
|
1583
|
+
this._testType(key, 'zset', true, callback);
|
|
1584
|
+
const zset = this._getKey(key);
|
|
1585
|
+
const item = zset.find((item) => item.member === member);
|
|
1586
|
+
return this._handleCallback(callback, item ? item.score : null);
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Sorted Set - 获取范围内的成员
|
|
1590
|
+
* @param key
|
|
1591
|
+
* @param start
|
|
1592
|
+
* @param stop
|
|
1593
|
+
* @param callback
|
|
1594
|
+
*/
|
|
1595
|
+
zrange(key, start, stop, callback) {
|
|
1596
|
+
if (!this._hasKey(key)) {
|
|
1597
|
+
return this._handleCallback(callback, []);
|
|
1598
|
+
}
|
|
1599
|
+
this._testType(key, 'zset', true, callback);
|
|
1600
|
+
const zset = this._getKey(key);
|
|
1601
|
+
const length = zset.length;
|
|
1602
|
+
if (stop < 0) {
|
|
1603
|
+
stop = length + stop;
|
|
1604
|
+
}
|
|
1605
|
+
if (start < 0) {
|
|
1606
|
+
start = length + start;
|
|
1607
|
+
}
|
|
1608
|
+
const retVal = zset.slice(start, stop + 1).map((item) => item.member);
|
|
1609
|
+
return this._handleCallback(callback, retVal);
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Sorted Set - 获取成员数量
|
|
1613
|
+
* @param key
|
|
1614
|
+
* @param callback
|
|
1615
|
+
*/
|
|
1616
|
+
zcard(key, callback) {
|
|
1617
|
+
if (!this._hasKey(key)) {
|
|
1618
|
+
return this._handleCallback(callback, 0);
|
|
1619
|
+
}
|
|
1620
|
+
this._testType(key, 'zset', true, callback);
|
|
1621
|
+
const zset = this._getKey(key);
|
|
1622
|
+
return this._handleCallback(callback, zset.length);
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Sorted Set - 删除成员
|
|
1626
|
+
* @param key
|
|
1627
|
+
* @param member
|
|
1628
|
+
* @param callback
|
|
1629
|
+
*/
|
|
1630
|
+
zrem(key, member, callback) {
|
|
1631
|
+
let retVal = 0;
|
|
1632
|
+
if (this._hasKey(key)) {
|
|
1633
|
+
this._testType(key, 'zset', true, callback);
|
|
1634
|
+
const zset = this._getKey(key);
|
|
1635
|
+
const index = zset.findIndex((item) => item.member === member);
|
|
1636
|
+
if (index !== -1) {
|
|
1637
|
+
zset.splice(index, 1);
|
|
1638
|
+
retVal = 1;
|
|
1639
|
+
this._setKey(key, zset);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return this._handleCallback(callback, retVal);
|
|
1643
|
+
}
|
|
1181
1644
|
}
|
|
1182
1645
|
|
|
1183
1646
|
/*
|
|
@@ -1197,7 +1660,12 @@ class MemoryStore {
|
|
|
1197
1660
|
* @memberof MemoryStore
|
|
1198
1661
|
*/
|
|
1199
1662
|
constructor(options) {
|
|
1200
|
-
this.options =
|
|
1663
|
+
this.options = {
|
|
1664
|
+
maxKeys: 1000,
|
|
1665
|
+
evictionPolicy: 'lru',
|
|
1666
|
+
ttlCheckInterval: 60000, // 1分钟
|
|
1667
|
+
...options
|
|
1668
|
+
};
|
|
1201
1669
|
this.client = null;
|
|
1202
1670
|
}
|
|
1203
1671
|
/**
|
|
@@ -1209,7 +1677,11 @@ class MemoryStore {
|
|
|
1209
1677
|
getConnection() {
|
|
1210
1678
|
if (!this.pool) {
|
|
1211
1679
|
this.pool = new MemoryCache({
|
|
1212
|
-
database: this.options.db
|
|
1680
|
+
database: this.options.db || 0,
|
|
1681
|
+
maxKeys: this.options.maxKeys,
|
|
1682
|
+
maxMemory: this.options.maxMemory,
|
|
1683
|
+
evictionPolicy: this.options.evictionPolicy,
|
|
1684
|
+
ttlCheckInterval: this.options.ttlCheckInterval
|
|
1213
1685
|
});
|
|
1214
1686
|
}
|
|
1215
1687
|
if (!this.client) {
|
|
@@ -1225,8 +1697,10 @@ class MemoryStore {
|
|
|
1225
1697
|
* @memberof MemoryStore
|
|
1226
1698
|
*/
|
|
1227
1699
|
async close() {
|
|
1228
|
-
this.client
|
|
1229
|
-
|
|
1700
|
+
if (this.client) {
|
|
1701
|
+
this.client.end();
|
|
1702
|
+
this.client = null;
|
|
1703
|
+
}
|
|
1230
1704
|
}
|
|
1231
1705
|
/**
|
|
1232
1706
|
* release
|
|
@@ -1269,6 +1743,20 @@ class MemoryStore {
|
|
|
1269
1743
|
return -1;
|
|
1270
1744
|
}
|
|
1271
1745
|
}
|
|
1746
|
+
/**
|
|
1747
|
+
* 获取缓存统计信息
|
|
1748
|
+
*/
|
|
1749
|
+
getStats() {
|
|
1750
|
+
if (this.client) {
|
|
1751
|
+
return this.client.info();
|
|
1752
|
+
}
|
|
1753
|
+
return {
|
|
1754
|
+
keys: 0,
|
|
1755
|
+
memory: 0,
|
|
1756
|
+
hits: 0,
|
|
1757
|
+
misses: 0
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1272
1760
|
}
|
|
1273
1761
|
|
|
1274
1762
|
/*
|
|
@@ -1289,6 +1777,9 @@ class RedisStore {
|
|
|
1289
1777
|
options;
|
|
1290
1778
|
pool;
|
|
1291
1779
|
client;
|
|
1780
|
+
reconnectAttempts = 0;
|
|
1781
|
+
maxReconnectAttempts = 5;
|
|
1782
|
+
reconnectDelay = 1000; // 初始重连延迟1秒
|
|
1292
1783
|
/**
|
|
1293
1784
|
* Creates an instance of RedisStore.
|
|
1294
1785
|
* @param {RedisStoreOpt} options
|
|
@@ -1348,7 +1839,7 @@ class RedisStore {
|
|
|
1348
1839
|
return opt;
|
|
1349
1840
|
}
|
|
1350
1841
|
/**
|
|
1351
|
-
* create connection by native
|
|
1842
|
+
* create connection by native with improved error handling
|
|
1352
1843
|
*
|
|
1353
1844
|
* @param {number} [connNum=0]
|
|
1354
1845
|
* @returns {*} {Promise<Redis | Cluster>}
|
|
@@ -1360,32 +1851,77 @@ class RedisStore {
|
|
|
1360
1851
|
}
|
|
1361
1852
|
const defer = helper__namespace.getDefer();
|
|
1362
1853
|
let connection;
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
this.options.keyPrefix = "";
|
|
1371
|
-
connection.on('end', () => {
|
|
1372
|
-
if (connNum < 3) {
|
|
1373
|
-
connNum++;
|
|
1374
|
-
defer.resolve(this.connect(connNum));
|
|
1854
|
+
try {
|
|
1855
|
+
if (!helper__namespace.isEmpty(this.options.clusters)) {
|
|
1856
|
+
connection = new ioredis.Cluster([...this.options.clusters], {
|
|
1857
|
+
redisOptions: this.options,
|
|
1858
|
+
enableOfflineQueue: false,
|
|
1859
|
+
retryDelayOnFailover: 100
|
|
1860
|
+
});
|
|
1375
1861
|
}
|
|
1376
1862
|
else {
|
|
1377
|
-
|
|
1378
|
-
|
|
1863
|
+
connection = new ioredis.Redis({
|
|
1864
|
+
...this.options,
|
|
1865
|
+
enableOfflineQueue: false,
|
|
1866
|
+
retryDelayOnFailover: 100,
|
|
1867
|
+
lazyConnect: true
|
|
1868
|
+
});
|
|
1379
1869
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1870
|
+
// 去除prefix, 防止重复
|
|
1871
|
+
this.options.keyPrefix = "";
|
|
1872
|
+
connection.on('error', (err) => {
|
|
1873
|
+
koatty_logger.DefaultLogger.Error(`Redis connection error: ${err.message}`);
|
|
1874
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
1875
|
+
this.scheduleReconnect(connNum);
|
|
1876
|
+
}
|
|
1877
|
+
else {
|
|
1878
|
+
defer.reject(new Error(`Redis connection failed after ${this.maxReconnectAttempts} attempts`));
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
connection.on('end', () => {
|
|
1882
|
+
koatty_logger.DefaultLogger.Warn('Redis connection ended');
|
|
1883
|
+
if (connNum < 3) {
|
|
1884
|
+
this.scheduleReconnect(connNum + 1);
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
this.close();
|
|
1888
|
+
defer.reject(new Error('Redis connection end after 3 attempts'));
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
connection.on('ready', () => {
|
|
1892
|
+
koatty_logger.DefaultLogger.Info('Redis connection ready');
|
|
1893
|
+
this.client = connection;
|
|
1894
|
+
this.reconnectAttempts = 0; // 重置重连计数
|
|
1895
|
+
defer.resolve(connection);
|
|
1896
|
+
});
|
|
1897
|
+
// 主动连接
|
|
1898
|
+
if (connection instanceof ioredis.Redis) {
|
|
1899
|
+
await connection.connect();
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
catch (error) {
|
|
1903
|
+
koatty_logger.DefaultLogger.Error(`Failed to create Redis connection: ${error.message}`);
|
|
1904
|
+
defer.reject(error);
|
|
1905
|
+
}
|
|
1385
1906
|
return defer.promise;
|
|
1386
1907
|
}
|
|
1387
1908
|
/**
|
|
1388
|
-
*
|
|
1909
|
+
* 计划重连,使用指数退避策略
|
|
1910
|
+
* @private
|
|
1911
|
+
* @param {number} connNum
|
|
1912
|
+
*/
|
|
1913
|
+
scheduleReconnect(connNum) {
|
|
1914
|
+
this.reconnectAttempts++;
|
|
1915
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
1916
|
+
koatty_logger.DefaultLogger.Info(`Scheduling Redis reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
|
|
1917
|
+
setTimeout(() => {
|
|
1918
|
+
this.connect(connNum).catch(err => {
|
|
1919
|
+
koatty_logger.DefaultLogger.Error(`Reconnect attempt ${this.reconnectAttempts} failed: ${err.message}`);
|
|
1920
|
+
});
|
|
1921
|
+
}, delay);
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* get connection from pool with improved configuration
|
|
1389
1925
|
*
|
|
1390
1926
|
* @returns {*}
|
|
1391
1927
|
* @memberof RedisStore
|
|
@@ -1396,38 +1932,57 @@ class RedisStore {
|
|
|
1396
1932
|
create: () => {
|
|
1397
1933
|
return this.connect();
|
|
1398
1934
|
},
|
|
1399
|
-
destroy: () => {
|
|
1400
|
-
|
|
1935
|
+
destroy: (resource) => {
|
|
1936
|
+
if (resource && typeof resource.disconnect === 'function') {
|
|
1937
|
+
resource.disconnect();
|
|
1938
|
+
}
|
|
1939
|
+
return Promise.resolve();
|
|
1401
1940
|
},
|
|
1402
1941
|
validate: (resource) => {
|
|
1403
|
-
return Promise.resolve(resource.status === 'ready');
|
|
1942
|
+
return Promise.resolve(resource && resource.status === 'ready');
|
|
1404
1943
|
}
|
|
1405
1944
|
};
|
|
1406
1945
|
this.pool = genericPool.createPool(factory, {
|
|
1407
1946
|
max: this.options.poolSize || 10, // maximum size of the pool
|
|
1408
|
-
min: 2 // minimum size of the pool
|
|
1947
|
+
min: Math.min(2, this.options.poolSize || 2), // minimum size of the pool
|
|
1948
|
+
acquireTimeoutMillis: 30000, // 30秒获取连接超时
|
|
1949
|
+
testOnBorrow: true, // 借用时测试连接
|
|
1950
|
+
evictionRunIntervalMillis: 30000, // 30秒检查一次空闲连接
|
|
1951
|
+
idleTimeoutMillis: 300000, // 5分钟空闲超时
|
|
1952
|
+
softIdleTimeoutMillis: 180000 // 3分钟软空闲超时
|
|
1409
1953
|
});
|
|
1410
1954
|
this.pool.on('factoryCreateError', function (err) {
|
|
1411
|
-
koatty_logger.DefaultLogger.Error(err);
|
|
1955
|
+
koatty_logger.DefaultLogger.Error(`Redis pool create error: ${err.message}`);
|
|
1412
1956
|
});
|
|
1413
1957
|
this.pool.on('factoryDestroyError', function (err) {
|
|
1414
|
-
koatty_logger.DefaultLogger.Error(err);
|
|
1958
|
+
koatty_logger.DefaultLogger.Error(`Redis pool destroy error: ${err.message}`);
|
|
1415
1959
|
});
|
|
1416
1960
|
}
|
|
1417
1961
|
return this.pool.acquire();
|
|
1418
1962
|
}
|
|
1419
1963
|
/**
|
|
1420
|
-
* close connection
|
|
1964
|
+
* close connection with proper cleanup
|
|
1421
1965
|
*
|
|
1422
1966
|
* @returns {*}
|
|
1423
1967
|
* @memberof RedisStore
|
|
1424
1968
|
*/
|
|
1425
1969
|
async close() {
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1970
|
+
try {
|
|
1971
|
+
if (this.pool) {
|
|
1972
|
+
await this.pool.drain();
|
|
1973
|
+
await this.pool.clear();
|
|
1974
|
+
this.pool = null;
|
|
1975
|
+
}
|
|
1976
|
+
if (this.client) {
|
|
1977
|
+
if (typeof this.client.disconnect === 'function') {
|
|
1978
|
+
this.client.disconnect();
|
|
1979
|
+
}
|
|
1980
|
+
this.client = null;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
catch (error) {
|
|
1984
|
+
koatty_logger.DefaultLogger.Error(`Error closing Redis connection: ${error.message}`);
|
|
1985
|
+
}
|
|
1431
1986
|
}
|
|
1432
1987
|
/**
|
|
1433
1988
|
*
|
|
@@ -1470,16 +2025,16 @@ class RedisStore {
|
|
|
1470
2025
|
try {
|
|
1471
2026
|
conn = await this.defineCommand("getCompare", {
|
|
1472
2027
|
numberOfKeys: 1,
|
|
1473
|
-
lua: `
|
|
1474
|
-
local remote_value = redis.call("get",KEYS[1])
|
|
1475
|
-
|
|
1476
|
-
if (not remote_value) then
|
|
1477
|
-
return 0
|
|
1478
|
-
elseif (remote_value == ARGV[1]) then
|
|
1479
|
-
return redis.call("del",KEYS[1])
|
|
1480
|
-
else
|
|
1481
|
-
return -1
|
|
1482
|
-
end
|
|
2028
|
+
lua: `
|
|
2029
|
+
local remote_value = redis.call("get",KEYS[1])
|
|
2030
|
+
|
|
2031
|
+
if (not remote_value) then
|
|
2032
|
+
return 0
|
|
2033
|
+
elseif (remote_value == ARGV[1]) then
|
|
2034
|
+
return redis.call("del",KEYS[1])
|
|
2035
|
+
else
|
|
2036
|
+
return -1
|
|
2037
|
+
end
|
|
1483
2038
|
`
|
|
1484
2039
|
});
|
|
1485
2040
|
return conn.getCompare(name, value);
|
|
@@ -1519,7 +2074,7 @@ const defaultOptions = {
|
|
|
1519
2074
|
class CacheStore {
|
|
1520
2075
|
client;
|
|
1521
2076
|
options;
|
|
1522
|
-
static
|
|
2077
|
+
static instances = new Map();
|
|
1523
2078
|
/**
|
|
1524
2079
|
* Creates an instance of CacheStore.
|
|
1525
2080
|
* @param {StoreOptions} options
|
|
@@ -1539,17 +2094,70 @@ class CacheStore {
|
|
|
1539
2094
|
}
|
|
1540
2095
|
}
|
|
1541
2096
|
/**
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
2097
|
+
* 获取单例实例,支持多配置实例管理
|
|
1544
2098
|
* @static
|
|
1545
|
-
* @
|
|
2099
|
+
* @param {StoreOptions} [options]
|
|
2100
|
+
* @param {string} [instanceKey='default'] 实例键名,用于区分不同配置的实例
|
|
2101
|
+
* @returns {CacheStore}
|
|
2102
|
+
*/
|
|
2103
|
+
static getInstance(options, instanceKey = 'default') {
|
|
2104
|
+
// 生成配置哈希作为实例键的一部分
|
|
2105
|
+
const configHash = options ? this.generateConfigHash(options) : 'default';
|
|
2106
|
+
const fullKey = `${instanceKey}_${configHash}`;
|
|
2107
|
+
if (this.instances.has(fullKey)) {
|
|
2108
|
+
return this.instances.get(fullKey);
|
|
2109
|
+
}
|
|
2110
|
+
const instance = new CacheStore(options);
|
|
2111
|
+
this.instances.set(fullKey, instance);
|
|
2112
|
+
return instance;
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* 生成配置哈希
|
|
2116
|
+
* @private
|
|
2117
|
+
* @static
|
|
2118
|
+
* @param {StoreOptions} options
|
|
2119
|
+
* @returns {string}
|
|
1546
2120
|
*/
|
|
1547
|
-
static
|
|
1548
|
-
|
|
1549
|
-
|
|
2121
|
+
static generateConfigHash(options) {
|
|
2122
|
+
const configStr = JSON.stringify({
|
|
2123
|
+
type: options.type,
|
|
2124
|
+
host: options.host,
|
|
2125
|
+
port: options.port,
|
|
2126
|
+
db: options.db,
|
|
2127
|
+
keyPrefix: options.keyPrefix
|
|
2128
|
+
});
|
|
2129
|
+
// 简单哈希函数
|
|
2130
|
+
let hash = 0;
|
|
2131
|
+
for (let i = 0; i < configStr.length; i++) {
|
|
2132
|
+
const char = configStr.charCodeAt(i);
|
|
2133
|
+
hash = ((hash << 5) - hash) + char;
|
|
2134
|
+
hash = hash & hash; // 转换为32位整数
|
|
1550
2135
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
2136
|
+
return Math.abs(hash).toString(36);
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* 清理指定实例
|
|
2140
|
+
* @static
|
|
2141
|
+
* @param {string} [instanceKey='default']
|
|
2142
|
+
*/
|
|
2143
|
+
static async clearInstance(instanceKey = 'default') {
|
|
2144
|
+
const keysToRemove = Array.from(this.instances.keys()).filter(key => key.startsWith(`${instanceKey}_`));
|
|
2145
|
+
for (const key of keysToRemove) {
|
|
2146
|
+
const instance = this.instances.get(key);
|
|
2147
|
+
if (instance) {
|
|
2148
|
+
await instance.close();
|
|
2149
|
+
this.instances.delete(key);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* 清理所有实例
|
|
2155
|
+
* @static
|
|
2156
|
+
*/
|
|
2157
|
+
static async clearAllInstances() {
|
|
2158
|
+
const promises = Array.from(this.instances.values()).map(instance => instance.close());
|
|
2159
|
+
await Promise.all(promises);
|
|
2160
|
+
this.instances.clear();
|
|
1553
2161
|
}
|
|
1554
2162
|
getConnection() {
|
|
1555
2163
|
return this.client.getConnection();
|
|
@@ -1560,11 +2168,13 @@ class CacheStore {
|
|
|
1560
2168
|
release(conn) {
|
|
1561
2169
|
return this.client.release(conn);
|
|
1562
2170
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2171
|
+
/**
|
|
2172
|
+
* 获取底层实现客户端,用于访问特定实现的功能
|
|
2173
|
+
* 例如:Redis的defineCommand, getCompare等
|
|
2174
|
+
* @returns {MemoryStore | RedisStore}
|
|
2175
|
+
*/
|
|
2176
|
+
getRawClient() {
|
|
2177
|
+
return this.client;
|
|
1568
2178
|
}
|
|
1569
2179
|
/**
|
|
1570
2180
|
* handler for native client
|
|
@@ -1624,13 +2234,6 @@ class CacheStore {
|
|
|
1624
2234
|
expire(name, timeout) {
|
|
1625
2235
|
return this.wrap('expire', [`${this.options.keyPrefix || ""}${name}`, timeout]);
|
|
1626
2236
|
}
|
|
1627
|
-
/**
|
|
1628
|
-
* 删除key
|
|
1629
|
-
* @param name
|
|
1630
|
-
*/
|
|
1631
|
-
rm(name) {
|
|
1632
|
-
return this.wrap('del', [`${this.options.keyPrefix || ""}${name}`]);
|
|
1633
|
-
}
|
|
1634
2237
|
/**
|
|
1635
2238
|
*
|
|
1636
2239
|
*
|
|
@@ -1668,8 +2271,8 @@ class CacheStore {
|
|
|
1668
2271
|
* @param incr
|
|
1669
2272
|
* @returns {*}
|
|
1670
2273
|
*/
|
|
1671
|
-
incrby(name,
|
|
1672
|
-
return this.wrap('incrby', [`${this.options.keyPrefix || ""}${name}`,
|
|
2274
|
+
incrby(name, increment) {
|
|
2275
|
+
return this.wrap('incrby', [`${this.options.keyPrefix || ""}${name}`, increment]);
|
|
1673
2276
|
}
|
|
1674
2277
|
/**
|
|
1675
2278
|
* 将 key 所储存的值减去减量
|
|
@@ -1677,8 +2280,8 @@ class CacheStore {
|
|
|
1677
2280
|
* @param {any} name
|
|
1678
2281
|
* @param {any} decr
|
|
1679
2282
|
*/
|
|
1680
|
-
decrby(name,
|
|
1681
|
-
return this.wrap('decrby', [`${this.options.keyPrefix || ""}${name}`,
|
|
2283
|
+
decrby(name, decrement) {
|
|
2284
|
+
return this.wrap('decrby', [`${this.options.keyPrefix || ""}${name}`, decrement]);
|
|
1682
2285
|
}
|
|
1683
2286
|
/**
|
|
1684
2287
|
* 哈希写入
|
|
@@ -1687,13 +2290,16 @@ class CacheStore {
|
|
|
1687
2290
|
* @param value
|
|
1688
2291
|
* @param timeout
|
|
1689
2292
|
*/
|
|
1690
|
-
hset(name, key, value, timeout) {
|
|
1691
|
-
const
|
|
1692
|
-
if (typeof timeout
|
|
1693
|
-
|
|
2293
|
+
async hset(name, key, value, timeout) {
|
|
2294
|
+
const result = await this.wrap('hset', [`${this.options.keyPrefix || ""}${name}`, key, value]);
|
|
2295
|
+
if (typeof timeout === 'number') {
|
|
2296
|
+
await this.set(`${name}:${key}_ex`, 1, timeout);
|
|
1694
2297
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
2298
|
+
else {
|
|
2299
|
+
// 如果没有指定timeout,设置一个永久标记,避免hget时误删
|
|
2300
|
+
await this.set(`${name}:${key}_ex`, 1);
|
|
2301
|
+
}
|
|
2302
|
+
return result;
|
|
1697
2303
|
}
|
|
1698
2304
|
/**
|
|
1699
2305
|
* 哈希获取
|
|
@@ -1726,7 +2332,7 @@ class CacheStore {
|
|
|
1726
2332
|
this.hdel(name, key);
|
|
1727
2333
|
return 0;
|
|
1728
2334
|
}
|
|
1729
|
-
return dataArr[1] || 0;
|
|
2335
|
+
return Number(dataArr[1]) || 0;
|
|
1730
2336
|
});
|
|
1731
2337
|
}
|
|
1732
2338
|
/**
|
|
@@ -1735,10 +2341,10 @@ class CacheStore {
|
|
|
1735
2341
|
* @param key
|
|
1736
2342
|
* @returns {*}
|
|
1737
2343
|
*/
|
|
1738
|
-
hdel(name, key) {
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
return
|
|
2344
|
+
async hdel(name, key) {
|
|
2345
|
+
await this.del(`${name}:${key}_ex`);
|
|
2346
|
+
const result = await this.wrap('hdel', [`${this.options.keyPrefix || ""}${name}`, key]);
|
|
2347
|
+
return result;
|
|
1742
2348
|
}
|
|
1743
2349
|
/**
|
|
1744
2350
|
* 返回哈希表 key 中域的数量
|
|
@@ -1752,11 +2358,11 @@ class CacheStore {
|
|
|
1752
2358
|
* 给哈希表指定key,增加increment
|
|
1753
2359
|
* @param name
|
|
1754
2360
|
* @param key
|
|
1755
|
-
* @param
|
|
2361
|
+
* @param increment
|
|
1756
2362
|
* @returns {*}
|
|
1757
2363
|
*/
|
|
1758
|
-
hincrby(name, key,
|
|
1759
|
-
return this.wrap('hincrby', [`${this.options.keyPrefix || ""}${name}`, key,
|
|
2364
|
+
hincrby(name, key, increment) {
|
|
2365
|
+
return this.wrap('hincrby', [`${this.options.keyPrefix || ""}${name}`, key, increment]);
|
|
1760
2366
|
}
|
|
1761
2367
|
/**
|
|
1762
2368
|
* 返回哈希表所有key-value
|
|
@@ -1845,12 +2451,12 @@ class CacheStore {
|
|
|
1845
2451
|
* @param timeout
|
|
1846
2452
|
* @returns {*}
|
|
1847
2453
|
*/
|
|
1848
|
-
sadd(name, value, timeout) {
|
|
1849
|
-
const
|
|
1850
|
-
if (typeof timeout
|
|
1851
|
-
|
|
2454
|
+
async sadd(name, value, timeout) {
|
|
2455
|
+
const result = await this.wrap('sadd', [`${this.options.keyPrefix || ""}${name}`, value]);
|
|
2456
|
+
if (typeof timeout === 'number') {
|
|
2457
|
+
await this.wrap('expire', [`${this.options.keyPrefix || ""}${name}`, timeout]);
|
|
1852
2458
|
}
|
|
1853
|
-
return
|
|
2459
|
+
return result;
|
|
1854
2460
|
}
|
|
1855
2461
|
/**
|
|
1856
2462
|
* 返回集合的基数(集合中元素的数量)
|
|
@@ -1902,7 +2508,7 @@ class CacheStore {
|
|
|
1902
2508
|
* @returns {*}
|
|
1903
2509
|
*/
|
|
1904
2510
|
smove(source, destination, member) {
|
|
1905
|
-
return this.wrap('smove', [`${this.options.keyPrefix || ""}${source}`, `${this.options.keyPrefix}${destination}`, member]);
|
|
2511
|
+
return this.wrap('smove', [`${this.options.keyPrefix || ""}${source}`, `${this.options.keyPrefix || ""}${destination}`, member]);
|
|
1906
2512
|
}
|
|
1907
2513
|
}
|
|
1908
2514
|
|