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设置成随机值,是为了更安全的释放锁,避免误删别的客户端获取的锁。

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

Redis: 分布式锁的官方算法RedLock以及Java版本实现库Redisson_第1张图片

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

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

Redis: 分布式锁的官方算法RedLock以及Java版本实现库Redisson_第2张图片

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

3.3.failover策略不可靠

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

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

Redis: 分布式锁的官方算法RedLock以及Java版本实现库Redisson_第3张图片

  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.引入依赖

普通版本


<dependency>
    <groupId>org.redissongroupId>
    <artifactId>redissonartifactId>
    <version>3.11.0version>
dependency>

Spring Boot Starter


<dependency>
    <groupId>org.redissongroupId>
    <artifactId>redisson-spring-boot-starterartifactId>
    <version>3.11.0version>
dependency>

4.2.关键操作

/**
 * 

RedLock的基本操作

* * @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;

/**
 * 

RedLock的简单并发测试

* * @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」获取锁失败。 

你可能感兴趣的:(Redis,分布式)