分布式锁详解及 Spring Boot 实战示例

发布于:2025-08-11 ⋅ 阅读:(15) ⋅ 点赞:(0)

在分布式系统中,多个服务实例可能同时操作共享资源(如数据库中的同一订单、库存记录),若缺乏协调机制,会导致数据不一致(如超卖、重复下单)。分布式锁正是解决这类问题的核心技术,它能保证同一时间只有一个服务实例执行特定临界区代码。

一、分布式锁的核心特性

一个可靠的分布式锁需满足以下特性:

  • 互斥性:任意时刻只有一个线程持有锁。
  • 安全性:锁只能被持有它的线程释放。
  • 可用性:即使部分节点故障,锁仍能正常获取和释放。
  • 防死锁:避免因线程崩溃导致锁永久无法释放。
  • 幂等性:重复获取 / 释放锁不会产生副作用。

二、分布式锁的实现方案及原理

常见实现方式包括基于数据库、Redis、ZooKeeper 等,不同方式的原理各有不同。

1. 基于数据库的分布式锁

原理:利用数据库的唯一索引或悲观锁来实现。例如,创建一张锁表,包含资源标识、持有线程标识、过期时间等字段,给资源标识字段创建唯一索引。当需要获取锁时,向表中插入一条记录,若插入成功则表示获取到锁;释放锁时,删除该记录。为防止死锁,可定期清理过期未释放的锁。

优缺点

  • 优点:实现简单,无需额外中间件。
  • 缺点:性能较差,数据库压力大;易出现锁表问题;不支持锁自动续期等高级特性。

2. 基于 Redis 的分布式锁

原理:利用 Redis 的SET命令的原子性。核心命令如下:

# 仅当key不存在时设置值,过期时间10秒,返回OK表示获取锁成功

SET lock:resource true NX PX 10000
  • NX:仅在键不存在时才设置(保证互斥性)。
  • PX 10000:设置键的过期时间为 10 秒(防死锁)。

释放锁时,需通过 Lua 脚本保证原子性,先判断锁是否由当前线程持有,再删除锁:

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

优缺点

  • 优点:性能高,操作简单;支持过期时间设置。
  • 缺点:在 Redis 集群环境下,可能存在主从同步延迟导致的锁丢失问题;需自行处理锁续期等问题。

由于 Redis 的高性能和易用性,它成为分布式锁的主流选择,本文后续重点介绍基于 Redis 的分布式锁实现。

3. 基于 ZooKeeper 的分布式锁

原理:利用 ZooKeeper 的节点特性和 Watcher 机制。ZooKeeper 的节点分为持久节点、临时节点、持久顺序节点、临时顺序节点。分布式锁通常使用临时顺序节点,当需要获取锁时,在指定节点下创建一个临时顺序节点,然后判断当前节点是否为序号最小的节点,若是则获取到锁;若不是,则监听序号比当前节点小的最后一个节点,当该节点被删除时,重新判断。释放锁时,删除创建的临时节点,由于是临时节点,当持有锁的线程崩溃时,节点会自动删除,避免死锁。

优缺点

  • 优点:可靠性高,不存在锁丢失问题;支持公平锁;自带 Watcher 机制,可实现锁的自动释放和唤醒。
  • 缺点:性能相对 Redis 较低;部署和维护成本高。

三、Spring Boot 集成分布式锁的相关依赖及对比

1. 基于 Redis 的依赖

  • spring-boot-starter-data-redis
    • 提供了 Redis 的基本操作模板(RedisTemplate),可用于手动实现分布式锁。
    • 需自行处理锁的获取、释放、续期等逻辑,实现相对复杂,但灵活性高。
  • redisson-spring-boot-starter
    • 是 Redis 官方推荐的 Java 客户端,内置了分布式锁的完整实现,支持自动续期、公平锁、可重入锁等高级特性。
    • 封装了复杂的底层逻辑,使用简单,适合生产环境。

2. 基于 ZooKeeper 的依赖

  • spring-cloud-starter-zookeeper-discovery
    • 主要用于服务发现,但也可借助 ZooKeeper 客户端操作 ZooKeeper 实现分布式锁。
    • 需要自行基于 ZooKeeper 的 API 实现锁的逻辑,较为繁琐。
  • curator-recipes
    • 是 ZooKeeper 的客户端框架,提供了分布式锁等常用功能的封装,如 InterProcessMutex 等类可直接用于实现分布式锁。
    • 简化了 ZooKeeper 分布式锁的实现,可靠性高。

3. 依赖对比

依赖

基于中间件

特点

适用场景

spring-boot-starter-data-redis

Redis

基础操作支持,需自行实现锁逻辑

简单场景,对灵活性要求高

redisson-spring-boot-starter

Redis

内置完整锁实现,支持高级特性

生产环境,复杂业务场景

spring-cloud-starter-zookeeper-discovery

ZooKeeper

主要用于服务发现,锁实现需自行开发

已使用 ZooKeeper 做服务发现,简单锁场景

curator-recipes

ZooKeeper

封装了分布式锁功能,可靠性高

对锁可靠性要求高的场景

四、Spring Boot 集成 Redis 分布式锁实战

1. 环境准备

pom.xml 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.redisson</groupId> <!-- 推荐使用Redisson简化锁操作 -->
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version>
</dependency>

Redis 配置(application.yml)

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 3000ms

2. 基于 Redisson 的分布式锁实现

Redisson 是 Redis 官方推荐的 Java 客户端,内置了分布式锁的完整实现,支持自动续期、公平锁、可重入锁等高级特性。

分布式锁工具类

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Component
public class RedisDistributedLock {

    private final RedissonClient redissonClient;

    public RedisDistributedLock(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    /**
     * 获取分布式锁
     * @param lockKey 锁标识
     * @param waitTime 等待时间(获取锁的最大等待时长)
     * @param leaseTime 锁持有时间(自动释放时间)
     * @return 锁对象
     */
    public RLock lock(String lockKey, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁,最多等待waitTime,持有leaseTime后自动释放
            boolean isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
            if (isLocked) {
                return lock;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return null;
    }

    /**
     * 释放锁
     * @param lock 锁对象
     */
    public void unlock(RLock lock) {
        if (lock != null && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

3. 业务场景示例:库存扣减

Service 层代码

import org.redisson.api.RLock;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class InventoryService {

    @Resource
    private RedisDistributedLock distributedLock;
    @Resource
    private InventoryMapper inventoryMapper;  // 假设已实现数据库操作

    /**
     * 扣减商品库存
     * @param productId 商品ID
     * @param quantity 扣减数量
     * @return 操作结果
     */
    public boolean deductInventory(Long productId, int quantity) {
        // 锁标识:通常用业务资源唯一标识(如商品ID)
        String lockKey = "lock:inventory:" + productId;
        RLock lock = null;
        try {
            // 获取锁:最多等待3秒,持有10秒后自动释放
            lock = distributedLock.lock(lockKey, 3, 10);
            if (lock == null) {
                // 获取锁失败(如超时)
                return false;
            }

            // 临界区代码:查询库存并扣减
            int currentStock = inventoryMapper.selectStockByProductId(productId);
            if (currentStock >= quantity) {
                inventoryMapper.updateStock(productId, currentStock - quantity);
                return true;
            } else {
                // 库存不足
                return false;
            }
        } finally {
            // 确保锁释放
            distributedLock.unlock(lock);
        }
    }
}

五、关键注意事项

  • 锁的粒度:锁标识应精准到具体资源(如lock:order:123而非lock:order),避免锁范围过大导致性能瓶颈。
  • 过期时间设置:需大于业务执行时间,Redisson 的watch dog机制会自动续期(默认每 30 秒续期一次)。
  • 异常处理:必须在finally块中释放锁,避免因业务异常导致锁泄漏。
  • 重试机制:获取锁失败时可添加有限重试逻辑(如循环 3 次),提高成功率。

六、进阶优化方向

  • 公平锁:通过redissonClient.getFairLock(lockKey)实现,避免线程饥饿。
  • 红锁(RedLock):在多 Redis 节点环境中,通过多个实例获取锁提高可靠性(适合极高一致性场景)。
  • 缓存与数据库一致性:结合本地锁(synchronized)与分布式锁,减少分布式锁的使用频率。

通过不同的分布式锁实现方式和相关依赖,Spring Boot 应用可在分布式环境中安全地操作共享资源。在实际开发中,需根据业务场景和性能需求选择合适的实现方式和依赖,Redisson 等成熟工具可大幅降低实现复杂度,建议在生产环境中优先采用。


网站公告

今日签到

点亮在社区的每一天
去签到