Redis: 分布式锁的官方算法RedLock以及Java版本实现库Redisson

本文深入探讨Redis实现分布式锁的方法,包括setnx命令的基础实现及RedLock算法的高级应用,通过实例展示如何在Java环境中使用Redisson库实现分布式锁,确保在分布式系统中资源的安全访问。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.简介

在单机应用中,当多个线程访问共享资源时,我们通常通过synchronized关键字、Lock锁、线程安全对象等措施保证资源的安全使用。

在分布式环境下,上述措施不再能满足需求,这事,我们需要一种应用于分布式换件的加锁机制,即:分布式锁。

分布式锁的实现方式有多重,如:数据库、Redis、ZooKeeper等等。

本文主要讲解Redis的分布式锁实现方式,主要依据官方文档:Distributed locks with Redis

2.分布式锁三要素

一个分布式锁必须要满足的这个特性:

  1. 独享:任意时刻,只有一个客户端持有锁。
  2. 无死锁:即使客户端崩溃,或者是网络异常,锁仍可被获取。
  3. 容错:只要大部分Redis节点存活,客户端就可以正常使用分布式锁。

3.最普遍的实现方式:SetNx

3.1.setnx

其实根据现状,基于Redis实现分布式锁最常见的方案是通过setnx命令:

# 当且仅当可以不存在时,将set (key,value),并返回1;否则返回0.
SET key random_value NX PX 30000

setnx通过以下措施来实现一个分布式锁:

  • 独享特性:
    • 当客户端尝试加锁时,setnx一个Key;当释放锁时,del这个可以。
    • 单个Redis实例提供服务。
    • 将value设置成随机值。
  • 无死锁特性:这个Key有过期时间,使它会最终释放。
  • 容错:为了避免单点失败问题,构建master-slave架构,当master节点挂掉时,通过failover策略,将slave节点升级为master。

3.2.单实例的必要性

为什么这里强调redis的单实例,因为只有单实例才能保证setnx的原子性。

通过之前的文章Redis: 单线程模型、I/O多路复用、影响性能的因素、性能与QPS,可以确定,即使是多个连接同时执行setnx,最终这些setnx命令依然是按顺序执行的,不存在并发的可能。

3.3.设置过期时间

设置过期时间,使得即使因为其他原因(如:客户端崩溃、网络异常),这个键也会最终被释放掉,不会造成锁一直被占用的情况。

3.4.value设置成随机值

将value设置成随机值,是为了更安全的释放锁,避免误删别的客户端获取的锁。

我们先看看不设置随机值的情况:

在这里插入图片描述

可以看到,最终客户端A删掉了客户端B设置的可以。

下面,看一看设置了随机值的情况:

在这里插入图片描述

可以看到,每个客户端设置的key,分别持有唯一的随机值,可以防止自己的可以被其他客户端误删掉。

3.3.failover策略不可靠

上述方案的问题在于:主从同步通常是异步的,并不能真正的容错。

造成锁不独享的场景如下图所示:

在这里插入图片描述

  1. 客户端A申请从master实例获取锁key=test001,由于之前key=test001在master实例上不存在,所以客户端A获取锁成功。
  2. master在通过异步主从同步将key=test001同步至slave之前挂掉了,此时slave经过failover升级为master,但是此时slave上并无key=test001。
  3. 此时,客户端B申请从redis获取锁key=test001,由于此时slave上不存在key=test001,同样的,客户端B获取锁成功。
  4. 最终的结果是,由于关键时刻的master宕机,造成两个客户端同时加锁成功,这与分布式锁的独享特性相互违背。

4.官方推荐的实现方式:RedLock

Redis官方提出一种算法,叫Redlock,认为这种实现比普通的单实例实现更安全。

RedLock有多种语言的实现包,其中Java版本的实现包叫做:Redisson

下面,描述如何通过Redisson实现分布式锁,并加以验证。

4.1.引入依赖

普通版本

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.11.0</version>
</dependency>

Spring Boot Starter

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

4.2.关键操作

/**
 * <p>RedLock的基本操作</P>
 *
 * @author hanchao
 */
@Slf4j
public class RedLockDemo {
    public static void main(String[] args) {
        //连接redis
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);
        log.info("连接Redis");

        //1.定义锁
        RLock lock = redisson.getLock("myTest001");

        try {
            //尝试加锁的超时时间
            Long timeout = 300L;
            //锁过期时间
            Long expire = 30L;
            //2.获取锁
            if (lock.tryLock(timeout, expire, TimeUnit.MILLISECONDS)) {
                //2.1.获取锁成功的处理
                log.info("加锁成功");
                //...do something
                log.info("使用完毕");
            } else {
                //2.2.获取锁失败的处理
                log.info("加锁失败");
                log.info("其他处理");
            }
        } catch (InterruptedException e) {
            log.error("尝试获取分布式锁失败", e);
        } finally {
            //3.释放锁
            try {
                lock.unlock();
                log.info("锁释放成功");
            } catch (Exception e) {
                //do nothing...
            }
        }

        //关闭连接
        redisson.shutdown();
        log.info("关闭redis连接");
    }
}

执行结果:

 INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:25 - 连接Redis 
 INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:37 - 加锁成功 
 INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:39 - 使用完毕 
 INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:46 - 锁释放成功 
 INFO pers.hanchao.basiccodeguideline.redlock.RedLockDemo:51 - 关闭redis连接 

4.3.并发测试

测试说明:1000个线程在线程池中执行,分别记录加锁成功的和失败的个数。

package pers.hanchao.basiccodeguideline.redlock;

import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;

/**
 * <p>RedLock的简单并发测试</P>
 *
 * @author hanchao
 */
@Slf4j
public class RedLockTestDemo {
    public static void main(String[] args) throws InterruptedException {
        //连接redis
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        //锁的名字
        String key = "myTest001";
        //尝试加锁的超时时间
        Long timeout = 1000L;
        //锁过期时间
        Long expire = 30L;
        //并发数
        Integer size = 1000;

        //定义线程池
        ExecutorService executorService = Executors.newFixedThreadPool(size);

        //定义倒计时门闩:以保证所有线程执行完毕再进行最后的计数
        CountDownLatch latchCount = new CountDownLatch(size);

        //计数器
        LongAdder adderSuccess = new LongAdder();
        LongAdder adderFail = new LongAdder();

        //多线程执行
        for (int i = 0; i < size; i++) {
            executorService.execute(() -> {
                //定义锁
                RLock lock = redisson.getLock(key);
                try {
                    //获取锁
                    if (lock.tryLock(timeout, expire, TimeUnit.MILLISECONDS)) {
                        //成功计数器累加1
                        adderSuccess.increment();
                        latchCount.countDown();
                    } else {
                        //失败计数器累加1
                        adderFail.increment();
                        latchCount.countDown();
                    }
                } catch (InterruptedException e) {
                    log.error("尝试获取分布式锁失败", e);
                } finally {
                    //释放锁
                    try {
                        lock.unlock();
                    } catch (Exception e) {
                        //do nothing
                    }
                }
            });
        }
        //等待所有线程执行完毕
        latchCount.await();

        //关闭线程池
        executorService.shutdown();

        //关闭连接
        redisson.shutdown();

        log.info("共计「{}」获取锁成功,「{}」获取锁失败。", adderSuccess.intValue(), adderFail.intValue());
    }
}

测试结果:

INFO traceId: pers.hanchao.basiccodeguideline.redlock.RedLockTestDemo:84 - 共计「84」获取锁成功,「916」获取锁失败。 
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值