在目前的大型互聯(lián)網(wǎng)公司中茵乱,常見(jiàn)的分布式鎖的實(shí)現(xiàn)方式大概分為三種孟岛?
利用數(shù)據(jù)庫(kù)條件命中索引實(shí)現(xiàn)樂(lè)觀鎖
基于redis的分布式鎖
基于zookeeper的分布式鎖的實(shí)現(xiàn)
當(dāng)然分布式鎖的實(shí)現(xiàn)方式有很多,但是不管哪個(gè)其底層的設(shè)計(jì)原則以及思想都是一致斤贰,我們分析一下設(shè)計(jì)分布式鎖的幾個(gè)重點(diǎn)要素次询。
為了保證分布式鎖的完整實(shí)現(xiàn),大概需要滿足4個(gè)核心的步驟:
① 互斥原則 -->不管在任何的時(shí)間段內(nèi)渗蟹,只有一個(gè)用戶線程能夠占有其鎖的資源。
② 避免死鎖的發(fā)生 --> 即使說(shuō)有一個(gè)用戶線程在持有鎖的時(shí)候發(fā)生異常授艰,不能影響接下來(lái)的線程獲取鎖資源世落。
③ 容錯(cuò)率問(wèn)題 --> 這個(gè)主要針對(duì)是大型項(xiàng)目的集群節(jié)點(diǎn)問(wèn)題,意思就是只要1/2的節(jié)點(diǎn)能夠正常運(yùn)行的情況下谷朝,那么就能夠正常的加鎖和釋放鎖武花。
④ 解鈴還須系鈴人 --> 主要的意思就是A線程獲取的鎖資源只能由線程A去釋放此鎖資源,客戶端的鎖資源不能讓別人給釋放了体箕。
java實(shí)現(xiàn)redis分布式鎖
引入java針對(duì)redis開(kāi)發(fā)依賴(lài)
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
coding:實(shí)現(xiàn)細(xì)節(jié)
package com.test.redis;
import redis.clients.jedis.Jedis;
/**
* redis實(shí)現(xiàn)分布式鎖的具體實(shí)現(xiàn)
*/
public class RedisTemple {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 嘗試獲取分布式鎖
* @param jedis Redis客戶端
* @param lockKey 鎖資源的key值
* @param requestId 唯一資源用戶請(qǐng)求標(biāo)識(shí)
* @param expireTime 鎖員超期時(shí)間
* @return 是否獲取成功
*/
public static boolean tryGetLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
具體代碼分析:
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
這行代碼是嘗試獲取redis鎖的實(shí)現(xiàn)累铅,大家可以看到有5個(gè)參數(shù)
lockKey:設(shè)置當(dāng)前redis鎖資源的key值,key值在項(xiàng)目中要缺保唯一性
requestId:這個(gè)參數(shù)可能很多的人都有些迷惑了菇民,因?yàn)槠綍r(shí)我們?cè)谄髽I(yè)中的redis鎖的實(shí)現(xiàn),知識(shí)存放一個(gè)key值就可以了阔馋,但是這里為什么要一個(gè)value值呢娇掏?這個(gè)就談到了我們開(kāi)始時(shí)說(shuō)的“解鈴還須系鈴人” requestId我們可以采用 UUID.randomUUID().toString() 去做或者采用其他的業(yè)務(wù)參數(shù)也是可以的。
SET_IF_NOT_EXIST 這個(gè)參數(shù)是我們r(jià)edis所需要的參數(shù)NX驹碍,意思就是 SET IF NOT EXIST 當(dāng)key值存在的時(shí)候就不能set 不存在就set,這個(gè)確保了互斥的原則怔球。
SET_WITH_EXPIRE_TIME 這個(gè)參數(shù)也是我們操作redis需要的浮还,我們給的是PX,意思是我們要給key設(shè)置一個(gè)超時(shí)的事件钧舌,集體事件由下一個(gè)參數(shù) expireTime 決定。
錯(cuò)誤案例:
我們經(jīng)常能見(jiàn)到的一些錯(cuò)誤的redis鎖的實(shí)現(xiàn)案例崭歧。
就是用redis的jedis.setnx()和jedis.expire()組合實(shí)現(xiàn)加鎖和解鎖的操作
public static void getRedisLock(Jedis jedis, String lockKey, String requestId, int expireTime){
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 此段代碼可以看到針對(duì)redis的鎖實(shí)現(xiàn)不是原子操作撞牢,如果次處發(fā)生異常,那么我們給key加時(shí)間就會(huì)失敗所宰,因此發(fā)生死鎖
jedis.expire(lockKey, expireTime);
}
}
當(dāng)然還有很多的錯(cuò)誤的案例畜挥,在這里就不和大家一一的解析了,只要遵循對(duì)的設(shè)計(jì)原則就是安全的蟹但。
接下來(lái)我們看一下真正正確的操作是怎樣的!
private static final Long RELEASE_SUCCESS = 1L;
/**
* 釋放分布式鎖
* @param jedis 表示redis客戶端
* @param lockKey 鎖資源key值
* @param requestId 唯一性請(qǐng)求標(biāo)識(shí)
* @return 是否釋放成功
*/
public static boolean redisRsleaseLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
上面代碼可以分析到斟冕,redis鎖的釋放只需要兩行的代碼我們就可以搞定了缅阳,我們先定義一個(gè)Lua的腳本代碼,這個(gè)大家可以去網(wǎng)上百度一下很多的秀撇,其主要就是為了保證此步的操作是原子的操作向族。
錯(cuò)誤的解鎖案例
public static void redisRelieaseLock(Jedis jedis, String lockKey, String requestId){
// 判斷加鎖與解鎖是不是同一個(gè)客戶端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此時(shí),這把鎖突然不是這個(gè)客戶端的件相,則會(huì)誤解鎖
jedis.del(lockKey);
}
}
好了,在此和大家分享到這里泛范,大家用到時(shí)候可以參考一下紊撕,具體的細(xì)節(jié)進(jìn)行優(yōu)化即可在正規(guī)的項(xiàng)目中去操作了!
關(guān)注:編程如此簡(jiǎn)單 一起學(xué)習(xí)一起成長(zhǎng)