在单个JVM中,我们可以很方便的用sychronized或者reentrantLock在资源竞争时进行加锁,保证高并发下数据线程安全。但是若是分布式环境下,多个JVM同时对一个资源进行竞争时,我们该如何保证线程安全呢?分布式锁便能实现我们的要求。
在设计思路上,分布式锁和java自带的锁采用的方法是一样的。reentrantLock是基于AQS的,在AQS基类中维护了一个int类型的state变量,采用CAS(CompareAndSwap)的方式修改state的值来判断是否取得锁,若是修改成功则判断获取锁,并设置锁的独占线程为当前线程。若CAS修改失败则获取失败。而分布式锁的设计,也是通过对一个第三者值的原子操作的成功、失败来判断是否获取到锁。
实现分布式锁的方式有多种多样,例如使用zookeeper实现,通过是否能创建固定路径的临时节点来判断是否获取锁,解锁时断开连接即可,临时节点自动删除。甚至可以实现若获取锁失败,则在此锁节点按顺序创建子节点,并对前一个子节点进行监听,当前节点有变化(删除)则再次尝试获取锁,通过这样来实现公平锁。zookeeper的curator框架便提供了InterProcessMutex这种分布式锁实现。我们还可以通过缓存如redis实现分布式锁。例如使用setnx(set if not exsit)命令。相对来说redis实现方式还是要优于zookeeper,因为zookeeper创建、销毁节点在性能上的开销是要远远大于redis的。
下面来介绍一下一种基于redis的分布式锁的实现:
首先定义锁的接口,里面有分布式锁的基本方法,也是以单机锁的方法为参照,分布式锁也可以支持响应中断、超时时间、阻塞获取等:
package common;
import java.util.concurrent.TimeUnit;
public interface DistributedLock {
/**
* 释放锁资源
*/
void release();
/**
* 阻塞式获取锁,不响应中断
*/
void lock();
/**
* 阻塞式获取锁,响应中断
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试获取锁,若未获取立即返回,不阻塞
*/
boolean tryLock();
/**
* 时自动返回的阻塞性的获取锁, 不响应中断
* @param time
* @param timeUnit
* @return {@code true} 若成功获取到锁, {@code false} 若在指定时间内未获取到锁
*/
boolean tryLock(long time,TimeUnit timeUnit);
/**
* 超时自动返回的阻塞性的获取锁, 响应中断
* @param time
* @param timeUnit
* @return {@code true} 若成功获取到锁, {@code false} 若在指定时间内未获取到锁
*/
boolean tryLockInterruptibly(long time,TimeUnit timeUnit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
}
然后,也定义一个锁的模板类,具体锁获取由子类完成:
package common;
import java.util.concurrent.TimeUnit;
/**
* 锁实现模板类, 真正的获取锁的步骤由子类去实现.
*/
public abstract class AbstractDistributedLock implements DistributedLock{
protected volatile boolean locked;
//当前jvm内持有该锁的线程
private Thread exclusiveOwnerThread;
// 阻塞加锁
public void lock(){
try{
lock(false,0,null,false);
}catch(InterruptedException e){
e.printStackTrace();
}
}
// 响应中断式阻塞加锁
public void lockInteruptibly() throws InterruptedException{
lock(false,0,null,true);
}
// 带超时时间的尝试加锁
public boolean tryLock(long time,TimeUnit timeUnit){
try{
return lock(true,time,timeUnit,false);
}catch(InterruptedException e){
e.printStackTrace();
}
return false;
}
// 响应中断的tryLock
public boolean tryLockInterruptibly(long time,TimeUnit timeUnit) throws InterruptedException{
return lock(true,time,timeUnit,false);
}
public void unlock(){
//检查是否是当前线程持有锁
if(Thread.currentThread()!=exclusiveOwnerThread){
throw new IllegalMonitorStateException("current thread does not hold the lock!");
}
releaseLock();
}
//解锁,将在子类实现
protected abstract void releaseLock();
//设置独占锁线程
protected void setExclusiveOwnerThread(Thread thread){
this.exclusiveOwnerThread = thread;
}
protected Thread getExclusiveOwnerThread(){
return exclusiveOwnerThread;
}
/**
* 阻塞式获取锁的实现 ,具体在子类实现
*
* @param useTimeout 是否判断超时
* @param time // 超时时间
* @param unit // 时间单位
* @param interrupt 是否响应中断
* @return boolean // 是否成功获取锁
* @throws InterruptedException
*/
protected abstract boolean lock(boolean useTimeout,long time,TimeUnit timeUnit,boolean interrupt) throws InterruptedException;
}
最后呈上加锁、解锁具体实现:
package common;
import java.util.concurrent.TimeUnit;import redis.clients.jedis.Jedis;/** * 分布式锁 */public class RedisDistributedLock extends AbstractDistributedLock{private Jedis jedis;private String lockKey;private long lockExpireTime;//构造Redis分布式锁public RedisDistributedLock(String lockKey,long lockExpireTime){this.lockKey = lockKey; // 分布式锁的key,不同的JVM竞争一把锁需用同样的key,连接同一份redis实例this.lockExpireTime = lockExpireTime;// 加锁超时时间this.jedis = new Jedis("localhost", 6379);}protected boolean lock(boolean useTimeout,long time,TimeUnit timeUnit,boolean interrupt) throws InterruptedException{//若为响应中断,则判断线程是否中断if(interrupt){checkInterruption();}long start = System.currentTimeMillis();//获取锁开始时间long timeout = timeUnit.toMillis(time);//获取超时时间while(useTimeout?isTimeOut(start,timeout):true){//若许判断超时则轮循判断if(interrupt){checkInterruption();}String lockExpire = String.valueOf(serverTimeMillis()+lockExpireTime+1);if(jedis.setnx(lockKey, lockExpire) == 1){//成功set值,表明获取到锁locked = true;setExclusiveOwnerThread(Thread.currentThread());return true;}String value = jedis.get(lockKey);if(value!=null && isTimeExpired(value)){//值已被set但是时间值已超过锁有效时间,表明锁已超时 // 假设多个线程(非单jvm)同时走到这里 String oldValue = jedis.getSet(lockKey, lockExpire); // getset is atomic // 但是走到这里时每个线程拿到的oldValue肯定不可能一样(因为getset是原子性的) // 加入拿到的oldValue依然是expired的,那么就说明拿到锁了 if (oldValue != null && isTimeExpired(oldValue)) { // TODO 成功获取到锁, 设置相关标识 locked = true; setExclusiveOwnerThread(Thread.currentThread()); return true; } }}return false;}public boolean tryLock(){String lockExpire = String.valueOf(serverTimeMillis()+lockExpireTime+1);if (jedis.setnx(lockKey, lockExpire) == 1) { // 获取到锁 // TODO 成功获取到锁, 设置相关标识 locked = true; setExclusiveOwnerThread(Thread.currentThread()); return true; } String value = jedis.get(lockKey); if (value != null && isTimeExpired(value)) { // lock is expired // 假设多个线程(非单jvm)同时走到这里 String oldValue = jedis.getSet(lockKey, lockExpire); // getset is atomic // 但是走到这里时每个线程拿到的oldValue肯定不可能一样(因为getset是原子性的) // 假如拿到的oldValue依然是expired的,那么就说明拿到锁了 if (oldValue != null && isTimeExpired(oldValue)) { // TODO 成功获取到锁, 设置相关标识 locked = true; setExclusiveOwnerThread(Thread.currentThread()); return true; } } return false;}public void release(){}//检查当前线程是否被中断private void checkInterruption() throws InterruptedException { if(Thread.currentThread().isInterrupted()) { throw new InterruptedException("current thread has been interrupted!"); } } //判断锁是否已超时private boolean isTimeOut(long start,long timeout){return start+timeout > System.currentTimeMillis();}//获取本地时间private long serverTimeMillis(){ return System.currentTimeMillis(); }//判断是否超时失效private boolean isTimeExpired(String value) { return Long.parseLong(value) < serverTimeMillis(); }public boolean isLocked() { if (locked) { return true; } else { String value = jedis.get(lockKey); // 是检测不出这种情况的.不过这个问题应该不会导致其它的问题出现, 因为这个方法的目的本来就 // 不是同步控制, 它只是一种锁状态的报告. return !isTimeExpired(value); } } @Overrideprotected void releaseLock() {// 判断锁是否过期 String value = jedis.get(lockKey); if (!isTimeExpired(value)) { //若锁仍未超时则删除key jedis.del(lockKey); } }}我们可以看到加锁过程可以总结如下:
1、若为tryLock则直接尝试setnx,value为当前时间+尝试时间,若set成功则获取锁成功,设置独占线程为当前线程,否则失败
2、若为阻塞方式获取锁,则若setnx失败,则先获取该锁的超时时间,若锁未超时则进入下一个setnx循环,若锁已超时则利用getSet命令尝试获取锁,若设置成功返回的原value小于当前时间,即说明此锁的value是本线程修改的,获取锁成功,否则便是被别的线程抢先修改,获取锁失败,继续进入循环,直至超时返回false