借助 SETNX(不完全正確)
Redis 中 SETNE 只有在 key 不存在時(shí)設(shè)置 key 的值劝术,因此非常容易就實(shí)現(xiàn)了鎖功能。只需要客戶端對(duì)指定 KEY 成功設(shè)置一個(gè)隨機(jī)值,借助這個(gè)值來防止其他的進(jìn)程取得鎖秦忿。
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 // 這里重新設(shè)置鎖的值,返回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 基本能實(shí)現(xiàn)一個(gè)不完全正確的鎖,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("釋放鎖成功 ·····");
}
}
}
}
運(yùn)行結(jié)果:
獲取鎖成功 ·····
釋放鎖成功 ·····
但是這個(gè)鎖是不完全正確的方面,缺少超時(shí)機(jī)制贺氓,缺少重試機(jī)制,釋放鎖的時(shí)候沒有驗(yàn)證當(dāng)前鎖是否由當(dāng)前進(jìn)程擁有等。
一個(gè)不完全正確的鎖會(huì)導(dǎo)致一些不正確的行為歇父,如:
- 當(dāng)缺少超時(shí)機(jī)制時(shí)翎冲,當(dāng)持有鎖的進(jìn)程死掉后钳枕,鎖得不釋放,造成死鎖。
- 當(dāng)持有鎖的進(jìn)程操作時(shí)間過長導(dǎo)致鎖自動(dòng)釋放欣舵,但是很進(jìn)程本身不知道,使得邏輯完成后錯(cuò)誤的釋放其他進(jìn)程的鎖(需要驗(yàn)證鎖是否是當(dāng)前進(jìn)程持有)。
- 當(dāng)持有鎖的進(jìn)程崩潰后雄可,其他進(jìn)程無法檢測(cè)到虐急,只能浪費(fèi)時(shí)間等待鎖達(dá)到超時(shí)時(shí)候被釋放敬惦。
- 當(dāng)一個(gè)進(jìn)程持有鎖過期后,其他多個(gè)進(jìn)程同時(shí)嘗試去獲取鎖,并且都獲取了鎖妨猩,而且都認(rèn)為自己是唯一一個(gè)獲取到鎖的進(jìn)程(需要驗(yàn)證鎖是否是當(dāng)前進(jìn)程持有)
使用 Luna 腳本 (基本正確)
Redis 中的命令是原子執(zhí)行销斟,的所以我們可以在 Lua 腳本中組合多個(gè)命令來完成我們的的邏輯。
lua 腳本獲取鎖
-- EXISTS 判斷是否存在 KEY ,如果存在,說明其他進(jìn)程已經(jīng)獲得鎖迈勋,不存在這,設(shè)置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 鎖的過期時(shí)間
* @param timeout 獲取所得超時(shí)時(shí)間脖母,毫秒
* @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("釋放鎖成功 ·····");
}
}
}
運(yùn)行結(jié)果:
b308b026-8b01-4cf0-b145-b9061bf617f6
獲取鎖成功 ·····
釋放鎖成功 ·····
在這個(gè)例子中通過傳入 timeout 設(shè)置獲取鎖的超時(shí)時(shí)間實(shí)現(xiàn)了鎖獲取的重試機(jī)制;同時(shí)猎醇,通過 expire 指定了 key 的過期時(shí)間梧税,避免照成了死鎖剔交。在獲取鎖時(shí)指定的值為UUID葫督,保證了鎖的唯一性竭鞍。此外,在釋放鎖時(shí)比較 UUID 成功避免錯(cuò)誤釋放其他進(jìn)程鎖的問題橄镜,因此也不會(huì)出現(xiàn)多個(gè)進(jìn)程多獲取到鎖的情況偎快。當(dāng)前實(shí)現(xiàn)已經(jīng)是基本正確的鎖實(shí)現(xiàn)了,能用于絕大部分應(yīng)用場(chǎng)景洽胶,但是依然沒有解決因?yàn)槌钟墟i的進(jìn)程崩潰造成其他進(jìn)程浪費(fèi)時(shí)間等待鎖過期的問題晒夹。