Redis 实战之多节点分布式锁【SpringBoot + RedLock】

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:
在这里插入图片描述

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
Spring Boot使用Redis分布式锁可以通过以下步骤实现: 1. 添加Redis依赖:在`pom.xml`文件添加Redis相关的依赖,例如`spring-boot-starter-data-redis`。 2. 配置Redis连接信息:在`application.properties`(或者`application.yml`)文件配置Redis的连接信息,包括主机名、端口号、密码等。 3. 创建Redis分布式锁的工具类:可以创建一个名为`RedisLockUtil`的工具类,其包含获取锁、释放锁等方法的实现。 ```java @Component public class RedisLockUtil { private static final long DEFAULT_LOCK_EXPIRE = 30000; // 默认锁的过期时间,30秒 @Autowired private StringRedisTemplate redisTemplate; /** * 获取锁 * @param lockKey 锁的键 * @param requestId 请求标识,用于区分不同的客户端 * @param expireTime 锁的过期时间 * @return true表示获取锁成功,false表示获取锁失败 */ public boolean tryLock(String lockKey, String requestId, long expireTime) { Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS); return result != null && result; } /** * 释放锁 * @param lockKey 锁的键 * @param requestId 请求标识,用于判断是否有权释放锁 */ public void releaseLock(String lockKey, String requestId) { String value = redisTemplate.opsForValue().get(lockKey); if (value != null && value.equals(requestId)) { redisTemplate.delete(lockKey); } } } ``` 4. 在需要加锁的地方使用Redis分布式锁: ```java @Autowired private RedisLockUtil redisLockUtil; public void doSomethingWithLock() { String lockKey = "lock:key"; String requestId = UUID.randomUUID().toString(); long expireTime = 5000; // 锁的过期时间为5秒 boolean locked = redisLockUtil.tryLock(lockKey, requestId, expireTime); if (locked) { try { // 获取到锁,执行业务逻辑 // ... } finally { // 释放锁 redisLockUtil.releaseLock(lockKey, requestId); } } else { // 获取锁失败,可能有其他线程正在处理 // ... } } ``` 以上是一个简单的使用Redis分布式锁的示例,你可以根据自己的实际需求进行调整和扩展。同时,还可以结合AOP、注解等方式来简化和统一锁的使用

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

响彻天堂丶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值