monsqlize 1.0.0 → 1.0.2
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 +92 -2419
- package/README.md +630 -1070
- package/index.d.ts +252 -15
- package/lib/cache.js +8 -8
- package/lib/common/validation.js +64 -1
- package/lib/connect.js +3 -3
- package/lib/errors.js +10 -0
- package/lib/index.js +118 -9
- package/lib/infrastructure/ssh-tunnel-ssh2.js +211 -0
- package/lib/infrastructure/ssh-tunnel.js +40 -0
- package/lib/infrastructure/uri-parser.js +35 -0
- package/lib/lock/Lock.js +66 -0
- package/lib/lock/errors.js +27 -0
- package/lib/lock/index.js +12 -0
- package/lib/logger.js +1 -1
- package/lib/model/examples/test.js +4 -4
- package/lib/mongodb/common/accessor-helpers.js +17 -3
- package/lib/mongodb/connect.js +68 -13
- package/lib/mongodb/index.js +140 -7
- package/lib/mongodb/management/collection-ops.js +4 -4
- package/lib/mongodb/management/index-ops.js +18 -18
- package/lib/mongodb/management/validation-ops.js +3 -3
- package/lib/mongodb/queries/aggregate.js +14 -5
- package/lib/mongodb/queries/chain.js +52 -45
- package/lib/mongodb/queries/count.js +16 -6
- package/lib/mongodb/queries/distinct.js +15 -6
- package/lib/mongodb/queries/find-and-count.js +22 -13
- package/lib/mongodb/queries/find-by-ids.js +5 -5
- package/lib/mongodb/queries/find-one-by-id.js +1 -1
- package/lib/mongodb/queries/find-one.js +12 -3
- package/lib/mongodb/queries/find-page.js +12 -0
- package/lib/mongodb/queries/find.js +15 -6
- package/lib/mongodb/queries/index.js +1 -0
- package/lib/mongodb/queries/watch.js +537 -0
- package/lib/mongodb/writes/delete-many.js +20 -11
- package/lib/mongodb/writes/delete-one.js +18 -9
- package/lib/mongodb/writes/find-one-and-delete.js +19 -10
- package/lib/mongodb/writes/find-one-and-replace.js +36 -20
- package/lib/mongodb/writes/find-one-and-update.js +36 -20
- package/lib/mongodb/writes/increment-one.js +16 -7
- package/lib/mongodb/writes/index.js +13 -13
- package/lib/mongodb/writes/insert-batch.js +46 -37
- package/lib/mongodb/writes/insert-many.js +22 -13
- package/lib/mongodb/writes/insert-one.js +18 -9
- package/lib/mongodb/writes/replace-one.js +33 -17
- package/lib/mongodb/writes/result-handler.js +14 -14
- package/lib/mongodb/writes/update-many.js +34 -18
- package/lib/mongodb/writes/update-one.js +33 -17
- package/lib/mongodb/writes/upsert-one.js +25 -9
- package/lib/operators.js +1 -1
- package/lib/redis-cache-adapter.js +3 -3
- package/lib/slow-query-log/base-storage.js +69 -0
- package/lib/slow-query-log/batch-queue.js +96 -0
- package/lib/slow-query-log/config-manager.js +195 -0
- package/lib/slow-query-log/index.js +237 -0
- package/lib/slow-query-log/mongodb-storage.js +323 -0
- package/lib/slow-query-log/query-hash.js +38 -0
- package/lib/transaction/DistributedCacheLockManager.js +240 -5
- package/lib/transaction/Transaction.js +1 -1
- package/lib/utils/objectid-converter.js +566 -0
- package/package.json +11 -5
|
@@ -2,17 +2,26 @@
|
|
|
2
2
|
* 分布式缓存锁管理器
|
|
3
3
|
* 基于 Redis 实现跨实例的事务缓存锁,确保事务隔离性
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* });
|
|
5
|
+
* v1.4.0 新增:业务级分布式锁支持
|
|
6
|
+
* - withLock(): 自动管理锁生命周期
|
|
7
|
+
* - acquireLock(): 手动获取锁(阻塞重试)
|
|
8
|
+
* - tryAcquireLock(): 尝试获取锁(不阻塞)
|
|
10
9
|
*
|
|
10
|
+
* @example
|
|
11
|
+
* // 事务缓存锁(原有功能)
|
|
11
12
|
* await lockManager.addLock('user:1', session);
|
|
12
13
|
* const isLocked = await lockManager.isLocked('user:1');
|
|
13
14
|
* await lockManager.releaseLocks(session);
|
|
15
|
+
*
|
|
16
|
+
* // 业务锁(v1.4.0 新增)
|
|
17
|
+
* await lockManager.withLock('inventory:SKU123', async () => {
|
|
18
|
+
* // 临界区代码
|
|
19
|
+
* });
|
|
14
20
|
*/
|
|
15
21
|
|
|
22
|
+
const Lock = require('../lock/Lock');
|
|
23
|
+
const { LockAcquireError } = require('../lock/errors');
|
|
24
|
+
|
|
16
25
|
class DistributedCacheLockManager {
|
|
17
26
|
/**
|
|
18
27
|
* @param {Object} options
|
|
@@ -233,6 +242,232 @@ class DistributedCacheLockManager {
|
|
|
233
242
|
this.logger.info('[DistributedCacheLockManager] Stopped');
|
|
234
243
|
}
|
|
235
244
|
}
|
|
245
|
+
|
|
246
|
+
// ==================== 业务锁 API (v1.4.0) ====================
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 业务锁:自动管理锁生命周期(推荐)
|
|
250
|
+
* @param {string} key - 锁的唯一标识
|
|
251
|
+
* @param {Function} callback - 获取锁后执行的函数
|
|
252
|
+
* @param {Object} [options] - 锁选项
|
|
253
|
+
* @param {number} [options.ttl=10000] - 锁过期时间(毫秒)
|
|
254
|
+
* @param {number} [options.retryTimes=3] - 重试次数
|
|
255
|
+
* @param {number} [options.retryDelay=100] - 重试间隔(毫秒)
|
|
256
|
+
* @param {boolean} [options.fallbackToNoLock=false] - Redis不可用时降级为无锁执行
|
|
257
|
+
* @returns {Promise<*>} callback 的返回值
|
|
258
|
+
*/
|
|
259
|
+
async withLock(key, callback, options = {}) {
|
|
260
|
+
try {
|
|
261
|
+
const lock = await this.acquireLock(key, options);
|
|
262
|
+
try {
|
|
263
|
+
return await callback();
|
|
264
|
+
} finally {
|
|
265
|
+
// 释放失败不应阻塞业务(锁会在 TTL 后自动过期)
|
|
266
|
+
await lock.release().catch(err => {
|
|
267
|
+
if (this.logger) {
|
|
268
|
+
this.logger.warn(`[Lock] Release failed: ${key}`, err);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// Redis 连接问题检测
|
|
274
|
+
if (this._isRedisConnectionError(error)) {
|
|
275
|
+
if (options.fallbackToNoLock) {
|
|
276
|
+
if (this.logger) {
|
|
277
|
+
this.logger.warn(`[Lock] Redis unavailable, proceeding without lock: ${key}`);
|
|
278
|
+
}
|
|
279
|
+
return callback();
|
|
280
|
+
}
|
|
281
|
+
throw new LockAcquireError(`Redis unavailable: ${error.message}`);
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 业务锁:手动获取锁(阻塞重试)
|
|
289
|
+
* @param {string} key - 锁的唯一标识
|
|
290
|
+
* @param {Object} [options] - 锁选项
|
|
291
|
+
* @returns {Promise<Lock>}
|
|
292
|
+
*/
|
|
293
|
+
async acquireLock(key, options = {}) {
|
|
294
|
+
const ttl = options.ttl || 10000; // 默认 10 秒
|
|
295
|
+
const retryTimes = options.retryTimes ?? 3;
|
|
296
|
+
const retryDelay = options.retryDelay || 100;
|
|
297
|
+
const lockId = this._generateLockId();
|
|
298
|
+
const fullKey = this.lockKeyPrefix + key;
|
|
299
|
+
|
|
300
|
+
for (let attempt = 0; attempt <= retryTimes; attempt++) {
|
|
301
|
+
try {
|
|
302
|
+
// 使用 SET NX PX 原子操作(毫秒级 TTL)
|
|
303
|
+
const result = await this.redis.set(
|
|
304
|
+
fullKey,
|
|
305
|
+
lockId,
|
|
306
|
+
'PX', ttl, // PX = 毫秒
|
|
307
|
+
'NX'
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (result === 'OK') {
|
|
311
|
+
this.stats.locksAcquired++;
|
|
312
|
+
if (this.logger) {
|
|
313
|
+
this.logger.debug(`[Lock] Acquired: ${key}`);
|
|
314
|
+
}
|
|
315
|
+
return new Lock(key, lockId, this, ttl);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 最后一次尝试失败
|
|
319
|
+
if (attempt === retryTimes) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 等待后重试
|
|
324
|
+
await this._sleep(retryDelay);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
// 如果是最后一次尝试,抛出错误
|
|
327
|
+
if (attempt === retryTimes) {
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
// 否则等待后重试
|
|
331
|
+
await this._sleep(retryDelay);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.stats.errors++;
|
|
336
|
+
throw new LockAcquireError(`Failed to acquire lock: ${key}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 业务锁:尝试获取锁(不阻塞)
|
|
341
|
+
* @param {string} key - 锁的唯一标识
|
|
342
|
+
* @param {Object} [options] - 锁选项(不包含 retryTimes)
|
|
343
|
+
* @returns {Promise<Lock|null>} Lock对象或null
|
|
344
|
+
*/
|
|
345
|
+
async tryAcquireLock(key, options = {}) {
|
|
346
|
+
const ttl = options.ttl || 10000;
|
|
347
|
+
const lockId = this._generateLockId();
|
|
348
|
+
const fullKey = this.lockKeyPrefix + key;
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const result = await this.redis.set(
|
|
352
|
+
fullKey,
|
|
353
|
+
lockId,
|
|
354
|
+
'PX', ttl,
|
|
355
|
+
'NX'
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (result === 'OK') {
|
|
359
|
+
this.stats.locksAcquired++;
|
|
360
|
+
if (this.logger) {
|
|
361
|
+
this.logger.debug(`[Lock] Acquired (try): ${key}`);
|
|
362
|
+
}
|
|
363
|
+
return new Lock(key, lockId, this, ttl);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return null;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (this.logger) {
|
|
369
|
+
this.logger.error(`[Lock] Try acquire error: ${key}`, error.message);
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 释放单个锁(由 Lock 对象调用)
|
|
377
|
+
* @param {string} key - 锁标识
|
|
378
|
+
* @param {string} lockId - 锁ID
|
|
379
|
+
* @returns {Promise<boolean>}
|
|
380
|
+
*/
|
|
381
|
+
async releaseLock(key, lockId) {
|
|
382
|
+
const fullKey = this.lockKeyPrefix + key;
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
// 使用 Lua 脚本确保原子性(只释放自己的锁)
|
|
386
|
+
const luaScript = `
|
|
387
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
388
|
+
return redis.call("del", KEYS[1])
|
|
389
|
+
else
|
|
390
|
+
return 0
|
|
391
|
+
end
|
|
392
|
+
`;
|
|
393
|
+
|
|
394
|
+
const result = await this.redis.eval(luaScript, 1, fullKey, lockId);
|
|
395
|
+
|
|
396
|
+
if (result === 1) {
|
|
397
|
+
this.stats.locksReleased++;
|
|
398
|
+
if (this.logger) {
|
|
399
|
+
this.logger.debug(`[Lock] Released: ${key}`);
|
|
400
|
+
}
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return false;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.stats.errors++;
|
|
407
|
+
if (this.logger) {
|
|
408
|
+
this.logger.error(`[Lock] Release error: ${key}`, error.message);
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 续期(延长锁的过期时间)
|
|
416
|
+
* @param {string} key - 锁标识
|
|
417
|
+
* @param {string} lockId - 锁ID
|
|
418
|
+
* @param {number} ttl - 新的过期时间(毫秒)
|
|
419
|
+
* @returns {Promise<boolean>}
|
|
420
|
+
*/
|
|
421
|
+
async renewLock(key, lockId, ttl) {
|
|
422
|
+
const fullKey = this.lockKeyPrefix + key;
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
// 使用 Lua 脚本确保只续期自己的锁
|
|
426
|
+
const luaScript = `
|
|
427
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
428
|
+
return redis.call("pexpire", KEYS[1], ARGV[2])
|
|
429
|
+
else
|
|
430
|
+
return 0
|
|
431
|
+
end
|
|
432
|
+
`;
|
|
433
|
+
|
|
434
|
+
const result = await this.redis.eval(luaScript, 1, fullKey, lockId, ttl);
|
|
435
|
+
return result === 1;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (this.logger) {
|
|
438
|
+
this.logger.error(`[Lock] Renew error: ${key}`, error.message);
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 生成唯一锁ID
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
_generateLockId() {
|
|
449
|
+
return `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 延迟
|
|
454
|
+
* @private
|
|
455
|
+
*/
|
|
456
|
+
_sleep(ms) {
|
|
457
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 检测是否是 Redis 连接错误
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
_isRedisConnectionError(error) {
|
|
465
|
+
const msg = error.message || '';
|
|
466
|
+
return msg.includes('ECONNREFUSED') ||
|
|
467
|
+
msg.includes('ETIMEDOUT') ||
|
|
468
|
+
msg.includes('ENOTFOUND') ||
|
|
469
|
+
msg.includes('Connection is closed');
|
|
470
|
+
}
|
|
236
471
|
}
|
|
237
472
|
|
|
238
473
|
module.exports = DistributedCacheLockManager;
|
|
@@ -216,7 +216,7 @@ class Transaction {
|
|
|
216
216
|
if (this.lockManager && this.session) {
|
|
217
217
|
this.lockManager.releaseLocks(this.session);
|
|
218
218
|
this.lockedKeys.clear();
|
|
219
|
-
this.logger?.debug(
|
|
219
|
+
this.logger?.debug('[Transaction] Released all cache locks');
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|