Skip to content

VavelinDev/treelock

Repository files navigation

🎋 TreeLock

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.

Build Known Vulnerabilities codecov

Usage

Declarative - For Spring Fans

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!

Imperative - Using pure Java API

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.

How the JDBC implementation works

The short story of tryLock and ulnock

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).

What is TreeLock

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. @Lock annotation 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.

Yet another usage example

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.

Set up your project

  1. 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'
}
  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).

  1. 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.

About

TreeLock - Distributed and Hierarchical Lock

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages