mlock-client 0.1.9 → 0.2.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 脉冲云
3
+ Copyright (c) 2020 Liang Xingchen https://github.com/liangxingchen
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,10 +1,407 @@
1
- # `mlock`
1
+ # mlock-client
2
2
 
3
- Maichong lock client for NodeJs
3
+ Multi-resource distributed lock client for Node.js
4
4
 
5
- ## Usage
5
+ ## 功能特性
6
6
 
7
+ - 原子性多资源锁
8
+ - 自动重连
9
+ - 连接池复用
10
+ - Promise/Async-await API
11
+ - 锁超时和续期
12
+ - 队列等待和超时控制
13
+
14
+
15
+ ## 使用场景
16
+
17
+ ### 电商下单防超卖
18
+
19
+ 在电商系统中,订单创建接口通常需要加锁以防止库存超卖。传统的分布式锁方案存在以下问题:
20
+
21
+ **方案一:全局锁**
22
+ ```
23
+ lock("order-create") // 所有订单创建操作串行执行
24
+ ```
25
+ 问题:即使客户购买的商品完全不同,也无法并发处理,严重影响系统吞吐量。
26
+
27
+ **方案二:按商品分锁**
28
+ ```
29
+ lock("product:1001") // 商品 1001
30
+ lock("product:1002") // 商品 1002
31
+ ```
32
+ 问题:一个订单包含多个商品时,无法保证原子性。可能在锁定商品 A 后,商品 B 被其他订单锁定,导致最终部分商品锁定失败或库存不一致。
33
+
34
+ **mlock 解决方案**
35
+ mlock 支持原子性地锁定多个资源,确保事务的完整性:
36
+ ```
37
+ lock("product:1001|product:1002|product:1003") // 原子性锁定多个商品
38
+ ```
39
+ 当所有涉及的商品资源可用时,才会成功锁定;如果任一商品已被锁定,则进入队列等待,直到所有资源同时可用。
40
+
41
+ ## 多资源锁机制
42
+
43
+ ### 原子性保证
44
+
45
+ 多资源锁的核心是**原子性**:要么所有资源同时锁定成功,要么全部失败(进入等待队列)。
46
+
47
+ ```
48
+ // 请求锁定 A、B、C 三个资源
49
+ lock("resource-a|resource-b|resource-c")
50
+ ```
51
+
52
+ ### 锁定流程
53
+
54
+ 1. **请求阶段**:客户端请求锁定多个资源(用 `|` 分隔)
55
+ 2. **检查阶段**:服务器检查每个资源的可用性
56
+ - 如果所有资源都可用 → 立即锁定成功
57
+ - 如果任一资源被锁定 → 进入队列等待
58
+ 3. **排队阶段**:锁请求在所有相关资源的队列中排队
59
+ 4. **激活阶段**:只有当所有资源同时可用时,锁才会被激活并返回 lockId
60
+
61
+ ### 队列机制
62
+
63
+ 每个资源维护一个独立的队列:
64
+
65
+ ```
66
+ resource-a 队列: [Lock1(A|B), Lock3(A|C), ...]
67
+ resource-b 队列: [Lock1(A|B), Lock2(B|D), ...]
68
+ resource-c 队列: [Lock3(A|C), Lock4(C|E), ...]
69
+ ```
70
+
71
+ **激活条件**:Lock1 请求 A+B,只有当 resource-a 和 resource-b 队列的首元素都是 Lock1 时才会激活。
72
+
73
+ ### 示例场景
74
+
75
+ **场景一:所有资源可用**
76
+ ```
77
+ 时刻1: client1.lock("res-a|res-b|res-c") // A、B、C 都可用
78
+ 时刻1: lock 成功,返回 lockId
79
+ ```
80
+
81
+ **场景二:部分资源被占用**
82
+ ```
83
+ 时刻1: client1.lock("res-a") // client1 锁定 A
84
+ 时刻2: client2.lock("res-a|res-b|res-c") // client2 请求 A+B+C
85
+ 时刻2: client2 进入队列等待(A 被占用)
86
+ 时刻3: client1.unlock("res-a") // client1 释放 A
87
+ 时刻3: client2 激活成功
88
+ ```
89
+
90
+ **场景三:多个请求排队**
91
+ ```
92
+ 时刻1: client1.lock("res-a") // 锁定 A
93
+ 时刻2: client2.lock("res-a|res-b") // 等待 A+B
94
+ 时刻3: client3.lock("res-a|res-b") // 等待 A+B(在队列中)
95
+ 时刻4: client1.unlock("res-a") // 释放 A
96
+ 时刻4: client2 激活成功(先到先得)
97
+ 时刻5: client2.unlock("res-a|res-b") // client2 释放
98
+ 时刻6: client3 激活成功
99
+ ```
100
+
101
+
102
+ ## 安装
103
+
104
+ ```bash
105
+ npm install mlock
106
+ ```
107
+
108
+ ## 快速开始
109
+
110
+ ### JavaScript
111
+
112
+ ```javascript
113
+ const Client = require('mlock');
114
+
115
+ const client = new Client({ host: 'localhost', port: 12340 });
116
+
117
+ // 锁定单个资源
118
+ const lockId = await client.lock('resource-1', 5000);
119
+
120
+ try {
121
+ // 执行业务逻辑
122
+ await doSomething();
123
+ } finally {
124
+ await client.unlock(lockId);
125
+ }
126
+ ```
127
+
128
+ ### TypeScript
129
+
130
+ ```typescript
131
+ import Client, { MlockError } from 'mlock';
132
+
133
+ const client = new Client({
134
+ host: 'localhost',
135
+ port: 12340,
136
+ ttl: 5000,
137
+ timeout: 10000
138
+ });
139
+
140
+ try {
141
+ // 锁定多个资源(原子性)
142
+ const lockId = await client.lock('product:1001|product:1002|product:1003');
143
+
144
+ try {
145
+ // 执行业务逻辑
146
+ await processOrder();
147
+ } finally {
148
+ await client.unlock(lockId);
149
+ }
150
+ } catch (error) {
151
+ if (error instanceof MlockError) {
152
+ console.error(`Lock error (${error.type}):`, error.message);
153
+ }
154
+ }
155
+ ```
156
+
157
+ ## 配置选项
158
+
159
+ | 参数 | 类型 | 默认值 | 说明 |
160
+ |------|------|--------|------|
161
+ | uri | string | - | 连接 URI,格式如 `mlock://localhost:12340?timeout=5000&prefix=lock:` |
162
+ | host | string | localhost | 服务器地址 |
163
+ | port | number | 12340 | 服务器端口 |
164
+ | prefix | string | - | 资源名称前缀,所有资源会自动加上此前缀 |
165
+ | ttl | number | - | 默认锁生存时间(毫秒) |
166
+ | timeout | number | - | 默认上锁超时时间(毫秒) |
167
+ | tolerate | number | - | 默认容忍队列长度 |
168
+ | socketId | string | - | Socket ID(用于断线重连后的身份识别) |
169
+ | debug | boolean | false | 调试模式 |
170
+
171
+ ### URI 配置
172
+
173
+ 支持通过 URI 配置所有选项:
174
+
175
+ ```javascript
176
+ const client = new Client('mlock://localhost:12340?timeout=5000&ttl=3000&prefix=order:');
177
+
178
+ // 等价于
179
+ const client = new Client({
180
+ host: 'localhost',
181
+ port: 12340,
182
+ timeout: 5000,
183
+ ttl: 3000,
184
+ prefix: 'order:'
185
+ });
186
+ ```
187
+
188
+ ## API
189
+
190
+ ### constructor(options: string | ClientOptions)
191
+
192
+ 创建客户端实例。
193
+
194
+ ```javascript
195
+ // 使用对象配置
196
+ const client = new Client({
197
+ host: 'localhost',
198
+ port: 12340,
199
+ ttl: 5000
200
+ });
201
+
202
+ // 使用 URI 配置
203
+ const client = new Client('mlock://localhost:12340');
204
+ ```
205
+
206
+ ### lock(resource: string, ttl?, timeout?, tolerate?): Promise<string>
207
+
208
+ 上锁。如果资源已被锁定,会进入队列等待直到超时或获取到锁。
209
+
210
+ **参数:**
211
+ - `resource`: 资源描述字符串,多个资源用 `|` 分隔
212
+ - `ttl`: 锁生存时间(毫秒),未指定使用配置的默认值
213
+ - `timeout`: 上锁超时时间(毫秒),未指定使用配置的默认值
214
+ - `tolerate`: 容忍队列长度,未指定使用配置的默认值
215
+
216
+ **返回:** 锁 ID
217
+
218
+ **示例:**
219
+
220
+ ```javascript
221
+ // 锁定单个资源
222
+ const lockId = await client.lock('product:1001', 5000, 10000, 10);
223
+
224
+ // 锁定多个资源(原子性,只有所有资源都可用时才会成功)
225
+ const lockId = await client.lock('product:1001|product:1002|product:1003');
226
+
227
+ try {
228
+ // 执行业务逻辑
229
+ await updateInventory(['1001', '1002', '1003']);
230
+ } finally {
231
+ await client.unlock(lockId);
232
+ }
233
+ ```
234
+
235
+ ### extend(lockId: string, ttl?): Promise<number>
236
+
237
+ 续期锁,延长锁的过期时间。
238
+
239
+ **参数:**
240
+ - `lockId`: 锁 ID
241
+ - `ttl`: 续期时间(毫秒),未指定使用配置的默认值
242
+
243
+ **返回:** 新的过期时间戳
244
+
245
+ **示例:**
246
+
247
+ ```javascript
248
+ const lockId = await client.lock('resource', 5000);
249
+
250
+ // 长时间任务,定期续期
251
+ const renewInterval = setInterval(async () => {
252
+ const newExpiredAt = await client.extend(lockId, 5000);
253
+ console.log('Lock extended to:', new Date(newExpiredAt));
254
+ }, 4000);
255
+
256
+ try {
257
+ await longRunningTask();
258
+ } finally {
259
+ clearInterval(renewInterval);
260
+ await client.unlock(lockId);
261
+ }
262
+ ```
263
+
264
+ ### unlock(lockId: string): Promise<void>
265
+
266
+ 解锁,释放指定的锁。
267
+
268
+ **示例:**
269
+
270
+ ```javascript
271
+ const lockId = await client.lock('resource', 5000);
272
+ try {
273
+ await doSomething();
274
+ } finally {
275
+ await client.unlock(lockId);
276
+ }
7
277
  ```
8
- const mlock = require('mlock');
9
278
 
279
+ ### ping(): Promise<'pong'>
280
+
281
+ Ping 服务器,检测连接是否正常。
282
+
283
+ ```javascript
284
+ try {
285
+ const pong = await client.ping();
286
+ console.log('Connection is alive:', pong);
287
+ } catch (error) {
288
+ console.error('Connection error:', error);
289
+ }
10
290
  ```
291
+
292
+ ### status(): Promise<any>
293
+
294
+ 获取服务器状态。
295
+
296
+ **返回:** 服务器状态对象
297
+
298
+ ```javascript
299
+ const status = await client.status();
300
+ console.log('Socket count:', status.socketCount);
301
+ console.log('Current locks:', status.currentLocks);
302
+ console.log('Live time:', status.liveTime, 'ms');
303
+ ```
304
+
305
+ ### destroy(): void
306
+
307
+ 销毁客户端,释放连接资源。
308
+
309
+ ```javascript
310
+ client.destroy();
311
+ ```
312
+
313
+ ## 错误处理
314
+
315
+ mlock 使用自定义的 `MlockError` 类。
316
+
317
+ **错误类型:**
318
+
319
+ | 类型 | 说明 |
320
+ |------|------|
321
+ | connection | 连接错误(网络问题、服务器不可达等) |
322
+ | request | 请求错误(参数错误、资源名称无效等) |
323
+ | tolerate | 队列溢出(等待队列超过容忍值) |
324
+ | timeout | 超时错误(获取锁超时) |
325
+
326
+ **示例:**
327
+
328
+ ```javascript
329
+ import Client, { MlockError } from 'mlock';
330
+
331
+ const client = new Client({ host: 'localhost', port: 12340 });
332
+
333
+ try {
334
+ const lockId = await client.lock('resource', 5000);
335
+ // ...
336
+ } catch (error) {
337
+ if (error instanceof MlockError) {
338
+ switch (error.type) {
339
+ case 'timeout':
340
+ console.error('获取锁超时');
341
+ break;
342
+ case 'tolerate':
343
+ console.error('队列已满,无法等待');
344
+ break;
345
+ case 'connection':
346
+ console.error('连接服务器失败');
347
+ break;
348
+ case 'request':
349
+ console.error('请求参数错误:', error.message);
350
+ break;
351
+ }
352
+ }
353
+ }
354
+ ```
355
+
356
+ ## 使用场景
357
+
358
+ ### 电商订单
359
+
360
+ ```javascript
361
+ const client = new Client({ prefix: 'order:' });
362
+
363
+ // 原子性锁定多个商品
364
+ const lockId = await client.lock('product:1001|product:1002|product:1003', 5000);
365
+
366
+ try {
367
+ // 减少库存
368
+ await deductInventory(['1001', '1002', '1003']);
369
+ // 创建订单
370
+ await createOrder([...]);
371
+ } finally {
372
+ await client.unlock(lockId);
373
+ }
374
+ ```
375
+
376
+ ### 定时任务防重
377
+
378
+ ```javascript
379
+ const client = new Client({ prefix: 'task:' });
380
+
381
+ const taskName = 'daily-report';
382
+ const lockId = await client.lock(taskName, 60000); // 1分钟
383
+
384
+ if (lockId) {
385
+ try {
386
+ await generateDailyReport();
387
+ } finally {
388
+ await client.unlock(lockId);
389
+ }
390
+ } else {
391
+ console.log('Task is already running');
392
+ }
393
+ ```
394
+
395
+ ## 连接池
396
+
397
+ 同一 `host:port` 的客户端会自动复用连接:
398
+
399
+ ```javascript
400
+ const client1 = new Client({ host: 'localhost', port: 12340 });
401
+ const client2 = new Client({ host: 'localhost', port: 12340 });
402
+ // client1 和 client2 共享同一个 TCP 连接
403
+ ```
404
+
405
+ ## 许可证
406
+
407
+ MIT
package/index.d.ts CHANGED
@@ -1,10 +1,14 @@
1
+ /**
2
+ * 客户端配置选项
3
+ */
1
4
  export interface ClientOptions {
2
5
  /**
3
- * 链接URI,优先于host/port
6
+ * 连接 URI,格式如 `mlock://localhost:12340?timeout=5000&prefix=lock:`
7
+ * URI 参数会覆盖 host、port、timeout、prefix 等选项
4
8
  */
5
9
  uri?: string;
6
10
  /**
7
- * 服务器主机地址,默认localhost
11
+ * 服务器主机地址,默认 localhost
8
12
  */
9
13
  host?: string;
10
14
  /**
@@ -12,60 +16,108 @@ export interface ClientOptions {
12
16
  */
13
17
  port?: number;
14
18
  /**
15
- * 资源前缀
19
+ * 资源名称前缀,所有资源名称会自动加上此前缀
20
+ * 前缀中不能包含 `|` 或空格
16
21
  */
17
22
  prefix?: string;
18
23
  /**
19
- * 默认锁TTL,单位毫秒
24
+ * 默认锁的生存时间(TTL),单位毫秒
25
+ * 锁过期后会被自动释放
20
26
  */
21
27
  ttl?: number;
22
28
  /**
23
29
  * 默认上锁超时时间,单位毫秒
30
+ * 如果在指定时间内无法获取锁,则返回超时错误
24
31
  */
25
32
  timeout?: number;
26
33
  /**
27
- * 默认容忍锁队列中等待个数,如果超过容忍数,直接报错,不用等待到timeout
34
+ * 默认容忍锁队列中等待的个数
35
+ * 如果队列中等待的锁数量超过此值,直接报错,不等待到 timeout
36
+ * 用于避免大量请求堆积
28
37
  */
29
38
  tolerate?: number;
39
+ /**
40
+ * 客户端 Socket ID,用于断线重连后的身份识别
41
+ */
30
42
  socketId?: string;
43
+ /**
44
+ * 是否开启调试模式,开启后会输出详细日志
45
+ */
31
46
  debug?: boolean;
32
47
  }
33
48
 
49
+ /**
50
+ * 分布式锁客户端
51
+ *
52
+ * 支持连接到 mlock-server 进行资源的分布式锁管理
53
+ * 支持自动重连、连接池、多路复用等功能
54
+ */
34
55
  export default class Client {
56
+ /**
57
+ * 创建客户端实例
58
+ * @param options 客户端配置选项,或连接 URI 字符串
59
+ */
35
60
  constructor(options: string | ClientOptions);
36
61
 
37
62
  /**
38
- * 上锁,上锁成功后返回锁ID
39
- * @param {string} resource 资源描述字符串,同时锁定多个资源用 | 分隔
40
- * @param {number} [ttl]
41
- * @param {number} [timeout]
42
- * @param {number} [tolerate]
63
+ * 上锁
64
+ * 如果资源已被锁定,会进入队列等待,直到超时或获取到锁
65
+ * @param resource 资源描述字符串,同时锁定多个资源用 `|` 分隔
66
+ * @param ttl 锁的生存时间,单位毫秒,未指定则使用配置的默认值
67
+ * @param timeout 上锁超时时间,单位毫秒,未指定则使用配置的默认值
68
+ * @param tolerate 容忍队列长度,未指定则使用配置的默认值
69
+ * @returns Promise<string> 返回锁ID
70
+ * @throws {MlockError} 超时、队列溢出、请求错误等情况下抛出异常
43
71
  */
44
72
  lock(resource: string, ttl?: number, timeout?: number, tolerate?: number): Promise<string>;
45
73
 
46
74
  /**
47
- * 延时,成功后返回新的过期时间戳
48
- * @param {string} lock 锁ID
49
- * @param {number} [ttl]
75
+ * 续期锁
76
+ * 延长锁的过期时间
77
+ * @param lock 锁ID
78
+ * @param ttl 续期时间,单位毫秒,未指定则使用配置的默认值
79
+ * @returns Promise<number> 返回新的过期时间戳
80
+ * @throws {MlockError} 锁不存在或未上锁时抛出异常
50
81
  */
51
82
  extend(lock: string, ttl?: number): Promise<number>;
52
83
 
53
84
  /**
54
85
  * 解锁
86
+ * 释放指定的锁
87
+ * @param lock 锁ID
55
88
  */
56
89
  unlock(lock: string): Promise<void>;
57
90
 
58
91
  /**
59
- * ping
92
+ * Ping 服务器
93
+ * 用于检测连接是否正常
94
+ * @returns Promise<'pong'> 连接正常返回 'pong'
60
95
  */
61
96
  ping(): Promise<'pong'>;
62
97
 
63
98
  /**
64
99
  * 获取服务器状态
100
+ * 返回服务器的统计信息和当前锁状态
101
+ * @returns Promise<any> 服务器状态对象,包含 socketCount、currentLocks 等信息
65
102
  */
66
103
  status(): Promise<any>;
104
+
105
+ /**
106
+ * 销毁客户端,释放连接资源
107
+ */
108
+ destroy(): void;
67
109
  }
68
110
 
111
+ /**
112
+ * mlock 自定义错误类
113
+ */
69
114
  export class MlockError extends Error {
115
+ /**
116
+ * 错误类型
117
+ * - connection: 连接错误(网络问题、服务器不可达等)
118
+ * - request: 请求错误(参数错误、资源名称无效等)
119
+ * - tolerate: 队列溢出(等待队列超过容忍值)
120
+ * - timeout: 超时错误(获取锁超时)
121
+ */
70
122
  type?: 'connection' | 'request' | 'tolerate' | 'timeout';
71
123
  }
package/lib/index.js CHANGED
@@ -42,6 +42,9 @@ class Client {
42
42
  if (parseInt(url.searchParams.get('tolerate'))) {
43
43
  this.options.tolerate = parseInt(url.searchParams.get('tolerate'));
44
44
  }
45
+ if (parseInt(url.searchParams.get('ttl'))) {
46
+ this.options.ttl = parseInt(url.searchParams.get('ttl'));
47
+ }
45
48
  if (url.searchParams.get('prefix')) {
46
49
  this.options.prefix = url.searchParams.get('prefix');
47
50
  }
@@ -88,7 +91,7 @@ class Client {
88
91
  await new Promise((resolve, reject) => {
89
92
  this.lockCallbacks[lockId] = (error) => {
90
93
  delete this.lockCallbacks[lockId];
91
- error ? reject(error) : resolve();
94
+ error ? reject(error) : resolve(null);
92
95
  };
93
96
  });
94
97
  return lockId;
@@ -146,7 +149,7 @@ class MultiplexSocket extends events_1.default.EventEmitter {
146
149
  case 'connected':
147
150
  this.connected = true;
148
151
  if (!this.pingTimer) {
149
- this.pingTimer = setInterval(this.ping, 20000);
152
+ this.pingTimer = setInterval(() => this.ping(), 20000);
150
153
  }
151
154
  this._onConnect();
152
155
  break;
@@ -171,9 +174,6 @@ class MultiplexSocket extends events_1.default.EventEmitter {
171
174
  }
172
175
  setImmediate(this.onPacket);
173
176
  };
174
- this.ping = () => {
175
- return this.send('ping', []);
176
- };
177
177
  this.host = host;
178
178
  this.port = port;
179
179
  this.debug = debug;
@@ -334,9 +334,16 @@ class MultiplexSocket extends events_1.default.EventEmitter {
334
334
  return this.send('unlock', [lockId]);
335
335
  }
336
336
  async status() {
337
+ if (!this.connected)
338
+ await this.connect();
337
339
  let status = await this.send('status', []);
338
340
  return JSON.parse(status);
339
341
  }
342
+ async ping() {
343
+ if (!this.connected)
344
+ await this.connect();
345
+ return (await this.send('ping', []));
346
+ }
340
347
  }
341
348
  function generateId() {
342
349
  return Math.random().toString(16).substr(2);
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "mlock-client",
3
- "version": "0.1.9",
4
- "description": "Maichong lock client for NodeJs",
5
- "author": "Liang <liang@maichong.it> (https://github.com/liangxingchen)",
6
- "homepage": "https://github.com/maichong/mlock/tree/master/packages/mlock#readme",
3
+ "version": "0.2.0",
4
+ "description": "Multi-resource distributed lock client for Node.js",
5
+ "author": {
6
+ "name": "Liang",
7
+ "email": "liang@miaomo.cn",
8
+ "url": "https://github.com/liangxingchen"
9
+ },
10
+ "homepage": "https://github.com/liangxingchen/mlock/tree/master/packages/mlock#readme",
7
11
  "license": "MIT",
8
12
  "main": "lib/index.js",
9
13
  "types": "index.d.ts",
@@ -13,19 +17,19 @@
13
17
  ],
14
18
  "repository": {
15
19
  "type": "git",
16
- "url": "git+https://github.com/maichong/mlock.git"
20
+ "url": "git+https://github.com/liangxingchen/mlock.git"
17
21
  },
18
22
  "scripts": {
19
23
  "build": "tsc"
20
24
  },
21
25
  "bugs": {
22
- "url": "https://github.com/maichong/mlock/issues"
26
+ "url": "https://github.com/liangxingchen/mlock/issues"
23
27
  },
24
28
  "dependencies": {
25
29
  "packet-wrapper": "^0.1.0"
26
30
  },
27
31
  "devDependencies": {
28
- "typescript": "^4.1.3"
32
+ "typescript": "^5.9.3"
29
33
  },
30
- "gitHead": "2f039368e09100e126f9aed9e8234630a426307d"
34
+ "gitHead": "fc9fbe81475b3fbdc8a54aff9015f152aa6f4edb"
31
35
  }