Skip to content

原子锁

原子锁,也称为 mutex,用于同步对共享资源的访问。换句话说,它可以防止多个进程或并发代码同时执行某段代码。

AdonisJS 团队创建了一个与框架无关的包叫做 Verrou@adonisjs/lock 包基于此包,所以请确保也阅读更详细的 Verrou 文档

安装

使用以下命令安装和配置包:

sh
node ace add @adonisjs/lock

:::disclosure

  1. 使用检测到的包管理器安装 @adonisjs/lock 包。

  2. adonisrc.ts 文件中注册以下服务提供者。

    ts
    {
      providers: [
        // ...其他 providers
        () => import('@adonisjs/lock/lock_provider')
      ]
    }
  3. 创建 config/lock.ts 文件。

  4. start/env.ts 文件中定义以下环境变量及其验证。

    ts
    LOCK_STORE=redis
  5. 可选地,如果使用 database 存储,则创建 locks 表的数据库迁移。

:::

配置

锁的配置存储在 config/lock.ts 文件中。

ts
import env from '#start/env'
import { defineConfig, stores } from '@adonisjs/lock'

const lockConfig = defineConfig({
  default: env.get('LOCK_STORE'),
  stores: {
    redis: stores.redis({}),

    database: stores.database({
      tableName: 'locks',
    }),

    memory: stores.memory()
  },
})

export default lockConfig

declare module '@adonisjs/lock/types' {
  export interface LockStoresList extends InferLockStores<typeof lockConfig> {}
}

default

用于管理锁的 default 存储。存储在同一配置文件的 stores 对象中定义。

stores

您计划在应用程序中使用的存储集合。我们建议始终配置 memory 存储,可在测试期间使用。


环境变量

默认锁存储使用 LOCK_STORE 环境变量定义,因此您可以在不同环境中切换不同的存储。例如,在测试期间使用 memory 存储,在开发和生产中使用 redis 存储。

此外,环境变量必须经过验证以允许预配置的存储之一。验证在 start/env.ts 文件中使用 Env.schema.enum 规则定义。

ts
{
  LOCK_STORE: Env.schema.enum(['redis', 'database', 'memory'] as const),
}

Redis 存储

redis 存储对 @adonisjs/redis 包有对等依赖;因此,您必须在使用 Redis 存储之前配置此包。

以下是 Redis 存储接受的选项列表:

ts
{
  redis: stores.redis({
    connectionName: 'main',
  }),
}
connectionName

connectionName 属性引用 config/redis.ts 文件中定义的连接。

数据库存储

database 存储对 @adonisjs/lucid 包有对等依赖,因此您必须在使用数据库存储之前配置此包。

以下是数据库存储接受的选项列表:

ts
{
  database: stores.database({
    connectionName: 'postgres',
    tableName: 'my_locks',
  }),
}

connectionName

引用 config/database.ts 文件中定义的数据库连接。如果未定义,我们将使用默认数据库连接。

tableName

用于存储速率限制的数据库表。

内存存储

memory 存储是一个简单的内存存储,可用于测试目的,但不仅限于此。有时,对于某些用例,您可能希望有一个仅对当前进程有效而不在多个进程之间共享的锁。

内存存储基于 async-mutex 包构建。

ts
{
  memory: stores.memory(),
}

锁定资源

配置好锁存储后,您可以开始在应用程序的任何位置使用锁来保护资源。

以下是如何使用锁保护资源的简单示例。

:::codegroup

ts
// title: 手动锁定
import { errors } from '@adonisjs/lock'
import locks from '@adonisjs/lock/services/main'
import { HttpContext } from '@adonisjs/core/http'

export default class OrderController {
  async process({ response, request }: HttpContext) {
    const orderId = request.input('order_id')

    /**
     * 尝试立即获取锁(不重试)
     */
    const lock = locks.createLock(`order.processing.${orderId}`)
    const acquired = await lock.acquireImmediately()
    if (!acquired) {
      return 'Order is already being processed'
    }

    /**
     * 已获取锁。我们可以处理订单
     */
    try {
      await processOrder()
      return 'Order processed successfully'
    } finally {
      /**
       * 始终使用 `finally` 块释放锁,以便
       * 我们确保即使在处理过程中抛出异常也会释放锁。
       */
      await lock.release()
    }
  }
}
ts
// title: 自动锁定
import { errors } from '@adonisjs/lock'
import locks from '@adonisjs/lock/services/main'
import { HttpContext } from '@adonisjs/core/http'

export default class OrderController {
  async process({ response, request }: HttpContext) {
    const orderId = request.input('order_id')

    /**
     * 仅当锁可用时才运行函数
     * 函数执行后也会自动释放锁
     */
    const [executed, result] = await locks
      .createLock(`order.processing.${orderId}`)
      .runImmediately(async (lock) => {
        /**
         * 已获取锁。我们可以处理订单
         */
        await processOrder()
        return 'Order processed successfully'
      })

    /**
     * 无法获取锁,函数未执行
     */
    if (!executed) return 'Order is already being processed'

    return result
  }
}

:::

这是如何在应用程序中使用锁的快速示例。

还有许多其他方法可用于管理锁,例如用于延长锁持续时间的 extend、获取锁过期前剩余时间的 getRemainingTime、配置锁的选项等。

为此,请确保阅读 Verrou 文档以获取更多详细信息。提醒一下,@adonisjs/lock 包基于 Verrou 包,因此您在 Verrou 文档中阅读的所有内容也适用于 @adonisjs/lock 包。

使用其他存储

如果您在 config/lock.ts 文件中定义了多个存储,可以使用 use 方法为特定锁使用不同的存储。

ts
import locks from '@adonisjs/lock/services/main'

const lock = locks.use('redis').createLock('order.processing.1')

否则,如果仅使用 default 存储,可以省略 use 方法。

ts
import locks from '@adonisjs/lock/services/main'

const lock = locks.createLock('order.processing.1')

跨多个进程管理锁

有时,您可能希望一个进程创建和获取锁,另一个进程释放它。例如,您可能希望在 web 请求中获取锁,并在后台作业中释放它。这可以使用 restoreLock 方法实现。

ts
// title: 您的主服务器
import locks from '@adonisjs/lock/services/main'

export class OrderController {
  async process({ response, request }: HttpContext) {
    const orderId = request.input('order_id')

    const lock = locks.createLock(`order.processing.${orderId}`)
    await lock.acquire()

    /**
     * 分派后台作业来处理订单。
     * 
     * 我们还将序列化的锁传递给作业,以便作业
     * 可以在订单处理完成后释放锁。
     */
    queue.dispatch('app/jobs/process_order', {
      lock: lock.serialize()
    })
  }
}
ts
// title: 您的后台作业
import locks from '@adonisjs/lock/services/main'

export class ProcessOrder {
  async handle({ lock }) {
    /**
     * 我们从序列化版本恢复锁
     */
    const handle = locks.restoreLock(lock)

    /**
     * 处理订单
     */
    await processOrder()

    /**
     * 释放锁
     */
    await handle.release()
  }
}

测试

在测试期间,您可以使用 memory 存储以避免发出真实的网络请求来获取锁。您可以通过在 .env.testing 文件中将 LOCK_STORE 环境变量设置为 memory 来实现此目的。

env
// title: .env.test
LOCK_STORE=memory

创建自定义锁存储

首先,请确保查阅 Verrou 文档,其中更深入地介绍了创建自定义锁存储。在 AdonisJS 中,它几乎是相同的。

让我们创建一个简单的 Noop 存储,它不做任何事情。首先,我们必须创建一个实现 LockStore 接口的类。

ts
import type { LockStore } from '@adonisjs/lock/types'

class NoopStore implements LockStore {
  /**
   * 将锁保存到存储中。
   * 如果给定的键已被锁定,此方法应返回 false
   *
   * @param key 要锁定的键
   * @param owner 锁的所有者
   * @param ttl 锁的生存时间(以毫秒为单位)。Null 表示无过期
   *
   * @returns 如果获取了锁则返回 True,否则返回 false
   */
  async save(key: string, owner: string, ttl: number | null): Promise<boolean> {
    return false
  }

  /**
   * 如果锁由给定所有者拥有,则从存储中删除锁
   * 否则应抛出 E_LOCK_NOT_OWNED 错误
   *
   * @param key 要删除的键
   * @param owner 所有者
   */
  async delete(key: string, owner: string): Promise<void> {
    return false
  }

  /**
   * 强制从存储中删除锁而不检查所有者
   */
  async forceDelete(key: string): Promise<Void> {
    return false
  }

  /**
   * 检查锁是否存在。如果存在返回 true,否则返回 false
   */
  async exists(key: string): Promise<boolean> {
    return false
  }

  /**
   * 延长锁过期时间。如果锁不由给定所有者拥有则抛出错误
   * 持续时间以毫秒为单位
   */
  async extend(key: string, owner: string, duration: number): Promise<void> {
    return false
  }
}

定义存储工厂

创建存储后,您必须定义一个简单的工厂函数,@adonisjs/lock 将使用该函数创建存储实例。

ts
function noopStore(options: MyNoopStoreConfig) {
  return { driver: { factory: () => new NoopStore(options) } }
}

使用自定义存储

完成后,您可以按如下方式使用 noopStore 函数:

ts
import { defineConfig } from '@adonisjs/lock'

const lockConfig = defineConfig({
  default: 'noop',
  stores: {
    noop: noopStore({}),
  },
})