概述:
Redis使用WATCH命令来代替对数据进行加锁,因为WATCH只会在数据被其他客户端抢先修改了的情况下通知执行了这个命令的客户端,而不会阻止其他客户端对数据进行修改,所以这个命令被称为乐观锁。分布式锁是由不同机器上的不同Redis客户端进行获取和释放的。
分布式锁实现原理:
为了对数据进行排他性访问,程序首先要做的就是获取锁。利用SETNX命令来实现锁的获取功能,这个命令只会在键不存在的情况下为键设置值。
加锁:
将一个随机生成的UUID设置为键的值,并使用这个值来防止锁被其他进程取得。如果程序在尝试获取锁的时候失败,那么它将不断地进行重试,直到成功地取得锁或者超过给定的时限为止。
解锁:
根据UUID的值判断进程是否持有锁,没有则删除对应的lock_key,释放锁。
注意:
为了解决持有者崩溃的时候不会自动被释放,而导致的锁一直处于已被获取的状态。我们将为锁加上超时功能,为了给锁加上超时限制特性,程序将在取得锁之后,调用EXPIRE命令来为锁设置过期时间,使得Redis可以向动删除超时的锁。为了确保锁在客户端已经崩溃的情况下仍然能够自动被释放,客户端会在尝试获取锁失败之后,检查锁的超时时间,并为未设置超时时间的锁设置超时时间。因此锁总会带有超时时间,并最终因为超时而自动被释放,使得其他客户端可以继续尝试获取已被释放的锁。
锁的常见问题:
1)持有锁的进程因为操作时间过长而导致锁被自动释放,但进程本身并不知晓这一点,甚至还可能会错误地释放掉了其他进程持有的锁。
2)一个持有锁并打算执行长时间操作的进程已经崩溃,但其他想要获取锁的进程不知道哪个进程持有着锁,也无法检测出持有锁的进程已经崩溃,只能自由地浪费时间等待锁被释放。
3)在一个进程持有的锁过期之后,其他多个进程同时尝试去获取锁,并且都获得了锁。
4)上面提到的第一种情况和第三种情况同时出现,导致有多个进程获得了锁,而每个进程都以为自己是唯一一个获得锁的进程。
代码实现:
package com.tianlh.service.impl.redis;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import redis.clients.jedis.*;
/**
* @author 黑子
* @Date 2017/12/30 15:34
*/
public class SimpleLock {
public String acquireLock(Jedis conn, String lockname, Integer acquireTimeout, Integer lockTimeout) {
String identifier = UUID.randomUUID().toString().replace("-", "");
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockname, identifier) == 1) {
conn.expireAt(lockname, lockTimeout);
return identifier;
} else if (conn.ttl(lockname) == -1) {
// -2已经过期 -1没有设置过期时间,永不失效
conn.expire(lockname, lockTimeout);
}
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
public boolean releaseLock(Jedis conn, String lockName, String identifier) {
while (true) {
conn.watch(lockName);
if (conn.get(lockName).equals(identifier)) {
redis.clients.jedis.Transaction transaction = conn.multi();
Response<String> response = transaction.watch("");
if(!"OK".equals(response.get())){
continue;
}
transaction.del(lockName);
List<Object> exec = transaction.exec();
if (exec.get(0).equals("OK")) {
try {
transaction.close();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
}
break;
}
return false;
}
}