目录
- 一、问题
- 二、问题复现
- 三、为什么产生这个错误
- 四、解决方案
一、问题
第一次设置锁成功, 但是返回false, 后续在循环获取的时候, 因为已经设置成功, 调用setIfAbsent不会返回true, 导致等锁3s失败
private boolean lockWait(String key, long wait, long expire) {
long totalWait = 0L;
long interval = 200L;
boolean isOk;
while(true) {
Boolean setResult = this.masterRedisTemplate.opsForValue().setIfAbsent(key, "1", expire, TimeUnit.SECONDS);
isOk = setResult == null ? false : setResult;
if (isOk || totalWait > wait) {
break;
}
try {
Thread.sleep(interval);
totalWait += interval;
} catch (InterruptedException var13) {
break;
}
}
return isOk;
}
二、问题复现
为复现场景,我们编写以下代码进行测试,使用3个线程,每个线程循环1000次setIfAbsent指令,当遇到失败情况时打印日志。
测试代码
@SneakyThrows
@RequestMapping(value = “/test”)
public void test() {
run();
}
@Resource(name = "frequenceMasterRedisTemplate")
private RedisTemplate<String, String> frequenceMasterRedisTemplate;
private void run() throws InterruptedException {
new TestThread("A", frequenceMasterRedisTemplate).start();
new TestThread("B", frequenceMasterRedisTemplate).start();
new TestThread("C", frequenceMasterRedisTemplate).start();
}
static class TestThread extends Thread {
String keyPrefix;
RedisTemplate<String, String> redisTemplate;
long allStart = System.currentTimeMillis();
public TestThread(String keyPrefix, RedisTemplate<String, String> redisTemplate) {
this.keyPrefix = keyPrefix;
this.redisTemplate = redisTemplate;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
String key = keyPrefix + "-" + i;
long start = System.currentTimeMillis();
Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, "1", 3, TimeUnit.SECONDS);
long now = System.currentTimeMillis();
if (lock == null || !lock) {
log.error("lockFalse {} {} {} {} {}", key, lock, now - allStart, now - start, redisTemplate.opsForValue().get(key) == null);
break;
} else {
log.info("lock {} {} {}", key, lock, now - start);
}
}
}
}
当我们调用接口的时候,会发现你的控制面板始终能打印lockFalse日志,这说明我们复现了错误!
三、为什么产生这个错误
这个问题php使用相同指令执行没有问题,怀疑过是Luttuce客户端问题,达哥也在github提过Issue,作者不认为这是Luttuce问题,所以我们项目本身的问题可能性更大,经过洪州提示配置在这个类LettuceConfig
LuttuceConfig
@Configuration
@Slf4j
public class LettuceConfig {
public static ClientResources createClientResources() {
return ClientResources.builder()
.nettyCustomizer(new NettyCustomizer() {
@Override
public void afterChannelInitialized(Channel channel) {
int readerIdleTimeSeconds = 12;
int writerIdleTimeSeconds = 12;
int allIdleTimeSeconds = 12;
channel.pipeline().addLast(new IdleStateHandler(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds));
channel.pipeline().addLast(new ChannelDuplexHandler() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
ctx.channel().disconnect();
} else {
super.userEventTriggered(ctx, evt);
}
}
});
}
}).commandLatencyCollector(CommandLatencyCollector.disabled())
.commandLatencyPublisherOptions(DefaultEventPublisherOptions.disabled())
.commandLatencyCollectorOptions(DefaultCommandLatencyCollectorOptions.disabled()).build();
}
@Bean(name = "commonClientResources", destroyMethod = "shutdown")
public ClientResources commonClientResources() {
return createClientResources();
}
@Bean(name = "commonClientOptions")
public ClientOptions commonClientOptions() {
return ClientOptions.builder()
.socketOptions(SocketOptions.builder().keepAlive(true).build())
.build();
}
}
我们可以看到,主要配置了Luttuce的连接读空闲,写空闲,和读写空闲时间,作用是可以检测和处理空闲连接,当我们程序中长时间没有进行读操作,写操作时,将会自动触发一个IdleState事件,这时我们可以执行自己的操作,比如关闭连接。
正常情况下的Redis这样配置没有问题,然而在我们项目中,frequenceMasterRedisTemplate实例只是用来做分布式锁,其他场景和业务不会使用这个实例,也就是只会使用setIfAbsent指令,这是一个写操作。
简单来说就是,我们长时间没有使用frequenceMasterRedisTemplate进行读操作,比如get,系统自动触发了一个读空闲超时,然后程序将连接给关闭了,这将会导致正在进行的Redis操作产生意外的结果,比如我们前文所说,执行成功但是返回失败,实际测试中我也发现有可能执行失败,返回也失败。
四、解决方案
既然是配置问题,那么就明朗了,我们只需要清楚 readerIdleTimeSeconds,writerIdleTimeSeconds,allIdleTimeSeconds 三个参数配置后具体会做什么就可以了。
• readerIdleTime: 表示读空闲时间,即多长时间没有读取就会触发事件,默认值为0,表示禁用读空闲检测。
• writerIdleTime: 表示写空闲时间,即多长时间没有写入就会触发事件,默认值为0,表示禁用写空闲检测。
• allIdleTime: 表示读写空闲时间,即多长时间没有读取或写入就会触发事件,默认值为0,表示禁用读写空闲检测。
显然,对于frequenceMasterRedisTemplate的使用场景,我们设置readerIdleTimeSeconds=0,writerIdleTimeSeconds=0,allIdleTimeSeconds=12相对来说比较合理。
参考下Dubbo是如何配置的参数:
总之,没有固定的配置模板,根据项目特点和使用场景选择适当的配置参数即可。
另外,Redis分布式锁市面已经有成熟的方案,可以学习下Redisson是怎么做的加解锁,看下我们自研的分布式锁还有哪些问题。