Redisson 分布式锁
基于redis实现分布式锁基本逻辑图<待补充>
实现多个实例竞争执行任务,最多只有一个实例获得锁进行工作。
期间模拟随机出现错误,抛出异常,检验 demo 的合理性。
执行脚本
cd D:\src\mytest\demo-schedule\target
start java -jar demo-schedule-0.0.1-SNAPSHOT.jar --application.hostname=host1 --server.port=8081 --logging.root.level=info
start java -jar demo-schedule-0.0.1-SNAPSHOT.jar --application.hostname=host2 --server.port=8082 --logging.root.level=info
start java -jar demo-schedule-0.0.1-SNAPSHOT.jar --application.hostname=host3 --server.port=8083 --logging.root.level=info
三个实例运行截图
代码说明
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@Configuration
@ConfigurationProperties("application")
public class SomeWorker2 {
// 用于区分当前服务的名称
@Value("${application.hostname}")
public String hostname;
// 用于测试的锁持续时间
@Value("${application.lockDuration}")
public long lockDuration;
@Autowired
private String lockKey="demoschedule-lock";
private String globalIndexKey="globalIndexKey";
@Resource
private RedissonClient redissonClient;
public void task() throws InterruptedException {//throws Exception
boolean workFlag=true;
boolean getLock=false;
long index=0;
Random r = new Random();
// 初始化
RAtomicLong globalIndex = redissonClient.getAtomicLong(globalIndexKey);
globalIndex.compareAndSet(0,1);
RLock rLock = redissonClient.getLock(lockKey);
while (workFlag) {
// 重置,避免影响日志(用了上次的index显示日志)
index=-1;
try {
// 每轮竞争需要等待一下,不能持续抢着
Thread.sleep(r.nextInt(500)+100);
// 加锁,说明:Duration 设置为 -1,由 redisson 负责锁续期
getLock=rLock.tryLock(100, -1, TimeUnit.MICROSECONDS);
if (getLock) {
// 清除 printWork的行,避免日志粘一起
System.out.println();
// 全局任务加1
index = globalIndex.getAndIncrement();
// 模拟工作
mockWork(rLock , index);
// 清除 printWork的行,避免日志粘一起
System.out.println();
}
else{
log.debug(String.format("%s 获取处理权: 竞争失败", hostname));
System.out.print(".");
}
}
catch(Exception ex){
// 清除 printWork的行,避免日志粘一起
System.out.println();
log.warn(String.format("%s:[%d] Exception-业务异常 [%s]:%s",
hostname, index,
rLock.isLocked()?"lock":"--",
ex.getMessage()));
//ex.printStackTrace();
}
finally {
// 释放锁
try {
log.debug(String.format("%s:[%d] finally : getLock=%s, isLock=%s, isHeldByCurrentThread=%s",
hostname, index,
getLock?"true":"false",
rLock.isLocked()?"true":"false",
rLock.isHeldByCurrentThread()?"true":"false" ));
// 备注:如果 mockWork 抛出异常,rLock.isLocked 将为 false
// 如果 mockWork 执行了 tryLock(),rLock.isHeldByCurrentThread 将为 false
// 解锁时,需要检查一下,不能把别人的 锁给解了
if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
log.info(String.format("%s:[%d] 释放锁", hostname, index));
rLock.unlock();
}else {
// 上面判断不能解锁,但是有又获得锁的标记,那么可能是发生错误了
if (getLock) {
log.info(String.format("%s:[%d] 不执行解锁??", hostname, index));
}
}
} catch(Exception ex){
log.warn(String.format("%s:[%d] Exception-释放锁异常,可能出现超卖 ********************\n%s",
hostname, index, ex.getMessage()));
//ex.printStackTrace();
}
}
}
}
private int mockWork(RLock rLock, long index) throws InterruptedException {
Random r = new Random();
int workTime = r.nextInt((int)lockDuration);// 模拟工作时间
log.info(String.format("%s:[%d] 获取处理权: (workTime=%d)",
hostname, index, workTime));
// 不要一次 sleep 很长时间,那会失去控制权,所以我们分片处理
for (int i=0; i<workTime/100; i++) {
Thread.sleep(100);
// 打印工作状态,让我们看到改任务在跑
printWork(i);
// 模拟错误
if (r.nextInt(1000) % 999 == 0){
Thread.sleep(100);
throw new RuntimeException ("模拟错误发生");
}
}
return workTime;
}
private void printWork(int i) {
switch (i%4){
case 0:System.out.print("\r-");break;
case 1:System.out.print("\r\\");break;
case 2:System.out.print("\r|");break;
case 3:System.out.print("\r/");break;
}
}
}
代码下载:阿里云仓库
https://code.aliyun.com/greenery2/redissonDemo.git
遇到的问题
redisson 配置错误1:提示密码错误
redisson.yml
# 单节点配置
singleServerConfig:
address: "redis://10.1.1.1:6789"
password:
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisson' defined in class path resource [org/redisson/spring/starter/RedissonAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.redisson.api.RedissonClient]: Factory method 'redisson' threw exception; nested exception is org.redisson.client.RedisConnectionException: Unable to connect to Redis server: /10.1.1.1:6789
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.redisson.api.RedissonClient]: Factory method 'redisson' threw exception; nested exception is org.redisson.client.RedisConnectionException: Unable to connect to Redis server: /10.1.1.1:6789
Caused by: org.redisson.client.RedisConnectionException: Unable to connect to Redis server: /10.1.1.1:6789
Caused by: org.redisson.client.RedisException: ERR Client sent AUTH, but no password is set. channel: [id: 0x534e4e24, L:/10.1.62.19:61460 - R:/10.1.1.1:6789] command: (AUTH), params: (password masked)
日志提示密码错误,但是我的redis没设置密码啊?
这时候应该把 password
字段注释掉就好了
redisson 配置错误2:配置项错误
redisson.yml
# 单节点配置
singleServerConfig:
address: "redis://10.1.1.1:6789"
# 配置看门狗的默认超时时间为30s,这里改为 10s
lockWatchdogTimeout: 10000
Caused by: com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'singleServerConfig': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
at [Source: (String)"singleServerConfig:
address: "redis://10.252.51.13:31379"
database: 1
password:
# 配置看门狗的默认超时时间为30s,这里改为 10s
lockWatchdogTimeout: 10000
"; line: 1, column: 19]
日志提示 json 格式错误?
这 lockWatchdogTimeout
字段应该和 singleServerConfig 平级的,空格缩进不正确导致,正确如下
# 单节点配置
singleServerConfig:
address: "redis://10.1.1.1:6789"
# 配置看门狗的默认超时时间为30s,这里改为 10s
lockWatchdogTimeout: 10000
释放锁失败
attempt to unlock lock, not locked by current thread by node id: ff268641-8280-4ec9-a692-00c443d87184 thread-id: 1
java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: ff268641-8280-4ec9-a692-00c443d87184 thread-id: 1
main(){
...
getLock=rLock.tryLock(100, 10000, TimeUnit.MICROSECONDS);
if (getLock) {
mockWork(rLock , index);
rLock.unLock();
}
}
private int mockWork(RLock rLock, long index) {
...
rLock.tryLock(100, 10000, TimeUnit.MICROSECONDS);
}
由于 mockWork
函数重复获取锁,导致外面这层释放失败。
如果要对锁续期,可以在获取锁时,参数 持续时间
设置为 -1
,由 redisson 负责自动续期。