Java採用Redis相關命令實現分散式鎖
阿新 • • 發佈:2018-12-21
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 你的實現 } }