1. 为什么需要分布式锁
在单机应用中,我们使用Java内置的锁机制(如synchronized或ReentrantLock)就能很好地解决多线程并发问题。但在分布式系统中,当多个服务实例需要访问共享资源时,单机锁就无能为力了。
想象一下电商系统中的库存扣减场景:多个用户同时抢购同一件商品,如果仅使用单机锁,不同服务器上的请求会各自加锁,导致超卖问题。这就是我们需要分布式锁的根本原因。
Redis作为高性能的内存数据库,其原子性操作和丰富的特性使其成为实现分布式锁的理想选择。通过Redis实现的分布式锁需要满足以下基本特性:
- 互斥性:同一时刻只能有一个客户端持有锁
- 可重入性:同一个客户端可以多次获取同一把锁
- 锁超时:防止死锁,即使锁未被显式释放也能自动过期
- 高可用:锁服务需要具备高可用性
- 容错性:客户端在获取锁后崩溃,锁也能被正确释放
2. 单机锁的局限性
让我们先看一个典型的单机锁实现:
java复制@Service
public class SaleService {
@Autowired
private RedisTemplate redisTemplate;
// 单机锁
Lock lock = new ReentrantLock();
private static final String stockKeyId = "stock_key_id:1001";
public void sale() {
lock.lock();
try {
// 业务逻辑
Object countObj = redisTemplate.opsForValue().get(stockKeyId);
int count = countObj == null ? 0 : Integer.valueOf(String.valueOf(countObj));
if (count > 0) {
redisTemplate.opsForValue().set(stockKeyId, (count-1));
}
} finally {
lock.unlock();
}
}
}
这个实现在单机环境下工作良好,但在分布式场景下会完全失效。原因在于:
- 每个服务实例都有自己的锁实例,无法跨进程互斥
- 当服务实例崩溃时,锁无法自动释放
- 无法处理网络分区等分布式系统特有的问题
重要提示:在分布式系统中,单机锁只能保证单个JVM内的线程安全,无法解决跨进程的资源竞争问题。
3. 基于Redis的分布式锁演进
3.1 第一版:基础实现
最简单的Redis分布式锁实现是利用SETNX命令:
java复制public void sale() {
String uuid = UUID.randomUUID().toString();
// 尝试获取锁
while (!redisTemplate.opsForValue().setIfAbsent("lock", uuid)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
try {
// 业务逻辑
} finally {
// 释放锁
redisTemplate.delete("lock");
}
}
这个版本存在明显问题:
- 如果获取锁后服务崩溃,锁永远不会释放(死锁)
- 释放锁时可能误删其他客户端持有的锁
3.2 第二版:设置过期时间
为解决死锁问题,我们给锁设置过期时间:
java复制while (!redisTemplate.opsForValue()
.setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS)) {
// 重试逻辑
}
现在即使客户端崩溃,锁也会在30秒后自动释放。但仍然存在以下问题:
- 业务执行时间超过锁过期时间,导致其他客户端获取锁
- 释放锁时仍可能误删其他客户端的锁
3.3 第三版:安全的锁释放
通过Lua脚本确保只有锁的持有者才能释放锁:
java复制String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("lock"), uuid);
这个版本解决了锁误删问题,但仍有改进空间:
- 不支持可重入
- 锁过期时间固定,无法适应不同业务场景
3.4 第四版:可重入锁实现
通过Redis的Hash结构实现可重入锁:
java复制String lockLuaScript =
"if redis.call('exists',KEYS[1])==0 then " +
" redis.call('hset',KEYS[1],ARGV[1],1) " +
" redis.call('expire',KEYS[1],ARGV[2]) " +
" return 1 " +
"elseif redis.call('hexists',KEYS[1],ARGV[1])==1 then " +
" redis.call('hincrby',KEYS[1],ARGV[1],1) " +
" redis.call('expire',KEYS[1],ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
解锁时也需要对应的Lua脚本:
java复制String unlockScript =
"if redis.call('hexists',KEYS[1],ARGV[1])==1 then " +
" if redis.call('hincrby',KEYS[1],ARGV[1],-1)==0 then " +
" redis.call('del',KEYS[1]) " +
" end " +
" return 1 " +
"else " +
" return 0 " +
"end";
3.5 第五版:自动续期机制
为防止业务执行时间超过锁过期时间,实现自动续期:
java复制private void autoRenewal() {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
String script =
"if redis.call('hexists',KEYS[1],ARGV[1])==1 then " +
" return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
" return 0 " +
"end";
if (Boolean.TRUE.equals(redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList(lockName),
lockObj, expireTime.toString()))) {
autoRenewal();
}
}
}, expireTime * 1000 * 2 / 3); // 在过期时间2/3时续期
}
4. 完整实现方案
下面是一个完整的Redis分布式锁实现:
java复制public class RedisDistributedLock implements Lock {
private final String lockName;
private final String lockValue;
private final RedisTemplate<String, Object> redisTemplate;
private final long expireTime;
public RedisDistributedLock(String lockName, RedisTemplate<String, Object> redisTemplate, long expireTime) {
this.lockName = lockName;
this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
this.redisTemplate = redisTemplate;
this.expireTime = expireTime;
}
@Override
public void lock() {
while (!tryLock()) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
@Override
public boolean tryLock() {
String script =
"if redis.call('exists',KEYS[1])==0 then " +
" redis.call('hset',KEYS[1],ARGV[1],1) " +
" redis.call('expire',KEYS[1],ARGV[2]) " +
" return 1 " +
"elseif redis.call('hexists',KEYS[1],ARGV[1])==1 then " +
" redis.call('hincrby',KEYS[1],ARGV[1],1) " +
" redis.call('expire',KEYS[1],ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
Boolean acquired = redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList(lockName),
lockValue,
String.valueOf(expireTime));
if (Boolean.TRUE.equals(acquired)) {
scheduleRenewal();
return true;
}
return false;
}
private void scheduleRenewal() {
Timer timer = new Timer(true);
timer.schedule(new TimerTask() {
@Override
public void run() {
String script =
"if redis.call('hexists',KEYS[1],ARGV[1])==1 then " +
" return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
" return 0 " +
"end";
Boolean renewed = redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList(lockName),
lockValue,
String.valueOf(expireTime));
if (Boolean.TRUE.equals(renewed)) {
scheduleRenewal();
}
}
}, expireTime * 1000 * 2 / 3);
}
@Override
public void unlock() {
String script =
"if redis.call('hexists',KEYS[1],ARGV[1])==1 then " +
" local count = redis.call('hincrby',KEYS[1],ARGV[1],-1) " +
" if count == 0 then " +
" redis.call('del',KEYS[1]) " +
" end " +
" return 1 " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockName),
lockValue);
}
// 其他Lock接口方法实现...
}
5. 使用示例
java复制@Service
public class InventoryService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String STOCK_KEY = "product:1001:stock";
private static final String LOCK_KEY = "product:1001:lock";
public boolean reduceStock(int quantity) {
Lock lock = new RedisDistributedLock(LOCK_KEY, redisTemplate, 30);
try {
lock.lock();
Integer stock = (Integer) redisTemplate.opsForValue().get(STOCK_KEY);
if (stock == null || stock < quantity) {
return false;
}
redisTemplate.opsForValue().set(STOCK_KEY, stock - quantity);
return true;
} finally {
lock.unlock();
}
}
}
6. 性能优化与注意事项
- 锁粒度:锁的粒度要尽可能小,只锁定必要的资源
- 超时时间:根据业务特点设置合理的锁超时时间
- 重试策略:获取锁失败后的重试间隔应随机化,避免惊群效应
- Redis集群:在Redis集群环境下,需要考虑Redlock算法
- 监控告警:对锁等待时间过长的情况进行监控
实践经验:在高并发场景下,建议对锁操作进行监控,记录获取锁的等待时间和持有时间,这有助于发现潜在的性能问题。
7. 常见问题排查
-
锁无法释放:
- 检查是否所有代码路径都确保锁会被释放
- 检查锁的过期时间是否设置合理
-
锁被误删:
- 确保使用唯一标识作为锁的值
- 确保释放锁时验证锁的持有者
-
性能问题:
- 检查Redis服务器负载
- 考虑使用本地缓存减少锁竞争
-
锁等待时间过长:
- 考虑优化业务逻辑减少锁持有时间
- 考虑使用分段锁减少竞争
8. 与现成方案的对比
相比Redisson等成熟的Redis分布式锁实现,我们的自定义实现有以下特点:
优势:
- 更轻量,不依赖额外库
- 更透明,完全掌控实现细节
- 可根据业务特点灵活定制
劣势:
- 缺少一些高级特性(如公平锁)
- 需要自行处理Redis集群等复杂场景
- 缺少完善的测试覆盖
在实际项目中,如果业务场景简单,自定义实现是个不错的选择;如果需要更全面的功能支持,建议考虑Redisson等成熟方案。