Two queue workers picking up the same shipment at the same moment will both mark it dispatched, and the customer gets the box twice.
Cache::lock()
already solves that, but you write the key format and the owner token yourself every time, and you remember to release it in a
finally
. Laravel Lock, by
Md Mahedi Zaman Zaber
, puts that behind a builder that takes an action name and a target, and stores the lock in either the cache or a database table.
Main features
- Fluent builder
:
Lock::for('shipment_dispatch', $shipment)->ttl(120)->acquire()returns a boolean, andblock()wraps a callback so you never write the release. - Model-scoped locks
: 这
HasLockstrait adds$shipment->lock('dispatch'), and the key includes the model's morph class and primary key. - 路由中间件
: A
lockalias that takes the lock before your controller runs and releases it afterwards, even when the controller throws. - Two storage drivers
:
cachefor any Laravel cache store,database对于lockstable that survives a cache flush. - Waiting
:
acquire()和block()both take a number of seconds to keep retrying before they give up. - Lock inspection
: A readonly
LockInfoobject with the key, owner token and expiry, plus helpers likeremainingSeconds()和isOwnedBy()。
Acquiring and Releasing Locks
这
Lock
facade builds a pending lock from an action string and an optional target. Keep that builder in a variable, because the release has to come from the same instance:
使用
ZaberDev\Lock\Facades\Lock
;$lock
=
锁
::
为了
(
'shipment_dispatch'
, $shipment)
->
生存时间
(
120
(英文):如果
($lock
->
获得
()){
尝试
{$carrier
->
派遣
($shipment);}
最后
{$lock
->
发布
();}}
Each builder makes its own UUID owner token the first time it needs one, and both drivers check that token before they delete anything. Build a second
Lock::for(...)
并调用
release()
on that instead, and it carries a different token, so the release does nothing and hands back
false
. You can set the token yourself with
owner('worker-7')
when the acquire and the release happen in different processes.
The default TTL is 60 seconds. There is
forSeconds()
和
forMinutes()
next to
ttl()
, 和
refresh()
extends a lock you still hold without releasing it first.
block()
does the same job in one call and returns whatever the callback returns:
$manifest
=
锁
::
为了
(
'shipment_dispatch'
, $shipment)
->
堵塞
(
功能
()
使用
($shipment, $carrier) {
返回
$carrier
->
派遣
($shipment);});
If the lock is already held,
block()
抛出
LockAcquisitionException
rather than returning null, and the exception carries the
LockInfo
for the lock that is holding it up. In a queue job that means the job fails instead of quietly skipping the work.
Both methods can wait instead of failing straight away.
acquire()
takes the number of seconds to keep trying, and
block()
takes it as a third argument, with a 250 millisecond pause between attempts:
$lock
->
获得
(
blockSeconds
:
5
(英文):锁
::
为了
(
'stock_allocation'
, $warehouse)
->
堵塞
($callback,
60
,
5
(英文):
To look without acquiring, there is
isLocked()
,
isOwnedByCurrent()
,
remaining()
和
info()
, 和
enforce()
throws if someone else holds the lock.
forceRelease()
deletes the record no matter who owns it, for cleaning up after a worker that died holding one.
Locking a Model
添加
HasLocks
trait to a model and the target is filled in for you:
使用
照亮\数据库\雄辩\模型
;使用
ZaberDev\Lock\HasLocks
;班级
Shipment
延伸
模型{
使用
HasLocks
;}
$lock
=
$shipment
->
锁
(
'dispatch'
)
->
生存时间
(
120
(英文):$shipment
->
isLocked
(
'dispatch'
(英文):$shipment
->
forceReleaseLock
(
'dispatch'
(英文):
The key is the action, then the morph class with backslashes swapped for underscores, then the primary key:
dispatch:App_Models_Shipment:42
. Register a morph map and you get the shorter alias instead. Scalar targets work too, so
Lock::for('stock_allocation', $sku)
gives you
stock_allocation:SKU-1180
。
For a target that is not an Eloquent model, there is a
Lockable
interface with one method,
getLockTargetIdentifier(): string
. Implement it on a value object and that string becomes the second half of the key.
On the database driver,
HasLocks
also gives you a
locks()
morph relationship pointing at the
locks
table, for listing what a model currently holds:
$shipment
->
locks
()
->
在哪里
(
'expires_at'
,
'>'
,
现在
())
->
得到
();
Protecting a Route
The service provider registers a
lock
middleware alias. Pass it the action, a TTL in seconds, and a driver if you do not want the default:
路线
::
邮政
(
'/warehouse/reconcile'
,[
ReconcileController
::班级
,
'店铺'
])
->
中间件
(
'lock:warehouse_reconcile,300'
(英文):路线
::
邮政
(
'/warehouse/reindex'
,[
ReindexController
::班级
,
'店铺'
])
->
中间件
(
'lock:warehouse_reindex,600,database'
(英文):
That form passes no target, so the lock covers the endpoint for everyone. One reconcile runs at a time no matter who started it.
To scope the lock to a single record, put a route parameter in the action name. The middleware swaps
{shipment}
for that parameter's value, or for its primary key when the parameter is a bound model. The README does not mention this form, but the package's tests use it:
路线
::
邮政
(
'/shipments/{shipment}/dispatch'
,[
ShipmentController
::班级
,
'dispatch'
])
->
中间件
(
'lock:shipment_dispatch:{shipment},60'
(英文):
A second request for the same shipment is rejected while the first one is still running. Requests for different shipments do not see each other at all. The middleware does not wait, so there is no queueing here, only a rejection.
When the lock is already held, the middleware throws
LockAcquisitionException
before the controller runs. It extends
RuntimeException
rather than one of Laravel's HTTP exceptions, so an unhandled one reaches the client as a 500. The exception code is 423, matching the
423 Locked
status, but nothing applies that to the response for you. Handle the exception to pick the status the caller gets:
使用
ZaberDev\Lock\Exceptions\LockAcquisitionException
;$异常
->
使成为
(
功能
(
LockAcquisitionException
$e) {
返回
回复
()
->
json
([
'信息'
=>
'Already processing. Try again in a moment.'
,
'retry_after'
=>
$e
->
lockInfo
?->
remainingSeconds
(),],
429
(英文):});
The release happens in a
finally
around
$next($request)
. A validation error or an unhandled exception releases the lock the same way a 200 response does.
Cache or Database Storage
config/locks.php
sets the default driver through the
LOCK_DRIVER
environment variable, and
using()
switches it for a single lock:
锁
::
为了
(
'inventory_sync'
, $warehouse)
->
使用
(
'缓存'
)
->
生存时间
(
15
)
->
获得
();锁
::
为了
(
'stock_reconciliation'
, $warehouse)
->
使用
(
'数据库'
)
->
生存时间
(
600
)
->
获得
();
The cache driver stores a small payload under a
lock:
prefix and uses
Cache::add()
for atomicity, the same primitive behind
Laravel's atomic cache locks
. It is the faster of the two and fine for short locks on Redis or Memcached.
The database driver writes a row to a
locks
table with a unique
key
column, and every acquire runs in a transaction that selects with
lockForUpdate()
before it inserts. Those rows are still there after a cache flush or a Redis restart, and you can query them with Eloquent. You pay a write and a row lock for every acquisition.
Expired rows go away two ways. Reading an expired lock deletes it, and
LockModel
uses Laravel's
Prunable
trait for the rest:
使用
照明\支持\立面\日程安排
;使用
ZaberDev\Lock\Models\LockModel
;日程
::
命令
(
'model:prune'
,[
'--model'
=>
LockModel
::班级
])
->
日常的
();
Three events fire when
locks.events.dispatch
is on:
LockAcquired
with the key, owner, TTL, expiry and
LockInfo
,
LockFailed
with the key, owner and whatever lock got in the way, and
LockReleased
和
$forced
flag that tells a normal release apart from a
forceRelease()
. Listening for
LockFailed
tells you which actions in your app actually contend, which is useful data when you are debugging
race conditions
。
LockManager
extends Laravel's
Manager
class, so you register your own driver with
Lock::extend()
:
锁
::
延长
(
'dynamodb'
,
fn
($app) =>
新的
DynamoLockDriver
($app[
'dynamodb'
]));
安装
Laravel Lock needs PHP 8.2 or newer and supports Laravel 11, 12, and 13:
作曲家
要求
zaber-dev/laravel-lock
The config and migration publish under separate tags, and you only need the migration for the database driver:
php
工匠
供应商:发布
--tag=locks-configphp
工匠
供应商:发布
--tag=locks-migrationsphp
工匠
迁移
The package also ships a
Laravel Boost
skill covering its API patterns. It publishes under the
locks-skill
tag, and the service provider copies it to
.ai/skills/laravel-lock
during an Artisan command if it finds Boost installed or an existing
.ai/skills
目录。
Source and documentation are on the Laravel Lock GitHub repository 。







