思考:生产环境下Redis集群环境,怎么保证锁的同步?
我们先来回顾一下分布式锁的作用:就是保证同一时间只有一个客户端可以对共享资源进行操作。
当我们集群环境部署的时候,假如节点一在主节点获取分布式锁成功。Redis主节点再同步数据到从节点时宕机,数据没同步成功;高可用机制 则Redis从节点升级为主节点 (但是没有锁信息)。节点二 在新的Redis上也成功获取分布式锁,导致一个锁资源同时被两个节点获取,这个就出现了问题。
那么我们应该怎么去解决这个问题?
采用 RedLock 算法,Redis从3.0版本开始支持 Redlock 算法,通过在多个 Redis 节点上创建同一个锁来防止主从节点之间出现的数据不一致的问题。在 Redlock 算法中,需要从多个Redis节点获取锁,并对取锁结果进行校验,从而避免数据不一致性带来的问题。锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。需要注意的是, redlock算法会引入一定的错误率,需要根据业务场景进行权衡和控制。
RedLock加锁流程
- 客户端 获取当前毫秒级时间戳,并设置超时时间 TTL
- 依次向 N 个 Redis 服务发出请求,用能够保证全局唯一的 value 申请锁 key
- 如果从 N/2+1 个 redis 服务中都获取锁成功,那本次分布式锁的获取被视为成功,否则获取锁失败
- 如果获取锁失败,或执行达到 TTL,则向所有 Redis 服务都发出解锁请求
如果想搭建一个能够允许 N 台机器 down 掉的集群,那么就要部署一个由 2*N+1 台服务器构成的 集群。高可用部署最少的节点数计算公式 :N( 总节点数) = 2 * X(宕机数) +1,X > 0,最少需要 3 台,半数以上节点选举,不包含半数。
- 宕机一台还可以高可用:3 = 2 * 1 + 1
- 宕机二台还可以高可用:5= 2 * 2 + 1
这种架构上redis全部节点都是主节点,没有从节点,抛弃了主从的异步复制。各个节点之间没关系,不是集群也不是主从,互相独立 。由于使用比较复杂且概率性较低,但多数公司还是采用了主从架构,在一些特定的场景且要求高的情况才会采用,且节点数会根据情况增加。
ok,下面我们先来进行环境的搭建。采用Docker的方式搭建三个Redis节点。
docker run -itd --name redlock-1 -p 6380:6379 redis:7.0.8 --requirepass 123456
docker run -itd --name redlock-2 -p 6381:6379 redis:7.0.8 --requirepass 123456
docker run -itd --name redlock-3 -p 6382:6379 redis:7.0.8 --requirepass 123456
查看Redis节点部署情况。
OK,接下来我们正式进入编码环节。我们采用SpringBoot+Redisson+Redis来实现RedLock
首先创建SpringBoot项目添加依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.20.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.0.6</version>
</dependency>
</dependencies>
随后,我们编写程序主类,Redis的配置类。
/**
* @author lixiang
* @date 2023/6/25 11:33
*/
@SpringBootApplication
public class RedLockApplication {
public static void main(String[] args) {
SpringApplication.run(RedLockApplication.class, args);
}
}
/**
* @author lixiang
* @date 2023/6/25 10:59
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置key和value的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// 设置hashKey和hashValue的序列化规则
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 设置支持事物
//redisTemplate.setEnableTransactionSupport(true);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public Redisson redissonClient1(){
Config config = new Config();
config.useSingleServer().setAddress("redis://ip:6380").setDatabase(0).setPassword("123456");
return (Redisson) Redisson.create(config);
}
@Bean
public Redisson redissonClient2(){
Config config = new Config();
config.useSingleServer().setAddress("redis://ip:6381").setDatabase(0).setPassword("123456");
return (Redisson) Redisson.create(config);
}
@Bean
public Redisson redissonClient3(){
Config config = new Config();
config.useSingleServer().setAddress("redis://ip:6382").setDatabase(0).setPassword("123456");
return (Redisson) Redisson.create(config);
}
}
编写Controller逻辑。
/**
* @author lixiang
* @date 2023/6/25 11:04
*/
@RestController
@RequestMapping("/")
public class RedLockController {
/**
* 定义锁的名字
*/
public static final String CACHE_KEY_REDLOCK = "redlock";
@Autowired
private RedissonClient redissonClient1;
@Autowired
private RedissonClient redissonClient2;
@Autowired
private RedissonClient redissonClient3;
@RequestMapping("getRedLock")
public String getRedLock(){
//每个客户端分别获取锁资源
RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);
//创建红锁 客户端
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
//定义获取锁标志位,默认是获取失败
boolean isLockBoolean = false;
try {
/**
* waitTime:等待获取锁的最长时间。如果在等待时间内无法获取锁,并且没有其他锁释放,则返回 false。如果 waitTime < 0,则无限期等待,直到获得锁定。
* leaseTime:就是redis key的过期时间,锁的持有时间,可以使用 ttl 查看过期时间。
* 如果在持有时间结束前锁未被释放,则锁将自动过期,没有进行key续期,并且其他线程可以获得此锁。如果 leaseTime = 0,则锁将永久存在,直到被显式释放。
*/
isLockBoolean = redLock.tryLock(1, 20, TimeUnit.SECONDS);
System.out.printf("线程:"+Thread.currentThread().getId()+",是否拿到锁:" +isLockBoolean +"\n");
if (isLockBoolean) {
System.out.println("线程:"+Thread.currentThread().getId() + ",加锁成功,进入业务操作");
try {
//业务逻辑,40s模拟,超过了key的过期时间
TimeUnit.SECONDS.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
System.err.printf("线程:"+Thread.currentThread().getId()+"发生异常,加锁失败");
e.printStackTrace();
} finally {
// 无论如何,最后都要解锁
redLock.unlock();
}
return isLockBoolean?"success":"fail";
}
}
测试启动,接口调用。
模拟加锁故障
-
3个节点停止1个节点,加锁成功
-
3个节点停止2个节点,加锁失败
注意,RedLock也是非绝对安全的
RedLock解决了单Redis节点的分布式锁在failover的时候锁失效的问题,但节点如果出现奔溃重启,对锁的安全性依旧存在问题。
- 假如一共有5个Redis节点:A, B, C, D, E
- 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
- 节点C崩溃重启,但客户端1在C上加的锁没有持久化下来,aof机制导致。
- 节点C重启后,客户端2锁住了C, D, E,获取锁成功。
- Redis 的 AOF 持久化方式是每秒执行fsync写一次磁盘,最坏情况下可能丢失1秒的数据。
- 尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但会降低性能。
- 即使执行了fsync也仍然有可能丢失数据,因为也取决操作系统的刷盘策略,文件系统写到了buffer里面。
- 建议 延迟重启,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间。
- 那这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。
OK,RedLock相关的知识点以及案例实战,我们就介绍到这里了哦,记得三连下哦!