Distributed Lock Backed by Your Database
dlock is a simple and reliable distributed locking solution for Java applications, using your existing database (JDBC) as the synchronization mechanism.
Why limit yourself to complex infrastructure like Redis or Zookeeper when your relational database can handle distributed locking with ACID guarantees?
- Simplicity: No extra infrastructure required. Uses standard JDBC.
- Reliability: Relies on database ACID transactions for strong consistency.
- Declarative: Use
@Lockannotation with Spring. - Flexible: Supports manual locking via
KeyLockAPI. - Thread-safe: A single
KeyLockinstance can be shared across multiple threads.
The most common way to use dlock is with Spring Framework support.
Add the following to your build.gradle:
// build.gradle.kts
implementation("io.github.pmalirz:dlock-spring:3.0.0")
implementation("io.github.pmalirz:dlock-jdbc:3.0.0")Or pom.xml:
<dependency>
<groupId>io.github.pmalirz</groupId>
<artifactId>dlock-spring</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.github.pmalirz</groupId>
<artifactId>dlock-jdbc</artifactId>
<version>3.0.0</version>
</dependency>Enable the aspect and configure the KeyLock bean.
@Configuration
@ComponentScan("io.github.pmalirz.dlock") // Scan for LockAspect
public class DLockConfig {
@Bean
public KeyLock keyLock(DataSource dataSource) {
return new JDBCKeyLockBuilder()
.dataSource(dataSource)
.databaseType(DatabaseType.H2) // or ORACLE, POSTGRESQL
.createDatabase(true) // Automatically creates the DLCK table
.build();
}
}Annotate your methods with @Lock.
Important:
- If the lock cannot be acquired (e.g., held by another node), the method execution is skipped, and
nullis returned. - This pattern is best suited for scheduled tasks or void methods where "skip if running" is the desired behavior.
- If the method returns a value, the caller must handle
nullin case of lock failure.
@Service
public class InvoiceService {
@Lock(key = "invoice-processing-{invoiceId}", expirationSeconds = 60)
public void processInvoice(@LockKeyParam("invoiceId") Long invoiceId) {
// Critical section: only one instance processes this invoice at a time.
// If locked, this logic is skipped entirely.
// ...
}
}You can also use the API directly without Spring.
// 1. Initialize KeyLock (singleton)
KeyLock keyLock = new JDBCKeyLockBuilder()
.dataSource(dataSource)
.databaseType(DatabaseType.H2)
.build();
// 2. Try to acquire a lock
Optional<LockHandle> lockHandle = keyLock.tryLock("my-resource-lock", 300); // 300 seconds expiration
if (lockHandle.isPresent()) {
try {
// Critical section
performTask();
} finally {
// Always release the lock!
keyLock.unlock(lockHandle.get());
}
} else {
// Lock is currently held by someone else
log.info("Could not acquire lock, skipping task.");
}For a simpler pattern where the lock is automatically released after the action completes:
// With Consumer (void)
keyLock.tryLock("my-resource-lock", 300, handle -> {
// This block is executed only if lock is acquired.
performTask();
});
// With Function (returns value)
Optional<String> result = keyLock.tryLock("my-calc-lock", 300, handle -> {
return computeResult();
});dlock uses a dedicated table (default DLCK) to store active locks.
- Acquire (
tryLock): Attempts toINSERTa record with the lock key. If the key exists (unique constraint), the insert fails, meaning the lock is already held. - Release (
unlock): Performs aDELETEon the record using the lock handle ID. - Expiration: Locks have an expiration time. If a lock is not released (e.g., process crash), it can be reclaimed after expiration.
Example DLCK Table Schema (H2):
CREATE TABLE IF NOT EXISTS "DLCK" (
"LCK_KEY" varchar(1000) PRIMARY KEY,
"LCK_HNDL_ID" varchar(100) NOT NULL,
"CREATED_TIME" DATETIME NOT NULL,
"EXPIRE_SEC" int NOT NULL
);
CREATE UNIQUE INDEX "DLCK_HNDL_UX" ON "DLCK" ("LCK_HNDL_ID");sequenceDiagram
autonumber
participant App as Application
participant Lock as KeyLock (Core)
participant Repo as LockRepository (JDBC)
participant DB as Database (DLCK Table)
App->>Lock: tryLock(key, time)
Lock->>Repo: tryLock(key, expiration)
Repo->>DB: INSERT INTO DLCK ...
alt Lock Acquired
DB-->>Repo: 1 row inserted
Repo-->>Lock: true
Lock-->>App: Optional[dlockHandle]
else Lock Busy
DB-->>Repo: 0 rows / Constraint Violation
Repo-->>Lock: false
Lock-->>App: Optional.empty
end
Mutual exclusion is guaranteed even under concurrent lock expiration reclaim across multiple nodes. See dlock-jdbc Safety Guarantees for the full analysis.
When using the KeyLock API, keep the following constraints in mind:
lockKeymust be a non-blank string, up to 1000 characters (the database column limit).expirationSecondsmust be greater than 0.- Lock keys should be descriptive and scoped (e.g.,
"/invoice/{id}") to avoid unintended collisions.
- dlock-api: Core interfaces (
KeyLock,LockHandle). - dlock-core: Base implementation logic (expiration policies, utilities).
- dlock-jdbc: JDBC implementation (H2, Oracle, PostgreSQL support).
- dlock-spring: Spring integration (
@Lockaspect).
graph TD
subgraph "dlock Modules"
Spring[dlock-spring]
JDBC[dlock-jdbc]
Core[dlock-core]
API[dlock-api]
end
Spring --> API
JDBC -- realizes --> Core
Core --> API
Prerequisites: JDK 17+
Build the project:
./gradlew buildRun benchmarks:
./gradlew :dlock-core:jmh
./gradlew :dlock-jdbc:jmhThe following benchmarks demonstrate the throughput of dlock-core (in-memory) and dlock-jdbc (H2 database) implementations.
- CPU: AMD Ryzen 9 5900X (12 cores / 24 threads, max 3.7 GHz)
- RAM: 64 GB DDR4 3600 MHz
- OS: Windows 11 Pro
- Database: H2 (TCP mode) for JDBC tests
- Threads: 12 (matching number of physical cores)
| Module | Benchmark Scenario | Description | Score (ops/s) | Error (ops/s) |
|---|---|---|---|---|
| dlock-core | tryAndReleaseLockNoCollision |
Acquire & release unique key (random UUID); no collision | 895.5 k | ± 523.8 k |
tryLockAlwaysCollision |
Attempt to acquire an active lock; 100% collision | 48.9 M | ± 2.8 M | |
tryLockExpiresEverySecond |
High-frequency attempts on single key (1s expiration) | 45.2 M | ± 10.4 M | |
| dlock-jdbc | tryAndReleaseLockNoCollision |
Acquire & release unique key (random UUID); no collision; DB pre-populated with 100k locks | 11.1 k | ± 0.2 k |
tryLockAlwaysCollision |
Attempt to acquire an active lock; 100% collision | 47.6 k | ± 5.4 k | |
tryLockNoCollision |
Acquire unique key (random UUID) without release | 18.3 k | ± 17.2 k |
Note:
dlock-coreis purely in-memory and serves as a baseline for overhead.dlock-jdbcinvolves actual network/database round-trips to the H2 server (local, file-based server, not in-memory).The
tryAndReleaseLockNoCollisionJDBC benchmark pre-populates the database with 100k existing locks before measurement to simulate a realistic "noisy" table and measure performance under non-trivial data volume.
This project is licensed under the Apache License 2.0. See LICENSE for details.