分布式锁要求:
可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
这把锁要是一把可重入锁(避免死锁)
这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好
redis分布式锁
setnx和get,getset三个方法
setnx成功,加锁成功
setnx失败,get锁的过期时间与当前时间比较,如果锁过期了,getset设置锁的新的过期时间,返回旧的过期时间。
如果两个过期时间相等,加锁成功
如果失败,150毫秒后重试,最多10秒还失败,就加锁失败
del释放锁
public class RedisLock {
private final String LOCK = "LOCK";
private String LOCK_KEY = LOCK;
private volatile boolean locked = false;
/*锁有效期1分钟, 前端对接口调用时间有1分钟限制, 所以锁有效期1分钟可以的*/
private int expireMillis = 60 * 1000;
/*锁等待10秒, 10秒拿不到锁, 就返回失败*/
private int timeoutMillis = 10 * 1000;
private RedisTemplate redisTemplate;
private Random random = new Random();
public RedisLock(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public RedisLock(RedisTemplate redisTemplate, String key) {
this.redisTemplate = redisTemplate;
this.LOCK_KEY = LOCK + ":" + key;
}
public RedisLock(RedisTemplate redisTemplate, String key, int timeoutMillis) {
this.redisTemplate = redisTemplate;
this.LOCK_KEY = LOCK + ":" + key;
this.timeoutMillis = timeoutMillis;
}
public RedisLock(RedisTemplate redisTemplate, String key, int expireMillis, int timeoutMillis) {
this.redisTemplate = redisTemplate;
this.LOCK_KEY = LOCK + ":" + key;
this.expireMillis = expireMillis;
this.timeoutMillis = timeoutMillis;
}
/**
* 如果加锁后, redis崩溃, 锁就删不掉了, 所以锁要加有效期
* unlock加锁了, lock也要加锁
* 执行过程:
* 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
* 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
* @throws InterruptedException
*/
public synchronized boolean lock() {
log.debug("redis尝试加锁, {}", LOCK_KEY);
long sleepMillis = getSleepMillis();
int timeout = timeoutMillis;
while (timeout >= 0) {
String expireTime = System.currentTimeMillis() + expireMillis + "";
//把锁的过期时间写入锁的值
//原子方法,应该也是同步的
boolean setnx = setnx(LOCK_KEY, expireTime);
//拿到锁
if (setnx) {
locked = true;
log.debug("redis加锁成功, {} = {}", LOCK_KEY, expireTime);
return true;
}
//没拿到锁, 但是锁过期了
String oldExpireTime = get(LOCK_KEY);
if (oldExpireTime != null && Long.parseLong(oldExpireTime) < System.currentTimeMillis()) {
//getSet是同步的, 多个线程串行
//第一个线程getSet的结果和get的结果相同, 拿到锁
String beforeExpireTime = getSet(LOCK_KEY, expireTime);
if (beforeExpireTime != null && beforeExpireTime.equals(oldExpireTime)) {
locked = true;
log.debug("redis加锁成功, {} = {}", LOCK_KEY, expireTime);
return true;
}
}
//随机时间, 防止多个线程同时抢锁
try {
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
timeout -= sleepMillis;
}
log.debug("redis加锁失败, {}", LOCK_KEY);
return false;
}
/**
* locked非原子操作, 必须加锁
* unlock必须用finally包围, 否则锁删不掉
*/
public synchronized void unlock() {
//必须是加锁成功, 才可以解锁. 即locked=true
//如果加锁失败也能解锁的话, 就会把加锁成功正在进行业务的锁删除掉
if (locked) {
redisTemplate.delete(LOCK_KEY);
locked = false;
log.debug("redis解锁成功, {}", LOCK_KEY);
}
}
private long getSleepMillis() {
int nextInt = random.nextInt(100);
int result = nextInt + 50;
return result;
}
private boolean setnx(String key, String value) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
private String get(String key) {
Object o = redisTemplate.opsForValue().get(key);
return o == null ? null : o.toString();
}
private String getSet(String key, String value) {
Object o = redisTemplate.opsForValue().getAndSet(key, value);
return o == null ? null : o.toString();
}
public boolean isLocked() {
return locked;
}
/**
* 使用举例
*/
@Test
public void testLock() {
RedisLock redisLock = new RedisLock(null, "asd");
try {
redisLock.lock();
//业务
} finally {
redisLock.unlock();
}
}
}
注意事项:
考虑锁超时
方法的原子性
误删别的线程的锁
优点
性能好,实现起来较为方便
缺点
非阻塞?while重复执行。
非可重入?在一个线程获取到锁之后,把当前主机信息和线程信息保存起来,下次再获取之前先检查自己是不是当前锁的拥有者。
通过超时时间来控制锁的失效时间并不是十分的靠谱。
Zookeeper分布式锁
左边的整个区域表示一个Zookeeper集群,locker是Zookeeper的一个持久节点,node_1、node_2、node_3是locker这个持久节点下面的临时顺序节点。client_1、client_2、client_n表示多个客户端,Service表示需要互斥访问的共享资源。
获取锁:
a、在获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。
b、客户端调用createNode方法在locker下创建临时顺序节点,然后调用getChildren(“locker”)来获取locker下面的所有子节点,注意此时不用设置任何Watcher。
c、客户端获取到所有的子节点path之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。
d、如果发现自己创建的节点并非locker所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。
e、之后,让这个被关注的节点删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是locker子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。
Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。
其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端可ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。
使用Zookeeper实现分布式锁的优点
有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
使用Zookeeper实现分布式锁的缺点
性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。
Redlock
需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,redlock 也使用「大多数机制」。 加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx) 指令,只要过半节点 set 成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock 需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些