一、为什么需要分布式锁?
分布式锁就像共享办公区的唯一会议室门禁:同一时间只能有一个团队持有门禁权限使用会议室;使用完必须归还权限;如果持有门禁的人突然离职,门禁必须能自动重置,不会一直被占用;同时要保证,只有持有门禁的人才能开门,别人不能随便打开。
在单体应用中,我们通过JVM自带的synchronized或ReentrantLock就能实现线程间的并发控制,保证同一时间只有一个线程操作共享资源。但在分布式架构下,应用会被部署到多个服务器节点,形成多个独立的JVM进程,本地锁只能控制本节点内的线程,无法跨节点、跨进程实现互斥。
最典型的场景就是电商商品库存扣减:假设商品库存仅剩10件,同时有100个用户发起下单请求,订单服务集群部署了3个节点。如果仅使用本地锁,3个节点的线程可以同时进入库存扣减的临界区,最终会导致超卖,造成资损。
分布式锁的本质,就是实现跨进程、跨机器、跨网络的互斥控制,保证同一时间只有一个客户端可以持有锁,操作共享资源。
二、分布式锁的核心设计准则
评判一个分布式锁方案是否合格、是否适合生产环境,核心看以下8个核心准则,这也是所有分布式锁实现的底层逻辑根基:
- 互斥性:锁的核心本质,同一时间只能有一个客户端持有锁,绝对不能出现多个客户端同时持有锁的情况。
- 防死锁:即使持有锁的客户端崩溃、网络中断、服务宕机,锁最终一定能被释放,不会永久占用共享资源。
- 可重入性:同一个客户端的同一个线程,在已经持有锁的情况下,可以再次获取同一把锁,不会出现自己把自己锁死的情况。
- 高可用:锁服务需要支持集群部署,少数节点宕机不影响锁的正常获取与释放,不会出现锁服务整体不可用。
- 高性能:锁的获取与释放操作耗时低,能支撑高并发场景的请求量,不会成为系统的性能瓶颈。
- 锁释放安全性:只能由持有锁的客户端释放自己的锁,绝对不能出现客户端A释放了客户端B持有的锁的情况。
- 公平性(可选):按照客户端请求锁的顺序依次分配锁,避免部分线程长期抢不到锁,出现饥饿问题。
- 锁续期能力(可选):当业务执行时间超过锁的预设过期时间时,可以自动为锁续期,避免业务还没执行完,锁就被提前释放。
三、主流分布式锁方案全拆解
3.1 基于MySQL的分布式锁
MySQL分布式锁是实现成本最低的方案,无需额外部署中间件,适合低并发、已有MySQL基础设施的项目,核心有三种实现方式。
3.1.1 基于唯一索引的排他锁(生产推荐)
这是MySQL分布式锁最成熟的实现方式,核心依靠InnoDB的唯一索引约束保证互斥性,通过过期时间防止死锁,通过持有者标识保证锁释放安全,支持可重入性。
1. 表结构设计(MySQL 8.0)
CREATE TABLE `distributed_lock` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`lock_key` varchar(128) NOT NULL COMMENT '锁的唯一标识',
`lock_holder` varchar(64) NOT NULL COMMENT '锁持有者唯一标识',
`reentrant_count` int NOT NULL DEFAULT '1' COMMENT '重入次数',
`expire_time` datetime NOT NULL COMMENT '锁过期时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_lock_key` (`lock_key`) COMMENT '锁key唯一索引,保证互斥性'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='分布式锁表';
2. 核心代码实现
Maven核心依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.4</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.1.0-jre</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.52</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>
实体类
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("distributed_lock")
@Schema(description = "分布式锁实体")
public class DistributedLock {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@TableField("lock_key")
@Schema(description = "锁的唯一标识")
private String lockKey;
@TableField("lock_holder")
@Schema(description = "锁持有者唯一标识")
private String lockHolder;
@TableField("reentrant_count")
@Schema(description = "重入次数")
private Integer reentrantCount;
@TableField("expire_time")
@Schema(description = "锁过期时间")
private LocalDateTime expireTime;
@TableField("create_time")
@Schema(description = "创建时间")
private LocalDateTime createTime;
@TableField("update_time")
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
Mapper接口
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.DistributedLock;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DistributedLockMapper extends BaseMapper<DistributedLock> {
}
核心锁服务类
package com.jam.demo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.jam.demo.entity.DistributedLock;
import com.jam.demo.mapper.DistributedLockMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* MySQL分布式锁服务
*
* @author ken
*/
@Slf4j
@Service
public class MysqlDistributedLockService {
private final DistributedLockMapper lockMapper;
private final TransactionTemplate transactionTemplate;
private static final long DEFAULT_EXPIRE_SECONDS = 30L;
private static final ThreadLocal<String> HOLDER_ID_LOCAL = new ThreadLocal<>();
public MysqlDistributedLockService(DistributedLockMapper lockMapper, TransactionTemplate transactionTemplate) {
this.lockMapper = lockMapper;
this.transactionTemplate = transactionTemplate;
}
/**
* 获取锁持有者唯一标识
*
* @return 持有者标识
*/
private String getHolderId() {
String holderId = HOLDER_ID_LOCAL.get();
if (!StringUtils.hasText(holderId)) {
holderId = UUID.randomUUID() + "-" + Thread.currentThread().getId();
HOLDER_ID_LOCAL.set(holderId);
}
return holderId;
}
/**
* 尝试获取分布式锁
*
* @param lockKey 锁的唯一标识
* @return 获取结果
*/
public boolean tryLock(String lockKey) {
return tryLock(lockKey, DEFAULT_EXPIRE_SECONDS);
}
/**
* 尝试获取分布式锁(指定过期时间)
*
* @param lockKey 锁的唯一标识
* @param expireSeconds 锁过期时间(秒)
* @return 获取结果
*/
public boolean tryLock(String lockKey, long expireSeconds) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
String holderId = getHolderId();
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireSeconds);
return transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
LambdaQueryWrapper<DistributedLock> queryWrapper = new LambdaQueryWrapper<DistributedLock>()
.eq(DistributedLock::getLockKey, lockKey);
DistributedLock existLock = lockMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(existLock)) {
DistributedLock newLock = new DistributedLock();
newLock.setLockKey(lockKey);
newLock.setLockHolder(holderId);
newLock.setExpireTime(expireTime);
newLock.setReentrantCount(1);
try {
lockMapper.insert(newLock);
return true;
} catch (Exception e) {
log.debug("锁插入冲突,获取失败: {}", lockKey);
return false;
}
}
if (holderId.equals(existLock.getLockHolder())) {
LambdaUpdateWrapper<DistributedLock> updateWrapper = new LambdaUpdateWrapper<DistributedLock>()
.eq(DistributedLock::getLockKey, lockKey)
.set(DistributedLock::getReentrantCount, existLock.getReentrantCount() + 1)
.set(DistributedLock::getExpireTime, expireTime);
lockMapper.update(null, updateWrapper);
return true;
}
if (existLock.getExpireTime().isBefore(LocalDateTime.now())) {
LambdaUpdateWrapper<DistributedLock> updateWrapper = new LambdaUpdateWrapper<DistributedLock>()
.eq(DistributedLock::getLockKey, lockKey)
.eq(DistributedLock::getLockHolder, existLock.getLockHolder())
.set(DistributedLock::getLockHolder, holderId)
.set(DistributedLock::getReentrantCount, 1)
.set(DistributedLock::getExpireTime, expireTime);
int updateCount = lockMapper.update(null, updateWrapper);
return updateCount > 0;
}
return false;
}
});
}
/**
* 释放分布式锁
*
* @param lockKey 锁的唯一标识
* @return 释放结果
*/
public boolean releaseLock(String lockKey) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
String holderId = getHolderId();
return transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
LambdaQueryWrapper<DistributedLock> queryWrapper = new LambdaQueryWrapper<DistributedLock>()
.eq(DistributedLock::getLockKey, lockKey);
DistributedLock existLock = lockMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(existLock)) {
HOLDER_ID_LOCAL.remove();
return true;
}
if (!holderId.equals(existLock.getLockHolder())) {
log.error("非锁持有者,无法释放锁: {}", lockKey);
return false;
}
int currentCount = existLock.getReentrantCount() - 1;
if (currentCount > 0) {
LambdaUpdateWrapper<DistributedLock> updateWrapper = new LambdaUpdateWrapper<DistributedLock>()
.eq(DistributedLock::getLockKey, lockKey)
.set(DistributedLock::getReentrantCount, currentCount);
lockMapper.update(null, updateWrapper);
return true;
}
lockMapper.delete(queryWrapper);
HOLDER_ID_LOCAL.remove();
return true;
}
});
}
}
业务使用示例
package com.jam.demo.controller;
import com.jam.demo.service.MysqlDistributedLockService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/order")
@Tag(name = "订单管理", description = "订单相关接口")
public class OrderController {
private final MysqlDistributedLockService distributedLockService;
public OrderController(MysqlDistributedLockService distributedLockService) {
this.distributedLockService = distributedLockService;
}
@PostMapping("/create")
@Operation(summary = "创建订单", description = "分布式锁控制库存扣减")
public String createOrder(
@Parameter(description = "商品ID", required = true) @RequestParam String productId,
@Parameter(description = "购买数量", required = true) @RequestParam Integer num) {
String lockKey = "order:stock:" + productId;
boolean lockSuccess = distributedLockService.tryLock(lockKey, 30);
if (!lockSuccess) {
return "系统繁忙,请稍后重试";
}
try {
log.info("获取锁成功,开始执行业务逻辑,商品ID:{}", productId);
// 库存扣减、订单创建等核心业务逻辑
return "订单创建成功";
} finally {
distributedLockService.releaseLock(lockKey);
}
}
}
3.1.2 其他MySQL锁实现
- 悲观锁(for update):基于InnoDB行锁实现,通过
select * from distributed_lock where lock_key = ? for update;加锁,事务提交后释放锁。缺点是长事务会占用数据库连接,高并发下性能极差,容易出现死锁。 - 乐观锁(版本号机制):通过
update table set stock = stock -1, version = version +1 where id = ? and version = ?实现,适合单表更新操作,不适合作为通用分布式锁,需要修改业务表,重试成本高。
3.1.3 高可用与优缺点
- 高可用方案:采用MySQL MGR集群或主从半同步复制,避免单点故障,保证数据一致性。
- 优点:实现简单,无需额外中间件,成本极低,天然支持强一致性。
- 缺点:性能差,受限于数据库IO,高并发下压力大;锁过期依赖系统时间,存在时间同步风险;长连接占用会导致数据库性能下降。
3.2 基于Redis原生SETNX的分布式锁
Redis分布式锁是互联网项目最常用的方案,依托Redis单线程模型的原子性命令,实现高性能的互斥控制,单机QPS可达10万以上。
3.2.1 底层核心原理
Redis的单线程模型保证所有命令都是原子执行的,核心依靠SET命令的扩展参数实现互斥与防死锁:
NX:Only set the key if it does not already exist,只有key不存在时才设置成功,保证互斥性。PX milliseconds:Set the specified expire time, in milliseconds,设置key的过期时间,保证客户端宕机后锁能自动释放,防止死锁。- 唯一值:每个锁设置唯一的持有者标识,保证只有锁的持有者才能释放锁,避免误释放。
核心原子命令
SET lock_key unique_value NX PX 30000
释放锁的原子Lua脚本释放锁必须通过Lua脚本实现原子操作,避免出现“判断value相等后、删除key前,锁过期被其他客户端获取,导致误释放”的问题。
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
3.2.2 代码实现
Maven核心依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.2.4</version>
</dependency>
核心锁服务类
package com.jam.demo.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Redis原生SETNX分布式锁服务
*
* @author ken
*/
@Slf4j
@Service
public class RedisNativeLockService {
private final StringRedisTemplate stringRedisTemplate;
private static final long DEFAULT_EXPIRE_MS = 30000L;
private static final ThreadLocal<String> HOLDER_ID_LOCAL = new ThreadLocal<>();
private static final DefaultRedisScript<Long> RELEASE_LOCK_SCRIPT;
static {
RELEASE_LOCK_SCRIPT = new DefaultRedisScript<>();
RELEASE_LOCK_SCRIPT.setScriptText("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end");
RELEASE_LOCK_SCRIPT.setResultType(Long.class);
}
public RedisNativeLockService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
private String getHolderId() {
String holderId = HOLDER_ID_LOCAL.get();
if (!StringUtils.hasText(holderId)) {
holderId = UUID.randomUUID() + "-" + Thread.currentThread().getId();
HOLDER_ID_LOCAL.set(holderId);
}
return holderId;
}
/**
* 尝试获取分布式锁
*
* @param lockKey 锁的唯一标识
* @return 获取结果
*/
public boolean tryLock(String lockKey) {
return tryLock(lockKey, DEFAULT_EXPIRE_MS, TimeUnit.MILLISECONDS);
}
/**
* 尝试获取分布式锁(指定过期时间)
*
* @param lockKey 锁的唯一标识
* @param expire 过期时间
* @param timeUnit 时间单位
* @return 获取结果
*/
public boolean tryLock(String lockKey, long expire, TimeUnit timeUnit) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
String holderId = getHolderId();
Boolean result = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, holderId, expire, timeUnit);
return Boolean.TRUE.equals(result);
}
/**
* 释放分布式锁
*
* @param lockKey 锁的唯一标识
* @return 释放结果
*/
public boolean releaseLock(String lockKey) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
String holderId = getHolderId();
Long result = stringRedisTemplate.execute(
RELEASE_LOCK_SCRIPT,
Collections.singletonList(lockKey),
holderId
);
HOLDER_ID_LOCAL.remove();
return Long.valueOf(1L).equals(result);
}
}
3.2.3 高可用与优缺点
- 高可用方案:采用Redis哨兵集群或Redis Cluster集群,保证Redis服务的高可用。
- 优点:性能极高,实现简单,轻量化,适合高并发场景。
- 缺点:不支持可重入性;无锁续期机制,业务执行超时会导致锁提前释放;集群异步复制存在脑裂风险,可能导致锁丢失;不支持公平锁与阻塞获取。
3.3 基于Redisson的分布式锁
Redisson是Redis官方推荐的Java客户端,封装了生产级的分布式锁实现,解决了原生SETNX方案的所有缺陷,是互联网高并发场景的首选方案。
3.3.1 底层核心原理
- 可重入性实现:采用Redis Hash结构存储锁信息,key为锁名称,hash field为持有者唯一标识,value为重入次数,通过Lua脚本实现原子操作。
- 看门狗机制:客户端获取锁后,启动定时任务,每隔锁过期时间的1/3(默认30秒过期,每10秒执行一次),刷新锁的过期时间,保证业务未执行完时锁不会过期;客户端宕机后,定时任务停止,锁自动过期释放。
- 阻塞获取实现:基于Redis的发布订阅机制,锁释放时发布通知,等待的客户端收到通知后重新尝试获取锁,无需轮询,性能更高。
- 红锁(RedLock):解决集群脑裂问题,需要部署多个独立的Redis主节点,客户端必须在超过半数的节点上成功获取锁,且总耗时小于锁过期时间,才算获取成功,保证极端场景下的互斥性。
3.3.2 代码实现
Maven核心依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.0</version>
</dependency>
核心锁服务类
package com.jam.demo.service;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
/**
* Redisson分布式锁服务
*
* @author ken
*/
@Slf4j
@Service
public class RedissonLockService {
private final RedissonClient redissonClient;
private static final long DEFAULT_WAIT_TIME = 3L;
private static final long DEFAULT_LEASE_TIME = 30L;
public RedissonLockService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
/**
* 尝试获取可重入锁
*
* @param lockKey 锁的唯一标识
* @return 获取结果
*/
public boolean tryLock(String lockKey) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(DEFAULT_WAIT_TIME, DEFAULT_LEASE_TIME, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取锁中断: {}", lockKey, e);
return false;
}
}
/**
* 尝试获取可重入锁(自定义超时时间)
*
* @param lockKey 锁的唯一标识
* @param waitTime 最大等待时间
* @param leaseTime 锁持有时间
* @param timeUnit 时间单位
* @return 获取结果
*/
public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit timeUnit) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, timeUnit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取锁中断: {}", lockKey, e);
return false;
}
}
/**
* 获取公平锁
*
* @param lockKey 锁的唯一标识
* @return 获取结果
*/
public boolean tryFairLock(String lockKey) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
RLock fairLock = redissonClient.getFairLock(lockKey);
try {
return fairLock.tryLock(DEFAULT_WAIT_TIME, DEFAULT_LEASE_TIME, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取公平锁中断: {}", lockKey, e);
return false;
}
}
/**
* 释放锁
*
* @param lockKey 锁的唯一标识
*/
public void releaseLock(String lockKey) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
RLock lock = redissonClient.getLock(lockKey);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
红锁实现示例
package com.jam.demo.service;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* Redisson红锁服务
*
* @author ken
*/
@Service
public class RedissonRedLockService {
private final RedissonClient redissonClient1;
private final RedissonClient redissonClient2;
private final RedissonClient redissonClient3;
private final RedissonClient redissonClient4;
private final RedissonClient redissonClient5;
public RedissonRedLockService(RedissonClient redissonClient1, RedissonClient redissonClient2, RedissonClient redissonClient3, RedissonClient redissonClient4, RedissonClient redissonClient5) {
this.redissonClient1 = redissonClient1;
this.redissonClient2 = redissonClient2;
this.redissonClient3 = redissonClient3;
this.redissonClient4 = redissonClient4;
this.redissonClient5 = redissonClient5;
}
public boolean tryRedLock(String lockKey, long waitTime, long leaseTime, TimeUnit timeUnit) {
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
RLock lock4 = redissonClient4.getLock(lockKey);
RLock lock5 = redissonClient5.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
try {
return redLock.tryLock(waitTime, leaseTime, timeUnit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
public void releaseRedLock(String lockKey) {
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
RLock lock4 = redissonClient4.getLock(lockKey);
RLock lock5 = redissonClient5.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
if (redLock.isHeldByCurrentThread()) {
redLock.unlock();
}
}
}
3.3.3 高可用与优缺点
- 高可用方案:普通场景采用Redis哨兵/Cluster集群;强一致性场景采用红锁方案,部署5个独立的Redis主节点,通过过半机制保证锁的可靠性。
- 优点:性能优异,原生支持可重入、锁续期、公平锁、阻塞获取、读写锁等高级特性;API简单易用,生产级成熟方案,社区活跃,解决了原生Redis锁的几乎所有缺陷。
- 缺点:红锁方案部署与运维成本高;普通集群模式仍存在极小概率的脑裂锁丢失风险。
3.4 基于ZooKeeper的分布式锁
ZooKeeper分布式锁依托ZAB一致性协议,实现强一致性的互斥控制,适合金融级、对锁的可靠性有极致要求的场景。
3.4.1 底层核心原理
- 临时顺序节点:每个锁对应一个持久化父节点,客户端获取锁时,在父节点下创建一个临时顺序节点;客户端会话失效后,临时节点会自动删除,天然解决死锁问题。
- 顺序性保证:客户端创建节点后,获取父节点下的所有子节点,按序号排序,判断自己创建的节点是否为序号最小的节点,是则获取锁成功。
- Watcher监听机制:如果当前节点不是最小的,客户端监听前一个序号节点的删除事件,进入等待状态;当前一个节点被删除(锁释放),Watcher触发,客户端再次判断自己是否为最小节点,是则获取锁成功,实现公平锁与阻塞获取。
- 可重入性:通过会话ID+线程ID标识持有者,同一持有者再次获取锁时,直接返回成功,更新重入次数。
生产环境推荐使用Apache Curator框架,它封装了成熟的分布式锁实现,处理了会话中断、重连、Watcher重复注册等所有边界问题。
3.4.2 代码实现
Maven核心依赖
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.6.0</version>
</dependency>
核心锁服务类
package com.jam.demo.service;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
/**
* ZooKeeper分布式锁服务
*
* @author ken
*/
@Slf4j
@Service
public class ZookeeperLockService {
private final CuratorFramework curatorFramework;
private static final String LOCK_ROOT_PATH = "/distributed_lock";
private static final long DEFAULT_WAIT_TIME = 3L;
public ZookeeperLockService(CuratorFramework curatorFramework) {
this.curatorFramework = curatorFramework;
}
/**
* 尝试获取分布式锁
*
* @param lockKey 锁的唯一标识
* @return 获取结果
*/
public boolean tryLock(String lockKey) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
String lockPath = LOCK_ROOT_PATH + "/" + lockKey;
InterProcessMutex mutex = new InterProcessMutex(curatorFramework, lockPath);
try {
return mutex.acquire(DEFAULT_WAIT_TIME, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("获取ZK锁异常: {}", lockKey, e);
return false;
}
}
/**
* 尝试获取分布式锁(自定义等待时间)
*
* @param lockKey 锁的唯一标识
* @param waitTime 最大等待时间
* @param timeUnit 时间单位
* @return 获取结果
*/
public boolean tryLock(String lockKey, long waitTime, TimeUnit timeUnit) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
String lockPath = LOCK_ROOT_PATH + "/" + lockKey;
InterProcessMutex mutex = new InterProcessMutex(curatorFramework, lockPath);
try {
return mutex.acquire(waitTime, timeUnit);
} catch (Exception e) {
log.error("获取ZK锁异常: {}", lockKey, e);
return false;
}
}
/**
* 释放分布式锁
*
* @param lockKey 锁的唯一标识
*/
public void releaseLock(String lockKey) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
String lockPath = LOCK_ROOT_PATH + "/" + lockKey;
InterProcessMutex mutex = new InterProcessMutex(curatorFramework, lockPath);
try {
if (mutex.isAcquiredInThisProcess()) {
mutex.release();
}
} catch (Exception e) {
log.error("释放ZK锁异常: {}", lockKey, e);
}
}
}
3.4.3 高可用与优缺点
- 高可用方案:部署5节点ZooKeeper集群,通过ZAB协议的过半机制保证集群可用性,只要半数以上节点存活,集群就能正常提供服务,无脑裂风险。
- 优点:天然强一致性,无锁过期时间问题,自动释放死锁,原生支持公平锁、阻塞获取、可重入性,锁释放安全性极高,适合金融级强一致性场景。
- 缺点:性能低于Redis,写操作需要过半节点同步,高并发下吞吐量有限;集群部署与运维成本高;网络抖动可能导致会话断开,临时节点被删除,锁被提前释放。
3.5 基于Etcd的分布式锁
Etcd是基于Raft一致性协议的分布式键值存储,是云原生场景的标准组件,分布式锁实现兼顾强一致性与性能,适合K8s生态下的项目。
3.5.1 底层核心原理
- Raft一致性协议:保证集群数据的强一致性,过半写入才返回成功,无脑裂风险。
- Revision全局版本号:Etcd的每个写操作都会生成一个全局唯一、递增的Revision号,通过Revision的顺序性实现公平锁,替代ZooKeeper的顺序节点。
- Lease租约机制:客户端获取锁时绑定一个租约,定期发送心跳续期;客户端宕机后,租约到期自动过期,key被删除,锁自动释放,防止死锁。
- Watch机制:客户端如果没有获取到锁,会Watch前一个Revision的key的删除事件,锁释放时收到通知,重新尝试获取锁,实现阻塞获取。
3.5.2 代码实现
Maven核心依赖
<dependency>
<groupId>io.etcd</groupId>
<artifactId>jetcd-core</artifactId>
<version>0.7.7</version>
</dependency>
核心锁服务类
package com.jam.demo.service;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.Lease;
import io.etcd.jetcd.Lock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
/**
* Etcd分布式锁服务
*
* @author ken
*/
@Slf4j
@Service
public class EtcdLockService {
private final Client etcdClient;
private static final long DEFAULT_LEASE_TTL = 30L;
private static final long DEFAULT_WAIT_TIME = 3L;
public EtcdLockService(Client etcdClient) {
this.etcdClient = etcdClient;
}
/**
* 尝试获取分布式锁
*
* @param lockKey 锁的唯一标识
* @return 获取结果
*/
public boolean tryLock(String lockKey) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
Lock lockClient = etcdClient.getLockClient();
Lease leaseClient = etcdClient.getLeaseClient();
try {
long leaseId = leaseClient.grant(DEFAULT_LEASE_TTL).get().getID();
lockClient.lock(lockKey.getBytes(), leaseId).get(DEFAULT_WAIT_TIME, TimeUnit.SECONDS);
return true;
} catch (Exception e) {
log.error("获取Etcd锁异常: {}", lockKey, e);
return false;
}
}
/**
* 释放分布式锁
*
* @param lockKey 锁的唯一标识
*/
public void releaseLock(String lockKey) {
if (!StringUtils.hasText(lockKey)) {
throw new IllegalArgumentException("锁key不能为空");
}
Lock lockClient = etcdClient.getLockClient();
try {
lockClient.unlock(lockKey.getBytes()).get();
} catch (Exception e) {
log.error("释放Etcd锁异常: {}", lockKey, e);
}
}
}
3.5.3 高可用与优缺点
- 高可用方案:部署3-5节点Etcd集群,通过Raft协议的过半机制保证集群可用性,半数以上节点存活即可正常服务,强一致性无脑裂风险。
- 优点:强一致性,性能优于ZooKeeper,租约机制灵活,Watch机制高效,云原生生态适配性好,天然支持K8s环境。
- 缺点:部署运维成本高,Java客户端生态不如Redisson成熟,国内使用场景少于Redis与ZooKeeper,性能略低于Redis。
四、全方案核心维度横向对比
| 对比维度 | MySQL悲观锁 | MySQL唯一索引锁 | Redis原生SETNX | Redisson分布式锁 | ZooKeeper(Curator) | Etcd |
| 核心互斥性保障 | InnoDB行锁 | 唯一索引约束 | Redis单线程原子命令 | Lua脚本+Hash结构 | 临时顺序节点+Watcher | Raft+Revision+Lease |
| 强一致性 | 是 | 是 | 否(异步复制) | 否(普通集群)/是(红锁) | 是(ZAB协议) | 是(Raft协议) |
| 可重入性 | 不支持 | 手动实现 | 不支持 | 原生支持 | 原生支持 | 原生支持 |
| 防死锁能力 | 弱(长事务) | 中(过期时间) | 中(过期时间) | 强(看门狗+过期时间) | 强(临时节点自动删除) | 强(租约自动过期) |
| 锁释放安全性 | 中(事务提交释放) | 中(手动判断持有者) | 中(Lua脚本) | 强(Lua脚本校验持有者) | 强(只能删除自己的节点) | 强(只能操作自己的租约key) |
| 锁续期能力 | 不支持 | 手动实现 | 手动实现 | 原生支持(看门狗) | 无需(会话保活) | 原生支持(租约续期) |
| 公平性 | 不支持 | 不支持 | 不支持 | 原生支持 | 原生支持(顺序节点) | 原生支持(Revision顺序) |
| 阻塞获取能力 | 不支持(轮询) | 不支持(轮询) | 不支持(轮询) | 原生支持(发布订阅) | 原生支持(Watcher) | 原生支持(Watch) |
| 单机QPS性能 | 低(<1000) | 低(<2000) | 高(>10W) | 高(>8W) | 中(<1W) | 中高(<5W) |
| 高可用保障 | 主从/MGR | 主从/MGR | 哨兵/Cluster | 哨兵/Cluster/红锁 | 集群过半机制 | 集群过半机制 |
| 部署成本 | 低(已有MySQL) | 低(已有MySQL) | 中(需部署Redis) | 中(需部署Redis) | 高(需部署ZK集群) | 高(需部署Etcd集群) |
| 适用场景 | 低并发、短事务 | 低并发、无额外中间件 | 简单场景、低并发 | 高并发、互联网通用场景 | 强一致性、金融级场景 | 云原生、K8s生态场景 |
五、生产环境选型指南
- 初创项目、低并发场景:已有MySQL基础设施,无额外中间件预算,优先选择MySQL唯一索引分布式锁,实现简单,成本极低,满足基础互斥需求。
- 互联网高并发通用场景:电商、订单、库存、营销等90%以上的业务场景,优先选择Redisson分布式锁,性能优异,功能全面,成熟稳定,社区活跃,踩坑成本低。
- 金融级强一致性场景:支付、转账、清算等对锁的互斥性零容忍的场景,优先选择ZooKeeper或Etcd分布式锁,天然强一致性,无锁丢失风险,满足金融级可靠性要求。
- 云原生K8s环境:基于K8s部署的微服务项目,优先选择Etcd分布式锁,与云原生生态完美适配,无需额外部署一致性组件。
- 跨机房、超高可靠性场景:跨地域部署、对锁的可靠性有极致要求的场景,选择Redisson红锁方案,通过多独立节点过半机制,避免极端场景下的锁丢失问题。
六、分布式锁生产踩坑避坑指南
6.1 锁超时与业务执行时间不匹配
问题:业务执行时间超过锁的预设过期时间,锁被提前释放,其他客户端获取到锁,导致并发问题与数据不一致。解决方案:使用Redisson看门狗机制实现自动续期;评估业务最长执行时间,设置合理的过期时间,预留2-3倍的冗余;禁止使用无过期时间的锁。
6.2 锁误释放
问题:客户端A释放了客户端B持有的锁,导致互斥性失效。解决方案:必须为每个锁设置唯一的持有者标识,释放锁时必须校验持有者身份;释放锁必须通过原子操作(Lua脚本、事务)实现,禁止直接删除锁;只能由持有锁的线程释放锁。
6.3 事务与锁的时序问题
问题:锁释放后,事务还未提交,其他客户端获取到锁,读取到未提交的数据,出现脏读与更新丢失。解决方案:严格控制时序,必须在事务提交之后再释放锁;优先使用编程式事务,手动控制事务提交与锁释放的顺序。
6.4 集群脑裂导致锁丢失
问题:Redis主从集群中,主节点宕机,异步复制导致锁数据未同步到从节点,从节点升级为主后,原锁数据丢失,多个客户端同时获取到锁。解决方案:强一致性场景使用ZooKeeper/Etcd;Redis场景配置min-replicas-to-write,保证写操作至少同步到1个从节点才返回成功;极端场景使用Redisson红锁方案。
6.5 锁粒度不合理
问题:锁粒度太大,导致并发度低,系统性能差;锁粒度太小,导致锁覆盖不全,出现并发漏洞。解决方案:锁的粒度必须与共享资源一一对应,比如商品库存锁使用商品ID作为锁key,禁止使用全商品的全局锁;避免在锁内执行非共享资源的操作,尽量缩短锁的持有时间。
6.6 网络抖动导致的锁提前释放
问题:ZooKeeper/Etcd场景中,网络抖动导致客户端与服务端会话断开,会话超时后,锁被自动释放,客户端仍在执行业务,导致并发问题。解决方案:调整会话超时时间,设置合理的心跳间隔;添加会话状态监听,会话断开时立即中断业务执行;配置客户端重连与重试机制。
七、总结
分布式锁没有银弹,不存在完美的方案,只有最适合业务场景的方案。
所有分布式锁的实现,本质上都是围绕核心设计准则做的取舍:追求高性能,就要牺牲一定的一致性;追求强一致性,就要接受性能的损耗;追求简单低成本,就要接受功能的局限性。
只有真正理解分布式锁的底层原理,才能结合业务场景做出正确的选型,在生产环境中正确使用,避免踩坑,真正解决分布式系统的并发互斥问题。