分布式锁技术解读
2025/12/3大约 8 分钟
分布式锁技术解读
1. 技术概述
在高并发分布式系统中,为了保证数据一致性,经常需要在多个服务实例间协调对共享资源的访问。淘票票项目通过实现一套完整的分布式锁机制来解决这个问题,基于Redisson实现,提供了多种类型的分布式锁和灵活的使用方式。
2. 为什么需要分布式锁
分布式系统中,多个服务实例共享同一资源(如订单、库存)时,本地锁(synchronized、ReentrantLock)只能控制单实例内的并发,跨实例的并发冲突成为"重灾区"。3类典型问题:
- 重复支付:用户快速点击两次支付按钮,两个服务实例同时发起扣款,导致用户被重复收费
- 库存超卖:热门场次电影票库存仅剩1张,两个用户同时下单,均查询到"有库存"并扣减,最终超卖
- 状态混乱:同一笔订单的"支付回调"和"手动取消"同时执行,订单状态被交替修改,出现既"已支付"又"已取消"的矛盾状态
这些问题源于多个进程/线程在无协调的情况下并发操作同一资源。分布式锁的核心作用是让所有服务实例"共享同一把锁",确保同一时间只有一个实例能操作关键资源。
3. 为什么选择Redisson
3.1 原生Redis实现分布式锁的痛点
- 不可重入导致死锁:同一线程无法重复获取锁
- 锁过期时间难平衡:设短了业务没执行完就释放,设长了服务宕机会导致长期锁定
- 释放锁可能误删他人锁:线程A的锁过期后,线程B加锁,线程A执行完会误删线程B的锁
3.2 Redisson解决方案
- 可重入性:用
Hash存储"线程标识+重入次数",同一线程可重复获取锁 - 看门狗机制:锁过期前自动续约,业务未执行完不会释放
- 安全释放:释放前检查"当前线程是否持有锁",避免误删
- 原子性保障:通过Lua脚本确保所有操作原子性
- 丰富锁类型:支持可重入锁、公平锁、读写锁等多种类型
4. 分布式锁技术与最佳实践
4.1 数据结构设计
Redisson在Redis中用Hash结构存储锁信息:
key:锁的唯一标识(如taopiaopiao:pay:ORDER123456)field:加锁线程的唯一标识(格式:线程ID:UUID,避免不同服务实例的线程ID冲突)value:该线程的重入次数(同一线程多次加锁时递增)
示例:
# 锁key为"taopiaopiao:pay:ORDER123456"的Hash结构
HGETALL taopiaopiao:pay:ORDER123456
1) "16384:5f4dcc3b-6bb2-4453-91bf-9e2e4dcc8e5d" # 线程标识(线程ID:UUID)
2) "2" # 重入次数(该线程第2次获取锁)4.2 加锁逻辑
通过Lua脚本实现原子性加锁操作,核心逻辑分3种场景:
- 锁不存在:直接创建
Hash结构,设置field=当前线程标识、value=1,并设置锁过期时间(默认30秒) - 锁存在且属于当前线程:重入次数+1,重置锁过期时间(触发看门狗续约)
- 锁存在且属于其他线程:返回加锁失败,等待或直接退出
核心加锁Lua脚本(简化版):
local lockKey = KEYS[1] -- 锁key
local threadId = ARGV[1] -- 当前线程标识(field)
local expireTime = ARGV[2] -- 过期时间(毫秒)
-- 场景1:锁不存在,创建锁
if redis.call('exists', lockKey) == 0 then
redis.call('hset', lockKey, threadId, 1) -- 设置Hash
redis.call('pexpire', lockKey, expireTime) -- 设置过期时间
return 1 -- 加锁成功
end
-- 场景2:锁存在且属于当前线程,重入次数+1
if redis.call('hexists', lockKey, threadId) == 1 then
redis.call('hincrby', lockKey, threadId, 1) -- 重入次数+1
redis.call('pexpire', lockKey, expireTime) -- 重置过期时间
return 1 -- 加锁成功
end
-- 场景3:锁被其他线程持有,加锁失败
return 04.3 看门狗机制
- 自动启动条件:成功获取锁且未指定过期时间
- 续约逻辑:每隔
leaseTime/3(默认10秒)检查一次,若仍持有锁则重置过期时间 - 停止时机:线程释放锁或异常终止时
4.4 释放锁逻辑
- 检查锁归属:只允许锁的持有者释放锁
- 重入次数递减:若重入次数>1,递减后继续持有锁
- 删除锁:若重入次数=1,完全释放锁
5. 项目集成方案
5.1 模块概述
分布式锁模块位于taopiaopiao-service-lock-framework目录下,基于Redisson实现,提供了多种类型的分布式锁和灵活的使用方式。
5.2 核心组件
- ServiceLock注解 - 声明式使用分布式锁的入口
- ServiceLockAspect切面 - 实现分布式锁的核心逻辑
- ServiceLocker接口及实现类 - 定义分布式锁操作规范
- ServiceLockFactory工厂类 - 根据锁类型创建对应的锁实现
- ServiceLockTool工具类 - 编程式使用分布式锁的工具类
- ServiceLockAutoConfiguration - 自动配置类,注册相关Bean
5.3 3层架构设计
- 底层引擎:Redisson提供分布式锁的基础实现
- 切面层:AOP拦截标注自定义注解的方法,执行锁操作
- 业务层:开发者只需在方法上标注注解,无需关心锁的实现细节
6. 核心代码实现
6.1 注解定义
package com.taopiaopiao.servicelock.annotion;
import com.taopiaopiao.servicelock.LockType;
import com.taopiaopiao.servicelock.info.LockTimeOutStrategy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁注解
*/
@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface ServiceLock {
/**
* 锁的类型(默认 可重入锁)
*/
LockType lockType() default LockType.Reentrant;
/**
* 业务名称
* @return name
*/
String name() default "";
/**
* 自定义业务key
* @return keys
*/
String [] keys();
/**
* 尝试加锁失败最多等待时间
* @return waitTime
*/
long waitTime() default 10;
/**
* 时间单位
* @return TimeUnit
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 加锁超时的处理策略
* @return LockTimeOutStrategy
*/
LockTimeOutStrategy lockTimeoutStrategy() default LockTimeOutStrategy.FAIL;
/**
* 自定义加锁超时的处理策略
* @return customLockTimeoutStrategy
*/
String customLockTimeoutStrategy() default "";
}6.2 切面实现
package com.taopiaopiao.servicelock.aspect;
import com.taopiaopiao.constant.LockInfoType;
import com.taopiaopiao.util.StringUtil;
import com.taopiaopiao.lockinfo.LockInfoHandle;
import com.taopiaopiao.lockinfo.factory.LockInfoHandleFactory;
import com.taopiaopiao.servicelock.LockType;
import com.taopiaopiao.servicelock.ServiceLocker;
import com.taopiaopiao.servicelock.annotion.ServiceLock;
import com.taopiaopiao.servicelock.factory.ServiceLockFactory;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁切面
*/
@Slf4j
@Aspect
@Order(-10) // 设置切面优先级,数值越小优先级越高
@AllArgsConstructor
public class ServiceLockAspect {
private final LockInfoHandleFactory lockInfoHandleFactory;
private final ServiceLockFactory serviceLockFactory;
/**
* 环绕通知处理分布式锁逻辑
*
* @param joinPoint 连接点
* @param servicelock 分布式锁注解
* @return 方法执行结果
* @throws Throwable 异常
*/
@Around("@annotation(servicelock)")
public Object around(ProceedingJoinPoint joinPoint, ServiceLock servicelock) throws Throwable {
// 获取锁信息处理器
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.SERVICE_LOCK);
// 构造锁名称
String lockName = lockInfoHandle.getLockName(joinPoint, servicelock.name(), servicelock.keys());
// 获取锁类型、等待时间、时间单位等配置
LockType lockType = servicelock.lockType();
long waitTime = servicelock.waitTime();
TimeUnit timeUnit = servicelock.timeUnit();
// 获取对应类型的锁实例
ServiceLocker lock = serviceLockFactory.getLock(lockType);
// 尝试获取锁
boolean result = lock.tryLock(lockName, timeUnit, waitTime);
if (result) {
// 获取锁成功,执行目标方法
try {
return joinPoint.proceed();
} finally {
// 释放锁
lock.unlock(lockName);
}
} else {
// 获取锁失败,超时处理
log.warn("Timeout while acquiring serviceLock:{}", lockName);
String customLockTimeoutStrategy = servicelock.customLockTimeoutStrategy();
if (StringUtil.isNotEmpty(customLockTimeoutStrategy)) {
// 使用自定义超时处理策略
return handleCustomLockTimeoutStrategy(customLockTimeoutStrategy, joinPoint);
} else {
// 使用默认超时处理策略
servicelock.lockTimeoutStrategy().handler(lockName);
}
return joinPoint.proceed();
}
}
/**
* 处理自定义锁超时策略
*/
public Object handleCustomLockTimeoutStrategy(String customLockTimeoutStrategy, JoinPoint joinPoint) {
// 实现细节省略...
// 通过反射调用自定义超时处理方法
return null;
}
}7. 锁类型支持
淘票票分布式锁模块支持多种锁类型,以满足不同场景的需求:
- 可重入锁(Reentrant):最常用的锁类型,基于Redisson的RLock实现
- 公平锁(Fair):保证获取锁的顺序性,避免线程饥饿
- 读锁(Read):读写锁中的读锁,支持多个线程同时读取
- 写锁(Write):读写锁中的写锁,独占访问
8. 使用示例
8.1 注解式使用
@ServiceLock(
name = "PAY", // 业务名称
keys = {"#payDto.orderNo"}, // 锁key的动态参数(支持SpEL表达式)
lockType = LockType.REENTRANT, // 锁类型
waitTime = 5, // 获取锁的最大等待时间
timeUnit = TimeUnit.SECONDS // 时间单位
)
public Boolean payOrder(PayDto payDto) {
// 支付业务逻辑
return true;
}8.2 编程式使用
@Service
public class OrderService {
@Autowired
private ServiceLockTool serviceLockTool;
public void processOrder(String orderId) {
String lockKey = "order:process:" + orderId;
try {
if (serviceLockTool.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
// ...
} finally {
// 释放锁
serviceLockTool.unlock(lockKey);
}
} else {
// 获取锁失败处理
throw new RuntimeException("订单处理中,请稍后再试");
}
} catch (Exception e) {
// 异常处理
}
}
}9. 最佳实践
9.1 锁设计原则
- 锁粒度:尽量缩小锁的范围,避免长时间持有锁
- 锁key设计:使用业务标识+唯一ID,避免锁冲突
- 超时处理:合理设置等待时间和超时策略
- 异常处理:在finally块中释放锁,确保资源正确释放
9.2 性能优化
- 避免热点key:锁的粒度不要过大,避免不同业务竞争同一把锁
- 合理设置超时:根据业务执行时间设置合适的超时时间
- 减少持有时间:在锁内只执行必要的操作,耗时操作移到锁外
9.3 监控与告警
- 监控锁获取失败:对锁获取失败的情况进行记录和告警
- 监控锁持有时间:对长时间持有锁的情况进行监控,及时发现死锁风险
- 监控Redis健康状态:确保Redis服务正常运行,避免成为系统瓶颈
9.4 常见问题与解决方案
- 死锁风险:确保在finally块中释放锁,即使发生异常
- 锁穿透:合理设置等待时间和超时策略,避免无限等待
- 锁竞争激烈:考虑使用分段锁或优化业务流程,减少锁竞争
- Redis单点故障:使用Redis集群或哨兵模式,提高可用性