1 Setnx+Lua缺陷
在Redis主从+哨兵模式下,正常逻辑如下:
如果用户1获取锁成功,但是master还没把数据同步到slave,master宕机了。哨兵将slave升级到master。假设用户1还没有执行完,用户2是可以在新master里获取到锁的。
2 RedLock
Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。
锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。
设计理念:
该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。
假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁客户端执行以下操作:
1 获取当前时间,以毫秒为单位;
2 依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁;
3 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功;
4 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
5 如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
3 实现
3.1 Redis准备
docker run -p 6379:6379 --name redis-6379 \
-v /usr/local/many-redis/redis-6379/data:/data \
-v /usr/local/many-redis/redis-6379/conf/redis.conf:/etc/redis/redis.conf \
-d redis:6.2.3 redis-server /etc/redis/redis.conf
docker run -p 6380:6379 --name redis-6380 \
-v /usr/local/many-redis/redis-6380/data:/data \
-v /usr/local/many-redis/redis-6380/conf/redis.conf:/etc/redis/redis.conf \
-d redis:6.2.3 redis-server /etc/redis/redis.conf
docker run -p 6381:6379 --name redis-6381 \
-v /usr/local/many-redis/redis-6381/data:/data \
-v /usr/local/many-redis/redis-6381/conf/redis.conf:/etc/redis/redis.conf \
-d redis:6.2.3 redis-server /etc/redis/redis.conf
3.2 SpringBoot
3.2.1 pom
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- 集成redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--guava Google 开源的 Guava 中自带的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.71</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2.2 yml
server:
port: 9000
spring:
redisson:
redis-6379:
host: 192.168.38.80
port: 6379
password:
redis-6380:
host: 192.168.38.80
port: 6380
password:
redis-6381:
host: 192.168.38.80
port: 6381
password:
#超时时间为3s
timeout: 3000
3.2.3 Redis配置文件
@ConfigurationProperties(prefix = "spring.redisson")
@Data
@Configuration
public class RedisProperties {
private RedisNode redis6379;
private RedisNode redis6380;
private RedisNode redis6381;
private Integer timeout;
@Data
static class RedisNode {
private String host;
private String port;
private String password;
}
}
@Configuration
public class RedisConfig {
@Autowired
private RedisProperties redisProperties;
@Bean
public RedissonClient redissonClient6379() {
return Redisson.create(getRedisSonConfig(redisProperties.getRedis6379()));
}
@Bean
public RedissonClient redissonClient6380() {
return Redisson.create(getRedisSonConfig(redisProperties.getRedis6380()));
}
@Bean
public RedissonClient redissonClient6381() {
return Redisson.create(getRedisSonConfig(redisProperties.getRedis6381()));
}
private Config getRedisSonConfig(RedisProperties.RedisNode redisNode) {
String address = "redis://" + redisNode.getHost() + ":" + redisNode.getPort();
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer().setAddress(address).setTimeout(redisProperties.getTimeout());
if (StringUtils.isNotBlank(redisNode.getPassword())) {
singleServerConfig.setPassword(redisNode.getPassword());
}
return config;
}
}
3.3 demo
案例: 设置redLock为抢锁等待时间3秒,锁有效期为5分钟,5个线程去抢锁,查看redis。
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class RedisLockTest {
private static final Integer THREAD_COUNTS = 5;
private static final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNTS);
private static final String REDIS_LOCK = "redis_lock";
@Autowired
@Qualifier("redissonClient6379")
private RedissonClient redissonClient6379;
@Autowired
@Qualifier("redissonClient6380")
private RedissonClient redissonClient6380;
@Autowired
@Qualifier("redissonClient6381")
private RedissonClient redissonClient6381;
@Test
public void contextLoads() {
for (int i = 1; i <= THREAD_COUNTS; i++) {
new Thread(this::addCount, "thread" + i).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void addCount() {
//CACHE_KEY_REDLOCK为redis 分布式锁的key
RLock lock1 = redissonClient6379.getLock(REDIS_LOCK);
RLock lock2 = redissonClient6380.getLock(REDIS_LOCK);
RLock lock3 = redissonClient6381.getLock(REDIS_LOCK);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
//抢锁等待时间为3秒,锁的有效期为5分钟
if (redLock.tryLock(3, 300, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 抢到了锁");
Thread.sleep(60 * 1000);
} else {
System.out.println(Thread.currentThread().getName() + " 抢锁失败");
}
} catch (Exception e) {
throw new RuntimeException("分布式锁抛出异常,异常原因:{}", e.getCause());
} finally {
countDownLatch.countDown();
redLock.unlock();
}
}
}
redis-6379:
redis-6380:
redis-6381: