【分布式】一文搞懂分布式锁 - Redisson

参考视频 https://www.bilibili.com/video/BV1kd4y1G7dM/?spm_id_from=333.337.search-card.all.click&vd_source=18bbf3535176020cbe0513d95ae59cc8

【分布式锁介绍】

分布式锁

  分布式锁,即分布式系统中的锁。在单体应用中我们通过锁(Synchronized、ReentrantLock 等)解决的是控制共享资源访问的问题。而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。

  与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。多个节点(通常是多台机器)可能需要访问共享资源,而如果多个节点在同一时刻访问同一资源,就可能导致数据冲突、重复执行等问题。为了避免这种情况,可以通过分布式锁来确保每次只有一个节点能够访问共享资源,其他节点必须等待锁的释放才能继续访问。

分布式锁特点

  1. 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行。
  2. 高可用的获取锁与释放锁
  3. 高性能的获取锁与释放锁
  4. 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
  5. 具备锁失效机制,即自动解锁,防止死锁
  6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

分布式锁主流实现方式

  • 基于数据库实现: 核心理念基于唯一索引,使用版本号或 CAS 机制。实现简单,无需额外组件引入。但是性能差并且没有自动超时释放,数据库单点故障风险。
  • 基于 Redis 实现: 单节点 Redis 采用 SETNX 操作保证原子性,多节点使用 ResLock 算法,可集成 Redisson 快速实现。优点在于性能高、支持自动续期。缺点在于 Redis 单节点脑裂问题,且 RedLock 算法争议较大【主流方案】
  • 基于 ZooKeeper 实现: 客户端创建临时节点,判断是否是最小编号节点,前序节点删除时触发回调获取锁。强一致性(ZAB 协议保证),天然支持锁释放(会话断开自动删除节点)。缺点在于性能低,适用于对一致性要求极高的场景。
  • 基于 K8S 的 ETCD 实现: 使用 etcdctl lock 命令或客户端 SDK,基于 Raft 协议实现强一致性。优点在于高可用和强一致性,且支持 Lease 自动续期,但是运维复杂度较高并且社区生态不活跃。
  • 阿里云 Tair: 基于 Redis 增强版,支持 EXISTS + SET 原子指令【收费】

【自行实现分布式锁】

MySQL 实现方式

  基于数据库的锁实现有两种方式,一是基于数据库表的增删,另一种是基于数据库排他锁。优点是直接借助数据库,简单容易理解。缺点是操作数据库需要一定的开销,性能问题需要考虑。这种锁的可靠性依赖于数据库。建议设置备库,避免单点,进一步提高可靠性。锁没有失效时间,一旦释放锁操作失败就会导致锁记录一直在数据库中,其它线程无法获得锁。这个缺陷也很好解决,比如可以做一个定时任务去定时清理。

  • 基于数据库表增删: 首先创建一张锁的表主要包含下列字段:类的全路径名+方法名,时间戳等字段。当需要锁住某个方法时,往该表中插入一条相关的记录。类的全路径名+方法名是有唯一性约束的,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。执行完毕之后,需要 delete 该记录。
  • 基于数据库排他锁: 基于 MySql 的 InnoDB 引擎,在查询语句后面增加 for update,数据库会在查询过程中给数据库表增加排他锁。获得排它锁的线程即可获得分布式锁,当获得锁之后,可以执行方法的业务逻辑,执行完方法之后,释放锁 connection.commit()。当某条记录被加上排他锁之后,其他线程无法获取排他锁并被阻塞。

Redis 独立实现分布式锁



【Redisson】

Redisson 介绍

  Redisson 是基于 Redis 的高性能 Java 客户端,它提供了丰富的工具来简化分布式系统中的常见问题。分布式锁是其中一个重要的功能。Redisson 分布式锁允许在分布式环境中保证对共享资源的独占访问。通过 Redisson 提供的 RLock 类,用户可以很容易地实现分布式锁机制。

Redisson 集成 SpringBoot

<!-- SpringBoot Redisson依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.7</version>
</dependency>
# --------------------- application.yml --------------------
spring:
  redis:
    # Redisson 配置
    redisson:
      config: |
        singleServerConfig:
          password: foobared
          address: "redis://127.0.0.1:6379"
          database: 10
// ------------------------ 启动类 ----------------------------
@SpringBootApplication
// 加入Redisson注解
@ImportAutoConfiguration(RedissonAutoConfiguration.class)
public class App {
    public static void main(String[] args) {SpringApplication.run(App.class, args);}
}
// ------------------------ 分布式锁 ----------------------------
@Slf4j
@RestController
public class RedissonController {
    // 注入RedissonClient对象
    @Autowired
    private RedissonClient redissonClient;

    public void testLock() {
        // 创建锁对象
        RLock lock = redissonClient.getLock("UUID" + "业务主键");
        try {
            // 尝试获取锁,超时时间为10秒,锁自动释放时间为30秒
            if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
                log.info("获取锁成功,处理业务逻辑");
            }
        } catch (InterruptedException e) {
            log.error("获取锁失败,原因:{}", e.getMessage());
            throw new RuntimeException(e);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

lock() 与 tryLock()

  • lock(): 阻塞当前线程并一直获取锁,直到获取到锁为止。可能造成死锁。
  • tryLock(): 通过不同的重载方法,可配置获取锁最大等待时间以及持有锁时间,如果超过获取锁最大等待时间还没有获取到锁,会报错并返回。如果配置了最大持有锁时间,在方法执行超过时间会释放锁,无论方法是否执行完成。如果不设置最大持有时间,会触发看门狗机制,如果方法一直没有执行完(没有主动释放锁),会一直增加持有时间,直到主动释放锁。

【Redisson 高级知识】

WatchDog 看门狗机制

  Redisson 的看门狗机制(WatchDog)是 Redisson 分布式锁实现中的一个重要特性。它主要用于解决在分布式系统中,当锁的持有者因为某些原因(如线程崩溃、系统重启等)无法正常释放锁时,可能导致锁永久丢失或死锁的问题。看门狗机制通过自动续期锁的方式来维护锁的状态,确保锁在持有期间始终有效,从而保护系统的并发安全性。

  当一个线程成功获取到分布式锁后,Redisson 会启动一个后台定时任务。该任务定期向 Redis 服务器发送续约请求,以延长锁的有效期。续约操作通常通过执行 Redis 的 LUA 脚本来实现,以确保操作的原子性。注意:看门狗机制开启的前提就是程序未设置锁持有时间,也就是 tryLock()的 leaseTime 参数。

// 就是这个方法的leaseTime参数
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
  1. ‌ 自动续期 ‌: 看门狗机制能够自动处理锁的续期,无需开发者干预。
  2. ‌ 防止死锁 ‌: 如果持有锁的线程发生异常或崩溃,看门狗会停止续约,锁会在一段时间后自动释放,避免死锁的发生。比如系统崩溃,定时任务也就不存在,锁会因为看门狗默认的 30S 而自动消失,不会死锁。
  3. ‌ 高可靠性 ‌: 通过确保锁在持有期间始终有效,看门狗机制能够提升系统的并发安全性。

Redisson 可重入锁

  Redisson 默认实现了可重入锁,底层是以 Hash 数据结构的形式将锁存储在 Redis 中,并且 Redisson 分布式锁具有可重入性,每次获取锁,都将 value 的值+1,每次释放锁,都将 value 的值-1,只有锁的 value 值归 0 时才会真正的释放锁,从而确保锁的可重入性。

  • 可重入锁: 又称之为递归锁,也就是一个线程可以反复获取锁多次,一个线程获取到锁之后,内部如果还需要获取锁,可以直接再获取锁,前提是同一个对象或者 class。可重入锁的最重要作用就是避免死锁的情况。
  • 不可重入锁: 又称之为自旋锁,底层是一个循环加上 unsafe 和 cas 机制,就是一直循环直到抢到锁,这个过程通过 cas 进行限制,如果一个线程获取到锁,cas 会返回 1,其它线程包括自己就不能再持有锁,需要等线程释放锁。

Redisson 读写锁

  Redisson 中支持分布式读写锁,这种锁允许同时有多个读锁和一个写锁对同一个资源进行加锁。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

Redisson 联锁

  Redisson 联锁(MultiLock)是 Redisson 提供的一个分布式锁机制,它允许将多个分布式锁组合成一个联锁,从而实现对多个资源的同步访问控制。这种机制在需要同时锁定多个资源以确保数据一致性的场景中非常有用。Redisson 联锁的工作原理基于 Redis 的分布式锁机制和 Lua 脚本的原子性操作。当线程尝试获取联锁时,Redisson 会依次尝试获取每个组成联锁的分布式锁。如果所有锁都成功获取,则线程获得联锁并可以继续执行后续操作。如果任何一个锁获取失败,则线程无法获得联锁,并会等待或返回失败。

不同模式下的配置

  如果项目中已经引入了 Redis,可以直接复用 spring.redis.xxx 的配置。无需额外配置。

# ------------------------------- 单机模式 ------------------------------
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    # Redisson 扩展配置
    redisson:
      config: |
        singleServerConfig:
          idleConnectionTimeout: 10000
          connectTimeout: 5000
          timeout: 3000
          retryAttempts: 3
          retryInterval: 1500
          password: null
          subscriptionsPerConnection: 5
          clientName: "my-redisson"
          address: "redis://${spring.redis.host}:${spring.redis.port}"
          database: ${spring.redis.database}
# ------------------------------- 集群模式 ------------------------------
spring:
  redis:
    redisson:
      config: |
        clusterServersConfig:
          nodeAddresses:
            - "redis://xxx1:6379"
            - "redis://xxx2:6379"
          scanInterval: 2000
          retryAttempts: 3
          retryInterval: 1500
          password: "your_password"

【Redisson 源码解析】

详细源码解析文档:https://blog.youkuaiyun.com/weixin_52152676/article/details/144665255

tryLock()尝试加锁源码分析

  tryLock()是 Redisson 加锁的核心代码,在加锁成功之后返回 true,可以传入三个参数分别为:获取锁等待时间、持有锁最长时间、时间单位。而 tryLock()最终通过调用 tryLockInnerAsync()方法内部的 Lua 脚本,进行加锁的逻辑处理,所有的逻辑均为 Lua 中实现。

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
            // 判断锁是否存在(当前key)
            "if (redis.call('exists', KEYS[1]) == 0) then " +
            // 如果key没有会创建
            "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
            // 刷新过期时间
            "redis.call('pexpire', KEYS[1], ARGV[1]); " +
            "return nil; " +
            "end; " +
            // 如果key有会value+1
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
            "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
            "redis.call('pexpire', KEYS[1], ARGV[1]); " +
            "return nil; " +
            "end; " +
            "return redis.call('pttl', KEYS[1]);",
            Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

Watch Dog 看门狗源码分析

  WatchDog 机制其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到 EXPIRATION_RENEWAL_MAP 里面,然后每隔 10 秒(internalLockLeaseTime / 3)检查一下,如果客户端还持有锁 key(判断客户端是否还持有 key,其实就是遍历 EXPIRATION_RENEWAL_MAP 里面线程 id,然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间),那么就会不断的延长锁 key 的生存时间,重置锁的超时时间。

  当我们没有设置 leaseTime 的时候,也就是内部 leaseTime=-1 的时候,过期时间为默认 internalLockLeaseTime。查看代码可知 internalLockLeaseTime 调用 getLockwatchdogTimeout()赋值默认时间是 30s。renewExpiration()方法用于开启一个定时任务,不断的去更新有效期,定时任务的的时间就是 internalLockLeaseTime / 3,默认也就是 10s 后刷新有效期。

// -------------------------------- tryAcquireAsync()部分代码 ------------------------------------
CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {
    // 加锁成功之后执行,如果leaseTime小于0(默认给-1),就会触发看门狗机制,也就是scheduleExpirationRenewal()方法
    if (acquired) {
        if (leaseTime > 0) {
            // 如果设置了持有锁的超时时间,就会走自己的,并且不会自动续期。
            internalLockLeaseTime = unit.toMillis(leaseTime);
        } else {
            // 开启定时
            scheduleExpirationRenewal(threadId);
        }
    }
    return acquired;
});
return new CompletableFutureWrapper<>(f);

// -------------------- scheduleExpirationRenewal 调用 renewExpiration 定时 -----------------------------
private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }

    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        // 这里就是开启了定时,时间是internalLockLeaseTime的1/3,也就是10S
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }

            CompletionStage<Boolean> future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getRawName() + " expiration", e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }

                if (res) {
                    // reschedule itself
                    renewExpiration();
                } else {
                    cancelExpirationRenewal(null);
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    ee.setTimeout(task);
}

【分布式锁应用场景】

分布式锁应用场景

  Redisson 分布式锁作为一种常用的同步工具,能够有效解决多个节点或服务对共享资源的并发访问问题。它在分布式系统中的应用非常广泛,尤其在以下几种常见场景中,能够确保操作的原子性、避免冲突、提高系统稳定性。

场景一:防止重复执行任务

  在分布式系统中,多个节点可能会并发执行相同的任务,尤其是一些定时任务或异步任务。没有协调机制的情况下,多个节点可能会重复处理同一批数据,导致资源浪费、数据冗余或其他不可预期的错误。

  比如多个服务节点定时执行某项任务(如清理过期数据、同步数据等)。如果没有分布式锁,每个节点可能都会重复执行相同的任务,导致冗余计算和资源浪费。使用 Redisson 分布式锁,可以确保同一时刻只有一个节点能够执行该任务。通过 Redisson 提供的分布式锁,在任务执行前锁定任务资源,确保只有一个节点能获取锁并执行任务。任务完成后,锁释放,其他节点才有机会执行。

场景二:分布式计数器/限流

  当多个节点同时访问并更新同一个计数器(如库存、访问次数等)时,可能会导致计数器值的竞争问题,甚至出现数据不一致的情况。例如,多个用户并发请求购买同一商品时,库存数量需要实时更新,若没有适当的锁机制,可能会发生超卖问题。

  电商平台在促销活动时,可能有多个用户同时下单购买某个商品。使用分布式锁,确保库存数量的更新操作是串行的,从而避免了多个请求并发更新导致的超卖问题。通过 Redisson 分布式锁保证同一时刻只有一个节点能更新库存值。当库存被更新时,锁被释放,其他请求才能继续执行。

场景三:资源访问控制

  对于某些共享资源(例如数据库表、文件、缓存等),通常我们需要确保同一时刻只有一个线程或节点能够访问和修改这些资源。否则,多个节点并发访问时可能会引发数据冲突或破坏资源的完整性。

  • 数据库: 当多个节点需要执行某个数据库更新操作时,如果没有协调机制,可能会出现数据不一致。例如,多个节点同时对同一条记录进行更新,可能会丢失数据或导致更新冲突。使用分布式锁确保每次数据库操作时,只有一个节点能够获取锁并执行数据库操作,避免了并发操作导致的冲突。
  • 文件: 在多服务协作的场景下,某个文件可能会被多个节点处理或上传。如果多个节点同时操作同一文件,可能会导致文件内容被覆盖或损坏。通过分布式锁,确保只有一个节点能够在任何时刻处理该文件,保证文件操作的安全性。

场景四:缓存一致性控制

  分布式系统中,多个节点通常会共享缓存数据。例如,Redis 常作为各节点间的共享缓存。在缓存更新时,如果没有协调机制,可能会发生缓存不一致的问题。多个节点同时修改缓存的同一项数据,可能会导致数据竞争或部分节点看到过时的数据。

  假设一个电商网站中,多个服务器节点缓存了商品的库存数量。当用户下单购买商品时,需要更新缓存中的库存数据。如果没有分布式锁,不同节点可能会同时更新库存缓存,导致缓存数据的不一致。使用 Redisson 分布式锁,确保只有一个节点可以在同一时刻更新缓存,其他节点需要等待,直到该节点完成更新并释放锁,从而避免缓存更新时的并发问题。

场景五:确保全局唯一性

  在一些场景下,需要保证某个操作或资源是全局唯一的,避免多个节点同时生成重复的资源(如唯一 ID、订单号等)。通过分布式锁,可以确保生成全局唯一值的操作不被并发执行。

  比如在一个电商系统中,为每个订单生成一个全局唯一的订单号。如果没有分布式锁,多个节点可能同时生成相同的订单号,导致订单号重复。通过 Redisson 分布式锁,保证只有一个节点可以生成唯一的订单号,其他节点只能等待,直到锁被释放。


【RedLock 红锁】

RedLock 介绍

  红锁(RedLock)是一种分布式锁算法,由 Redis 的作者 Salvatore Sanfilippo(也称为 Antirez)设计,用于在分布式系统中实现可靠的锁机制。它的设计解决了单一 Redis 实例作为分布式锁可能出现的单点故障问题。其核心思想是在多个独立的 Redis 实例上同时获取锁,只有当大多数 Redis 实例加锁成功时,才认为成功获取了分布式锁。

RedLock 实现原理

  • 多节点加锁: RedLock 不在单个 Redis 实例上加锁,而是在多个独立的 Redis 实例上同时尝试获取锁。通常建议使用奇数个 Redis 实例(如 5 个),以确保系统具有较好的容错性。
  • 多数节点同意: 系统只有在获得了大多数 Redis 实例的锁(即 N/2 + 1 个节点,N 为节点总数)之后,才认为成功获取了分布式锁。这样即使部分 Redis 实例发生故障,整体锁服务仍然可用。
  • 时间同步: 为防止客户端在持有锁的过程中发生故障而导致锁无法释放,RedLock 会在获取锁时设置一个超时时间。如果客户端在锁超时之前未能完成任务并释放锁,其他客户端可以在锁超时后重新尝试获取。
  • 锁释放: 释放锁时,客户端需要向所有 Redis 实例发送释放锁的命令,以确保所有实例上的锁都被清除。

Redisson 实现 RedLock

  在 Java 中,可以使用 Redisson 框架来实现 RedLock。Redisson 提供了 RedissonMultiLock 类,它可以同时管理多个锁,并保证操作的原子性。

// 初始化 Redisson 客户端
RedissonClient redisson;
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");

RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
try {
   if (multiLock.tryLock()) {
       // 成功获取锁,执行业务逻辑
   } else {
       // 获取锁失败
   }
} finally {
   multiLock.unlock(); // 释放锁
}

RedLock 问题

  RedLock 通过多节点加锁、多数节点同意、时间同步和锁释放机制,提高了分布式锁的可用性和安全性。然而,RedLock 也存在一些问题,如性能问题和并发安全性问题,并且由于这些问题,Redisson 中已经废弃了 RedLock。目前主流还是基于单 Redis 节点的分布式锁。如果对性能要求较高,且能够接受单点故障的风险,可以使用基于单个 Redis 实例的分布式锁。可以使用 Redisson 提供的 RLock 或 FairLock,并通过主从复制或哨兵模式来提高可用性。


【分布式锁面试题】

面试题链接:https://www.bjpowernode.com/distributedmst/distributedlock.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值