A Spring Boot-based lock starter that provides a unified lock interface with multiple implementations including Redis standalone, Redis cluster, Redis sentinel, Zookeeper, local locks, and a no-op for tests. You can easily use locking functionality in your project through simple configuration.
- Support for multiple lock implementations:
- Redis standalone mode
- Redis cluster mode
- Redis sentinel mode
- Zookeeper distributed lock (with optional
digestACL auth) - Standalone
ReentrantLock none— no-op lock (always succeeds), useful for disabling locking in tests or staging
- Multiple retry strategies:
- Fixed interval retry
- Exponential backoff retry
- Random backoff retry
- Annotation-based locking mechanism
- Support for SpEL expressions to define lock keys (including
@beanNamebean references) - Configurable retry count and waiting time
retryCount = Nmeans N additional retries (soN + 1total attempts;N = 0means a single attempt)- All Redis / ZooKeeper connection pools are shut down cleanly on Spring container close
<dependency>
<groupId>io.github.wb04307201.flexible-lock</groupId>
<artifactId>flexible-lock-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency><build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
</plugins>
</build>Add the @Locking annotation to methods that require locking:
@Service
public class BusinessService {
@Locking(key = "#userId + '-' + #orderId")
public void processOrder(String userId, String orderId) {
// Business logic
}
}
⚠️ Self-invocation caveat:@Lockingis implemented with Spring AOP proxies. A method on the same bean calling another@Locking-annotated method onthisbypasses the proxy and the lock will NOT be acquired. Either inject the bean into itself, split the method into a separate bean, or use AspectJ weaving.
| Property | Type | Default | Description |
|---|---|---|---|
| key | String | Lock key, required, supports SpEL expressions (including @beanName) |
|
| waitTime | long | -1 | Lock timeout in ms, -1 means using global configuration |
| retryCount | int | -1 | Number of retries AFTER the initial attempt, -1 means using global configuration; 0 means single attempt |
retryCount and waitTime are validated at startup with @Min(0). Negative
values other than the -1 "use global default" sentinel will be rejected by
Spring Boot's configuration validator.
@Locking can also be placed on a class. The annotation then applies to every
method on that class that does NOT have its own @Locking. A method-level
annotation overrides the class-level one.
@Locking(key = "'order-' + #orderId", waitTime = 5000)
@Service
public class OrderService {
// Uses the class-level key/waitTime/retryCount
public void create(Order order) { ... }
// Method-level overrides class-level: different key, no retries
@Locking(key = "'order-strict-' + #orderId", retryCount = 0)
public void forceUpdate(Order order) { ... }
// No lock at all — neither class-level nor method-level applies
public Order find(String orderId) { ... }
}When all retryCount + 1 attempts fail to acquire the lock, the aspect throws
LockRuntimeException (a RuntimeException subclass). The exception message
includes the lock key for diagnostics. Catch it at the boundary of your
business flow if you want to degrade gracefully (e.g., return a 503):
try {
orderService.create(order);
} catch (LockRuntimeException e) {
// e.getMessage() contains the key that could not be acquired
return ResponseEntity.status(503).build();
}Transport-level errors (e.g., Redis connection refused) short-circuit the
retry loop and surface the original exception as the cause of
LockRuntimeException — retrying a broken connection does not help, so we
fail fast and preserve the root cause for observability.
The SpEL key expression runs against a context that exposes:
| Variable | Type | Description |
|---|---|---|
#method |
java.lang.reflect.Method |
The currently locked method |
#<param> |
the method parameter's type | Each parameter bound by name (requires <parameters>true</parameters> at compile time) |
@beanName |
any registered Spring bean | Resolved via BeanFactoryResolver |
Example using #method:
@Locking(key = "#method.declaringClass.simpleName + ':' + #method.name")
public void foo() { ... }Security note: SpEL keys run in a StandardEvaluationContext, which means
T(...) type references and arbitrary method calls are available. Treat the
SpEL string as developer-controlled configuration: it is a compile-time
annotation parameter, not a runtime user input, so the injection surface is
typos in your code rather than external attacks. Do not build SpEL strings by
concatenating untrusted input.
By default, standalone ReentrantLock is used with fixed retry strategy.
flexible:
lock:
retryCount: 3
waitTime: 3000
retryStrategyType: fixed
lockType: standaloneflexible:
lock:
lockType: redis
retryCount: 3
waitTime: 3000
redis:
host: "redis://127.0.0.1"
port: 6379
password: ""
database: 0flexible:
lock:
lockType: redis_cluster
redisCluster:
nodes:
- "redis://127.0.0.1:7000"
- "redis://127.0.0.1:7001"
password: ""flexible:
lock:
lockType: redis_sentinel
redisSentinel:
nodes:
- "redis://127.0.0.1:26379"
- "redis://127.0.0.1:26380"
master: "mymaster"
password: ""
database: 0flexible:
lock:
lockType: zookeeper
zookeeper:
connectString: "127.0.0.1:2181"
maxElapsedTimeMs: 1000
sleepMsBetweenRetries: 4
root: "/locks"
# Optional: "user:password" for digest ACL on the ZK nodes.
# Without this, the client cannot create /locks if an ACL is set.
digest: "user:password"flexible:
lock:
lockType: noneThe none backend always succeeds, so @Locking is a no-op. Useful for
local development, integration tests, or staging environments where you want
to skip the locking overhead.
logging:
level:
cn:
wubo:
flexible:
lock: debug| Type | Behavior |
|---|---|
fixed |
Each retry waits exactly waitTime ms (default) |
exponential |
Wait time doubles each retry: waitTime * 2^(retryCount - 1), capped to avoid overflow |
random |
Random wait in [waitTime, waitTime * (retryCount + 1)) ms |
If you already manage a RedissonClient or CuratorFramework elsewhere in your
application, register it as a Spring bean and the starter will not create its
own. Internal clients created by the starter are shut down automatically when
the Spring context closes.
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}The repo ships with a runnable demo app under flexible-lock-test/. It
exposes a Thymeleaf console that exercises every @Locking shape the starter
supports — useful for kicking the tyres, comparing backends, or showing the
library to a colleague.
cd flexible-lock-test
mvn spring-boot:run
# then open http://127.0.0.1:8089/The page has three panels:
- DemoService — five methods demonstrating one
@Lockingshape each: simple key, explicitretryCount/waitTime, bean reference (@systemClock), and#methodSpEL. - OrderService — class-level
@Lockingversus a method-level override side-by-side. Force a contention burst onforceUpdateto see the method-level override fail in~50mswhile the class-level default keeps waiting. - Live log — every invocation is recorded (key, status, duration, message) and the page polls it once per second, so you can watch a burst resolve in real time.
Each row has a 调用 button (single call) and a 并发 button (fire N
concurrent requests, see who wins and who gets a LockRuntimeException).
By default the demo uses the in-process STANDALONE backend. To try Redis:
docker run -d --name flexible-lock-redis -p 6379:6379 redis:6-alpine
mvn spring-boot:run -Dspring-boot.run.arguments="\
--flexible.lock.lockType=redis \
--flexible.lock.redis.host=redis://127.0.0.1 \
--flexible.lock.redis.port=6379"
# remember to stop and remove the container when doneThe badge in the page header reflects the active backend:
| Badge | Backend |
|---|---|
STANDALONE (default) |
in-process ReentrantLock |
REDIS |
Redisson Redis standalone |
REDIS_CLUSTER |
Redisson Redis cluster |
REDIS_SENTINEL |
Redisson Redis sentinel |
ZOOKEEPER |
Curator + Spring Integration |
NONE |
disabled (always succeeds) |
