Spring Boot 整合 Redisson 实现分布式锁详解

文章目录

  • 一、分布式锁基础概念​
  • [二、Redisson 简介​](#二、Redisson 简介)
  • [三、Redisson 分布式锁的使用场景​](#三、Redisson 分布式锁的使用场景)
  • [四、Spring Boot 整合 Redisson 的详细步骤​](#四、Spring Boot 整合 Redisson 的详细步骤)
  • [五、Redisson 分布式锁的核心原理​](#五、Redisson 分布式锁的核心原理)
  • 六、注意事项​

在分布式系统架构中,随着业务规模的扩大和服务集群化部署的普及,多节点并发操作共享资源的场景日益增多。传统的单机锁(如 Java 中的 synchronized 关键字和 ReentrantLock)已无法满足分布式环境下的数据一致性需求,分布式锁应运而生。本文将详细介绍如何在 Spring Boot 项目中整合 Redisson 实现分布式锁,包括其核心原理、使用场景、集成步骤及注意事项。​

一、分布式锁基础概念​

分布式锁是控制分布式系统中多个进程或线程对共享资源进行有序访问的一种机制。它需要满足以下核心特性:​

  • 互斥性: 在任意时刻,只能有一个客户端持有锁,确保共享资源的独占访问。
  • 安全性: 避免死锁,即使持有锁的客户端崩溃或网络中断,锁也能在一定时间后自动释放。
  • 可用性: 锁服务需要具备高可用性,避免单点故障导致整个分布式系统瘫痪。
  • 对称性: 加锁和解锁必须是同一个客户端,防止客户端解锁其他客户端持有的锁。

目前实现分布式锁的主流方案有基于数据库(如 MySQL 的行锁)、缓存(如 Redis)和分布式协调服务(如 ZooKeeper)三种。其中,基于 Redis 的分布式锁因性能优异、部署简单而被广泛采用,而 Redisson 则是 Redis 官方推荐的 Java 分布式锁实现框架。​

二、Redisson 简介​

Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式 Java 对象,还实现了分布式锁、分布式集合等多种分布式服务。Redisson 的分布式锁实现具有以下优势:​

  • 支持可重入锁(Reentrant Lock),允许同一线程多次获取同一把锁。
  • 支持公平锁和非公平锁两种模式,公平锁可避免线程饥饿问题。
  • 提供自动过期释放机制,有效防止死锁。
  • 支持锁的续约机制(Watch Dog),当业务执行时间超过锁的过期时间时,自动延长锁的持有时间。

具备高可用性,通过 Redis 集群确保锁服务的稳定性。​

三、Redisson 分布式锁的使用场景​

Redisson 分布式锁在实际业务中有着广泛的应用,以下是几个典型场景:​

  1. 库存扣减​

    在电商平台的秒杀活动中,多个用户同时抢购同一商品时,需要通过分布式锁保证库存扣减的原子性。例如,当商品库存仅剩 10 件时,100 个用户同时发起抢购请求,若没有分布式锁控制,可能会出现超卖现象(实际扣减数量超过库存总量)。通过 Redisson 分布式锁,可确保每次只有一个请求能执行库存扣减操作,有效防止超卖。​

  2. 分布式任务调度​

    在分布式任务调度系统中,多个节点可能同时触发同一个定时任务(如数据备份、报表生成)。使用 Redisson 分布式锁可保证同一时间只有一个节点执行任务,避免任务重复执行造成的资源浪费和数据不一致。例如,某系统需要每天凌晨 2 点执行数据同步任务,通过分布式锁控制,确保只有一个服务节点执行该任务。​

  3. 分布式 ID 生成​

    在分布式系统中,生成全局唯一 ID 是常见需求(如订单号、用户 ID)。若多个节点同时生成 ID,可能出现重复。通过 Redisson 分布式锁,可确保同一时间只有一个节点生成 ID,结合自增序列或雪花算法,保证 ID 的唯一性。​

  4. 缓存更新​

    在缓存更新场景中,当缓存失效时,多个请求可能同时穿透到数据库查询数据并更新缓存,导致数据库压力增大。使用 Redisson 分布式锁,可控制只有一个请求去数据库查询并更新缓存,其他请求等待缓存更新完成后直接从缓存获取数据,减轻数据库负担。​

四、Spring Boot 整合 Redisson 的详细步骤​

下面将详细介绍在 Spring Boot 项目中整合 Redisson 实现分布式锁的具体步骤:​

  1. 环境准备​
  • JDK 1.8 及以上版本
  • Spring Boot 2.5.x 及以上版本
  • Redis 5.0 及以上版本(推荐使用集群模式提高可用性)
  • Maven 3.6.x 及以上版本
  1. 添加依赖​
    在 Spring Boot 项目的 pom.xml 文件中添加 Redisson 的依赖:
xml 复制代码
​<!-- Redisson -->​
<dependency>​
    <groupId>org.redisson</groupId>​
    <artifactId>redisson-spring-boot-starter</artifactId>​
    <version>3.17.6</version>​
</dependency>

Redisson 的 Spring Boot Starter 会自动配置 RedissonClient 实例,简化集成过程。需要注意的是,Redisson 版本需与 Redis 版本兼容,具体兼容关系可参考 Redisson 官方文档。

  1. 配置 Redisson​

在 application.yml(或 application.properties)中配置 Redis 连接信息。Redisson 支持多种 Redis 部署模式,包括单机、主从、哨兵和集群模式,以下是常见的配置示例:​

(1)单机模式配置​

yml 复制代码
spring:​
  redis:​
    host: localhost​
    port: 6379​
    password: 123456  # 若Redis未设置密码,可省略此配置​
    database: 0​
​
redisson:​
  singleServerConfig:​
    address: "redis://${spring.redis.host}:${spring.redis.port}"​
    password: ${spring.redis.password}​
    database: ${spring.redis.database}​
    connectionMinimumIdleSize: 10  # 最小空闲连接数​
    connectionPoolSize: 64  # 连接池大小​
    idleConnectionTimeout: 10000  # 空闲连接超时时间(毫秒)​
    connectTimeout: 10000  # 连接超时时间(毫秒)​
    timeout: 3000  # 命令执行超时时间(毫秒)​

(2)集群模式配置​

yml 复制代码
redisson:​
  clusterServersConfig:​
    scanInterval: 2000  # 集群节点扫描间隔(毫秒)​
    password: 123456​
    nodeAddresses:​
      - "redis://192.168.1.101:6379"​
      - "redis://192.168.1.102:6379"​
      - "redis://192.168.1.103:6379"​
    connectionMinimumIdleSize: 10​
    connectionPoolSize: 64​
    idleConnectionTimeout: 10000​
    connectTimeout: 10000​
    timeout: 3000​

  1. 注入 RedissonClient​

在 Spring Boot 组件(如 Service、Component)中注入 RedissonClient 实例,用于操作分布式锁:​

java 复制代码
import org.redisson.api.RedissonClient;​
import org.springframework.beans.factory.annotation.Autowired;​
import org.springframework.stereotype.Service;​
​
@Service​
public class DistributedLockService {​
​
    @Autowired​
    private RedissonClient redissonClient;​
​
    // 后续分布式锁操作将使用该实例​
}​
​

RedissonClient 是 Redisson 的核心接口,提供了获取各种分布式对象(包括锁)的方法。​

  1. 实现分布式锁的基本操作​
    Redisson 提供了多种分布式锁实现,其中最常用的是 RLock(可重入锁)。以下是使用 RLock 实现分布式锁的基本操作示例:
    (1)获取锁并释放锁
java 复制代码
import org.redisson.api.RLock;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class StockService {

    @Autowired
    private RedissonClient redissonClient;

    // 库存扣减方法
    public void deductStock(Long productId, Integer quantity) {
        // 定义锁的名称,通常使用业务标识+资源ID(如商品ID)
        String lockKey = "stock:deduct:" + productId;
        
        // 获取分布式锁
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,参数分别为:等待时间、锁的持有时间、时间单位
            // 此处表示最多等待10秒,获取锁后持有30秒,超时自动释放
            boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (isLocked) {
                // 成功获取锁,执行库存扣减业务逻辑
                // 1. 查询商品当前库存
                Integer currentStock = queryStock(productId);
                // 2. 校验库存是否充足
                if (currentStock >= quantity) {
                    // 3. 扣减库存
                    updateStock(productId, currentStock - quantity);
                    System.out.println("库存扣减成功,商品ID:" + productId + ",扣减数量:" + quantity);
                } else {
                    System.out.println("库存不足,商品ID:" + productId + ",当前库存:" + currentStock);
                }
            } else {
                // 获取锁失败,处理逻辑(如返回错误信息)
                throw new RuntimeException("获取分布式锁失败,商品ID:" + productId);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("线程被中断,商品ID:" + productId, e);
        } finally {
            // 确保锁被释放,只有持有锁的线程才能释放
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    // 模拟查询库存
    private Integer queryStock(Long productId) {
        // 实际业务中从数据库或缓存查询
        return 100; // 假设初始库存为100
    }

    // 模拟更新库存
    private void updateStock(Long productId, Integer newStock) {
        // 实际业务中更新数据库或缓存
    }
}​

(2)可重入锁特性演示​

Redisson 的 RLock 支持可重入性,即同一线程可以多次获取同一把锁:​

java 复制代码
​​public void reentrantLockDemo() {
    String lockKey = "reentrant:lock:demo";
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        lock.lock(); // 获取锁
        System.out.println("第一次获取锁成功");
        
        // 同一线程再次获取锁(可重入)
        lock.lock();
        System.out.println("第二次获取锁成功");
        
        // 执行业务逻辑
        doBusiness();
    } finally {
        // 释放锁,需要与获取锁的次数一致
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
            System.out.println("第一次释放锁");
        }
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
            System.out.println("第二次释放锁");
        }
    }
}

private void doBusiness() {
    System.out.println("执行业务逻辑...");
}​

(3)公平锁的使用​

默认情况下,Redisson 的锁是非公平的,即线程获取锁的顺序不确定。若需要保证线程获取锁的顺序(先到先得),可使用公平锁:​

java 复制代码
// 获取公平锁​
RLock fairLock = redissonClient.getFairLock(lockKey);​
​
try {​
    // 尝试获取公平锁,参数与可重入锁一致​
    boolean isLocked = fairLock.tryLock(10, 30, TimeUnit.SECONDS);​
    if (isLocked) {​
        // 执行公平锁保护的业务逻辑​
    }​
} catch (InterruptedException e) {​
    // 异常处理​
} finally {​
    if (fairLock.isHeldByCurrentThread()) {​
        fairLock.unlock();​
    }​
}​

公平锁适用于对线程执行顺序有严格要求的场景,但由于需要维护线程等待队列,性能略低于非公平锁,应根据实际业务需求选择。​

  1. 分布式锁的续约机制(Watch Dog)
    在实际业务中,很难精确预估锁的持有时间。若业务执行时间超过锁的持有时间,锁会被自动释放,可能导致其他线程获取锁并修改资源,引发数据不一致。Redisson 的 Watch Dog 机制可解决这一问题:
  • 当使用lock()方法(无持有时间参数)获取锁时,Redisson 会启动一个后台线程(Watch Dog),默认每 30 秒检查一次。
  • 若锁仍被当前线程持有,Watch Dog 会自动将锁的持有时间延长 30 秒(默认值,可通过配置修改)。
  • 当线程释放锁或线程终止时,Watch Dog 会停止续约。

使用示例:​

java 复制代码
// 使用lock()方法获取锁,不指定持有时间,默认启用Watch Dog​
lock.lock();​
​
// 或指定等待时间,不指定持有时间​
lock.tryLock(10, TimeUnit.SECONDS);​

注意:若使用tryLock(waitTime, leaseTime, unit)方法并指定了leaseTime(持有时间),则 Watch Dog 机制不会生效,锁会在leaseTime后自动释放。​

五、Redisson 分布式锁的核心原理​

Redisson 分布式锁的实现基于 Redis 的 Hash 结构和 Lua 脚本,确保加锁、解锁操作的原子性。以下是其核心原理:​

  1. 加锁过程
    Redisson 在加锁时,会向 Redis 发送类似以下的命令:
java 复制代码
HMSET lockKey uuid:threadId 1  # 存储锁的持有者信息(UUID+线程ID)和重入次数​
PEXPIRE lockKey 30000  # 设置锁的过期时间为30秒​
  • 其中,uuid是 Redisson 客户端的唯一标识,threadId是当前线程 ID,确保解锁时只有持有者才能操作。
  • 若锁已存在,会检查持有者是否为当前线程,若是则增加重入次数,否则返回加锁失败。
  1. 解锁过程
    解锁时,Redisson 会先检查当前线程是否持有锁,若是则减少重入次数;若重入次数为 0,则删除锁:
lua 复制代码
-- Lua脚本,确保解锁操作的原子性​
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then​
    return nil  -- 锁不存在或持有者不是当前线程​
end​
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1)​
if counter > 0 then​
    redis.call('pexpire', KEYS[1], ARGV[2])  -- 重入次数>0,更新过期时间​
    return 0​
else​
    redis.call('del', KEYS[1])  -- 重入次数=0,删除锁​
    return 1​
end​

Lua 脚本的使用保证了检查持有者、减少重入次数和删除锁的操作是原子性的,避免并发问题。​

  1. Watch Dog 机制原理
  • 当启用 Watch Dog 时,Redisson 会在获取锁成功后,启动一个定时任务(默认 30 秒执行一次)。
  • 定时任务会检查锁是否仍被当前线程持有,若是则执行PEXPIRE命令延长锁的过期时间。
  • 定时任务的执行间隔为锁过期时间的 1/3(默认 10 秒),确保在锁过期前完成续约。

六、注意事项​

在使用 Redisson 分布式锁时,需注意以下事项,以避免潜在问题:​

  1. 锁的粒度设计​

    锁的粒度应尽可能小,即只锁定需要保护的资源,避免大范围锁定导致并发性能下降。例如,在库存扣减场景中,应按商品 ID 锁定(stock:deduct:1001),而不是锁定整个库存表(stock:deduct),否则不同商品的库存扣减会相互阻塞。​

  2. 避免死锁​

    尽管 Redisson 的过期机制和 Watch Dog 可减少死锁风险,但仍需注意:​

  • 确保在finally块中释放锁,避免业务逻辑异常导致锁未释放。
  • 避免在持有锁的情况下调用外部服务(如 HTTP 请求、消息发送),若外部服务响应缓慢,可能导致锁超时释放。
  • 合理设置锁的持有时间和等待时间,避免线程长时间等待锁或持有锁过久。
  1. Redis 的高可用性​

    分布式锁依赖 Redis 的可用性,若 Redis 单点故障,会导致锁服务不可用。因此,生产环境中应部署 Redis 集群(如主从 + 哨兵或 Redis Cluster),并配置适当的故障转移机制。​

  2. 锁的名称规范​

    锁的名称应具有唯一性和可读性,建议采用 "业务模块:操作:资源 ID" 的格式(如order:create:10001表示订单创建锁,资源 ID 为订单号),便于问题排查和监控。​

  3. 避免锁的误释放​

    解锁时必须先检查当前线程是否持有锁(lock.isHeldByCurrentThread()),否则可能释放其他线程持有的锁。例如,若线程 A 获取锁后因超时被释放,线程 B 获取锁,此时线程 A 若未检查直接解锁,会错误释放线程 B 的锁。​

  4. 性能考量​

  • 分布式锁本质上是网络操作,会增加系统开销,应避免在高频操作中过度使用。
  • 非公平锁的性能优于公平锁,若无特殊需求,建议使用默认的非公平锁。
  • 合理设置 Redis 的连接池