TreeLock is a distributed, hierarchical lock that leverages your database!
Synchronization is effectively carried out via this centralized repository which, in this case, is the database. The library is similar to the ShedLock library. There are some differences tough.
One can enable distributed lock mechanism by adding the @Lock annotation at the method level:
@Lock(key = "/invoice/pay/{invoiceId}", expirationSeconds = 900L)
public void payInvoice(@LockKeyParam("invoiceId") final Long invoiceId) {
// do processing...
}The module named treelock-spring is responsible for enabling this particular feature. Nevertheless, TreeLock does not require the Spring Framework and can be used with core Java API. Keep on reading!
TreeLock does not require the Spring Framework. Nevertheless nothing can stop you from using pure TreeLock API, applying the lock to any given fragment of your code.
Get a distributed lock using autocloseable API
final ClosableKeyLockProvider keyLockProvider = new ClosableKeyLockProvider(keyLock);
keyLockProvider.withLock("/invoice/pay/4587", 900, lockHandle -> {
// do processing...
});or in a standard way
final Optional<LockHandle> lockHandle = keyLock.tryLock("/invoice/pay/4587", 900);
if(lockHandle.isPresent()) {
try {
// perform some business logic
} finally {
handle.get().unlock();
}
}Of course, before we use a KeyLock instance, we have to initialized. Create the KeyLock global instance (one KeyLock instance can be shared across multiple threads)
val config = HikariConfig()
config.jdbcUrl = "jdbc:oracle:thin:@localhost:1521:XE"
config.username = "myshop"
config.password = "*****"
config.isAutoCommit = true
config.addDataSourceProperty("maximumPoolSize", "1000")
val dataSource = HikariDataSource(config)
val keyLock = JDBCKeyLockBuilder().dataSource(dataSource)
.databaseType(DatabaseType.ORACLE)
.createDatabase(false).build()
val keyLockProvider = ClosableKeyLockProvider(keyLock)The KeyLock instance works on a separate database connection / transaction.
The tryLock() method performs the following INSERT query on the LCK table.
final Optional<LockHandle> lockHandle = keyLock.tryLock("/invoice/pay/4587", 900);INSERT INTO LCK (LCK_KEY, LCK_HNDL_ID, CREATED_TIME, EXPIRE_SEC)
SELECT '/invoice/pay/4587', 'da74f856-27d0-11ea-978f-2e728ce88125', '2019-12-31T06:40:12.623', '900' FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM LCK WHERE LCK_KEY = '/invoice/pay/4587')The unlock() method performs the following DELETE query on the LCK table.
keyLock.unlock(lockHandle.get())DELETE FROM LCK WHERE LCK_HNDL_ID = 'da74f856-27d0-11ea-978f-2e728ce88125'The handle identifier (LCK_HNDL_ID) 'da74f856-27d0-11ea-978f-2e728ce88125' is known only to the lock's owner and is generated by the implementation of the LockHandleIdGenerator interface (LockHandleUUIDIdGenerator by default).
The treelock safety relies on the fact that the LCK table cannot have more than one record with
a given LCK_KEY (the name of your lock, in our example /invoice/pay/4587).
treelock aims to be a deadly simple and reliable solution for distributed locking problem. Safety and simplicity is the top priority of treelock.
treelock is a repository-based lock, meaning the locks are backed by a centrally placed database.
Central database is what majority of standard business project has in place. Why not to use it as a lock synchronization engine (distributed lock manager) too? treelock way is simple and sufficient approach in many cases.
The project is composed of the following modules (each module brings more concretization / context)
- treelock-api base interfaces for treelock implementations. It gives a good outline of treelock capabilities. Contains the key interface KeyLock, which is a humble API for working with locks.
- treelock-core base, abstract classes, common for all the specialized implementations (e.g. JDBC) such as: expiration policies, lock model, repository interfaces (still not RDBMS / JDBC specific).
- treelock-jdbc JDBC implementation of treelock backed by your central database. Can be adapted to any SQL-standard database. Also, has a few unit and jmh tests to test performance, thread-safety and consistency.
- treelock-spring
Support for spring framework.
@Lockannotation allows to declare a distributed lock at the method level.
All treelock implementations must be thread-safe. That being said, a KeyLock instance can be shared by many threads.
What's KeyLock? KeyLock is the main interface for treelock library. You get and release your named (named by key) locks.
The usage of JDBC implementation of the KeyLock interface (SimpleKeyLock class with the instance of JDBCLockRepository injected as the repository property)
should not take part in the long running business transaction, demarcated by a business method.
KeyLock must work inside its own transaction, flushing and committing internal SQL instructions
immediately once tryLock() or unlock() is called.
One of the good use cases for treelock is synchronizing schedules which run concurrently on all your nodes. Quartz does that synchronization in a very similar way, through the central database structures. We can still run our schedules without Quartz, synchronizing them with treelock
Here is the sample code for the static schedule:
// Somewhere in the initialization part / config code (e.g. singleton bean)
KeyLock keyLock = JDBCKeyLockBuilder().dataSource(dataSource).databaseType(DatabaseType.H2).build();
// Somewhere in the class
@Schedule("* */15 * * * *")
void generateInvoice() {
// Request a named ("create-invoice") lock which may last no longer that 300 seconds.
// After 300 seconds "create-invoice" lock expires (can be taken by an another thread / process)
final Optional<LockHandle> handle = keyLock.tryLock("create-invoice", 300);
// Test wheter we got a lock successfully. treelock does not throw exceptions in case lock is taken by other process,
// as such a situation is no an exceptional one
if(handle.isPresent()) {
try {
// generate an invoice...
} finally {
// Self explanatory, let's just tidy up after ourselves
handle.get().unlock();
}
}
}
// or:
@Schedule("* */15 * * * *")
@Lock(key = "create-invoice", expirationSeconds = 900L)
void generateInvoice() {
// ...
}You may have noticed the necessity of providing an estimated duration for the active state of your lock. We highly
recommend that this estimate errs on the side of pessimism. The importance of this stems from our dependence on
the unlock() function, which should, over time, release the lock.
In instances where execution of the unlock() function fails, the lock—with its specified name—will revert to being
accessible after the expiration of the declared time. This underscores another reason why careful estimation
of this time is critical.
- Download treelock library
Configure your pom.xml or build.gradle
repositories {
maven {
url "https://dl.bintray.com/pmalirz/malitools"
}
}
dependencies {
// must have
implementation 'dev.vavelin.treelock:treelock-api:2.0.1'
implementation 'dev.vavelin.treelock:treelock-core:2.0.1'
// when you configure treelock with the database
implementation 'dev.vavelin.treelock:treelock-jdbc:2.0.1'
// when you need a spring support (enables @Lock annotation)
implementation 'dev.vavelin.treelock:treelock-spring:2.0.1'
}- Create LCK table in your database
By default treelock uses LCK for the main table name. However you are free to change it (see pt 3). DDL scripts for different database types can by found inside the jar file or the treelock sources.
DDL for H2:
CREATE TABLE IF NOT EXISTS "@@tableName@@" (
"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 IF NOT EXISTS "@@tableName@@_HNDL_UX" ON "@@tableName@@" ("LCK_HNDL_ID");
LCK_KEY column has to be unique!
The database structures could be also created by the JDBCKeyLockBuilder (see the next point).
- Create your KeyLock instance
@Bean
public KeyLock createKeyLock() {
return new JDBCKeyLockBuilder().dataSource(dataSource).databaseType(DatabaseType.H2).build();
}or, the same but with automatic initialization of the required database structures
@Bean
public KeyLock createKeyLock() {
return new JDBCKeyLockBuilder().dataSource(dataSource).databaseType(DatabaseType.H2).createDatabase(true).build();
}Remember, KeyLock instance is thread-safe.
