Repository navigation
With other Spring annotations
Spring applies annotations such as @Transactional through advice, code that runs around the
method. When a method has several of them, the advice with the lower order runs first and wraps the
rest. Locksmith's advice runs at order Ordered.LOWEST_PRECEDENCE - 1:
- After Spring Security's method checks, such as
@PreAuthorizeand@Secured, so a call they deny never reaches Redis. - Before
@Transactionaland@Cacheableat their default order, so a transaction that starts at the locked method commits before the lock is released. Leave@Transactionalat its default order: an order belowLOWEST_PRECEDENCE - 1puts the transaction outside the lock, and the lock is then released before the commit. -
@Async, and Spring Framework's@Retryableand@ConcurrencyLimit, always run outside Locksmith, so each retry takes the lock again.
A transaction the caller already started is not covered. The locked method joins it, and the
lock is released when the method returns, before the caller commits; in that gap another instance
can take the lock and read data that is not committed yet. Put @Transactional on the locked
method itself, or on a method it calls, not only on its caller. If a caller may already hold a
transaction, use @Transactional(propagation = Propagation.REQUIRES_NEW) on the locked method: its
own transaction then commits before the unlock, and its changes stay committed even if the caller's
transaction later rolls back.
@Cacheable does not skip the lock. The cache runs inside the lock, so every call takes the
lock, a cache hit included. With the default try-once, two callers that hit the cache for one key at
the same moment collide, and one of them gets LockNotAcquiredException: in a test, 1,760 of 2,000
concurrent hits on a cached key failed. To serve cache hits without the lock, put @Cacheable on a
method of another bean that calls the locked method.
A method that returns a future must be @Async, on the method or on the class that declares it,
and declare Future or CompletableFuture; otherwise startup fails. Spring's @Async interceptor
runs first, so Locksmith runs on the worker thread, and the lock and permit cover the whole method
body:
@Async
@DistributedLock(key = "order:#{#orderId}", waitTime = "5s")
public CompletableFuture<Receipt> process(String orderId) {
return CompletableFuture.completedFuture(Receipt.from(client.send(orderId)));
}The lock is released when the method body returns. Work the body starts but does not wait for, such
as a future it returns before that future completes, runs on without the lock. With SKIP, the
caller's future completes with null.
Future and CompletableFuture are the only future types Spring's @Async returns: it fails every
call of a method declared to return CompletionStage, and a CompletableFuture subclass fails with
a ClassCastException after the method has run. For the same reason, an annotated @Async method
that returns anything but void, Kotlin Unit, Future or CompletableFuture fails startup.
@Async on a subclass does not count for a method the subclass inherits, although Spring would run
that method asynchronously: put @Async on the method there.
@PostAuthorize and @PostFilter see what SKIP or HANDLER returns. SKIP returns null for a
collection type, which @PostFilter rejects; on such a method use HANDLER and return an empty
collection.