分佈式鎖在分佈式應用中應用普遍,想要搞懂一個新事物首先得了解它的由來,這樣才能更加的理解甚至能夠觸類旁通。java
首先談到分佈式鎖天然也就聯想到分佈式應用。git
在咱們將應用拆分爲分佈式應用以前的單機系統中,對一些併發場景讀取公共資源時如扣庫存,賣車票之類的需求能夠簡單的使用同步或者是加鎖就能夠實現。github
可是應用分佈式了以後系統由之前的單進程多線程的程序變爲了多進程多線程,這時使用以上的解決方案明顯就不夠了。redis
所以業界經常使用的解決方案一般是藉助於一個第三方組件並利用它自身的排他性來達到多進程的互斥。如:數據庫
NX EX
參數。這裏主要基於 Redis 進行討論。api
既然是選用了 Redis,那麼它就得具備排他性才行。同時它最好也有鎖的一些基本特性:多線程
這裏利用 Redis set key
時的一個 NX 參數能夠保證在這個 key 不存在的狀況下寫入成功。而且再加上 EX 參數可讓該 key 在超時以後自動刪除。併發
因此利用以上兩個特性能夠保證在同一時刻只會有一個進程得到鎖,而且不會出現死鎖(最壞的狀況就是超時自動刪除 key)。dom
實現代碼以下:maven
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
public boolean tryLock(String key, String request) {
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)){
return true ;
}else {
return false ;
}
}
複製代碼
注意這裏使用的 jedis 的
String set(String key, String value, String nxxx, String expx, long time);
複製代碼
api。
該命令能夠保證 NX EX 的原子性。
必定不要把兩個命令(NX EX)分開執行,若是在 NX 以後程序出現問題就有可能產生死鎖。
同時也能夠實現一個阻塞鎖:
//一直阻塞
public void lock(String key, String request) throws InterruptedException {
for (;;){
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)){
break ;
}
//防止一直消耗 CPU
Thread.sleep(DEFAULT_SLEEP_TIME) ;
}
}
//自定義阻塞時間
public boolean lock(String key, String request,int blockTime) throws InterruptedException {
while (blockTime >= 0){
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)){
return true ;
}
blockTime -= DEFAULT_SLEEP_TIME ;
Thread.sleep(DEFAULT_SLEEP_TIME) ;
}
return false ;
}
複製代碼
解鎖也很簡單,其實就是把這個 key 刪掉就萬事大吉了,好比使用 del key
命令。
但現實每每沒有那麼 easy。
若是進程 A 獲取了鎖設置了超時時間,可是因爲執行週期較長致使到了超時時間以後鎖就自動釋放了。這時進程 B 獲取了該鎖執行很快就釋放鎖。這樣就會出現進程 B 將進程 A 的鎖釋放了。
因此最好的方式是在每次解鎖時都須要判斷鎖是不是本身的。
這時就須要結合加鎖機制一塊兒實現了。
加鎖時須要傳遞一個參數,將該參數做爲這個 key 的 value,這樣每次解鎖時判斷 value 是否相等便可。
因此解鎖代碼就不能是簡單的 del
了。
public boolean unlock(String key,String request){
//lua script
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = null ;
if (jedis instanceof Jedis){
result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
}else if (jedis instanceof JedisCluster){
result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
}else {
//throw new RuntimeException("instance is error") ;
return false ;
}
if (UNLOCK_MSG.equals(result)){
return true ;
}else {
return false ;
}
}
複製代碼
這裏使用了一個 lua
腳原本判斷 value 是否相等,相等才執行 del 命令。
使用 lua
也能夠保證這裏兩個操做的原子性。
所以上文提到的四個基本特性也能知足了:
我本身有擼了一個完整的實現,而且已經用於了生產,有興趣的朋友能夠開箱使用:
maven 依賴:
<dependency>
<groupId>top.crossoverjie.opensource</groupId>
<artifactId>distributed-redis-lock</artifactId>
<version>1.0.0</version>
</dependency>
複製代碼
配置 bean :
@Configuration
public class RedisLockConfig {
@Bean
public RedisLock build(){
RedisLock redisLock = new RedisLock() ;
HostAndPort hostAndPort = new HostAndPort("127.0.0.1",7000) ;
JedisCluster jedisCluster = new JedisCluster(hostAndPort) ;
// Jedis 或 JedisCluster 均可以
redisLock.setJedisCluster(jedisCluster) ;
return redisLock ;
}
}
複製代碼
使用:
@Autowired
private RedisLock redisLock ;
public void use() {
String key = "key";
String request = UUID.randomUUID().toString();
try {
boolean locktest = redisLock.tryLock(key, request);
if (!locktest) {
System.out.println("locked error");
return;
}
//do something
} finally {
redisLock.unlock(key,request) ;
}
}
複製代碼
使用很簡單。這裏主要是想利用 Spring 來幫咱們管理 RedisLock 這個單例的 bean,因此在釋放鎖的時候須要手動(由於整個上下文只有一個 RedisLock 實例)的傳入 key 以及 request(api 看起來不是特別優雅)。
也能夠在每次使用鎖的時候 new 一個 RedisLock 傳入 key 以及 request,這樣卻是在解鎖時很方便。可是須要自行管理 RedisLock 的實例。各有優劣吧。
項目源碼在:
歡迎討論。
在作這個項目的時候讓我不得不想提一下單測。
由於這個應用是強依賴於第三方組件的(Redis),可是在單測中咱們須要排除掉這種依賴。好比其餘夥伴 fork 了該項目想在本地跑一遍單測,結果運行不起來:
因此最好是要把這些外部不穩定的因素排除掉,單測只測咱們寫好的代碼。
因而就能夠引入單測利器 Mock
了。
它的想法很簡答,就是要把你所依賴的外部資源通通屏蔽掉。如:數據庫、外部接口、外部文件等等。
使用方式也挺簡單,能夠參考該項目的單測:
@Test
public void tryLock() throws Exception {
String key = "test";
String request = UUID.randomUUID().toString();
Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong())).thenReturn("OK");
boolean locktest = redisLock.tryLock(key, request);
System.out.println("locktest=" + locktest);
Assert.assertTrue(locktest);
//check
Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong());
}
複製代碼
這裏只是簡單演示下,能夠的話下次仔細分析分析。
它的原理其實也挺簡單,debug 的話能夠很直接的看出來:
這裏咱們所依賴的 JedisCluster 實際上是一個 cglib 代理對象
。因此也不難想到它是如何工做的。
好比這裏咱們須要用到 JedisCluster 的 set 函數並須要它的返回值。
Mock 就將該對象代理了,並在實際執行 set 方法後給你返回了一個你自定義的值。
這樣咱們就能夠爲所欲爲的測試了,徹底把外部依賴所屏蔽了。
至此一個基於 Redis 的分佈式鎖完成,可是依然有些問題。
感興趣的朋友還能夠參考 Redisson 的實現。
最近在總結一些 Java 相關的知識點,感興趣的朋友能夠一塊兒維護。