原子锁
原子锁,也称为 mutex,用于同步对共享资源的访问。换句话说,它可以防止多个进程或并发代码同时执行某段代码。
AdonisJS 团队创建了一个与框架无关的包叫做 Verrou。@adonisjs/lock 包基于此包,所以请确保也阅读更详细的 Verrou 文档。
安装
使用以下命令安装和配置包:
node ace add @adonisjs/lock:::disclosure
使用检测到的包管理器安装
@adonisjs/lock包。在
adonisrc.ts文件中注册以下服务提供者。ts{ providers: [ // ...其他 providers () => import('@adonisjs/lock/lock_provider') ] }创建
config/lock.ts文件。在
start/env.ts文件中定义以下环境变量及其验证。tsLOCK_STORE=redis可选地,如果使用
database存储,则创建locks表的数据库迁移。
:::
配置
锁的配置存储在 config/lock.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 规则定义。
{
LOCK_STORE: Env.schema.enum(['redis', 'database', 'memory'] as const),
}Redis 存储
redis 存储对 @adonisjs/redis 包有对等依赖;因此,您必须在使用 Redis 存储之前配置此包。
以下是 Redis 存储接受的选项列表:
{
redis: stores.redis({
connectionName: 'main',
}),
}- connectionName
connectionName属性引用config/redis.ts文件中定义的连接。
数据库存储
database 存储对 @adonisjs/lucid 包有对等依赖,因此您必须在使用数据库存储之前配置此包。
以下是数据库存储接受的选项列表:
{
database: stores.database({
connectionName: 'postgres',
tableName: 'my_locks',
}),
}connectionName
引用
config/database.ts文件中定义的数据库连接。如果未定义,我们将使用默认数据库连接。tableName
用于存储速率限制的数据库表。
内存存储
memory 存储是一个简单的内存存储,可用于测试目的,但不仅限于此。有时,对于某些用例,您可能希望有一个仅对当前进程有效而不在多个进程之间共享的锁。
内存存储基于 async-mutex 包构建。
{
memory: stores.memory(),
}锁定资源
配置好锁存储后,您可以开始在应用程序的任何位置使用锁来保护资源。
以下是如何使用锁保护资源的简单示例。
:::codegroup
// 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()
}
}
}// 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 方法为特定锁使用不同的存储。
import locks from '@adonisjs/lock/services/main'
const lock = locks.use('redis').createLock('order.processing.1')否则,如果仅使用 default 存储,可以省略 use 方法。
import locks from '@adonisjs/lock/services/main'
const lock = locks.createLock('order.processing.1')跨多个进程管理锁
有时,您可能希望一个进程创建和获取锁,另一个进程释放它。例如,您可能希望在 web 请求中获取锁,并在后台作业中释放它。这可以使用 restoreLock 方法实现。
// 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()
})
}
}// 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 来实现此目的。
// title: .env.test
LOCK_STORE=memory创建自定义锁存储
首先,请确保查阅 Verrou 文档,其中更深入地介绍了创建自定义锁存储。在 AdonisJS 中,它几乎是相同的。
让我们创建一个简单的 Noop 存储,它不做任何事情。首先,我们必须创建一个实现 LockStore 接口的类。
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 将使用该函数创建存储实例。
function noopStore(options: MyNoopStoreConfig) {
return { driver: { factory: () => new NoopStore(options) } }
}使用自定义存储
完成后,您可以按如下方式使用 noopStore 函数:
import { defineConfig } from '@adonisjs/lock'
const lockConfig = defineConfig({
default: 'noop',
stores: {
noop: noopStore({}),
},
})