Redis主从集群环境搭建以及分布式锁实现方案

文章内容

本文使用windows基于主从复制的方式搭建redsi集群,使用springboot和redisson包实现分布式锁,并进行了模拟测试。

Redis集群环境搭建

redis版本5.0.14.1

将zip包解压缩,然后复制六份。这里不能在一个文件夹里面启动多次,因为这样会导致redis的数据会持久化到同一个文件当中,所以需要复制六个文件夹。当然,如果你不觉得麻烦可以配置六次配置文件,修改他们持久化的文件。
在这里插入图片描述

修改redsi.window.conf文件中的相关配置,如下

这个是节点相关的配置
cluster-config-file nodes-6375.conf
超时的设置
cluster-node-timeout 15000
端口号
port 6375
如果需要密码的话可以放开
requirepass foobared

然后依次启动六个节点,注意要先把这六个节点全部启动起来,并且刚开始配置的时候里面不能有数据,必须保持一致。
启动命令:redis-server.exe [你的配置文件]
在这里插入图片描述
然后下面是最关键的一步,部署集群,也就是将所有的节点关联起来而不是向刚才那样各自独立:

redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6378 127.0.0.1:6377 127.0.0.1:6376 127.0.0.1:6375 127.0.0.1:6374  --cluster-replicas 1 -a foobared

然后我们就可以登录某一个节点了
登录命令:redis-cli -h localhost -p 6374 -a foobared -c,注意这个-c参数是集群环境下要加的,不然后面的操作会报错。
使用cluster nodes可以看到所有的节点的主从状态。这六个节点是相互关联的,其中一个节点加入数据在其他的节点都能够查询到数据。其中一个节点删除其他节点的数据一样会删除。这里相关的原理暂不做过多的分析。
在这里插入图片描述

Redis实现分布式锁方案

首先项目中添加依赖

<dependency>
  	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.4.0</version>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.9.1</version>
</dependency>
<dependency>

自动配置引入下面的依赖

 <dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-data-20 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-data-26</artifactId>
    <version>3.17.0</version>
</dependency>

配置文件的内容

spring:
  redis:
#    host: localhost
    cluster:
      nodes: localhost:6374,localhost:6375,localhost:6376,localhost:6377,localhost:6378,localhost:6379
    password: foobared
    port: 6379
    database: 0
    lettuce:
      pool:
        max-active: 8   #最大连接数据库连接数,设 -1 为没有限制
        max-idle: 8     #最大等待连接中的数量,设 0 为没有限制
        max-wait: -1ms  #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
        min-idle: 0     #最小等待连接中的数量,设 0 为没有限制
      shutdown-timeout: 100ms
    timeout: 100000

这个配置文件的内容会被spring读取并以RedisProperties进行封装。所以我们这里进行Redis相关的配置如下:


@Component
@Data
@ToString(callSuper = true)
public class RedisConfig {
    @Resource
    RedisProperties redisProperties;
    /**
     * 配置redisson
     * @return
     */
    @Bean
    public RedissonClient redisson() {
        RedisProperties.Cluster cluster = redisProperties.getCluster();
        String[] redisNodes = cluster.getNodes().stream().map(value -> ("redis://" + value)).toArray(String[]::new);
        Config config = new Config();
        ClusterServersConfig clusterServersConfig = config.useClusterServers()
                .addNodeAddress(redisNodes);
        clusterServersConfig.setPassword(redisProperties.getPassword());
        return  Redisson.create(config);
    }

    @Bean
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);

        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

下面我们对加锁和解锁进行封装,这里只是简单的演示,项目中还要自己进行封装。


@Component
public class RedisExcutor {

    private static final String LOCK_PREFIX = "*******";
    //这里注入的就是前面配置的bean,不然会注入失败
    @Autowired
    RedissonClient redissonClient;
    /**
     * 最终加强分布式锁
     * @return 是否获取到
     */
    public boolean lock() throws SQLException {
        RLock lock = redissonClient.getLock(LOCK_PREFIX);
        return lock.tryLock();
    }
    public void unlock() {
        RLock lock = redissonClient.getLock(LOCK_PREFIX);
        lock.unlock();
    }
}

分布式锁测试

我们这里使用单机模拟多线程并发操作看看分布式锁是否生效,验证的代码如下

@RestController
@RequestMapping("/redis")
public class RedisController {

    @Resource
    RedisExcutor redisExcutor;

    public volatile int a = 0;
    public volatile int b = 0;
    private int threadNum = 100;
    @GetMapping("/testunlock")
    public String testUnlock() throws SQLException, InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                try {
                    cyclicBarrier.await();
                    for (int j = 0; j < 1000; j++) {
                        a++;
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } catch (BrokenBarrierException e) {
                    throw new RuntimeException(e);
                } finally {
                    countDownLatch.countDown();
                }

            }).start();
        }
        countDownLatch.await();
        return "finally ,the value of a is " + a;
    }

    @GetMapping("/testlock")
    public String testLock() throws SQLException, InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(()->{
                try {
                    cyclicBarrier.await();
                    if (redisExcutor.lock()) {
                        try {
                            for (int j = 0; j < 1000; j++) {
                                b++;
                            }
                            Thread.sleep(1000);
                        } finally {
                            redisExcutor.unlock();
                        }
                    }
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                } catch (BrokenBarrierException e) {
                    throw new RuntimeException(e);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();
        return "finally ,the value of b is " + b;
    }

    @PostMapping("/clear")
    public String zero() {
        a = 0;
        b = 0;
        return "a = " + a + " and b = "+b;
    }
}

这里我们模拟一百个请求并发执行,每次请求循环操作1000次。
首先执行清零把a和b的值清零
在这里插入图片描述
测试无锁请求,可以发现存在问题,数据并不是预期的数据100000.
在这里插入图片描述
测试加锁请求,由于代码中加了一个sleep因此基本上只有一个线程可以获取到锁,最后的值应该是1000。
这里遇到一个问题,有点时候会出现2000,但是这里并没有问题,而是我电脑性能太低导致实际上各个线程并没有同时执行,而是其中有一个线程获取到锁并释放了然后另一个线程获取到了导致的。实验验证如下,我们修改了原来的代码,把并发的请求数量增加到500

 @GetMapping("/testlock")
    public String testLock() throws SQLException, InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(()->{
                try {
                    cyclicBarrier.await();
                    if (redisExcutor.lock()) {
                        Timestamp timestamp = new Timestamp(new Date().getTime());
                        System.out.println(timestamp+" 线程"+ Thread.currentThread().getName()+ "获取到了锁");
                        try {
                            for (int j = 0; j < 1000; j++) {
                                b++;
                            }
                            Thread.sleep(1000);
                        } finally {
                            redisExcutor.unlock();
                        }
                        timestamp = new Timestamp(new Date().getTime());
                        System.out.println(timestamp+" 线程"+ Thread.currentThread().getName()+ "获取到了锁");
                    }
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                } catch (BrokenBarrierException e) {
                    throw new RuntimeException(e);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();
        return "finally ,the value of b is " + b;
    }

打印的结果如下:
在这里插入图片描述
这里说明请求是顺序执行的,是其中一个线程释放锁之后另一个线程才获取的。

countdownlatch和cyclicbarrier的区别

countdownlatch是所有线程执行完了之后才让主线程继续执行,这样我们上面代码中输出的a和b就是最终所有线程操作结束之后的值,而不是获取到线程执行过程中的值。

cyclicbarrier是为了让我们所有的线程同时执行,而不是随着我们主程序的执行一个个启动。

Redisson如何避免死锁

Redission为了避免锁未被释放,采用了一个特殊的解决方案,若未设置过期时间的话,redission默认的过期时间是30s,同时未避免锁在业务未处理完成之前被提前释放,Redisson在获取到锁且默认过期时间的时候,会在当前客户端内部启动一个定时任务,每隔internalLockLeaseTime/3的时间去刷新key的过期时间,这样既避免了锁提前释放,同时如果客户端宕机的话,这个锁最多存活30s的时间就会自动释放(刷新过期时间的定时任务进程也宕机)。
但是如果仅仅是锁过期了但所没有释放,那么只有当前线程可以继续获取锁,其他线程永远获取不到锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北海冥鱼未眠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值