1. 程式人生 > >Java採用Redis相關命令實現分散式鎖

Java採用Redis相關命令實現分散式鎖

Java本地鎖(synchronized或J.U.C.Lock)只能解決當前jvm下的併發問題,如果是叢集環境下或者一個機器跑多個jvm例項且相互間有互動或重疊時,此時需要一個“中央鎖”來進行控制。

命令 SET resource-name anystring NX EX max-lock-time 是一種在 Redis 中實現鎖的簡單方法。

客戶端執行以上的命令:

如果伺服器返回 OK ,那麼這個客戶端獲得鎖。
如果伺服器返回 NIL ,那麼客戶端獲取鎖失敗,可以在稍後再重試。
設定的過期時間到達之後,鎖將自動釋放。

可以通過以下修改,讓這個鎖實現更健壯:

不使用固定的字串作為鍵的值,而是設定一個不可猜測(non-guessable)的長隨機字串,作為口令串(token)。
不使用 DEL 命令來釋放鎖,而是傳送一個 Lua 指令碼,這個指令碼只在客戶端傳入的值和鍵的口令串相匹配時,才對鍵進行刪除。
這兩個改動可以防止持有過期鎖的客戶端誤刪現有鎖的情況出現。

以下是一個簡單的解鎖指令碼示例:

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
這個指令碼可以通過 EVAL ...script... 1 resource-name token-value 命令來呼叫。

RedisLock.java

import java.util.UUID;

import redis.clients.jedis.Jedis;

/**
 * 
 * A class that acts as a distributed lock using the method
 * {@code Jedis.set(String key, String value, String nxxx, String expx, long time)}.
 * <h3>Usage Examples</h3>
 * <p>Here is an example:
 * <pre>{@code //the time value 10 means the method invocation in try block takes no more than 10 seconds
 * RedisLock lock = RedisLock.getLock("myLock",10);
 * if(lock!=null){
 *  try{
 *    //doSomething
 *  } finally{
 *    lock.unlock();
 *  }
 * }}
 */
public class RedisLock {

	/**
	 * A value that identifies the current lock
	 */
	private final String value;
	
	/**
	 * A key represents the lock
	 */
	private final String key;
	
	/**
	 * Constructs a {@code RedisLock} used for release the lock via {@link #unlock()}
	 * @param key the key
	 * @param value the value, which identifies the current lock
	 */
	private RedisLock(String key, String value){
		this.key = key;
		this.value = value;
	}
	
	/**
	 * Try to hold the lock represented by the specified key and
	 * set a timeout(in seconds) on the lock (that is, the specified key) 
	 * if the lock is successfully obtained to avoid the lock is never released
	 * when fails to invoke {@link #unlock()}(which means the method may be
	 * throws an uncaught exception, but not the return value {@code false}).
	 * 
	 * @param key the key used as lock object
	 * @param seconds expire time in seconds
	 * @return {@code RedisLock} if the lock is successfully obtained, else {@code null}
	 * @throws NullPointerException if the specified key is null
	 * @throws IllegalArgumentException if the key is empty, 
	 * or the time value is less then or equal to zero
	 */
	public static RedisLock getLock(String key, int seconds){
		check(key, seconds);
		String value = UUID.randomUUID().toString();
		Jedis jedis = null;
		try{
			jedis = RedisPool.get();
			//Redis 2.6.12 版本及以上 可以直接通過set命令實現
			String status = jedis.set(key, value, "NX", "EX", seconds);
			if("OK".equalsIgnoreCase(status)){
				return new RedisLock(key, value);
			}
			//Redis 2.6.12 版本以前 可通過指令碼實現
//			String[] param = {key, value, String.valueOf(seconds)};
//			String script = "local ok = redis.call('setnx', KEYS[1], ARGV[1]) if ok == 1 then redis.call('expire', KEYS[1], ARGV[2]) end return ok";
//			Object status = jedis.eval(script, 1, param);
//			if(((Number)status).intValue()==1){
//				return new RedisLock(key, value);
//			}
		} finally{
			RedisPool.close(jedis);
		}
		
		return null;
	}
	
	// 之所以不過載為getLock(String key, long millis)是因為int與long不好區分,想明確呼叫的話必須加字尾L
	// 由於網路來回傳輸比較耗時,毫秒的精度是否有必要?
	
//	/**
//	 * Try to hold the lock represented by the specified key and
//	 * set a timeout(in milliseconds) on the lock (that is, the specified key) 
//	 * if the lock is successfully obtained to avoid the lock is never released
//	 * when fails to invoke {@link #unlock()}(which means the method may be 
//	 * throws an uncaught exception, but not the return value {@code false}).
//	 * 
//	 * @param key the key used as lock object
//	 * @param millis expire time in milliseconds
//	 * @return {@code RedisLock} If the lock is successfully obtained, else {@code null}
//	 * @throws NullPointerException if the specified key is null
//	 * @throws IllegalArgumentException if the key is empty, 
//	 * or the time value is less then or equal to zero
//	 */
//	public static RedisLock getLockMillis(String key, long millis){
//		check(key, millis);
//		String value = UUID.randomUUID().toString();
//		Jedis jedis = null;
//		try{
//			jedis = RedisPool.get();
//			//Redis 2.6.12 版本及以上 可以直接通過set命令實現
//			String status = jedis.set(key, value, "NX", "PX", millis);
//			if("OK".equalsIgnoreCase(status)){
//				return new RedisLock(key, value);
//			}
//			//Redis 2.6.12 版本以前 可通過指令碼實現
////			String[] param = {key, value, String.valueOf(millis)};
////			String script = "local ok = redis.call('setnx', KEYS[1], ARGV[1]) if ok == 1 then redis.call('PEXPIRE', KEYS[1], ARGV[2]) end return ok";
////			Object status = jedis.eval(script, 1, param);
////			if(((Number)status).intValue()==1){
////				return new RedisLock(key, value);
////			}
//		} finally{
//			RedisPool.close(jedis);
//		}
//		
//		return null;
//	}
	
	private static void check(String key, long time){
		checkKey(key);
		if(time<=0){
			throw new IllegalArgumentException("Invalid time value:"+time);
		}
	}
	
	private static void checkKey(String key){
		if(key==null){
			throw new NullPointerException("Null key!");
		}
		if(key.isEmpty()){
			throw new IllegalArgumentException("Empty key!");
		}
	}
	
	/**
	 * Releases the lock.
	 * @return true if successfully releases by the current thread,
	 * false otherwise.
	 */
	public boolean unlock(){
		Jedis jedis = null;
		try{
			jedis = RedisPool.get();
			String[] param = {key, value};
			Object status = jedis.eval("if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end", 1, param);
			return ((Number)status).intValue()>0;
		} finally{
			RedisPool.close(jedis);
		}
	}
}

RedisPool.java

import redis.clients.jedis.Jedis;

/**
 * 
 * 這裡是你的Redis管理器的實現
 *
 */
public final class RedisPool {
 
	/**
	 * 從資源池裡獲取一個Jedis連線
	 * @return Jedis
	 */
	public static Jedis get(){
		//TODO 作為示例 直接返回null
		return null;
	}
	
	/**
	 * 將指定的Jedis連線歸還給資源池
	 * @param jedis
	 */
	public static void close(Jedis jedis){
		//TODO 你的實現
	}
}