public class DistributedLock extends io.atomix.resource.AbstractResource<DistributedLock>
The distributed lock resource provides a mechanism for nodes to synchronize access to cluster-wide shared resources.
This interface is an asynchronous version of Java's Lock.
atomix.getLock("my-lock").thenAccept(lock -> {
lock.lock().thenRun(() -> {
...
lock.unlock();
});
});
Locks are implemented as a simple replicated queue that tracks which client currently holds a lock. When a lock
is locked, if the lock is available then the requesting resource instance will immediately receive
the lock, otherwise the lock request will be queued. When a lock is unlocked, the next lock requester
will be granted the lock.
Distributed locks require no polling from the client. Locks are granted via session events published by the Atomix cluster to the lock instance. In the event that a lock's client becomes disconnected from the cluster, its session will expire after the configured cluster session timeout and the lock will be automatically released.
State of the lock. If the resource transitions to the
Resource.State#SUSPENDED state, that indicates that the underlying client is unable to communicate with the
cluster and another process may have been granted the lock. Lock holders should monitor the resource for state changes
and release the lock if the resource becomes suspended.
DistributedLock lock = atomix.getLock("my-lock").get();
lock.lock().thenRun(() -> {
lock.onStateChange(state -> {
if (state == DistributedLock.State.SUSPENDED) {
lock.unlock();
System.out.println("lost the lock");
}
});
// Do stuff
});
DistributedLock
provides a monotonically increasing, globally unique identifier to each process granted the lock. The token can
be used for optimistic concurrency control when accessing external resources.
lock.lock().thenAccept(token -> {
// Write to external data store, checking that the last written token is not greater than the current token
});
This is known as a fencing token. When using the lock to control concurrent writes to e.g. an external data
store, the monotonically increasing identifier provided when the lock is granted allows lock holders to
optimistically determine whether the lock has been granted to a more recent lock requester that has written
to the data store. If the last write to the external data store is greater than the local lock's token, that
indicates that another process has been granted the lock.
StateMachine.
When a lock is created, an instance of the lock state machine is created on each replica in the cluster.
The state machine instance manages state for the specific lock. When a client makes a lock()
request, it submits the request with a monotonically increasing identifier unique to the resource instance.
If the lock is not currently held by another process, the state machine grants the lock to the
requester and publishes
a lock event with the requested lock ID and a globally unique, monotonically increasing token to
the client. If the lock is held by another process, the lock request is enqueued to await availability of
the lock.
When a client-side DistributedLock receives a published lock event from the cluster, the
client associates the lock ID with a lock requested by the client and grants the lock to the requester.
The globally unique, monotonically increasing token provided by the cluster is provided to the user as
a fencing token. Because of garbage collection and other types of process pauses, we cannot guarantee that
two clients cannot perceive themselves to hold the same lock at the same time. The fencing token provides
a mechanism for optimistic locking when interacting with external resources.
When a lock holder unlocks a lock, a second request is sent to the cluster. The unlock
request is logged and replicated to a majority of the cluster and applied to the replicated state machine.
If another lock requester is awaiting the lock, the state machine will grant the lock to the next requester
in the queue.
Because a lock holder may become partitioned or crash before releasing a lock, the lock state machine is responsible for tracking which session currently holds the lock. In the event that the lock holder is partitioned or crashes, its session will eventually be expired by the state machine, and the lock will be granted to the next requester in the queue.
The lock state machine manages compaction in the replicated log by tracking which lock and unlock
requests contribute to the state of the lock. As long as a client holds a lock, the commit requesting the lock
will be retained in the replicated log. And as long as a client's request to acquire a lock is held in the state
machine's lock queue, the commit requesting the lock will be retained in the replicated log. This ensures that if
a replica crashes and recovers it will recover the correct state of the lock. Once a lock is released by a client,
both the lock and unlock commit associated with the lock will be released from the state machine
and eventually removed from the log during compaction.
| Constructor and Description |
|---|
DistributedLock(CopycatClient client,
Properties options) |
| Modifier and Type | Method and Description |
|---|---|
CompletableFuture<Long> |
lock()
Acquires the lock.
|
CompletableFuture<DistributedLock> |
open() |
CompletableFuture<Long> |
tryLock()
Attempts to acquire the lock if available.
|
CompletableFuture<Long> |
tryLock(Duration timeout)
Attempts to acquire the lock if available within the given timeout.
|
CompletableFuture<Void> |
unlock()
Releases the lock.
|
public DistributedLock(CopycatClient client, Properties options)
public CompletableFuture<DistributedLock> open()
open in interface Managed<DistributedLock>open in interface io.atomix.resource.Resource<DistributedLock>open in class io.atomix.resource.AbstractResource<DistributedLock>public CompletableFuture<Long> lock()
When the lock is acquired, this lock instance will publish a lock request to the cluster and await
an event granting the lock to this instance. The returned CompletableFuture will not be completed
until the lock has been acquired.
Once the lock is granted, the returned future will be completed with a positive Long value. This value
is guaranteed to be unique across all clients and monotonically increasing. Thus, the value can be used as a
fencing token for further concurrency control.
This method returns a CompletableFuture which can be used to block until the operation completes
or to be notified in a separate thread once the operation completes. To block until the operation completes,
use the CompletableFuture.join() method to block the calling thread:
lock.lock().join();
Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different
thread, use one of the many completable future callbacks:
lock.lock().thenRun(() -> System.out.println("Lock acquired!"));
public CompletableFuture<Long> tryLock()
When the lock is acquired, this lock instance will publish an immediate lock request to the cluster. If the
lock is available, the lock will be granted and the returned CompletableFuture will be completed
successfully. If the lock is not immediately available, the CompletableFuture will be completed
with a null value.
If the lock is granted, the returned future will be completed with a positive Long value. This value
is guaranteed to be unique across all clients and monotonically increasing. Thus, the value can be used as a
fencing token for further concurrency control.
This method returns a CompletableFuture which can be used to block until the operation completes
or to be notified in a separate thread once the operation completes. To block until the operation completes,
use the CompletableFuture.get() method to block the calling thread:
if (lock.tryLock().get()) {
System.out.println("Lock acquired!");
} else {
System.out.println("Lock failed!");
}
Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different
thread, use one of the many completable future callbacks:
lock.tryLock().thenAccept(locked -> {
if (locked) {
System.out.println("Lock acquired!");
} else {
System.out.println("Lock failed!");
}
});
public CompletableFuture<Long> tryLock(Duration timeout)
When the lock is acquired, this lock instance will publish an immediate lock request to the cluster. If the
lock is available, the lock will be granted and the returned CompletableFuture will be completed
successfully. If the lock is not immediately available, the lock request will be queued until the lock comes
available. If the lock timeout expires, the lock request will be cancelled and the returned
CompletableFuture will be completed successfully with a null result.
If the lock is granted, the returned future will be completed with a positive Long value. This value
is guaranteed to be unique across all clients and monotonically increasing. Thus, the value can be used as a
fencing token for further concurrency control.
Timeouts and wall-clock time
The provided timeout may not ultimately be representative of the actual timeout in the cluster. Because
of clock skew and determinism requirements, the actual timeout may be arbitrarily greater, but not less, than
the provided timeout. However, time will always progress monotonically. That is, time will never go
in reverse. A timeout of 10 seconds will always be greater than a timeout of 9 seconds, but the
times simply may not match actual wall-clock time.
This method returns a CompletableFuture which can be used to block until the operation completes
or to be notified in a separate thread once the operation completes. To block until the operation completes,
use the CompletableFuture.get() method to block the calling thread:
if (lock.tryLock(Duration.ofSeconds(10)).get()) {
System.out.println("Lock acquired!");
} else {
System.out.println("Lock failed!");
}
Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different
thread, use one of the many completable future callbacks:
lock.tryLock(Duration.ofSeconds(10)).thenAccept(locked -> {
if (locked) {
System.out.println("Lock acquired!");
} else {
System.out.println("Lock failed!");
}
});
timeout - The duration within which to acquire the lock.public CompletableFuture<Void> unlock()
When the lock is released, the lock instance will publish an unlock request to the cluster. Once the lock has
been released, if any other instances of this resource are waiting for a lock on this or another node, the lock
will be acquired by the waiting instance before this unlock operation is completed. Once the lock has been released
and granted to any waiters, the returned CompletableFuture will be completed.
This method returns a CompletableFuture which can be used to block until the operation completes
or to be notified in a separate thread once the operation completes. To block until the operation completes,
use the CompletableFuture.join() method to block the calling thread:
lock.unlock().join();
Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different
thread, use one of the many completable future callbacks:
lock.unlock().thenRun(() -> System.out.println("Lock released!"));
Copyright © 2013–2016. All rights reserved.