ClickHouse Lock Service
Relevant source files
The following files were used as context for generating this wiki page:
- src/main/java/liquibase/ext/clickhouse/lockservice/ClickHouseLockService.java
- src/main/java/liquibase/ext/clickhouse/sqlgenerator/LockDatabaseChangeLogClickHouse.java
- src/main/java/liquibase/ext/clickhouse/sqlgenerator/UnlockDatabaseChangelogClickHouse.java
- src/main/java/liquibase/ext/clickhouse/sqlgenerator/CreateDatabaseChangeLogLockTableClickHouse.java
- src/main/java/liquibase/ext/clickhouse/sqlgenerator/InitializeDatabaseChangeLogLockClickHouse.java
- src/main/java/liquibase/ext/clickhouse/sqlgenerator/SqlGeneratorUtil.java
- src/main/java/liquibase/ext/clickhouse/database/ClickHouseDatabase.java
The ClickHouse Lock Service is a specialized implementation of the Liquibase StandardLockService designed to manage concurrency and database change log locking specifically for ClickHouse databases. Its primary purpose is to ensure that only one Liquibase instance executes changes against a ClickHouse database at any given time, preventing race conditions and schema corruption.
This service overrides standard Liquibase locking behavior to accommodate ClickHouse-specific SQL syntax, such as ALTER TABLE ... UPDATE and ALTER TABLE ... DELETE statements, which are treated as mutations in ClickHouse. It also supports cluster-aware locking when integrated with the project's cluster configuration modules.
Sources: ClickHouseLockService.java:31-40, LockDatabaseChangeLogClickHouse.java:45-55
Architecture and Components
The locking system is composed of a service layer that manages the state of the lock table and a set of SQL generators that produce the specific DDL and DML required by ClickHouse.
ClickHouseLockService
This class acts as the orchestrator. It checks for the existence of the lock table, determines if it has been initialized with the required lock record, and interfaces with the Liquibase Executor to run queries.
- Priority: Sets
PRIORITY_DATABASE, ensuring it is preferred over the standard Liquibase lock service when aClickHouseDatabaseinstance is used. - State Management: Tracks
isLockTableInitializedto avoid redundant checks against the database.
Sources: ClickHouseLockService.java:42-55
SQL Generators for Locking
The system uses several generators to handle the lifecycle of a lock:
| Generator Class | Responsibility | SQL Operation |
|---|---|---|
CreateDatabaseChangeLogLockTableClickHouse | Creates the DATABASECHANGELOGLOCK table. | CREATE TABLE IF NOT EXISTS |
InitializeDatabaseChangeLogLockClickHouse | Ensures the table contains the initial lock record (ID=1). | INSERT INTO ... (ID, LOCKED) VALUES (1, 0) |
LockDatabaseChangeLogClickHouse | Acquires the lock by updating the status. | ALTER TABLE ... UPDATE LOCKED = 1 |
UnlockDatabaseChangelogClickHouse | Releases the lock. | ALTER TABLE ... UPDATE LOCKED = 0 |
Sources: CreateDatabaseChangeLogLockTableClickHouse.java:48-60, InitializeDatabaseChangeLogLockClickHouse.java:50-65, LockDatabaseChangeLogClickHouse.java:48-60, UnlockDatabaseChangelogClickHouse.java:47-58
Lock Lifecycle Flow
The following diagram illustrates the sequence of operations performed by the Lock Service during a typical Liquibase execution.
The service uses mutations_sync = 1 in its update queries to ensure that the lock state change is processed synchronously by ClickHouse before the service proceeds.
Sources: ClickHouseLockService.java:57-95, InitializeDatabaseChangeLogLockClickHouse.java:55-68, LockDatabaseChangeLogClickHouse.java:57-65
Data Model: DATABASECHANGELOGLOCK
The lock table structure is customized for ClickHouse, utilizing types like DateTime64 and supporting engine configurations for both standalone and clustered environments.
| Field | Type | Description |
|---|---|---|
| ID | Int64 | Primary identifier for the lock (usually 1). |
| LOCKED | UInt8 | Boolean-like flag (0 for unlocked, 1 for locked). |
| LOCKGRANTED | Nullable(DateTime64) | Timestamp when the lock was acquired. |
| LOCKEDBY | Nullable(String) | Description of the host/process holding the lock. |
Sources: CreateDatabaseChangeLogLockTableClickHouse.java:55-65
Cluster and Engine Configuration
The Lock Service leverages SqlGeneratorUtil to determine the appropriate ENGINE and ON CLUSTER clauses based on the ClusterConfig.
- Single Instance: Uses
ENGINE MergeTree() ORDER BY ID. - Cluster Mode: Uses
ENGINE ReplicatedMergeTree(...)with Zookeeper paths and adds theON CLUSTER '{clusterName}'clause to DDL and mutation statements.
Implementation Details
Lock Acquisition SQL
When acquiring a lock, the service generates a specific mutation query that checks the current state to ensure atomicity:
ALTER TABLE `schema`.DATABASECHANGELOGLOCK
ON CLUSTER 'my_cluster'
UPDATE LOCKED = 1, LOCKEDBY = 'host', LOCKGRANTED = '...'
WHERE ID = 1 AND LOCKED = 0
SETTINGS mutations_sync = 1
Sources: LockDatabaseChangeLogClickHouse.java:57-65
Lock Table Initialization
If the table is empty, the service performs a synchronous deletion of any existing records (to be safe) before inserting the base record.
String clearDatabaseQuery = String.format(
"ALTER TABLE `%s`.%s " + SqlGeneratorUtil.generateSqlOnClusterClause(properties) + "DELETE WHERE 1 SETTINGS mutations_sync = 1",
database.getDefaultSchemaName(),
database.getDatabaseChangeLogLockTableName());
Sources: InitializeDatabaseChangeLogLockClickHouse.java:55-60
Conclusion
The ClickHouse Lock Service provides a robust mechanism for managing Liquibase execution state in ClickHouse environments. By adapting standard Liquibase locking logic to ClickHouse's mutation-based update model and supporting replicated table engines, it ensures safe schema migrations across both single nodes and distributed clusters.