【redis学习之五】基于redis的分布式锁实现

    在单个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

扫描二维码关注公众号,回复: 4753832 查看本文章


猜你喜欢

转载自blog.csdn.net/smartValentines/article/details/80215356