前言
说到分布式,就必然涉及到分布式锁的概念,如何保证不同机器不同线程的分布式锁同步呢?
实现要点
-
互斥性,同一时刻,只能有一个客户端持有锁。 -
防止死锁发生,如果持有锁的客户端崩溃没有主动释放锁,也要保证锁可以正常释放及其他客户端可以正常加锁。 -
加锁和释放锁必须是同一个客户端。 -
容错性,只有redis还有节点存活,就可以进行正常的加锁解锁操作。
正确的redis分布式锁实现
错误加锁方式
错误方式一
public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {
Long result = jedis.setnx(key, uniqueId);
if (1 == result) {
//如果该redis实例崩溃,那就无法设置过期时间了
jedis.expire(key, expireTime);
}
}
错误方式二
public static boolean wrongLock(Jedis jedis, String key, int expireTime) {
long expireTs = System.currentTimeMillis() + expireTime;
// 锁不存在,当前线程加锁成果
if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
return true;
}
String value = jedis.get(key);
//如果当前锁存在,且锁已过期
if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {
//锁过期,设置新的过期时间
String oldValue = jedis.getSet(key, String.valueOf(expireTs));
if (oldValue != null && oldValue.equals(value)) {
// 多线程并发下,只有一个线程会设置成功
// 设置成功的这个线程,key的旧值一定和设置之前的key的值一致
return true;
}
}
// 其他情况,加锁失败
return true;
}
-
value设置为过期时间,就要求各个客户端严格的时钟同步,这就需要使用到同步时钟。即使有同步时钟,分布式的服务器一般来说时间肯定是存在少许误差的。 -
锁过期时,使用 jedis.getSet虽然可以保证只有一个线程设置成功,但是不能保证加锁和解锁为同一个客户端,因为没有标志锁是哪个客户端设置的嘛。
错误解锁方式
解锁错误方式一
public static void wrongReleaseLock(Jedis jedis, String key) {
//不是自己加锁的key,也会被释放
jedis.del(key);
}
解锁错误方式二
public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {
if (uniqueId.equals(jedis.get(key))) {
// 如果这时锁过期自动释放,又被其他线程加锁,该线程就会释放不属于自己的锁
jedis.del(key);
}
}
正确加锁释放锁方式
-
命令必须保证互斥 -
设置的key必须要有过期时间,防止崩溃时锁无法释放 -
value使用唯一id标志每个客户端,保证只有锁的持有者才可以释放锁
下面是具体的加锁和释放锁的代码:
@Slf4j
public class RedisDistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
// 锁的超时时间
private static int EXPIRE_TIME = 5 * 1000;
// 锁等待时间
private static int WAIT_TIME = 1 * 1000;
private Jedis jedis;
private String key;
public RedisDistributedLock(Jedis jedis, String key) {
this.jedis = jedis;
this.key = key;
}
// 不断尝试加锁
public String lock() {
try {
// 超过等待时间,加锁失败
long waitEnd = System.currentTimeMillis() + WAIT_TIME;
String value = UUID.randomUUID().toString();
while (System.currentTimeMillis() < waitEnd) {
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
if (LOCK_SUCCESS.equals(result)) {
return value;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (Exception ex) {
log.error("lock error", ex);
}
return null;
}
public boolean release(String value) {
if (value == null) {
return false;
}
// 判断key存在并且删除key必须是一个原子操作
// 且谁拥有锁,谁释放
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = new Object();
try {
result = jedis.eval(script, Collections.singletonList(key),
Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
log.info("release lock success, value:{}", value);
return true;
}
} catch (Exception e) {
log.error("release lock error", e);
} finally {
if (jedis != null) {
jedis.close();
}
}
log.info("release lock failed, value:{}, result:{}", value, result);
return false;
}
}
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️