wok-server 0.3.4 → 0.4.1

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/dist/index.js CHANGED
@@ -11,3 +11,4 @@ tslib_1.__exportStar(require("./mvc"), exports);
11
11
  tslib_1.__exportStar(require("./mysql"), exports);
12
12
  tslib_1.__exportStar(require("./http-client"), exports);
13
13
  tslib_1.__exportStar(require("./mongodb"), exports);
14
+ tslib_1.__exportStar(require("./lock"), exports);
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLockManager = void 0;
4
+ const crypto_1 = require("crypto");
5
+ /**
6
+ * 锁管理器,主要用于将不确定的顺序且有冲突的异步操作顺序执行,
7
+ * 防止异步流程庞大穿插执行造成的数据混乱和错误,常见于请求的处理。
8
+ */
9
+ class ServerLockManager {
10
+ /**
11
+ * 存储锁信息的表,值是一个随机值,用于验证锁是否为当前程序所拥有
12
+ */
13
+ lockMap = new Map();
14
+ constructor() {
15
+ // 定期清理,将过期的信息移除,防止内存泄漏
16
+ setTimeout(() => {
17
+ const keysToBeDeleted = [];
18
+ const now = Date.now();
19
+ for (const entry of this.lockMap.entries()) {
20
+ const [key, info] = entry;
21
+ if (info.expiresAt < now) {
22
+ keysToBeDeleted.push(key);
23
+ }
24
+ }
25
+ if (keysToBeDeleted.length) {
26
+ for (const key of keysToBeDeleted) {
27
+ this.lockMap.delete(key);
28
+ }
29
+ }
30
+ }, 10000);
31
+ }
32
+ /**
33
+ * 尝试竞争锁,如果成功获取到锁,则执行 run 函数
34
+ * @param opts
35
+ * @returns 是否获取到锁
36
+ */
37
+ async tryLock(opts) {
38
+ const value = (0, crypto_1.randomUUID)().toString();
39
+ const expiresInSeconds = typeof opts.expiresInSeconds === 'number' && opts.expiresInSeconds > 0
40
+ ? opts.expiresInSeconds
41
+ : 60;
42
+ const expiresAt = Date.now() + expiresInSeconds * 1000;
43
+ const waitSeconds = typeof opts.waitSeconds === 'number' && opts.waitSeconds > 0 ? opts.waitSeconds : 0;
44
+ const res = await this.waitLock({
45
+ key: opts.key,
46
+ value,
47
+ expiresAt,
48
+ waitSeconds
49
+ });
50
+ if (!res) {
51
+ return false;
52
+ }
53
+ try {
54
+ await opts.run();
55
+ }
56
+ finally {
57
+ // 解锁
58
+ const info = this.lockMap.get(opts.key);
59
+ if (info && info.value === value) {
60
+ this.lockMap.delete(opts.key);
61
+ }
62
+ }
63
+ return true;
64
+ }
65
+ /**
66
+ * 等待锁
67
+ * @param opts
68
+ */
69
+ async waitLock(opts) {
70
+ let milliseconds = 0;
71
+ while (true) {
72
+ const info = this.lockMap.get(opts.key);
73
+ // 锁不存在或已经过期
74
+ if (!info || info.expiresAt < Date.now()) {
75
+ // 成功获取到锁
76
+ this.lockMap.set(opts.key, {
77
+ value: opts.value,
78
+ expiresAt: opts.expiresAt
79
+ });
80
+ return true;
81
+ }
82
+ if (info.value === opts.value) {
83
+ return true;
84
+ }
85
+ if (milliseconds >= opts.waitSeconds * 1000) {
86
+ break;
87
+ }
88
+ milliseconds = milliseconds + 100;
89
+ await this.sleep();
90
+ }
91
+ return false;
92
+ }
93
+ /**
94
+ * 沉睡100ms
95
+ * @returns
96
+ */
97
+ sleep() {
98
+ return new Promise((resolve, reject) => {
99
+ setTimeout(resolve, 100);
100
+ });
101
+ }
102
+ }
103
+ let lockManager;
104
+ /**
105
+ * 获取锁管理器。锁管理器提供一个简单的异步并发控制,用于将不确定的顺序的有冲突的异步操作顺序执行,
106
+ * 防止异步流程庞大穿插执行造成的数据混乱和错误,常见于请求的处理。
107
+ * @returns
108
+ */
109
+ function getLockManager() {
110
+ if (!lockManager) {
111
+ lockManager = new ServerLockManager();
112
+ }
113
+ return lockManager;
114
+ }
115
+ exports.getLockManager = getLockManager;
@@ -208,6 +208,9 @@ class StaticHandler {
208
208
  return true;
209
209
  }
210
210
  async endRespWithBuffer(response, buffer) {
211
+ if (response.writableEnded || response.destroyed) {
212
+ return;
213
+ }
211
214
  await new Promise((resolve, reject) => {
212
215
  response.write(buffer, err => {
213
216
  if (err) {
@@ -21,6 +21,7 @@ Nodejs 后端开发框架,简洁,易上手,功能够用。使用 Typescrip
21
21
  - [mongodb](./mongodb.md)
22
22
  - [MVC](./mvc.md)
23
23
  - [任务](./task.md)
24
+ - [锁](./lock.md)
24
25
  - [Http 客户端](./http-client.md)
25
26
  - [单元测试](./test.md)
26
27
  - [工程化](./engineering.md)
@@ -0,0 +1,53 @@
1
+ # 锁
2
+
3
+ 锁的作用是协调多个异步操作流程,使这些流程可以按顺序执行,在并发请求时会很有用。
4
+ 一个请求的处理过程中可能有很多个异步操作,并发情况下,不同请求中的操作就会穿插执行,
5
+ 如果有修改操作,有可能会造成错误,比如减库存和抽奖等。
6
+
7
+ ## 使用
8
+
9
+ 使用 getLockManager 函数可以获取锁管理对象,调用 tryLock 方法来尝试上锁并在成功获取锁后执行逻辑。
10
+
11
+ 简单的场景示例代码:
12
+
13
+ ```ts
14
+ import { getLockManager } from 'wok-server'
15
+
16
+ interface Form {
17
+ id: string
18
+ quantity: number
19
+ }
20
+ // 创建 json 主动处理器,模拟减库存请求
21
+ createJsonHandler<Form>({
22
+ validation: {
23
+ id: [notBlank()],
24
+ quantity: [notNull(), min(1), max(10)]
25
+ },
26
+ async handle(body, exchange) {
27
+ const lock = getLockManager()
28
+ const res = lock.tryLock({
29
+ // 锁的标识,标识相同产生竞争关系
30
+ key: `reduce-quantity-${body.id}`,
31
+ // 最多等待两秒,两秒内如果获取不到锁 tryLock 函数就返回 false
32
+ waitSeconds: 2,
33
+ // 锁的过期时间,这里设置最多只能拥有锁 600 秒,防止死锁
34
+ // 如果超时就可以被其它的程序获取到锁,不管 run 函数执行完没有
35
+ expiresInSeconds: 600,
36
+ // 执行函数,获取锁成功就会被执行
37
+ // 在请求并发的情况下,没有获取锁的请求处理则会等待,然后排队执行
38
+ async run() {
39
+ const product = await findProductById(body.id)
40
+ if (product.quantity < body.quantity) {
41
+ throw new BusinessException('库存不足')
42
+ }
43
+ await reduceQuantity(body.id,body.quantity)
44
+ }
45
+ })
46
+ // tryLock 返回的结果是 boolean 类型,表示此次是否成功获取到锁
47
+ // 没有获取到锁,给业务提示
48
+ if (!res) {
49
+ throw new BusinessException('系统繁忙,请稍后重试')
50
+ }
51
+ }
52
+ })
53
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wok-server",
3
- "version": "0.3.4",
3
+ "version": "0.4.1",
4
4
  "packageManager": "pnpm@8.9.0",
5
5
  "description": "一个基于 NodeJs 和 TypeScript 的后端框架,轻量级、克制、简洁。A lightweight, restrained, and concise backend framework based on Node.js and TypeScript.",
6
6
  "scripts": {
@@ -50,5 +50,8 @@
50
50
  "mysql2": "^3.11.0"
51
51
  },
52
52
  "main": "dist/index.js",
53
- "types": "types/index.d.ts"
53
+ "types": "types/index.d.ts",
54
+ "engines": {
55
+ "node": ">=16.0.0"
56
+ }
54
57
  }
package/types/index.d.ts CHANGED
@@ -8,3 +8,4 @@ export * from './mvc';
8
8
  export * from './mysql';
9
9
  export * from './http-client';
10
10
  export * from './mongodb';
11
+ export * from './lock';
@@ -0,0 +1,64 @@
1
+ export interface LockInfo {
2
+ /**
3
+ * 一个随机值,用于验证锁是否为当前程序所拥有
4
+ */
5
+ value: string;
6
+ /**
7
+ * 锁过期的时间
8
+ */
9
+ expiresAt: number;
10
+ }
11
+ /**
12
+ * 锁管理器,主要用于将不确定的顺序且有冲突的异步操作顺序执行,
13
+ * 防止异步流程庞大穿插执行造成的数据混乱和错误,常见于请求的处理。
14
+ */
15
+ declare class ServerLockManager {
16
+ /**
17
+ * 存储锁信息的表,值是一个随机值,用于验证锁是否为当前程序所拥有
18
+ */
19
+ private lockMap;
20
+ constructor();
21
+ /**
22
+ * 尝试竞争锁,如果成功获取到锁,则执行 run 函数
23
+ * @param opts
24
+ * @returns 是否获取到锁
25
+ */
26
+ tryLock(opts: {
27
+ /**
28
+ * 竞争锁的标识,相同的 key 会处于竞争关系,从而按顺序执行
29
+ */
30
+ key: string;
31
+ /**
32
+ * 等待秒数,如果没有获取锁,要等待的时间,单位秒。
33
+ * 不设置的情况下则不会等待,获取不到锁就立即返回。
34
+ */
35
+ waitSeconds?: number;
36
+ /**
37
+ * 运行函数,成功获取锁就会执行
38
+ * @returns
39
+ */
40
+ run: () => Promise<void>;
41
+ /**
42
+ * 过期时间,单位秒。目的是防止一个程序长期占用锁,导致其它程序获取不到锁一直不能被执行
43
+ * 导致的死锁问题。锁一旦过期,当前程序就不再拥有锁,其它程序就可以获取到锁。默认 60 秒。
44
+ */
45
+ expiresInSeconds?: number;
46
+ }): Promise<boolean>;
47
+ /**
48
+ * 等待锁
49
+ * @param opts
50
+ */
51
+ private waitLock;
52
+ /**
53
+ * 沉睡100ms
54
+ * @returns
55
+ */
56
+ private sleep;
57
+ }
58
+ /**
59
+ * 获取锁管理器。锁管理器提供一个简单的异步并发控制,用于将不确定的顺序的有冲突的异步操作顺序执行,
60
+ * 防止异步流程庞大穿插执行造成的数据混乱和错误,常见于请求的处理。
61
+ * @returns
62
+ */
63
+ export declare function getLockManager(): ServerLockManager;
64
+ export {};