分布式锁详解及 Spring Boot 实战示例
在分布式系统中,多个服务实例可能同时操作共享资源(如数据库中的同一订单、库存记录),若缺乏协调机制,会导致数据不一致(如超卖、重复下单)。分布式锁正是解决这类问题的核心技术,它能保证同一时间只有一个服务实例执行特定临界区代码。
一、分布式锁的核心特性
一个可靠的分布式锁需满足以下特性:
- 互斥性:任意时刻只有一个线程持有锁。
- 安全性:锁只能被持有它的线程释放。
- 可用性:即使部分节点故障,锁仍能正常获取和释放。
- 防死锁:避免因线程崩溃导致锁永久无法释放。
- 幂等性:重复获取 / 释放锁不会产生副作用。
二、分布式锁的实现方案及原理
常见实现方式包括基于数据库、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] thenreturn redis.call('del', KEYS[1])
elsereturn 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: localhostport: 6379database: 0timeout: 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 {@Resourceprivate RedisDistributedLock distributedLock;@Resourceprivate 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 等成熟工具可大幅降低实现复杂度,建议在生产环境中优先采用。