1. 为什么需要分布式锁?
在现代分布式系统中,多个服务实例同时访问共享资源时会产生竞态条件。比如电商系统中的库存扣减、秒杀系统中的订单创建等场景,都需要保证同一时间只有一个客户端能够执行关键操作。单机环境下的synchronized或ReentrantLock在分布式场景下完全失效,这就是分布式锁要解决的问题。
Redis因其高性能和丰富的数据结构,成为实现分布式锁的首选方案之一。但很多开发者在没有深入理解Redis分布式锁原理的情况下直接使用,导致生产环境出现各种诡异问题。我见过最典型的案例是某金融系统因为锁失效导致重复转账,直接损失数十万元。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis分布式锁的核心原理
2.1 基础实现:SETNX命令
Redis实现分布式锁最基础的方式是使用SETNX(SET if Not eXists)命令:
bash复制SETNX lock_key unique_value
当key不存在时设置成功返回1,否则返回0。看似简单,但隐藏着几个关键点:
- unique_value必须是全局唯一标识(如UUID),用于安全释放锁
- 必须设置过期时间,防止客户端崩溃导致死锁
- 获取锁和设置过期时间必须是原子操作
常见错误实现:
java复制// 错误示例!非原子操作可能导致死锁
Boolean result = redisTemplate.opsForValue().setIfAbsent("lock", "value");
redisTemplate.expire("lock", 10, TimeUnit.SECONDS);
正确写法应该使用原子命令:
java复制// 正确实现:使用SET命令的NX和EX选项
Boolean result = redisTemplate.opsForValue().set(
"lock",
"unique_value",
10,
TimeUnit.SECONDS,
RedisStringCommands.SetOption.SET_IF_ABSENT
);
2.2 锁续期问题
当业务操作耗时超过锁过期时间时,会出现:
- 客户端A的锁自动过期
- 客户端B获取到锁
- 客户端A完成操作并释放了客户端B的锁
解决方案是使用守护线程定期续期,这就是Redisson的WatchDog机制原理。
2.3 可重入性实现
本地锁(如ReentrantLock)支持同一个线程重复获取锁,但Redis的SETNX默认不支持。实现可重入需要:
- 记录锁持有者和重入次数
- 使用Hash结构存储:
code复制key: lock_key field: thread_id value: count
3. Spring Boot中的最佳实践
3.1 原生RedisTemplate实现
java复制public class RedisLock {
private final StringRedisTemplate redisTemplate;
public boolean tryLock(String key, String value, long expire) {
return redisTemplate.opsForValue().setIfAbsent(
key,
value,
expire,
TimeUnit.MILLISECONDS
);
}
public boolean unlock(String key, String value) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
return redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
value
) == 1;
}
}
关键点:
- 使用Lua脚本保证判断和删除的原子性
- value必须全局唯一(建议UUID)
- 过期时间根据业务合理设置(通常1-30秒)
3.2 Redisson专业方案
对于生产环境,推荐直接使用Redisson:
java复制@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public void createOrder() {
RLock lock = redissonClient.getLock("order_lock");
try {
// 尝试加锁,最多等待100秒,上锁后30秒自动解锁
if (lock.tryLock(100, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
lock.unlock();
}
}
}
Redisson的优势:
- 自动续期(WatchDog)
- 可重入锁支持
- 丰富的锁类型(公平锁、联锁、红锁等)
- 完善的监控和统计
4. 开发者常踩的9大坑
4.1 锁过期时间设置不当
问题现象:业务未执行完锁已过期
解决方案:
- 合理评估业务最长时间,设置足够长的过期时间
- 使用Redisson自动续期功能
- 考虑将大事务拆分为小事务
4.2 非原子性加锁
错误示例:
java复制if(setNx("lock", value)) {
expire("lock", 10);
}
风险:在setNx和expire之间进程崩溃会导致死锁
正确做法:使用SET命令的NX和EX选项
4.3 错误释放他人锁
错误示例:
java复制// 简单删除key
redisTemplate.delete("lock");
风险:可能删除其他客户端持有的锁
正确做法:使用Lua脚本验证value再删除
4.4 未考虑Redis集群故障
问题现象:主节点崩溃,从节点提升为主时锁丢失
解决方案:
- 使用RedLock算法(需要多数节点确认)
- 评估是否真的需要强一致性,考虑Zookeeper等方案
4.5 锁重入问题
问题现象:同一线程递归调用导致死锁
java复制public void methodA() {
lock.lock();
methodB();
lock.unlock();
}
public void methodB() {
lock.lock();
// ...
lock.unlock();
}
解决方案:使用支持可重入的锁实现
4.6 锁等待导致系统雪崩
问题现象:高并发时大量线程阻塞等待锁
解决方案:
- 设置合理的等待超时时间
- 引入排队机制或令牌桶限流
- 考虑分段锁优化
4.7 锁未释放
常见原因:
- 未在finally块中释放
- 业务异常导致提前退出
- 自动过期时间设置过短
4.8 锁竞争激烈
优化方案:
- 减小锁粒度(如按订单ID加锁而非全局锁)
- 使用读写锁分离
- 考虑无锁方案(如CAS)
4.9 过度依赖分布式锁
反模式:
- 用分布式锁实现幂等性(应使用唯一约束)
- 用分布式锁替代缓存(应考虑缓存过期策略)
- 用分布式锁控制定时任务(应考虑分布式调度框架)
5. 性能优化实战技巧
5.1 锁粒度控制
错误做法:
java复制// 全局锁严重影响并发
lock.lock("global_order_lock");
优化方案:
java复制// 按订单ID加锁
lock.lock("order_lock_" + orderId);
5.2 锁分段技术
对于库存扣减场景:
java复制// 原始方案(性能差)
lock.lock("inventory_lock");
// 优化方案:将库存拆分为10个段
int segment = itemId.hashCode() % 10;
lock.lock("inventory_lock_" + segment);
5.3 读写锁分离
java复制RReadWriteLock rwLock = redisson.getReadWriteLock("cache_lock");
// 读操作
rwLock.readLock().lock();
try {
// 查询缓存
} finally {
rwLock.readLock().unlock();
}
// 写操作
rwLock.writeLock().lock();
try {
// 更新缓存
} finally {
rwLock.writeLock().unlock();
}
5.4 锁等待优化
java复制// 设置合理的等待时间(避免长时间阻塞)
if (lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
// ...
} finally {
lock.unlock();
}
} else {
// 快速失败或降级处理
throw new BusyException("系统繁忙,请稍后重试");
}
6. 监控与问题排查
6.1 关键监控指标
- 锁等待时间
- 锁持有时间
- 锁获取失败率
- 锁续期次数
6.2 Redis监控命令
bash复制# 查看所有key
redis-cli keys '*lock*'
# 查看key剩余生存时间
redis-cli ttl "lock_key"
# 查看Redis慢查询
redis-cli slowlog get
6.3 日志记录建议
java复制public boolean tryLock(String key, String value, long expire) {
long begin = System.currentTimeMillis();
boolean acquired = redisTemplate.opsForValue().setIfAbsent(key, value, expire, TimeUnit.MILLISECONDS);
long cost = System.currentTimeMillis() - begin;
if (acquired) {
log.info("Acquired lock [{}] in {} ms", key, cost);
} else {
log.warn("Failed to acquire lock [{}] after {} ms", key, cost);
}
return acquired;
}
7. 替代方案选型
7.1 Zookeeper
适用场景:
- 需要强一致性保证
- 锁精度要求高
- 可以接受较低性能
7.2 数据库实现
实现方式:
sql复制-- 获取锁
INSERT INTO distributed_lock(lock_name, holder, expire_time)
VALUES ('order_lock', 'host1', NOW() + INTERVAL 10 SECOND);
-- 释放锁
DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND holder = 'host1';
缺点:
- 性能较差
- 需要处理死锁
- 数据库压力大
7.3 etcd
特点:
- 基于租约(Lease)机制
- 支持观察者模式
- 强一致性保证
8. 真实案例复盘
8.1 秒杀系统锁优化
初始方案:
- 全局Redis锁控制库存扣减
- QPS仅支持500左右
问题分析:
- 锁竞争过于激烈
- 网络往返开销大
优化方案:
- 采用分段锁(100个段)
- 本地库存+异步同步
- Lua脚本实现原子扣减
效果:
- QPS提升至15000+
- 99线耗时从200ms降至15ms
8.2 分布式任务调度
问题现象:
- 多个节点同时执行定时任务
- 部分任务重复执行
解决方案:
java复制@Scheduled(cron = "0 0/5 * * * ?")
public void scheduledTask() {
RLock lock = redisson.getLock("scheduled_task_lock");
if (lock.tryLock()) {
try {
// 执行任务逻辑
} finally {
lock.unlock();
}
}
}
优化点:
- 设置合理的锁过期时间(大于任务执行时间)
- 添加监控告警(长时间未释放锁)
- 记录任务执行日志
9. 面试常见问题
- Redis分布式锁的实现原理?
- SETNX和SET有什么区别?
- 如何处理锁过期但业务未执行完的情况?
- Redis集群环境下分布式锁有什么问题?
- RedLock算法的工作流程?
- 如何实现一个可重入的Redis分布式锁?
- 分布式锁和同步锁的区别?
- 什么场景下不应该使用分布式锁?
- 如何监控分布式锁的健康状态?
- 除了Redis还有哪些分布式锁实现方案?
10. 开发注意事项
- 环境隔离:开发、测试、生产环境使用不同的Redis命名空间
- 锁命名规范:按业务维度命名(如"order:create:{orderId}")
- 熔断降级:当Redis不可用时要有备用方案
- 压力测试:模拟高并发场景验证锁性能
- 文档记录:记录关键锁的使用场景和注意事项
关键建议:在非必要场景尽量避免使用分布式锁。我曾重构过一个系统,通过用本地缓存+消息队列替代分布式锁,性能提升了8倍。分布式锁应该是最后的选择,而不是首选的解决方案。
