在分布式系统中,多个服务实例同时操作共享资源(如库存扣减、订单创建)时,会出现 “超卖”“重复创建” 等并发问题 —— 本地锁(如 Java 的 synchronized)只能控制单实例内的并发,无法解决跨服务的竞争。分布式锁通过分布式协调机制(如 Redis、ZooKeeper),让多个服务实例共享一把 “锁”,确保同一时间只有一个实例能操作资源。
分布式锁的核心要求
一个可靠的分布式锁需满足:
- 互斥性:同一时间只有一个服务能获取锁
- 安全性:锁只能被持有锁的服务释放
- 可用性:锁服务不能单点故障
- 重入性:持有锁的服务可再次获取锁(可选,视场景而定)
- 超时释放:防止服务宕机导致锁永远不释放
主流实现方案
1. Redis 分布式锁:高性能场景的首选
基于 Redis 的SET NX(不存在则设置)命令实现,利用 Redis 的单线程特性保证原子性。
基础实现(Java) :
public class RedisDistributedLock {
private RedisTemplate<String, String> redisTemplate;
private String lockKey; // 锁的Key(如"stock:lock:1001")
private String lockValue; // 锁的值(随机UUID,用于标识持有者)
private long expireTime = 30000; // 锁过期时间(30秒)
private long waitTime = 10000; // 获取锁的最大等待时间(10秒)
public RedisDistributedLock(RedisTemplate<String, String> redisTemplate, String lockKey) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey;
this.lockValue = UUID.randomUUID().toString(); // 生成唯一值,避免误释放
}
// 获取锁
public boolean tryLock() {
// 使用SET NX + EX命令,确保原子性(不存在则设置,同时设置过期时间)
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(success);
}
// 带等待的获取锁
public boolean tryLockWithWait() throws InterruptedException {
long startTime = System.currentTimeMillis();
while (true) {
if (tryLock()) {
return true;
}
// 等待100ms重试
Thread.sleep(100);
// 超过最大等待时间则返回失败
if (System.currentTimeMillis() - startTime > waitTime) {
return false;
}
}
}
// 释放锁(必须用Lua脚本保证原子性)
public boolean unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = (Long) redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue
);
return result != null && result > 0;
}
}
使用示例:
// 扣减库存(防止超卖)
public boolean deductStock(Long productId, int num) {
String lockKey = "stock:lock:" + productId;
RedisDistributedLock lock = new RedisDistributedLock(redisTemplate, lockKey);
try {
// 获取锁
if (!lock.tryLockWithWait()) {
log.warn("获取锁失败,productId={}", productId);
return false;
}
// 业务逻辑:查询库存、扣减库存
Product product = productMapper.selectById(productId);
if (product.getStock() < num) {
return false;
}
product.setStock(product.getStock() - num);
productMapper.updateById(product);
return true;
} catch (InterruptedException e) {
log.error("获取锁异常", e);
return false;
} finally {
// 释放锁
lock.unlock();
}
}
2. 其他实现方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis | 性能高,实现简单 | 可能存在锁超时问题 | 高并发场景(如秒杀) |
| ZooKeeper | 可靠性高,支持重入锁 | 性能较低,部署复杂 | 高可靠性场景(如分布式事务) |
| 数据库 | 实现简单(基于唯一索引) | 性能差,易死锁 | 中小规模系统,快速实现 |
分布式锁的高级特性
1. 锁自动续期(防超时)
Redis 锁的过期时间若设置过短,可能导致业务未完成锁就释放;设置过长,又可能因服务宕机导致锁长期不释放。解决方案是 “自动续期”:
-
获取锁后,启动一个后台线程,每隔一段时间(如过期时间的 1/3)延长锁的过期时间
-
业务完成后,关闭续期线程并释放锁
实现思路:结合 Redis 的EXPIRE命令和 Java 的 ScheduledExecutorService 实现定时续期。
2. 公平锁与非公平锁
-
非公平锁:多个请求同时抢锁,谁先拿到算谁的(Redis 默认实现),优点是性能高,缺点是可能导致 “饥饿”(某些请求长期抢不到锁)
-
公平锁:按请求顺序获取锁(ZooKeeper 基于节点顺序实现),优点是避免饥饿,缺点是性能略低
选择建议:秒杀等对性能要求高的场景用非公平锁;订单处理等需公平性的场景用公平锁。
避坑指南
-
避免锁粒度过大:锁的范围应尽可能小(如锁 “商品 ID=1001” 而非锁整个 “商品表”),否则会降低并发效率
-
必须处理锁释放失败:即使解锁逻辑正确,也可能因网络问题导致释放失败,需依赖锁的过期时间兜底
-
集群环境的特殊处理:Redis 集群下,主从切换可能导致 “锁丢失”(主节点锁已写入但未同步到从节点就宕机),需使用 RedLock 算法(多节点加锁)
分布式锁是分布式系统并发控制的 “最后一道防线”,它通过跨服务的协调机制,让原本混乱的并发操作变得有序。但需注意,分布式锁不是银弹 —— 过度使用会降低系统吞吐量,应在 “数据一致性” 和 “性能” 之间找到平衡,这正是后端架构设计 “取舍之道” 的体现。