Redis——Redisson分布式锁

本文最后更新于:2 个月前

前言

前面我们基于Redis实现的分布式锁能处理最基本的互斥,但还有许多的功能没有实现,如可重入、可重试、自旋等,在企业开发环境下,通常会引入Redisson这个成熟的框架来完善这些功能。本文将详细介绍Redisson的分布式锁功能,包括不同类型的锁的特性、应用场景以及如何在Spring Boot项目中实现和配置这些锁。

Redisson分布式锁支持

Redisson是一个在Java环境下使用Redis实现分布式数据结构和同步机制的库,其中包括多种类型的分布式锁,这些分布式锁支持为企业提供了一种简单、可靠且高效的方式来同步分布式应用中的操作。

高性能

Redisson基于Redis,后者是一个高性能的内存数据存储。通过Redisson实现的分布式锁能够快速响应和处理锁请求,特别适用于高并发的环境。

高可用性和可靠性

Redisson可通过使用多个Redis节点和复制机制来保证锁服务的高可用性。即使部分Redis节点发生故障,也能保持锁服务的连续性。

简易性

Redisson提供了与Java标准锁接口相似的API,使得开发者可以很容易地在项目中使用它来实现分布式锁,无需担心底层的复杂实现。

灵活性和扩展性

Redisson支持多种配置选项,能够满足不同场景下的分布式锁需求,支持从简单的单Redis节点配置到复杂的集群模式。

成熟的社区和文档

Redisson有一个活跃的开发和支持社区。详尽的文档和社区支持使得企业能够解决在实现和运行分布式锁过程中遇到的问题。

Spring Boot集成Redisson锁

集成方式选择

Redisson作为Redis的高级客户端,提供了丰富的分布式数据结构和工具,如分布式锁、分布式集合、分布式计数器等。尽管Redisson提供了redisson-spring-boot-starter,许多开发者和文章却更倾向于单独集成Redisson,而对于操作Redis数据库则优先考虑spring-boot-starter-data-redis。主要出于以下方面的考虑:

职责分离与关注点分离

通过职责分离,项目可以更清晰地管理不同类型的Redis操作,避免混淆和潜在的配置冲突。

  • RedisTemplate:Spring Boot自带的RedisTemplate专注于标准的Redis操作,如存储、读取、删除数据等。它经过广泛优化,适用于大多数常见的缓存和数据操作场景。
  • Redisson:作为高级Redis客户端,Redisson提供了分布式锁、分布式集合、发布/订阅等高级功能。这些功能通常不在标准的RedisTemplate的使用范畴内。

避免依赖冲突与配置复杂性

提高系统的稳定性和可维护性,减少因配置冲突引发的问题。

  • 多客户端共存:在同一个项目中同时使用RedisTemplateRedisson时,自动配置可能会导致bean冲突或配置覆盖问题。单独集成Redisson可以避免这种情况。
  • 自定义配置:手动集成Redisson允许开发者对其进行更精细的配置,满足特定的业务需求,而不受Starter预设配置的限制。

性能与资源优化

提升整体系统的性能和响应速度,尤其在高并发和复杂业务场景下。

  • 优化针对性RedisTemplate是针对标准Redis操作高度优化的,而Redisson的高级功能可能需要不同的资源和优化策略。分离两者的使用可以更好地优化每个客户端的性能表现。

灵活性与可扩展性

提高系统的灵活性,便于根据业务需求进行独立调整和优化。

  • 独立管理:单独集成Redisson使其成为独立的组件,便于单独管理、扩展和升级,而不影响RedisTemplate的正常使用。

集成及配置

本文使用更常见的单独集成Redisson,而对于Redis存取等基本操作仍然采用RedisTemplate

  1. 添加Redisson依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>${redisson.version}</version>
    </dependency>
  2. 配置RedissonClient

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Configuration
    public class RedissonConfig {

    @Bean(destroyMethod = "shutdown") // 应用关闭时关闭 RedissonClient,释放资源
    public RedissonClient redissonClient() {
    Config config = new Config();
    config.useSingleServer()
    .setAddress("redis://127.0.0.1:6379")
    .setPassword("yourRedisPassword") // 如果没有密码,可以省略
    .setDatabase(0)
    .setConnectionPoolSize(64)
    .setConnectionMinimumIdleSize(24)
    .setTimeout(3000)
    .setRetryAttempts(3)
    .setRetryInterval(1500);
    return Redisson.create(config);
    }

    }
  3. 常用配置详解

    1. 连接配置

      • address:Redis服务器地址,格式为redis://host:portrediss://host:port(启用SSL)。

      • password:Redis认证密码。

      • database:使用的Redis数据库编号(默认为0)。

      • connectionPoolSize:连接池的最大连接数。

      • connectionMinimumIdleSize:连接池的最小空闲连接数。

      • idleConnectionTimeout:空闲连接超时时间(毫秒)。

      • connectTimeout:连接超时时间(毫秒)。

      • timeout:命令执行超时时间(毫秒)。

      • retryAttempts:重试次数。

      • retryInterval:重试间隔时间(毫秒)。

      • subscriptionsPerConnection:每个连接的订阅数量。

    2. 锁相关配置

      • lockWatchdogTimeout:锁看门狗超时时间(毫秒),默认30秒。用于自动续期,防止锁被意外释放。
    3. 集群和哨兵配置

      • scanInterval:集群状态扫描间隔时间(毫秒)。

      • masterName:哨兵模式下的主节点名称。

    4. 高级配置

      • codec:自定义序列化编解码器,如 org.redisson.codec.JsonJacksonCodec

      • threadsRedisson客户端内部线程池的线程数量。

      • nettyThreadsNetty客户端内部线程池的线程数量。

代码示例

下面将展示最基础的可重入锁的使用:创建订单时以用户id为锁粒度进行加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 注入RedissonClient Bean
@Resource
private RedissonClient redissonClient;

@Override
public void createOrder() throws InterruptedException {
// 获取锁
RLock lock = redissonClient.getLock("createOrder" + getUserId());
// 尝试获取锁,最多等待10秒,超时时间为20秒
long waitTime = 10000;
long leaseTime = 20000;
boolean locked = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
// 如果获取锁失败,直接返回
if (!locked) {
throw new RuntimeException("获取锁失败");
}
try {
// 获取锁成功,模拟创建订单
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("创建订单失败");
} finally {
// 创建订单结束后,释放锁
lock.unlock();
}
}

使用注意事项

  1. 锁的粒度设计:合理设计锁的粒度,过大的锁可能导致性能瓶颈,过小的锁可能增加复杂性。例如,针对具体资源加锁,而不是全局加锁。
  2. 锁的持有时间:设置合适的锁持有时间,避免因业务处理时间过长导致锁被自动释放,从而引发数据不一致问题。可以结合业务逻辑动态调整锁持有时间。
  3. 自动续期机制:未指定锁的释放时间情况下,Redisson提供了锁的自动续期功能,确保在业务处理时间较长时锁不会被意外释放。但需要注意,当业务逻辑异常终止时,Redisson仍会尝试续期,可能导致锁无法及时释放。可以通过合理的异常处理和锁释放机制来规避。
  4. 异常处理:在使用Redisson进行加锁和解锁时,需妥善处理异常,确保在任何情况下锁都能被正确释放,防止死锁。例如,在finally块中释放锁。
  5. 网络分区问题:在分布式环境中,网络分区可能导致锁状态不一致。建议结合Redis的高可用机制(如哨兵、集群)进行部署,确保Redis服务的稳定性和可用性。
  6. 资源释放:确保在应用关闭时,正确关闭RedissonClient实例,释放资源。可以通过在配置类中指定destroyMethod = "shutdown"来自动关闭。
  7. 性能监控:监控Redisson和Redis的性能,避免因锁竞争激烈导致系统性能下降。可以通过Redis的监控工具(如Redis MonitorRedis Sentinel)和Redisson的统计功能进行监控。
  8. 安全性:在生产环境中,确保Redis服务的安全,设置强密码,限制访问来源,使用SSL/TLS加密等,防止未经授权的访问和恶意操作。
  9. 版本兼容性:确保Redisson与Spring Boot及Redis服务器版本的兼容性,避免因版本不匹配导致的问题。

Redisson分布式锁类型

可重入锁(Reentrant Lock)

概念

可重入锁(Reentrant Lock)是一种允许同一个线程多次获得同一把锁的同步机制。在获取可重入锁时,如果当前线程已经持有该锁,则可以再次获取而不会造成死锁。每次获取锁,内部计数器都会增加,释放锁时计数器减少,直到计数器为零,锁才会被真正释放。

在分布式环境中,可重入锁不仅需要在单个线程内重入,还需要在分布式系统的多个进程或节点之间保持一致性,确保同一时间只有一个客户端能够持有锁。

特性

  • 可重入性:允许同一个客户端多次获取同一把锁,锁计数器会随之增加。
  • 自动续期Redisson自动续期锁的持有时间,避免因业务处理时间过长而锁被释放。
  • 公平性:支持公平锁,确保锁的请求按照先后顺序被处理,防止“饥饿”现象。
  • 挂起锁等待:当锁被占用时,客户端可以选择等待锁释放或立即返回。
  • 锁的可见性:通过Redis的监控和日志功能,可以实时查看锁的状态和持有者。

应用场景

  1. 分布式缓存更新:在分布式环境下,确保只有一个实例负责更新缓存,避免缓存击穿或雪崩。
  2. 订单生成:确保同一订单号在生成过程中不被重复创建,避免数据不一致。
  3. 资源分配:在分布式系统中,确保对有限资源的独占访问,如分布式文件系统的文件写操作。
  4. 分布式任务调度:确保定时任务在分布式环境下只被一个节点执行,避免重复执行。
  5. 分布式事务管理:在分布式事务中,确保事务的原子性和一致性,防止并发操作导致的数据不一致。

常用API

Redisson

API 说明
RLock getLock(String name) 获取指定名称的分布式可重入锁。
RLock getFairLock(String name) 获取公平的指定名称的分布式可重入锁。

RLock

API 说明
void lock() 加锁,阻塞直到获取到锁。
当需要确保某段代码在分布式环境下的互斥执行时使用。
void lock(long leaseTime, TimeUnit unit) 加锁,并设置锁的自动释放时间。
当需要限制锁的持有时间,防止锁被永久占用。
boolean tryLock() 尝试获取锁,立即返回结果(是否成功获取锁)
非阻塞地尝试获取锁,适用于快速失败的场景。
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) 在指定的等待时间内尝试获取锁,成功后设置锁的自动释放时间。
需要在一定时间内尝试获取锁,避免长时间阻塞。
void unlock() 释放锁。
在完成锁定的业务逻辑后,必须释放锁以允许其他线程获取。
boolean isLocked() 检查锁是否被任何线程持有。
监控锁的状态,进行相关逻辑处理。
boolean isHeldByCurrentThread() 检查当前线程是否持有锁。
确保锁的释放操作由持有锁的线程执行,防止非法释放。
void lockInterruptibly() 获取锁,允许线程在等待锁的过程中被中断。
需要响应线程中断信号的场景,防止线程长时间阻塞。
boolean forceUnlock() 强制释放锁,不管当前线程是否持有锁。
在特殊情况下需要强制释放锁,如系统异常恢复。
RFuture<Boolean> forceUnlockAsync() 异步方式强制释放锁。
需要非阻塞地强制释放锁。
RFuture<Void> unlockAsync() 异步释放锁。
在高性能场景下,避免阻塞当前线程。
RFuture<Void> lockAsync() 异步获取锁。
需要非阻塞地尝试获取锁。
RFuture<Boolean> tryLockAsync() 异步尝试获取锁。
需要非阻塞地尝试获取锁,并在获取后执行操作。

代码示例

以用户为锁粒度,对创建订单进行加锁处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Resource
private RedissonClient redissonClient;

@Override
public void createOrder() throws InterruptedException {
// 获取锁
// RLock lock = redissonClient.getLock("createOrder" + getUserId());
// 获取公平锁
RLock lock = redissonClient.getFairLock("createOrder" + getUserId());
// 尝试获取锁,最多等待10秒,超时时间为20秒
long waitTime = 10000;
long leaseTime = 20000;
boolean locked = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
// 如果获取锁失败,直接返回
if (!locked) {
throw new RuntimeException("获取锁失败");
}
try {
// 获取锁成功,模拟创建订单
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("更新失败");
} finally {
// 创建订单结束后,释放锁
lock.unlock();
}
}

读写锁(Read/Write Lock)

概念

读写锁(Read/Write Lock)是一种同步机制,允许多个线程同时读共享资源,但在写入时必须独占访问权。具体而言:

  • 读锁(Read Lock):允许多个线程同时持有,适用于读取操作。当没有线程持有写锁时,多个线程可以并行地获取读锁。
  • 写锁(Write Lock):独占锁,只有一个线程可以持有写锁,同时在写锁被持有期间,其他线程无法获取读锁或写锁。

读写锁的目的在于保持数据的一致性和防止脏读,特别是分布式系统中,读写操作可能发生在不同的节点上,这种机制就尤为重要。

特性

  • 读写分离:支持同时多个线程获取读锁,但写锁是排他的。
  • 可重入性:同一线程可以多次获取读锁或写锁,计数器会相应增加。
  • 分布式支持:锁的状态存储在Redis中,可在不同的应用实例间共享。
  • 自动续期:锁持有期间会自动续期,防止锁意外过期。
  • 公平锁支持:可配置为公平锁,按照请求的顺序获取锁。
  • 高可用性:通过Redis的高可用机制(如哨兵模式、集群模式)支持锁服务的高可用。

应用场景

  1. 分布式缓存更新:在缓存更新时,防止多个线程同时写入,使用写锁;读操作则可以并发进行,使用读锁。
  2. 配置中心:当读取配置时,可以并发读取;当更新配置时,需要获取写锁,防止读操作读取到不一致的数据。
  3. 实时数据分析与统计:获取统计数据时使用读锁,更新统计数据时使用写锁,确保统计数据在更新过程中,读取操作获取到的数据是一致且准确的,避免统计结果出现偏差。
  4. 共享资源管理:获取资源时使用读锁,更新资源时使用写锁,确保资源状态在修改过程中保持一致,防止读取到不完整或错误的资源状态。

常用API

Redisson

API 说明
RReadWriteLock getReadWriteLock(String name) 获取指定名称的分布式读写锁。

RReadWriteLock

API 说明
RLock readLock() 返回用于分布式读写锁读取的锁。
RLock writeLock() 返回用于分布式读写锁写入的锁。

RLock

详见可重入锁。

代码示例

为了确保订单数据的一致性,在更新订单时使用写锁,读取订单时使用读锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Resource
private RedissonClient redissonClient;

@Override
public void update(Long id) throws InterruptedException {
// 使用写锁
RLock lock = redissonClient.getReadWriteLock("order" + id).writeLock();
boolean locked = lock.tryLock(10000, 30000, TimeUnit.MILLISECONDS);
// 如果获取锁失败,直接返回
if (!locked) {
throw new RuntimeException("获取锁失败");
}
try {
// 获取锁成功,模拟更新订单
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("更新订单失败");
} finally {
// 更新订单结束后,释放锁
lock.unlock();
}
}

@Override
public Object getById(Long id) throws InterruptedException {
// 使用读锁
RLock lock = redissonClient.getReadWriteLock("order" + id).readLock();
boolean locked = lock.tryLock(5000, 30000, TimeUnit.MILLISECONDS);
// 如果获取锁失败,直接返回
if (!locked) {
return null;
}
try {
// 获取锁成功,模拟查询订单
return "订单信息";
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
// 查询订单结束后,释放锁
lock.unlock();
}
}

信号量(Semaphore)

概念

信号量(Semaphore)是一种用于控制多个线程对共享资源的访问的同步工具。它通过维护一个计数器来表示可用资源的数量。信号量有两种类型:

  1. 计数信号量(Counting Semaphore):允许多个线程同时访问特定数量的资源。
  2. 二元信号量(Binary Semaphore):类似于互斥锁(Mutex),只允许一个线程访问资源。

在分布式系统中,信号量需要跨多个进程或节点共享和管理,确保资源的有效利用和访问控制。

特性

  • 分布式支持:信号量的状态在Redis中共享,多个应用实例或节点可以共同管理和使用信号量。
  • 动态许可管理:允许动态调整信号量的许可数量,适应业务需求的变化。
  • 公平性配置:支持公平信号量,确保线程按请求顺序获取许可,避免“饥饿”现象。
  • 高可用性:通过Redis的高可用部署(如哨兵模式、集群模式),确保信号量服务的持续可用。
  • 线程安全:Redisson的信号量实现是线程安全的,适用于高并发场景。
  • 阻塞与非阻塞操作:支持阻塞获取许可(acquire)和非阻塞尝试获取许可(tryAcquire)。

应用场景

  1. API限流:限制单位时间内的API调用次数,保护后端服务不被过载。
  2. 任务队列控制:控制任务队列中的并发任务数量,防止系统资源耗尽。
  3. 资源池管理:控制同时资源池中的资源数量,优化系统资源利用。
  4. 分布式锁辅助工具:与分布式锁结合使用,控制多个服务实例对关键资源的访问。

常用API

Redisson

API 说明
RSemaphore getSemaphore(String name) 获取指定名称的分布式信号量对象。

RSemaphore

API 说明
void trySetPermits(int permits) 尝试设置信号量的许可数量,仅在信号量未初始化时有效。
void setPermits(int permits) 设置信号量的许可数量,覆盖当前许可数量。
void acquire() 获取一个许可,阻塞直到许可可用。
void acquire(int permits) 获取指定数量的许可,阻塞直到所有许可可用。
boolean tryAcquire() 尝试获取一个许可,立即返回结果。
boolean tryAcquire(int permits) 尝试获取指定数量的许可,立即返回结果。
boolean tryAcquire(long timeout, TimeUnit unit) 在指定的等待时间内尝试获取一个许可。
boolean tryAcquire(int permits, long timeout, TimeUnit unit) 在指定的等待时间内尝试获取指定数量的许可。
void release() 释放一个许可,增加信号量的许可数量。
void release(int permits) 释放指定数量的许可,增加信号量的许可数量。
int availablePermits() 获取当前可用的许可数量。
int drainPermits() 移除并返回当前可用的所有许可数量。
void reducePermits(int reduction) 减少信号量的许可数量。
void setFair(boolean fair) 设置信号量是否为公平信号量。公平信号量按照请求顺序分配许可,非公平信号量则不保证顺序。
void addListener(SemaphoreListener listener) 添加许可变化监听器,监控许可的获取和释放。

代码示例

对订单导出进行限流(注意需要先初始化信号量):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Resource
private RedissonClient redissonClient;

private static final String ORDER_EXPORT_SEMAPHORE_KEY = "orderExport";

/**
* 初始化信号量
*/
@PostConstruct
public void initializeSemaphore() {
RSemaphore semaphore = redissonClient.getSemaphore(ORDER_EXPORT_SEMAPHORE_KEY);
semaphore.trySetPermits(10);
}

@Override
public void export() throws InterruptedException {
// 信号量控制导出并发量
RSemaphore semaphore = redissonClient.getSemaphore(ORDER_EXPORT_SEMAPHORE_KEY);
boolean acquire = semaphore.tryAcquire(10000, TimeUnit.MILLISECONDS);
// 并发量数量超限
if (!acquire) {
throw new RuntimeException("并发量数量超限");
}
try {
// 模拟导出操作
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("导出失败");
} finally {
// 释放信号量
semaphore.release();
}
}

注意事项

  1. 初始化信号量:建议在组件初始化完成后,自动完成信号量的初始化。使用trySetPermits方法仅在信号量未初始化时设置许可数量,避免重复设置导致许可数量不准确。
  2. 许可数量:根据业务需求合理设置许可数量。
  3. 确保释放许可:无论业务逻辑是否成功执行,逻辑执行完成以后必须释放许可,防止许可被永久占用。
  4. 公平性配置:根据业务需求选择是否启用公平信号量。公平信号量按照请求顺序分配许可,避免“饥饿”现象,但可能带来性能开销。

多锁(MultiLock)

概念

多锁(MultiLock)是指在执行一个操作时,需要同时获取多个独立的锁,以确保这些操作的原子性和一致性。多锁通常用于需要同时访问或修改多个共享资源的场景,通过获取所有相关锁,可以防止部分操作成功而其他操作失败,避免数据不一致和资源冲突。

特性

  • 原子性获取MultiLock能够原子性地获取多个锁,确保在所有锁都成功获取之前,无法执行相关操作。
  • 容错性:在获取过程中,如果某一个锁无法获取,MultiLock会释放已获取的锁,避免死锁和资源占用。
  • 可重入性:支持可重入锁特性,允许同一线程多次获取同一锁。
  • 灵活性:可以组合任意数量和类型的锁(如单个锁、读写锁、公平锁等),满足不同业务需求。
  • 高可用性:基于Redis的高可用机制(如哨兵模式、集群模式),确保多锁服务的稳定性和可靠性。
  • 自动续期:支持锁的自动续期,防止在长时间业务处理中锁被错误释放。

应用场景

  1. 复杂业务流程同步:在执行需要多个步骤的业务流程时,锁定相关资源,确保流程的完整性和数据的一致性。
  2. 配置更新系统:在更新分布式配置时,同时锁定多个配置节点,确保配置的同步和一致性。

常用API

Redisson

API 说明
RLock getMultiLock(RLock… locks) 通过多个RLock对象获取一个多锁对象。

RLock

详见可重入锁。

代码示例

更新订单涉及订单以及用户,因此使用多锁重构更新订单方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Resource
private RedissonClient redissonClient;

@Override
public void update(Long id) throws InterruptedException {
// 订单锁及用户锁
RLock orderLock = redissonClient.getLock("order" + id);
RLock userLock = redissonClient.getLock("user" + getUserId());
// 尝试获取多个锁
RLock multiLock = redissonClient.getMultiLock(orderLock, userLock);
boolean locked = multiLock.tryLock();
// 如果获取锁失败,抛出异常
if (!locked) {
throw new RuntimeException("获取锁失败");
}
try {
// 模拟更新订单
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("更新订单失败");
} finally {
// 释放多个锁
multiLock.unlock();
}
}

选择

在分布式系统中,选择不同的锁策略取决于业务需求和资源管理的复杂性。以下是两种常见的多锁应用策略及其区别:

  1. 同时锁定所有资源(使用MultiLock
    • 适用场景
      • 跨资源操作:需要同时访问或修改多个资源,确保操作的原子性。
      • 资源依赖操作:操作之间存在资源依赖关系,必须同时获取相关资源的锁。
    • 优点
      • 原子性保障:确保所有资源在操作过程中保持一致性。
      • 避免部分成功:防止部分操作成功而其他操作失败,保持数据的一致性。
    • 缺点
      • 锁竞争增加:需要同时获取多个锁,可能增加锁竞争的概率,影响系统性能。
  2. 分别锁定各个资源(单独使用多个RLock
    • 适用场景
      • 独立资源操作:多个资源之间操作相互独立,可以分别锁定。
      • 高并发读写:在需要高并发访问的场景下,分别锁定资源以提高并发性能。
    • 优点
      • 更高的并发性:可以独立管理各个资源的锁,减少锁的粒度,提高系统的并发性能。
      • 灵活性:可以针对不同资源采用不同的锁策略(不单单是RLock),满足多样化的业务需求。
    • 缺点
      • 复杂性增加:需要管理多个锁的获取与释放,增加了代码的复杂性。
      • 一致性保障困难:在需要跨资源一致性的场景下,难以保证所有操作的原子性。

红锁(RedLock)

概念

红锁是由Redis提出的一个分布式锁算法。它要求使用多个独立的Redis节点来完成分布式锁的获取和释放,具备比传统单节点锁更高的可靠性和容错性。RedLock的核心思想是通过多个Redis实例来共同保证锁的有效性。

具体来说,RedLock锁定机制的步骤如下:

  1. 客户端尝试在多个独立的Redis实例上获取锁。
  2. 如果至少有多数(通常是 N/2 + 1)Redis实例成功获得锁,那么就认为获得锁成功。
  3. 如果客户端能够成功获取到锁,则开始执行保护的资源操作。
  4. 若无法获得锁,则可以选择等待或立即返回。

特性

  • 高可用性:由于使用多个Redis实例,红锁能够避免单点故障。如果某个Redis实例不可用,其他实例仍可保证锁的正常工作。
  • 可靠性:相较于基于单一Redis实例的分布式锁,红锁可以在网络延迟、节点宕机等异常情况下提供更强的保证。
  • 容错性:即使某些Redis节点发生故障,红锁依然能够有效地工作。

常用API

Redisson

API 说明
RLock getRedLock(RLock… locks) 通过多个RLock对象获取一个红锁对象。

RLock

详见可重入锁。

代码示例

配置多个RedissonClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 创建RedissonClient连接到第一个Redis节点
*/
@Bean(name = "redissonClient1")
public RedissonClient redissonClient1() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("password1") // 如果没有密码,可以省略
.setTimeout(3000)
.setRetryAttempts(3)
.setRetryInterval(1500);
return Redisson.create(config);
}

/**
* 创建RedissonClient连接到第二个Redis节点
*/
@Bean(name = "redissonClient2")
public RedissonClient redissonClient2() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6380")
.setPassword("password2") // 如果没有密码,可以省略
.setTimeout(3000)
.setRetryAttempts(3)
.setRetryInterval(1500);
return Redisson.create(config);
}

/**
* 创建RedissonClient连接到第三个Redis节点
*/
@Bean(name = "redissonClient3")
public RedissonClient redissonClient3() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6381")
.setPassword("password3") // 如果没有密码,可以省略
.setTimeout(3000)
.setRetryAttempts(3)
.setRetryInterval(1500);
return Redisson.create(config);
}

业务代码中使用多个RedissonClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Resource(name = "redissonClient1")
private RedissonClient redissonClient1;

@Resource(name = "redissonClient2")
private RedissonClient redissonClient2;

@Resource(name = "redissonClient3")
private RedissonClient redissonClient3;

@Override
public void update(Long id) throws InterruptedException {
// 配置多个RedissonClient连接不同Redis节点
RLock orderLock1 = redissonClient1.getLock("order" + id);
RLock orderLock2 = redissonClient2.getLock("order" + id);
RLock orderLock3 = redissonClient3.getLock("order" + id);
// 尝试获取多个锁
RLock rLock = redissonClient.getRedLock(orderLock1, orderLock2, orderLock3);
boolean locked = rLock.tryLock();
// 如果获取锁失败,抛出异常
if (!locked) {
throw new RuntimeException("获取锁失败");
}
try {
// 模拟更新订单
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("更新订单失败");
} finally {
// 释放锁
rLock.unlock();
}
}

对比多锁

看起来红锁和多锁类似,也能够支持多个锁的同时加锁,但它们的实现原理、适用场景和容错能力有很大的区别。

特性 多锁 红锁
锁粒度 是针对多个RLock 对象进行的组合锁,多个锁在同一 Redis 实例中。 是针对多个Redis 实例进行的分布式锁。
容错性 无容错性,仅对单一Redis实例提供锁保护,多个锁操作的失败会导致整体回滚。 容错性高,通过多个Redis实例来保证锁的高可用性。
应用场景 适用于单个Redis实例中对多个资源加锁的场景,主要用于资源的组合加锁。 适用于需要分布式高可用锁,且需要跨多个Redis实例管理锁的场景。
实现复杂度 简单,通过RLock对象组合进行加锁,适用于同一 Redis 实例。 相对较复杂,需要多个Redis实例,并且涉及时钟同步和网络延迟。
性能 性能较好,只需要在单个Redis实例中加锁。 相对较差,需要访问多个Redis实例,开销较大。
使用场景的可靠性要求 对可靠性要求较低的场景,适合较为简单的组合锁需求。 高可靠性、高可用性要求的分布式场景。

FencedLock

概念

FencedLockRedisson提供的分布式锁,旨在防止由于客户端崩溃或网络分区而导致的锁的错误释放。它与普通的RLock类似,但加入了锁版本控制,使得即使客户端崩溃或断开连接,其他客户端也不会误释放该锁。

其核心概念是锁版本(Fence),每次获取锁时,都会生成一个唯一的版本号,并且只有持有最新版本的锁的客户端才能成功释放锁。如果客户端持有的锁版本过期或已被其他客户端更新,则无法释放锁,从而避免了死锁或资源竞争。

特性

  • 锁版本控制FencedLock为每个锁引入了版本号,每次客户端获得锁时,都会得到一个唯一的版本号。客户端在释放锁时,需要确保自己持有的是最新版本的锁。
  • 客户端崩溃保护:当客户端崩溃或者因为某些原因失去连接时,FencedLock通过fence(栅栏机制)防止其他客户端释放锁,避免了可能的锁误释放问题。
  • 避免误释放锁:通过检查锁的版本,FencedLock确保只有当前持有锁的客户端能够释放锁。如果锁的版本号不同,释放锁会失败,从而避免了锁的误释放。
  • 适用于高并发和长时间持锁的场景FencedLock特别适用于需要长时间持锁并且对锁的可靠性有高要求的场景。例如在高并发的环境下,确保锁在分布式系统中的安全性和一致性。

应用场景

  1. 高并发任务:在需要对共享资源进行并发访问控制的场景中,如分布式队列的消费,或者多个线程需要协调对某个数据库表的访问时,FencedLock可以确保锁的可靠性,避免由于节点宕机或者崩溃导致的锁的误释放。
  2. 长时间任务:如果锁持有的时间比较长,例如批量处理任务、定时任务等,FencedLock可以确保在任务执行过程中,不会出现因为客户端崩溃或其他原因导致锁释放的问题。
  3. 分布式系统中的资源访问:在多节点的分布式环境中,如果不同的节点要访问某些共享资源,使用FencedLock可以有效避免锁丢失的情况,从而保证系统的稳定性和一致性。
  4. 防止死锁FencedLock可以避免在客户端崩溃时,由于其他客户端误释放锁而导致的死锁现象。

常用API

Redisson

API 说明
RFencedLock getFencedLock(String name) 获取指定名称的FencedLock对象。

RFencedLock

API 说明
Long getToken() 返回当前FencedLock令牌。
Long lockAndGetToken() 阻塞地获取锁并返回当前FencedLock令牌。
Long lockAndGetToken(long leaseTime, TimeUnit unit) 阻塞地获取锁,并设置锁地超时时间并返回当前FencedLock令牌。
Long tryLockAndGetToken() 尝试获取FencedLock,立刻返回结果。
Long tryLockAndGetToken(long waitTime, TimeUnit unit) 尝试获取FencedLock,并设置锁地自动释放时间,立刻返回结果。
Long tryLockAndGetToken(long waitTime, long leaseTime, TimeUnit unit) 尝试获取FencedLock,并设置锁地自动释放时间,等待指定时间后返回结果。

RLock

详见可重入锁。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Resource
private RedissonClient redissonClient;

@Override
public void update(Long id) throws InterruptedException {
RFencedLock orderLock = redissonClient.getFencedLock("order" + id);
Long token = orderLock.tryLockAndGetToken(10000, 30000, TimeUnit.MILLISECONDS);
// 如果获取锁失败,抛出异常
if (token == null) {
throw new RuntimeException("获取锁失败");
}
try {
// 模拟更新订单
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("更新订单失败");
} finally {
// 释放锁
orderLock.unlock();
}
}

对比RLock

RLockRedisson提供的一个常见的可重入分布式锁。它通过Redis来管理锁状态,当客户端持有锁时,其他客户端无法获取该锁。通常,它是基于Redis的键值对实现的,因此会有一些失效时间(TTL)来防止死锁。

RFencedLockRedisson提供的一种更安全、更可靠的锁实现,引入了锁版本(Fence)和栅栏机制。它的主要目的是避免因为客户端崩溃或异常退出,导致锁无法正常释放的问题。即使Redis节点失败或客户端崩溃,其他客户端也能基于锁版本进行可靠的获取和释放。

Redis集群或分布式架构:在Redis是分布式架构(如Redis集群或主从复制)时,推荐使用RFencedLock,因为它具备更高的可靠性和容错性,能够处理Redis节点故障和网络分区等问题,确保分布式锁的安全性和一致性。

Redis单节点架构:对于Redis单节点环境,使用RLock完全足够,它足够简单高效,可以很好地满足大多数分布式锁需求。并且,RLock在单节点环境下没有额外的性能开销,锁的获取和释放速度较快。

自旋锁(Spin Lock)

概念

自旋锁是一种轻量级锁,它的基本原理是在尝试获取锁时,如果锁已经被占用,当前线程不会被阻塞,而是会持续地检查锁的状态,直到获取到锁为止。这种方式的优点是避免了上下文切换的开销,适用于锁持有时间非常短且锁竞争不激烈的场景。

特性

  • 轻量级:实现相对轻量,没有阻塞机制,适合锁持有时间短,竞争不激烈的场景。
  • 避免线程阻塞:与传统的阻塞锁不同,自旋锁会不断地尝试获取锁,直到锁被释放,这避免了线程的上下文切换。
  • 适用于低竞争场景:当锁竞争较少时,自旋锁能够提供更好的性能,避免了进入等待队列的延迟。
  • 分布式锁实现:基于Redis实现,可以跨进程、跨机器共享同一个锁,确保分布式环境下的同步。
  • 减少上下文切换:自旋锁避免了线程的阻塞,减少了上下文切换的性能开销。

应用场景

  • 高并发、低锁竞争的场景:在锁的竞争较少且操作迅速的环境中,自旋锁可以避免线程阻塞,提高性能。比如某些高吞吐量的消息队列或数据缓存的操作。
  • 锁占用时间极短的场景:当锁的持有时间非常短(如几毫秒),传统的阻塞锁会造成较大性能损失,而自旋锁可以避免这种损失。比如频繁的缓存更新、短小的临界区操作等。
  • 低延迟要求的应用:在对低延迟要求较高的系统中,自旋锁能够减少等待时间,尤其是当锁的竞争较少时。比如在线支付系统、实时数据处理等场景。
  • 小规模并发环境:在并发线程较少的环境中,自旋锁的性能表现较好。自旋不会带来过多的 CPU 资源浪费。比如在小规模的微服务架构中,较少的线程竞争可以使用自旋锁。

常用API

Redisson

API 说明
RLock getSpinLock(String name) 获取指定名称的自旋锁对象。

RLock

详见可重入锁。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Resource
private RedissonClient redissonClient;

@Override
public void update(Long id) throws InterruptedException {
// 自旋锁
RLock cacheLock = redissonClient.getSpinLock("order" + id);
boolean locked = cacheLock.tryLock(1000, 10000, TimeUnit.MILLISECONDS);
// 如果获取锁失败,抛出异常
if (!locked) {
throw new RuntimeException("获取锁失败");
}
try {
// 模拟更新缓存
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("更新缓存失败");
} finally {
// 释放锁
cacheLock.unlock();
}
}

注意事项

使用自旋锁的前提是锁持有时间极短且竞争不激烈。若锁竞争激烈或持有时间较长,自旋锁会消耗大量CPU资源,得不偿失。

总结

通过本文的学习,开发者应能够充分理解Redisson提供的分布式锁的强大功能和灵活应用。文章不仅提供了关于各类型锁的详细信息,还展示了如何在实际项目中应用这些锁来解决复杂的同步问题,从基本的锁配置到高级功能如锁的自动续期和公平性处理。此外,通过提供的代码示例和配置方法,开发者可以快速在自己的项目中集成Redisson分布式锁,提升应用的性能和可靠性。