原子锁(Atomic Locks)
原子锁,也称为 mutex(互斥锁),用于同步对共享资源的访问。换句话说,它可以防止多个进程或并发代码同时执行某一段代码。
AdonisJS 团队创建了一个与框架无关的包,名为 Verrou。@adonisjs/lock 包基于此包,因此请务必同时阅读更为详细的 Verrou 文档。
安装
使用以下命令安装并配置该包:
node ace add @adonisjs/lock
-
使用检测到的包管理器安装
@adonisjs/lock包。 -
在
adonisrc.ts文件中注册以下服务提供者。{providers: [// ...other providers() => import('@adonisjs/lock/lock_provider')]} -
创建
config/lock.ts文件。 -
在
start/env.ts文件中定义以下环境变量及其校验规则。LOCK_STORE=redis -
如果使用
databasestore,可选地为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
-
用于管理锁的
defaultstore。该 store 在同一配置文件的stores对象中定义。 -
stores
-
你计划在应用中使用的 store 集合。我们建议始终配置一个可在测试期间使用的
memorystore。
环境变量
默认的锁 store 使用 LOCK_STORE 环境变量定义,因此你可以在不同环境中切换不同的 store。例如,在测试期间使用 memory store,在开发和生产环境中使用 redis store。
此外,必须对该环境变量进行校验,以只允许使用预配置的 store 之一。校验在 start/env.ts 文件中使用 Env.schema.enum 规则定义。
{
LOCK_STORE: Env.schema.enum(['redis', 'database', 'memory'] as const),
}
Redis store
redis store 对 @adonisjs/redis 包有一个对等依赖(peer dependency);因此,在使用 Redis store 之前,你必须先配置该包。
以下是 Redis store 接受的选项列表:
{
redis: stores.redis({
connectionName: 'main',
}),
}
- connectionName
-
connectionName属性引用在config/redis.ts文件中定义的连接。
Database store
database store 对 @adonisjs/lucid 包有一个对等依赖,因此,在使用 database store 之前,你必须先配置该包。
以下是 database store 接受的选项列表:
{
database: stores.database({
connectionName: 'postgres',
tableName: 'my_locks',
}),
}
-
connectionName
-
对
config/database.ts文件中定义的数据库连接的引用。如果未定义,我们将使用默认的数据库连接。 -
tableName
-
用于存储限流数据的数据库表。
Memory store
memory store 是一个简单的内存 store,不仅可用于测试目的。有时,在某些用例中,你可能希望拥有一个仅对当前进程有效、不跨多个进程共享的锁。
memory store 构建于 async-mutex 包之上。
{
memory: stores.memory(),
}
锁定资源
一旦配置好锁 store,你就可以开始在应用的任何地方使用锁来保护你的资源。
以下是一个如何使用锁来保护资源的简单示例。
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')
/**
* Try to acquire the lock immediately ( without retrying )
*/
const lock = locks.createLock(`order.processing.${orderId}`)
const acquired = await lock.acquireImmediately()
if (!acquired) {
return 'Order is already being processed'
}
/**
* Lock has been acquired. We can process the order
*/
try {
await processOrder()
return 'Order processed successfully'
} finally {
/**
* Always release the lock using the `finally` block, so that
* we are sure that the lock is released even when an exception
* is thrown during the processing.
*/
await lock.release()
}
}
}
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')
/**
* Will run the function only if lock is available
* Lock will also be automatically released once the function
* has been executed
*/
const [executed, result] = await locks
.createLock(`order.processing.${orderId}`)
.runImmediately(async (lock) => {
/**
* Lock has been acquired. We can process the order
*/
await processOrder()
return 'Order processed successfully'
})
/**
* Lock could not be acquired and function was not executed
*/
if (!executed) return 'Order is already being processed'
return result
}
}
这是一个如何在应用中使用锁的快速示例。
还有许多其他可用的方法来管理锁,例如用于延长锁持续时间的 extend、用于获取锁过期前剩余时间的 getRemainingTime、配置锁的各种选项等等。
为此,请务必阅读 Verrou 文档 以获取更多细节。提醒一下,@adonisjs/lock 包基于 Verrou 包,因此你在 Verrou 文档中读到的所有内容同样适用于 @adonisjs/lock 包。
使用另一个 store
如果你在 config/lock.ts 文件中定义了多个 store,你可以使用 use 方法为特定的锁使用不同的 store。
import locks from '@adonisjs/lock/services/main'
const lock = locks.use('redis').createLock('order.processing.1')
否则,如果只使用 default store,你可以省略 use 方法。
import locks from '@adonisjs/lock/services/main'
const lock = locks.createLock('order.processing.1')
跨多个进程管理锁
有时,你可能希望由一个进程创建并获取锁,而由另一个进程释放它。例如,你可能希望在 Web 请求中获取锁,并在后台任务中释放它。这可以通过 restoreLock 方法实现。
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()
/**
* Dispatch a background job to process the order.
*
* We also pass the serialized lock to the job, so that the job
* can release the lock once the order has been processed.
*/
queue.dispatch('app/jobs/process_order', {
lock: lock.serialize()
})
}
}
import locks from '@adonisjs/lock/services/main'
export class ProcessOrder {
async handle({ lock }) {
/**
* We are restoring the lock from the serialized version
*/
const handle = locks.restoreLock(lock)
/**
* Process the order
*/
await processOrder()
/**
* Release the lock
*/
await handle.release()
}
}
测试
在测试期间,你可以使用 memory store 以避免发起真实的网络请求来获取锁。你可以通过在 .env.testing 文件中将 LOCK_STORE 环境变量设置为 memory 来实现这一点。
LOCK_STORE=memory
创建自定义锁 store
首先,请务必查阅深入介绍如何创建自定义锁 store 的 Verrou 文档。在 AdonisJS 中,做法基本相同。
让我们创建一个什么都不做的简单 Noop store。首先,我们必须创建一个实现 LockStore 接口的类。
import type { LockStore } from '@adonisjs/lock/types'
class NoopStore implements LockStore {
/**
* Save the lock in the store.
* This method should return false if the given key is already locked
*
* @param key The key to lock
* @param owner The owner of the lock
* @param ttl The time to live of the lock in milliseconds. Null means no expiration
*
* @returns True if the lock was acquired, false otherwise
*/
async save(key: string, owner: string, ttl: number | null): Promise<boolean> {
return false
}
/**
* Delete the lock from the store if it is owned by the given owner
* Otherwise should throws a E_LOCK_NOT_OWNED error
*
* @param key The key to delete
* @param owner The owner
*/
async delete(key: string, owner: string): Promise<void> {
return false
}
/**
* Force delete the lock from the store without checking the owner
*/
async forceDelete(key: string): Promise<Void> {
return false
}
/**
* Check if the lock exists. Returns true if so, false otherwise
*/
async exists(key: string): Promise<boolean> {
return false
}
/**
* Extend the lock expiration. Throws an error if the lock is not owned by
* the given owner
* Duration is in milliseconds
*/
async extend(key: string, owner: string, duration: number): Promise<void> {
return false
}
}
定义 store 工厂函数
一旦创建好 store,你必须定义一个简单的工厂函数,供 @adonisjs/lock 用于创建你的 store 实例。
function noopStore(options: MyNoopStoreConfig) {
return { driver: { factory: () => new NoopStore(options) } }
}
使用自定义 store
完成后,你可以按如下方式使用 noopStore 函数:
import { defineConfig } from '@adonisjs/lock'
const lockConfig = defineConfig({
default: 'noop',
stores: {
noop: noopStore({}),
},
})