在分布式系统中,我们经常会遇到需要协调多个服务实例对共享资源进行访问的场景。比如电商平台的库存扣减、秒杀活动中的商品抢购、分布式任务调度等。这些场景下,传统的单机锁机制(如Java中的synchronized关键字或ReentrantLock)就显得力不从心了。
本地锁只能保证在单个JVM进程内的线程安全。举个例子:
这种场景下就会产生两个典型问题:
分布式锁需要实现的核心功能是:在分布式环境下,确保同一时刻只有一个线程能够执行临界区代码。这需要解决以下几个关键问题:
Redis的SETNX(SET if Not eXists)命令是实现分布式锁的基础:
bash复制SETNX key value
这个命令的原子性特性非常适合用来实现分布式锁。
我们先来看一个基于Jedis的基础实现:
java复制public class SimpleRedisLock {
private final JedisPool jedisPool;
private static final String LOCK_PREFIX = "lock:";
private static final int DEFAULT_EXPIRE_SECONDS = 10;
public boolean lock(String lockKey) {
try (Jedis jedis = jedisPool.getResource()) {
Long result = jedis.setnx(LOCK_PREFIX + lockKey, "1");
if (result == 1) {
jedis.expire(LOCK_PREFIX + lockKey, DEFAULT_EXPIRE_SECONDS);
return true;
}
return false;
}
}
public void unlock(String lockKey) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.del(LOCK_PREFIX + lockKey);
}
}
}
这个看似简单的实现实际上存在几个严重问题:
Redis 2.6.12版本后,SET命令支持了NX和EX选项,可以原子性地实现SETNX和EXPIRE:
java复制public boolean lock(String lockKey) {
try (Jedis jedis = jedisPool.getResource()) {
String result = jedis.set(LOCK_PREFIX + lockKey, "1", "NX", "EX", DEFAULT_EXPIRE_SECONDS);
return "OK".equals(result);
}
}
为每个锁设置唯一值,释放时先检查值是否匹配:
java复制private String lockValue = UUID.randomUUID().toString();
public boolean unlock(String lockKey) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
try (Jedis jedis = jedisPool.getResource()) {
Object result = jedis.eval(luaScript, 1, LOCK_PREFIX + lockKey, lockValue);
return "1".equals(result.toString());
}
}
对于执行时间不确定的业务,需要实现锁的自动续期:
java复制public void watchDog(String lockKey, long delay) {
new Thread(() -> {
while (true) {
try {
Thread.sleep(delay * 1000);
try (Jedis jedis = jedisPool.getResource()) {
String currentValue = jedis.get(LOCK_PREFIX + lockKey);
if (lockValue.equals(currentValue)) {
jedis.expire(LOCK_PREFIX + lockKey, DEFAULT_EXPIRE_SECONDS);
} else {
break;
}
}
} catch (InterruptedException e) {
break;
}
}
}).start();
}
Redisson是Redis官方推荐的Java客户端,提供了丰富的分布式对象和服务,其中就包括完善的分布式锁实现。
java复制@Autowired
private RedissonClient redissonClient;
public void doSomething(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待3秒,锁持有时间10秒
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 执行业务逻辑
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
Redisson使用Hash结构存储锁信息:
加锁流程通过Lua脚本保证原子性:
lua复制if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
在Redis主从架构中,如果主节点在锁信息同步到从节点前宕机,可能导致锁失效。
RedLock算法要求:
java复制RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
if (redLock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 执行业务逻辑
}
} finally {
redLock.unlock();
}
可能原因:
解决方案:
可能原因:
解决方案:
可能原因:
解决方案:
在实际项目中,我遇到过因为锁超时时间设置不当导致的业务问题。一个批处理任务执行时间不确定,有时会超过锁的默认过期时间,导致多个节点同时执行任务。后来通过Redisson的看门狗机制解决了这个问题,同时也调整了任务拆分策略,将大任务拆分为多个小任务分别加锁执行。