> 文章列表 > 公司分布式锁加锁错误原因

公司分布式锁加锁错误原因

公司分布式锁加锁错误原因

目录

  • 一、问题
  • 二、问题复现
  • 三、为什么产生这个错误
  • 四、解决方案

一、问题

第一次设置锁成功, 但是返回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;}@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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是怎么做的加解锁,看下我们自研的分布式锁还有哪些问题。