Redis的4种部署架构有:
Redlock分布式锁在非单机模式下才有意义,单机模式下可以直接使用普通分布式锁。
因为RedLock的实现完全基于普通分布式锁;
RedLock加锁过程: 向所有节点发送 set key value ex time nx指令,只要过半节点set成功,就认为加锁成功。
RedLock释放锁过程: 向所有节点发送del指令;
因为RedLock操作多个节点,所以效率略有下降;但避免了网络分区产生的多锁同时存在的情况;
// 构造redisson实现分布式锁必要的Config
Config config = new Config();
config.useSingleServer().setAddress("1.9.1.0:5379").setPassword("123").setDatabase(0);
// 构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 设置锁定资源名称
RLock disLock = redissonClient.getLock("DISLOCK");
boolean isLock;
try {
//尝试获取分布式锁
isLock = disLock.tryLock(500, 15000, TimeUnit.MILLISECONDS);
if (isLock) {
//TODO if get lock success, do something;
Thread.sleep(15000);
}
} catch (Exception e) {
} finally {
// 无论如何, 最后都要解锁
disLock.unlock();
}
通过代码可知,经过Redisson的封装,实现Redis分布式锁非常方便,我们再看一下Redis中的value是啥,和前文分析一样,hash结构,key就是资源名称,field就是UUID+threadId,value就是重入值,在分布式锁时,这个值为1(Redisson还可以实现重入锁,那么这个值就取决于重入次数了):
172.29.1.180:5379> hgetall DISLOCK
1) "01a6d806-d282-4715-9bec-f51b9aa98110:1"
2) "1"
即sentinel模式,实现代码和单机模式几乎一样,唯一的不同就是Config的构造:
Config config = new Config();
config.useSentinelServers().addSentinelAddress(
"redis://127.0.0.1:26378","redis://127.0.0.1:26379", "redis://127.0.0.1:26380")
.setMasterName("mymaster")
.setPassword("a123456").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
// 还可以getFairLock(), getReadWriteLock()
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
isLock = redLock.tryLock();
// 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
if (isLock) {
//TODO if get lock success, do something;
}
} catch (Exception e) {
} finally {
// 无论如何, 最后都要解锁
redLock.unlock();
}
集群模式构造Config如下:
Config config = new Config();
config.useClusterServers().addNodeAddress(
"redis://127.0.0.1:6375","redis://127.0.0.1:6376", "redis://127.0.0.1:6377",
"redis://127.0.0.1:6378","redis://127.0.0.1:6379", "redis://127.0.0.1:6380")
.setPassword("123").setScanInterval(5000);
RedissonClient redissonClient = Redisson.create(config);
// 还可以getFairLock(), getReadWriteLock()
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
isLock = redLock.tryLock();
// 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
if (isLock) {
//TODO if get lock success, do something;
}
} catch (Exception e) {
} finally {
// 无论如何, 最后都要解锁
redLock.unlock();
}
如果是多个没有关系的redis单机之间使用RedLock来做分布式锁
可以直接将各个单机的锁组成一个锁:
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
实现分布式锁的一个非常重要的点就是set的value要具有唯一性,redisson的value是怎样保证value的唯一性呢?答案是UUID+threadId。入口在redissonClient.getLock(“REDLOCK_KEY”),源码在Redisson.java和RedissonLock.java中:
protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
return id + ":" + threadId;
}
看门狗是客户端的行为,创建锁后每隔10秒帮你把key的超时时间设为30s这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。
lockWatchdogTimeout(监控锁的看门狗超时,单位:毫秒)
默认值:30000
监控锁的看门狗超时时间单位为毫秒。该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout参数的情况。如果该看门狗未使用lockWatchdogTimeout去重新调整一个分布式锁的lockWatchdogTimeout超时,那么这个锁将变为失效状态。这个参数可以用来避免由Redisson客户端节点宕机或其他原因造成死锁的情况。
key是唯一的,key里面的field为uuid:线程Id。uuid由调用系统生成,一个系统中同一个锁只会用同一个uuid,不通系统的相同锁的uuid不一样。
所以根据uuid+线程id就可以确定当前线程是否拥有锁,如果拥有,就把value+1,释放锁就-1;
获取锁的代码为redLock.tryLock()或者redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS),两者的最终核心源码都是下面这段代码,只不过前者获取锁的默认租约时间(leaseTime)是LOCK_EXPIRATION_INTERVAL_SECONDS,即30s:
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
// 获取锁时向5个redis实例发送的命令
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 首先分布式锁的KEY不能存在,如果确实不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间(也是锁的租约时间)
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 如果分布式锁的KEY已经存在,并且value也匹配,表示是当前线程持有的锁,那么重入次数加1,并且设置失效时间
"if (redis.call('hexists', KEYS[1], [2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 获取分布式锁的KEY的失效时间毫秒数
"return redis.call('pttl', KEYS[1]);",
// 这三个参数分别对应KEYS[1],ARGV[1]和ARGV[2]
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
获取锁的命令中,
KEYS[1]就是Collections.singletonList(getName()),表示分布式锁的key,即REDLOCK_KEY;
ARGV[1]就是internalLockLeaseTime,即锁的租约时间,默认30s;
ARGV[2]就是getLockName(threadId),是获取锁时set的唯一值,即UUID+threadId:
释放锁的代码为redLock.unlock(),核心源码如下:
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
// 向5个redis实例都执行如下命令
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果分布式锁KEY不存在,那么向channel发布一条消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
// 如果分布式锁存在,但是value不匹配,表示锁已经被占用,那么直接返回
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 如果就是当前线程占有分布式锁,那么将重入次数减1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,还不能删除
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
// 重入次数减1后的值如果为0,表示分布式锁只获取过1次,那么删除这个KEY,并发布解锁消息
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
// 这5个参数分别对应KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>springboot-redisartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>springboot-redisname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.projectreactorgroupId>
<artifactId>reactor-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.1.7
#spring.redis.cluster.nodes=127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
#spring.redis.password=123
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=200
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Redis配置文件的参数
*/
@Data
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
private String host;
private int port;
private String password;
private int database;
}
Redission配置类,主要作用就是建立redis连接,生成client对象
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Redission配置类,主要作用就是建立redis连接,生成client对象
*/
@Slf4j
@Configuration
public class RedissionConfig {
@Autowired
private RedisProperties redisProperties;
@Bean
public RedissonClient redissonClient() {
RedissonClient redissonClient;
Config config = new Config();
String url = redisProperties.getHost() + ":" + redisProperties.getPort();
config.useSingleServer().setAddress(url)
.setPassword(redisProperties.getPassword())
.setDatabase(redisProperties.getDatabase());
try {
redissonClient = Redisson.create(config);
return redissonClient;
} catch (Exception e) {
log.error("RedissonClient init redis url:[{}], Exception:", url, e);
return null;
}
}
}
规定了获得锁后,如何调用业务逻辑
/**
* 锁后业务接口规范
*/
public interface AquiredLockWorker<T> {
T invokeAfterLockAquire() throws Exception;
}
规定了如何调用加锁规范
/**
* redis加锁接口规范
*/
public interface RedisLocker {
/**
* 获取锁
* @param lockName 锁的名称
* @param worker 获取锁后的处理类
* @param
* @return 处理完具体的业务逻辑要返回的数据
* @throws UnableToAquireLockException
* @throws Exception
*/
<T> T lock(String lockName, AquiredLockWorker<T> worker) throws UnableToAquireLockException, Exception;
<T> T lock(String lockName, AquiredLockWorker<T> worker, int lockTime) throws UnableToAquireLockException, Exception;
}
实现了如何加锁、释放锁;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* redis加锁实现类
*/
@Component
public class RedisLockerImpl implements RedisLocker {
private final static String LOCKER_PREFIX = "lock:";
@Resource(name = "redissonClient")
private RedissonClient redissonClient;
@Override
public <T> T lock(String lockName, AquiredLockWorker<T> worker) throws InterruptedException, UnableToAquireLockException, Exception {
return lock(lockName, worker, 100);
}
@Override
public <T> T lock(String lockName, AquiredLockWorker<T> worker, int lockTime) throws UnableToAquireLockException, Exception {
//加锁
RLock lock = redissonClient.getLock(LOCKER_PREFIX + lockName);
// Wait for 100 seconds seconds and automatically unlock it after lockTime seconds
//加锁等待100秒,100秒后仍未获得锁则返回false。获得锁后lockTime后自动释放锁。
boolean success = lock.tryLock(100, lockTime, TimeUnit.SECONDS);
if (success) {
try {
return worker.invokeAfterLockAquire();
} finally {
lock.unlock();
}
}
throw new UnableToAquireLockException();
}
}
/**
* 异常类
*/
public class UnableToAquireLockException extends RuntimeException {
public UnableToAquireLockException() {
}
public UnableToAquireLockException(String message) {
super(message);
}
public UnableToAquireLockException(String message, Throwable cause) {
super(message, cause);
}
}
生成多个线程,进行锁竞争,完成业务逻辑。
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
/**
* @Author ccl
* @Date 2021/1/17 14:18
*/
@Slf4j
@RestController
public class LockTestController {
@Autowired
private RedisLocker redisLocker;
@RequestMapping(value = "/redlock")
public String testRedlock() throws Exception {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(5);
for (int i = 0; i < 5; ++i) {
// create and start threads
new Thread(() -> {
try {
startSignal.await();
//主要代码在这里,第一个参数为锁的名字,第二个参数为加锁后要执行的业务
redisLocker.lock("test", () -> {
doTask();
doneSignal.countDown();
return null;
});
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
startSignal.countDown(); // let all threads proceed
doneSignal.await();
log.info("All processors done. Shutdown connection");
return "redlock";
}
/**
* 加锁后要执行的业务逻辑
*/
void doTask() {
log.info(Thread.currentThread().getName() + " start");
Random random = new Random();
int _int = random.nextInt(200);
log.info(Thread.currentThread().getName() + " sleep{}millis", _int);
try {
Thread.sleep(_int);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("{}end", Thread.currentThread().getName());
}
}