借助 SETNX(不完全正確)
Redis 中 SETNE 只有在 key 不存在時設置 key 的值,因此非常容易就實現(xiàn)了鎖功能。只需要客戶端對指定 KEY 成功設置一個隨機值,借助這個值來防止其他的進程取得鎖。
127.0.0.1:6379> get simpleLock
(nil)
127.0.0.1:6379> setnx simpleLock Locked
(integer) 1 //成功得到鎖返回1
127.0.0.1:6379> get simpleLock
"Locked"
127.0.0.1:6379> setnx simpleLock Release
(integer) 0 // 這里重新設置鎖的值,返回0代表其他客戶端獲得鎖
127.0.0.1:6379> get simpleLock
"Locked"
127.0.0.1:6379>
127.0.0.1:6379> del simpleLock // 刪除鍵,釋放鎖
(integer) 1
127.0.0.1:6379> setnx simpleLock Release //再重新獲取鎖
(integer) 1
127.0.0.1:6379>
通過 SETNX 基本能實現(xiàn)一個不完全正確的鎖,Java代碼如下:
package me.touch.redis;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@RunWith(JUnit4.class)
public class SimpleLock {
private Jedis jedis;
private JedisPool pool;
@Before
public void setUp() {
pool = new JedisPool(new JedisPoolConfig(), "localhost");
jedis = pool.getResource();
}
@After
public void after() {
jedis.close();
pool.destroy();
}
/**
* 獲得簡單鎖
* @return
*/
public boolean acquireSimpleLock(String lockName){
return jedis.setnx(lockName, "Locked") == 1 ;
}
/**
* 釋放鎖
* @return
*/
public boolean releaseSimpleLock(String lockName){
return jedis.del(lockName, "Locked") == 1 ;
}
@Test
public void test(){
if(acquireSimpleLock("simpleLock")){
System.out.println("獲取鎖成功 ·····");
// Do something ........
if(releaseSimpleLock("simpleLock")){
System.out.println("釋放鎖成功 ·····");
}
}
}
}
運行結(jié)果:
獲取鎖成功 ·····
釋放鎖成功 ·····
但是這個鎖是不完全正確的,缺少超時機制,缺少重試機制,釋放鎖的時候沒有驗證當前鎖是否由當前進程擁有等。
一個不完全正確的鎖會導致一些不正確的行為,如:
- 當缺少超時機制時,當持有鎖的進程死掉后,鎖得不釋放,造成死鎖。
- 當持有鎖的進程操作時間過長導致鎖自動釋放,但是很進程本身不知道,使得邏輯完成后錯誤的釋放其他進程的鎖(需要驗證鎖是否是當前進程持有)。
- 當持有鎖的進程崩潰后,其他進程無法檢測到,只能浪費時間等待鎖達到超時時候被釋放。
- 當一個進程持有鎖過期后,其他多個進程同時嘗試去獲取鎖,并且都獲取了鎖,而且都認為自己是唯一一個獲取到鎖的進程(需要驗證鎖是否是當前進程持有)
使用 Luna 腳本 (基本正確)
Redis 中的命令是原子執(zhí)行,的所以我們可以在 Lua 腳本中組合多個命令來完成我們的的邏輯。
lua 腳本獲取鎖
-- EXISTS 判斷是否存在 KEY ,如果存在,說明其他進程已經(jīng)獲得鎖,不存在這,設置KEY
if redis.call('EXISTS', KEYS[1]) == 0 then
return redis.call('SETEX', KEYS[1], unpack(ARGV))
end
lua 腳本釋放鎖
-- GET 獲取 KEY 值,判斷是否與指定的值相等,相等則刪除KEY
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1]) or true
end
Java 源碼
/**
* 獲得鎖
* @param keyName 鎖的名稱
* @param keyVlaue 鎖的值,建議使用UUID
* @param expire 鎖的過期時間
* @param timeout 獲取所得超時時間,毫秒
* @return
*/
public boolean acquireLockWhithTimeOut(String keyName, String keyVlaue,
String expire, long timeout){
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('EXISTS', KEYS[1]) == 0 then \n")
.append(" return redis.call('SETEX', KEYS[1], unpack(ARGV)) \n")
.append("end");
long now = System.currentTimeMillis();
do{
if("OK".equals(jedis.eval(sb.toString(), 1, keyName, expire, keyVlaue))){
return true;
}
}while( System.currentTimeMillis() < (now + timeout));
return false;
}
/**
* 釋放鎖
* @param keyName 鎖名稱
* @param keyVlaue 鎖的值
* @return
*/
public boolean releaseLock(String keyName, String keyVlaue){
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('GET', KEYS[1]) == ARGV[1] then \n")
.append(" return redis.call('DEL', KEYS[1]) or true \n")
.append("end");
return ((Long) jedis.eval(sb.toString(), 1, keyName, keyVlaue)) == 1 ;
}
@Test
public void test() throws InterruptedException{
//使用 uuid 作為鎖的值
String uuid = UUID.randomUUID().toString();
if(acquireLockWhithTimeOut("simpleLock", uuid, "60", 60*1000)){
System.out.println("獲取鎖成功 ·····");
// Do something ........
TimeUnit.SECONDS.sleep(1); // 線程睡上30秒
if(releaseLock("simpleLock", uuid)){
System.out.println("釋放鎖成功 ·····");
}
}
}
運行結(jié)果:
b308b026-8b01-4cf0-b145-b9061bf617f6
獲取鎖成功 ·····
釋放鎖成功 ·····
在這個例子中通過傳入 timeout 設置獲取鎖的超時時間實現(xiàn)了鎖獲取的重試機制;同時,通過 expire 指定了 key 的過期時間,避免照成了死鎖。在獲取鎖時指定的值為UUID,保證了鎖的唯一性。此外,在釋放鎖時比較 UUID 成功避免錯誤釋放其他進程鎖的問題,因此也不會出現(xiàn)多個進程多獲取到鎖的情況。當前實現(xiàn)已經(jīng)是基本正確的鎖實現(xiàn)了,能用于絕大部分應用場景,但是依然沒有解決因為持有鎖的進程崩潰造成其他進程浪費時間等待鎖過期的問題。