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

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」获取锁失败。 
已标记关键词 清除标记
JAVA开发人员必备是HTML格式的 JavaTM 2 Platform Standard Edition 6 API 规范 本文档是 Java 2 Platform Standard Edition 6.0 的 API 规范。 请参见: 描述 Java 2 Platform 软件包 java.applet 提供创建 applet 所必需的类和 applet 用来与其 applet 上下文通信的类。 java.awt 包含用于创建用户界面和绘制图形图像的所有类。 java.awt.color 提供用于颜色空间的类。 java.awt.datatransfer 提供在应用程序之间和在应用程序内部传输数据的接口和类。 java.awt.dnd Drag 和 Drop 是一种直接操作动作,在许多图形用户界面系统中都会遇到它,它提供了一种机制,能够在两个与 GUI 中显示元素逻辑相关的实体之间传输信息。 java.awt.event 提供处理由 AWT 组件所激发的各类事件的接口和类。 java.awt.font 提供与字体相关的类和接口。 java.awt.geom 提供用于在与二维几何形状相关的对象上定义和执行操作的 Java 2D 类。 java.awt.im 提供输入方法框架所需的类和接口。 java.awt.im.spi 提供启用可以与 Java 运行时环境一起使用的输入方法开发的接口。 java.awt.image 提供创建和修改图像的各种类。 java.awt.image.renderable 提供用于生成与呈现无关的图像的类和接口。 java.awt.print 为通用的打印 API 提供类和接口。 java.beans 包含与开发 beans 有关的类,即基于 JavaBeansTM 架构的组件。 java.beans.beancontext 提供与 bean 上下文有关的类和接口。 java.io 通过数据流、序列化和文件系统提供系统输入和输出。 java.lang 提供利用 Java 编程语言进行程序设计的基础类。 java.lang.annotation 为 Java 编程语言注释设施提供支持。 java.lang.instrument 提供允许 Java 编程语言代理检测运行在 JVM 上的程序的服务。 java.lang.management 提供管理接口,用于监视和管理 Java 虚拟机以及 Java 虚拟机在其上运行的操作系统。 java.lang.ref 提供了引用对象类,支持在某种程度上与垃圾回收器之间的交互。 java.lang.reflect 提供类和接口,以获得关于类和对象的反射信息。 java.math 提供用于执行任意精度整数算法 (BigInteger) 和任意精度小数算法 (BigDecimal) 的类。 java.net 为实现网络应用程序提供类。 java.nio 定义作为数据容器的缓冲区,并提供其他 NIO 包的概述。 java.nio.channels 定义了各种通道,这些通道表示到能够执行 I/O 操作的实体(如文件和套接字)的连接;定义了用于多路复用的、非阻塞 I/O 操作的选择器。 java.nio.channels.spi 用于 java.nio.channels 包的服务提供者类。 java.nio.charset 定义用来在字节和 Unicode 字符之间转换的 charset、解码器和编码器。 java.nio.charset.spi java.nio.charset 包的服务提供者类。 java.rmi 提供 RMI 包。 java.rmi.activation 为 RMI 对象激活提供支持。 java.rmi.dgc 为 RMI 分布式垃圾回收提供了类和接口。 java.rmi.registry 提供 RMI 注册表的一个类和两个接口。 java.rmi.server 提供支持服务器端 RMI 的类和接口。 java.security 为安全框架提供类和接口。 java.security.acl 此包中的类和接口已经被 java.security 包中的类取代。 java.security.cert 提供用于解析和管理证书、证书撤消列表 (CRL) 和证书路径的类和接口。 java.security.interfaces 提供的接口用于生成 RSA Laboratory Technical Note PKCS#1 中定义的 RSA(Rivest、Shamir 和 Adleman AsymmetricCipher 算法)密钥,以及 NIST 的 FIPS-
相关推荐
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页