VOOZH about

URL: https://deepwiki.com/hypervel/cache/4.2-distributed-locking

⇱ Distributed Locking | hypervel/cache | DeepWiki


Loading...
Menu

Distributed Locking

Purpose and Scope

The distributed locking system provides coordination primitives for preventing race conditions across multiple processes, servers, or workers. Locks are obtained through cache stores that implement the LockProvider interface, enabling critical sections and mutual exclusion patterns with configurable TTLs and automatic expiration.

This document covers lock acquisition, release, ownership verification, TTL refresh operations, and the five concrete lock implementations. For cache storage mechanisms, see Cache Stores. For framework integration and dependency injection of lock providers, see Framework Integration.

Lock Lifecycle and Core Operations

All lock implementations extend a base Lock class and implement common lifecycle operations through the RefreshableLock interface. The lock lifecycle consists of acquisition, optional refresh operations, and explicit or automatic release.


Lock Lifecycle State Transitions

Core Lock Operations

OperationReturn TypeDescription
acquire()boolAttempts to obtain the lock, returns true if successful
release()boolReleases the lock if owned by current process
forceRelease()voidUnconditionally releases the lock regardless of ownership
refresh(?int $seconds)boolExtends the lock's TTL if owned by current process
getRemainingLifetime()?floatReturns seconds until expiration, or null if expired/permanent

The acquire() method returns false if the lock is already held by another process or if the current lock has not expired. All implementations guarantee that a lock can only be released by its owner, except through forceRelease() which bypasses ownership checks.

Sources: src/DatabaseLock.php75-105 src/RedisLock.php29-38 src/FileStore.php143-155 src/Contracts/RefreshableLock.php

Lock Implementations

The system provides five lock implementations with different storage backends, each offering specific trade-offs between performance, durability, and distribution capabilities.


Lock Provider Architecture

Sources: src/FileStore.php10 src/DatabaseLock.php10 src/RedisLock.php8

RedisLock

RedisLock provides distributed locking across multiple servers using Redis as the coordination backend. Lock operations use atomic Redis commands with optional TTL for automatic expiration.

Acquisition Strategy:

Atomic Operations: Release and refresh operations use Lua scripts to ensure atomicity by verifying ownership before modification.


The LuaScripts class provides pre-defined Lua scripts for releaseLock() and refreshLock() operations src/LuaScripts.php15-42

TTL Inspection: getRemainingLifetime() uses Redis TTL command, returning null for non-existent keys (TTL=-2) or permanent locks (TTL=-1) src/RedisLock.php90-100

Sources: src/RedisLock.php1-101 src/LuaScripts.php1-43

DatabaseLock

DatabaseLock stores lock state in SQL tables, providing ACID guarantees through database transactions. Suitable for applications already using database caching where Redis infrastructure is unavailable.

Table Schema Requirements: Lock records contain three columns: key, owner, and expiration src/DatabaseLock.php80-84

Acquisition Logic:

  1. Attempt to INSERT new lock record src/DatabaseLock.php80-86
  2. On conflict (duplicate key), UPDATE if owned by current process OR expired src/DatabaseLock.php88-98
  3. Returns true if either operation affected rows

Expiration Cleanup: Uses lottery-based probabilistic cleanup to remove expired locks. On each acquisition, with probability lottery[0]/lottery[1] (default 2/100), expired records are deleted src/DatabaseLock.php100-102

Permanent Locks: When seconds=0, uses defaultTimeoutInSeconds (default 86400 = 24 hours) to prevent indefinite database growth src/DatabaseLock.php145-150

Sources: src/DatabaseLock.php1-203

FileLock

FileLock uses filesystem-based locking through the FileStore implementation, leveraging OS-level file locking mechanisms. Suitable for single-server deployments with shared filesystem access.

The FileStore creates FileLock instances through its lock() method, optionally using a separate lockDirectory for lock files src/FileStore.php145-155

Locking Mechanism: Lock acquisition in FileStore.add() uses LockableFile with exclusive file locks via getExclusiveLock() src/FileStore.php87-95 This provides atomic read-check-write operations through OS file locking primitives.

Sources: src/FileStore.php143-163 src/FileStore.php83-112

ArrayLock

ArrayLock provides in-memory locking within a single PHP process, primarily for testing and development. Lock state is stored in ArrayStore::$locks array src/ArrayLock.php39-42

Expiration Handling: Uses Carbon instances to track lock expiration. Permanent locks (seconds=0) store null as expiration src/ArrayLock.php41 Expired locks are checked by comparing stored expiresAt timestamp with current time src/ArrayLock.php33-36

Refresh Implementation: Updates the expiresAt field in the locks array after verifying ownership src/ArrayLock.php117

Sources: src/ArrayLock.php1-143

NoLock

NoLock is a no-op implementation that always succeeds, used when locking is not required or the store backend does not support locking src/NoLock.php1-73

All operations return success values:

Sources: src/NoLock.php1-73

Atomic Guarantees

Lock implementations provide different atomicity guarantees based on their underlying storage mechanisms:

ImplementationAtomicity MechanismGuarantee Level
RedisLockLua scripts + Redis single-threaded executionAtomic across distributed systems
DatabaseLockSQL transactions + unique key constraintsACID compliant within database
FileLockOS file locking primitivesAtomic within shared filesystem
ArrayLockSingle-threaded PHP processAtomic within process only
NoLockNoneNo guarantees

Redis Atomicity: The RedisLock release operation uses a Lua script that executes atomically on the Redis server. The script performs owner verification and deletion as a single operation, preventing race conditions where the lock expires between the ownership check and deletion src/LuaScripts.php17-23

Database Atomicity: DatabaseLock relies on SQL unique key constraints for acquisition atomicity. The INSERT-or-UPDATE pattern ensures only one process can hold the lock, with the database enforcing mutual exclusion src/DatabaseLock.php80-98

Sources: src/LuaScripts.php1-43 src/DatabaseLock.php75-105 src/RedisLock.php43-46

Lock Ownership and Recovery

Each lock has an owner identifier (typically a random string) that uniquely identifies the process holding the lock. Ownership verification prevents processes from releasing locks they don't own.


Ownership Verification Flow

Owner Verification

The getCurrentOwner() protected method retrieves the stored owner value from the backing store src/DatabaseLock.php137-140 The isOwnedByCurrentProcess() method compares this with the lock instance's owner before allowing release operations.

Lock Recovery

Force Release: The forceRelease() method bypasses ownership checks, enabling lock recovery in failure scenarios:

Restore Lock: Stores implementing LockProvider expose restoreLock(string $name, string $owner) for recreating lock instances with known owner identifiers, useful for distributed systems needing lock transfer src/FileStore.php160-163

Sources: src/DatabaseLock.php110-122 src/DatabaseLock.php128-132 src/RedisLock.php51-54 src/FileStore.php160-163

TTL Management and Refresh

Lock time-to-live (TTL) determines automatic expiration. The refresh() method extends TTL for long-running operations without releasing and reacquiring the lock.

Permanent Locks

Locks with seconds=0 are permanent (no automatic expiration):

Permanent locks cannot be refreshed and return true immediately when refresh(null) is called src/RedisLock.php71-73

Refresh Operation

The refresh(?int $seconds) method extends lock TTL while maintaining ownership:


Refresh Decision Tree

Redis Implementation: Uses Lua script to atomically verify owner and call EXPIRE with new TTL src/LuaScripts.php33-41 src/RedisLock.php84

Database Implementation: Updates expiration column where key and owner match src/DatabaseLock.php172-179

Validation: Refreshing with non-positive TTL throws InvalidArgumentException to prevent accidental permanent lock creation src/DatabaseLock.php166-170

Remaining Lifetime

The getRemainingLifetime() method returns the number of seconds until lock expiration:

ImplementationCalculation Method
RedisLockRedis TTL command, returns -1 for permanent, -2 for missing src/RedisLock.php92-99
DatabaseLockexpiration - currentTime(), null if expired or missing src/DatabaseLock.php186-202
ArrayLockCarbon::now()->diffInSeconds($expiresAt) src/ArrayLock.php142
NoLockAlways returns null src/NoLock.php69-72

Sources: src/RedisLock.php69-85 src/DatabaseLock.php157-180 src/ArrayLock.php94-120 src/LuaScripts.php33-42

Usage Patterns

Basic Lock Acquisition


Block and Wait Pattern

The base Lock class (not shown in provided files) typically provides block() and get() methods for waiting until lock becomes available.

Long-Running Operations


Lock Recovery


Sources: src/DatabaseLock.php45-62 src/RedisLock.php21-26 src/FileStore.php145-163

Implementation Comparison

FeatureRedisLockDatabaseLockFileLockArrayLockNoLock
DistributionMulti-serverMulti-server (shared DB)Single-server (shared FS)Single-processN/A
PerformanceHighMediumMediumHighestInstant
DurabilityRedis persistenceACID compliantFilesystemNoneNone
Atomic OperationsLua scriptsSQL transactionsOS file locksIn-memoryNone
Auto ExpirationNative TTLLottery cleanupManualIn-memory checkN/A
Use CaseDistributed systemsDatabase-backed appsLocal coordinationTestingDisabled caching

Sources: src/RedisLock.php1-101 src/DatabaseLock.php1-203 src/FileStore.php143-155 src/ArrayLock.php1-143 src/NoLock.php1-73