Redisson分布式锁有效性测试
Redis是三主三从集群模式
node:ip1:port1,ip2:port2,ip3:port3,ip4:port4,ip5:port5,ip6:port6,
spring boot 2.1.7.RELEASE ;redission版本:3.11.6
@Bean
public RedissonClient getRedisson(){
Config config = new Config();
for(String node:nodes.split(",")){
config.useClusterServers().addNodeAddress("redis://"+node);
}
config.useClusterServers().setMasterConnectionPoolSize(200);
config.useClusterServers().setSlaveConnectionMinimumIdleSize(200);
config.useClusterServers().setMasterConnectionMinimumIdleSize(200);
config.useClusterServers().setSlaveConnectionPoolSize(200);
config.useClusterServers().setScanInterval(3000);
config.useClusterServers().setPassword(password);
return Redisson.create(config);
}
测试加锁代码1:
@RequestMapping("/lockTest")
public String lockTest() throws Exception {
RLock rl=redissonClient.getFairLock("myTestLock");
if(rl.tryLock(5,30,TimeUnit.SECONDS)) {
String name=Thread.currentThread().getName();
System.out.println(name +" get lock at "+ System.currentTimeMillis());
return "YES";
}
return "NO";
}
使用压测工具:Jmeter
运行35S,最终controller打印获取锁的情况是:
http-nio-9100-exec-6 get lock at 1588661240669
http-nio-9100-exec-6 get lock at 1588661240784
http-nio-9100-exec-6 get lock at 1588661240808
http-nio-9100-exec-6 get lock at 1588661240825
http-nio-9100-exec-6 get lock at 1588661242079
http-nio-9100-exec-6 get lock at 1588661242101
http-nio-9100-exec-6 get lock at 1588661242137
http-nio-9100-exec-6 get lock at 1588661242162
http-nio-9100-exec-6 get lock at 1588661242180
http-nio-9100-exec-6 get lock at 1588661242198
。。。。。
表面上看:锁的有效期是30S,为啥压测35s获取到锁是N次?
首先分清楚问题:谁持有锁?什么情况能再次获取到锁?
问题关系到的对象有:客户端请求,tomcat线程池,Redission客户端,Redis集群服务器。
参考资料:https://mp.weixin.qq.com/s/y_Uw3P2Ll7wvk_j5Fdlusw
Redission客户端会绑定锁到线程ID,同一线程如果是锁的持有者,执行加锁的时候会增加加锁次数,以及重新延长锁的有效时间,watchdog根据redisson客户端配置的scanInterval定时检测锁的有效期。
理解以上的原理,就很好解释了,tomcat线程池里面的线程是tomcat根据自己的算法分配到每一个请求,线程如果是未执行完的状态是不会分配给下一个请求的,如果已执行完并且是第一个锁的持有者的线程分配到该请求,那么该请求一定能够获取到锁。
上述示例代码的加锁后执行的代码时间极短,无论压测持续多久,获取到锁的线程ID始终是同一个:第一个获取到锁的线程。
只要是请求分配到了第一个获取到锁的线程,那么就能获取到锁,而且每次获取到锁之后,锁的有效期都会重新刷新为30S.
生产问题代码:
public void lockDemo(){
RLock lock=redissionClient.getLock("XXX");
if(!lock.tryLock(5, 300, TimeUnit.SECONDS)){
result.put("errorCode", "ERR01");
result.put("errorMsg", "请勿重复操作");
return result;
}
查询业务结果是否已处理,已处理则抛出异常
final RLock lock1=lock;
taskExecutor.execute(new Runnable(){
final RLock lockF = lock1;
执行主干业务。
lockF .unlock();
}
}
在测试环境单机模式下,一个人开多个浏览器窗口,同时点击,业务会偶尔出现重复处理现象,而且处理完毕后,unlock处理后锁依然未释放。
根据上述测试得出的结论:
1.主干加锁操作很快执行完毕,主干加锁业务是在主线程完成的,业务处理却是在异步的线程完成,
Redission要求加锁和解锁操作必须在同一个线程内完成,两者线程ID不相等,所以导致锁始终无法释放,只能等待锁的有效期过期。
2.由于主线程执行时间很快,第一次获取到锁的线程A在下一个同样的请求到达之后,由于线程A是已完结状态,Redission认为业务也已完结,只要线程A在主干的异步线程尚未完成处理时,又被分配给了下一个请求,tryLock是可以成功的。测试环境并发不大,而且测试环境是单机模式,所以出现的概率也会很高。
解决方案:1.如果不会引起客户端超时的情况下,将异步改为同步。
2.如果会引起客户端超时,业务处理复杂,加锁和业务处理都异步执行,客户端生成UUID,客户端不断轮训此UUID来查询业务处理结果状态。每个客户端重复处理同一个业务,但是请求ID不一样,都可以查到自己的处理结果。
测试案例2:
@RequestMapping("/lockTest")
public String lockTest() throws Exception {
RLock rl=redissonClient.getFairLock("XXH122232");
if(rl.tryLock(1,30,TimeUnit.SECONDS)) {
String name=Thread.currentThread().getName();
System.out.println(name +" get lock at "+ System.currentTimeMillis());
Thread.currentThread().sleep(20000);
return "YES";
}
return "NO";
}
Jmeter持续压测时间<20s,本地启动多个APP应用,测试应用集群模式+Ression集群模式下锁的有效性,在原有的基础上增加一个http Request。9000端口应用和9100端口应用连同一个redis集群。
压测结果:
http-nio-9100-exec-1 get lock at 1588663319488
只有其中一个APP应用获取到锁,另外的一个应用没有任何打印。
验证结果:锁在应用APP集群模式+Redis集群模式下是有效的。