symfony/lock
Symfony Lock component creates and manages locks to ensure exclusive access to shared resources. Provides a unified API with multiple storage backends (e.g., filesystem, Redis, PDO) for preventing race conditions in concurrent apps.
composer require symfony/lock
LockFactory in Laravel:
Add to config/app.php under providers:
Symfony\Component\Lock\LockFactory::class,
Bind it in AppServiceProvider:
public function register()
{
$this->app->singleton(\Symfony\Component\Lock\LockFactory::class, function ($app) {
return new \Symfony\Component\Lock\LockFactory([
new \Symfony\Component\Lock\Store\RedisStore($app['redis']),
]);
});
}
use Symfony\Component\Lock\LockFactory;
class ProcessPaymentJob implements ShouldQueue
{
public function __construct(private LockFactory $lockFactory) {}
public function handle()
{
$lock = $this->lockFactory->createLock('payment_' . $this->paymentId, 30);
$lock->acquire(function () {
// Critical section (e.g., update inventory, charge payment)
});
}
}
LockFactory: Central interface for creating locks with different stores.LockInterface: Core methods (acquire(), release(), isAcquired()).Store\*Store (e.g., RedisStore, PdoStore) for your infrastructure.Use for ensuring only one process executes a critical section (e.g., webhook handling, report generation):
$lock = $lockFactory->createLock('webhook_' . $eventId, 60);
$lock->acquire(function () use ($event) {
// Process event (e.g., update database, send notifications)
});
Use for checking availability without waiting (e.g., rate limiting, cache invalidation):
$lock = $lockFactory->createLock('cache_warmup', 10);
if ($lock->acquireNow()) {
// Only one process proceeds
$lock->release();
}
Wrap lock acquisition/release in a try-finally block:
$lock = $lockFactory->createLock('inventory_update', 30);
$lock->acquire(function () {
// Critical section
}); // Auto-releases on exit
Or use a custom trait:
trait UsesLocks
{
protected function withLock(string $name, int $ttl, Closure $callback)
{
$lock = $this->lockFactory->createLock($name, $ttl);
$lock->acquire($callback);
}
}
$store = new RedisStore($redisClient);
$lock = $lockFactory->createLock('redis_key', 60, $store);
Optimization: Use RedisStore with Lua scripts for atomic operations.$store = new PdoStore($pdo, 'locks_table');
Tip: Create the table with engine=InnoDB (MySQL) for row-level locking.$store = new FlockStore('/tmp/locks');
Caveat: Not reliable across network mounts (use for local dev only).Inject LockFactory into jobs and use acquire() in handle():
class ExportReportJob implements ShouldQueue
{
public function __construct(private LockFactory $lockFactory) {}
public function handle()
{
$this->lockFactory->createLock('export_report', 3600)
->acquire(fn () => $this->generateReport());
}
}
Protect commands from overlapping execution:
class GenerateSitemapCommand extends Command
{
protected $signature = 'sitemap:generate';
protected $description = 'Generate sitemap.xml';
public function handle(LockFactory $lockFactory)
{
$lock = $lockFactory->createLock('sitemap_generation', 1800);
$lock->acquire(function () {
// Generate and save sitemap
});
}
}
Prevent duplicate processing in failed() or retry() logic:
$lock = $lockFactory->createLock('retry_' . $jobId, 60);
if ($lock->acquireNow()) {
$this->retryJob();
$lock->release();
}
Use unique identifiers (e.g., user_{id}_action):
$lock = $lockFactory->createLock("user_{$userId}_update_profile", 30);
Configure multiple stores and fall back gracefully:
$lockFactory = new LockFactory([
new RedisStore($redis),
new FlockStore('/tmp/locks'), // Fallback
]);
Deadlocks
X and waits for Y, while Job B locks Y and waits for X).LockInterface::isAcquired() to check lock status before acquiring.Lock Expiration Too Short
Store-Specific Issues
RedisStore with retryOnFailure().InnoDB deadlocks can occur. Add FOR UPDATE hints in custom queries.Key Collisions
LockKeyNormalizer (Symfony 7.4+) or prefix keys:
$lock = $lockFactory->createLock("app:{$uniqueId}", 30);
Memory Leaks
try-finally or Laravel’s illuminate/support ensureClosed():
$lock->acquire(function () use ($lock) {
try {
// Work
} finally {
$lock->release(); // Ensure release
}
});
if ($lock->isAcquired()) {
echo "Lock held by PID: " . $lock->getOwnerPid();
}
redis-cli monitor # Watch lock commands in real-time
redis-cli ttl <key> # Check remaining TTL
SELECT * FROM locks_table WHERE name = 'your_lock';
$lockFactory = new LockFactory([], [
'logger' => $this->app->make(\Psr\Log\LoggerInterface::class),
]);
Redis Connection
$redis = new \Redis();
$redis->pconnect('127.0.0.1');
$store = new RedisStore($redis, 'locks', 0, 0, false); // disablePipelining = true
PDO Table Schema
ENGINE=InnoDB (MySQL) for row-level locking:
CREATE
How can I help you explore Laravel packages today?