Spring 基于 Lettuce Reactive API 实现 Redis 分布式锁
- 前言
- 实现细节
- Lock
- AbstractLock
- LettuceConfig
- RedisLock
- LettuceRedisLock
- 测试
- 总结
前言
通常都是基于 Redis
的 setnx
操作来实现分布式锁,思想不难理解:
- 获取锁资源,在一定时间内试图获取锁资源,即试图基于
setnx
设置锁标识,若设置失败说明锁资源已被其他对象持有。锁资源一定要有过期时间,否则持有锁资源的对象如果出于各种原因没有及时释放,会造成其他对象获取不到锁资源 - 释放锁资源,释放锁资源时要确认当前对象确实持有锁资源,可以通过锁资源的值进行匹配判断
如果基于 Redisson
整合 Redis
,有现成的 API
可以直接调用,因为个人习惯使用 Lettuce
客户端,且整个 分布式锁
的实现思想较简单,因此基于 Lettuce
实现 分布式锁
,同时也方便进行一定程度上的抽象
实现细节
Lock
/**
* 资源锁
*/
public interface Lock {
/**
* 获取一个 10s 过期的锁资源
* @param lockName
* @return
*/
default String acquire(String lockName) throws InterruptedException {
return acquire(lockName, 10);
}
/**
* 获取超时时长 expireTime(单位:s)的锁资源
* 默认 1s 内未获取到则返回 null
* @param lockName
* @param expireTime
* @return
*/
default String acquire(String lockName, int expireTime) throws InterruptedException {
return acquireWithWait(lockName, expireTime, 1);
}
/**
* 获取超时时长 expireTime(单位:s)的锁资源
* waitTime 秒内若未获取到则返回 null
* @param lockName
* @param expireTime
* @param waitTime
* @return
*/
String acquireWithWait(String lockName, int expireTime, int waitTime) throws InterruptedException;
/**
* 释放 lockName 的锁资源,以 lock 值匹配确保
* 释放锁的当前应用持有该锁资源
* @param lockName
* @param lock
* @return
*/
boolean release(String lockName, String lock);
}
定义顶层接口 Lock
,提供以下方法:
-
String acquire(String lockName)
,获取lockName
的锁资源,默认过期时长10s
,即10s
后无论释放锁资源都会过期,获取过程默认持续1s
-
String acquire(String lockName, int expireTime)
,可以指定锁资源的过期时长,获取过程默认持续1s
-
String acquireWithWait(String lockName, int expireTime, int waitTime)
,可以指定锁资源的过期时长,可以指定获取等待时长 -
boolean release(String lockName, String lock)
,释放锁资源,必须对锁资源的值lock
进行匹配,以判断当前对象是否持有锁资源(而不是锁资源过期而导致释放掉其他对象持有的锁)
AbstractLock
public abstract class AbstractLock implements Lock {
@Override
public String acquireWithWait(String lockName, int expireTime, int waitTime) throws InterruptedException {
String lock = null;
/**
* 试图在规定时间内获取锁资源
*/
long endTime = System.currentTimeMillis() + waitTime * 1000;
while (System.currentTimeMillis() < endTime) {
if ((lock = doAcquire(lockName, expireTime)) != null) {
break;
}
TimeUnit.MILLISECONDS.sleep(100);
}
return lock;
}
protected abstract String doAcquire(String lockName, int expireTime);
}
方法 acquireWithWait
的实现基调:
- 在规定等待时长中多次尝试获取,每次尝试间隔
100ms
- 核心逻辑委托给
doAcquire
方法,交给子类来实现锁资源的获取,比如:基于Lettuce
整合Redis
实现
LettuceConfig
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
// @ConditionalOnClass(RedisClient.class)
public class LettuceConfig {
@Bean
public RedisURI redisURI(RedisProperties redisProperties) {
RedisURI.Builder builder = RedisURI.builder()
.withHost(redisProperties.getHost())
.withPort(redisProperties.getPort())
.withDatabase(redisProperties.getDatabase())
.withSsl(redisProperties.isSsl());
Optional.ofNullable(redisProperties.getClientName())
.ifPresent(clientName -> builder.withClientName(clientName));
Optional.ofNullable(redisProperties.getTimeout())
.ifPresent(timeout -> builder.withTimeout(timeout));
if (StringUtils.hasText(redisProperties.getUsername())
&& StringUtils.hasText(redisProperties.getPassword())) {
builder.withAuthentication(
redisProperties.getUsername()
, redisProperties.getPassword()
);
}
return builder.build();
}
@Bean
public RedisClient redisClient(RedisURI redisURI) {
return RedisClient.create(redisURI);
}
// ...其他 Lettuce 组件声明
@Bean
public RedisReactiveCommands<String, String> redisReactiveCommands(
RedisClient redisClient
) {
return redisClient.connect().reactive();
}
}
Lettuce
配置类,本文打算基于 Reactive API
来实现,因此注册的 Bean
组件为 RedisReactiveCommands<String, String>
RedisLock
public abstract class RedisLock extends AbstractLock {
}
中间类,主要是方便拓展不同客户端的实现,比如 Redssion
Jedis
Lettuce
等
LettuceRedisLock
@Component
public class LettuceRedisLock extends RedisLock {
@Autowired
RedisReactiveCommands<String, String> redisClient;
AlternativeJdkIdGenerator idGenerator = new AlternativeJdkIdGenerator();
Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public String doAcquire(String lockName, int expireTime) {
// 生成锁资源值
String lock = idGenerator.generateId().toString();
// 基于 setnx 设置锁资源
Boolean block = redisClient.setnx(lockName, lock)
.block(Duration.ofSeconds(3));
/**
* 获取锁资源成功,则指定超时时间并返回
* 获取失败则说明锁已被其他对象持有,此时如果该锁资源并未
* 指定超时时间,则此处为了确保锁资源保证释放,未其
* 指定超时时间
*/
if (block) {
doExpire(lockName, expireTime);
return lock;
} else {
redisClient.ttl(lockName)
.subscribe(time -> {
if (time == -1) {
doExpire(lockName, expireTime);
}
});
}
return null;
}
/**
* 基于 expire 命令指定锁的超时时间
* @param lockName
* @param expireTime
*/
private void doExpire(String lockName, int expireTime) {
redisClient.expire(lockName, expireTime)
.doOnError(e -> logger.error(
"error occurred when set expire time for lock: {}", lockName
))
.subscribe();
}
/**
* 释放锁资源
* @param lockName
* @param lock
* @return
*/
@Override
public boolean release(String lockName, String lock) {
redisClient.get(lockName)
.subscribe(l -> {
if (lock.equals(l)) {
redisClient.del(lockName)
.doOnError(e -> release(lockName, lock))
.subscribe();
}
});
return true;
}
}
-
String doAcquire(String lockName, int expireTime)
,基于AlternativeJdkIdGenerator
生成UUID
唯一资源,基于setnx
命令获取锁资源,无论获取成功失败都要指定expire
超时时间,防止锁资源得不到正确释放 -
doExpire(String lockName, int expireTime)
,基于expire
命令指定超时时间 -
boolean release(String lockName, String lock)
,锁资源的释放,释放之前会把锁资源与当前对象持有的锁对象进行对比,以避免释放到其他对象持有的锁
测试
@Component
public class LettuceRedisLockTest implements CommandLineRunner {
@Autowired
LettuceRedisLock lettuceRedisLock;
Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void run(String... args) throws Exception {
for (int i = 0; i < 5; i++) {
new Thread(new TestRunner(i)).start();
}
Thread.currentThread().join();
}
private void handle(int i) throws InterruptedException {
String test = lettuceRedisLock.acquireWithWait("test", 10, 3);
if (test != null) {
logger.info("start:" + i);
TimeUnit.SECONDS.sleep(1);
logger.info("end:" + i);
lettuceRedisLock.release("test", test);
}
}
class TestRunner implements Runnable {
int i;
TestRunner(int i) {
this.i = i;
}
@Override
public void run() {
try {
handle(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
此处以 5
个线程模拟五个分布式应用,每个应用处理逻辑都需要 1s
,而锁的等待时长指定为 3s
,因此最后只有 3
个应用可以获取到锁执行代码,可以自己尝试下
总结
整体实现相对简单,也忽略了部分细节,比如参数的鉴定等,但大体上实现了分布式锁的思想,在个人或小团队内的开发使用问题应该不大