今天是五一,決定不出去,在家裏擼代碼,今天學習redis,因而準備寫個基於redis的分佈式鎖。因爲本人屬於菜鳥級別,在寫的過程當中遇到各類問題,功夫不負有心人,終於搞定,若是發現實現有問題,歡迎指導,感謝.git
在單機時代,雖然不須要分佈式鎖,但也面臨過相似的問題,只不過在單機的狀況下,若是有多個線程要同時訪問某個共享資源的時候,咱們能夠採用線程間加鎖的機制,即當某個線程獲取到這個資源後,就當即對這個資源進行加鎖,當使用完資源以後,再釋放鎖,其它線程就能夠接着使用了。JAVA中已提供相關工具類。可是到了分佈式系統的時代,這種線程或者進程之間的鎖機制,就可能沒做用了,系統可能會有多份而且部署在不一樣的機器上,這些資源已經不是在線程之間共享了,而是屬於進程之間共享的資源。所以,爲了解決這個問題,咱們就必須引入「分佈式鎖」。github
分佈式鎖,是指在分佈式的部署環境下,經過鎖機制來讓多客戶端互斥的對共享資源進行訪問。web
通常分佈式鎖要知足一下幾點要求:redis
目前主流的分佈式鎖實現主要有如下幾種數據庫
今天主要將基於redis實現分佈式鎖緩存
緩存過時安全
緩存能夠設置過時時間,redis根據時間自動進行清理。併發
setNx命令dom
將 key 的值設爲 value ,當且僅當 key 不存在。
若給定的 key 已經存在,則 SETNX 不作任何動做。
SETNX 是『SET if Not eXists』(若是不存在,則 SET)的簡寫。
複製代碼
lua腳本編輯器
腳本語言,用於支持redis原子操做。
熟悉以上redis知識,實現redis分佈式鎖比較容易了。
緩存過時
最好給加鎖的key設置緩存過時時間,能夠有效的防止死鎖,好比某個進程加鎖後沒來得及釋放鎖,宕機,說來負責釋放鎖?
set值
加鎖時,在redis中保存在各節點中惟一的值,防止不一樣進程誤解鎖
好比serviceA已經在redis中加鎖lock,通常serviceA執行時間爲1秒,則設置緩存過時時間2秒,某天因爲機器緣由serviceA執行了3秒,那麼對應的鎖已經失效,此期間B去加鎖,並加鎖成功, serviceA執行完會釋放鎖,致使serviceA會將B加的鎖釋放,因此產生誤刪鎖,採用惟一值,避免這種狀況產生。刪鎖會檢查值,若是加鎖與解鎖的值不相同則不容許解鎖。
一、利用SETNX命令加鎖
public static String set(String key, String value, long timeout) {
Jedis jedis = getJedis();
try {
String ret = jedis.set(key, value, "NX", "PX", timeout);
return ret;
} finally {
close(jedis);
}
}
複製代碼
二、實現阻塞加鎖和非阻塞加鎖
/** * 阻塞加鎖 */
public void lock() {
if (tryLock()) {
return;
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//lock
lock();
} /** * 基於setNx實現非阻塞鎖 * * @return */ public boolean tryLock() { String uuid = UUID.randomUUID().toString(); String ret = JedisUtis.set(LOCK_KEY, uuid, DEFAULT_TIME_OUT); if ("OK".equals(ret)) { //lock success LOCAL.set(uuid); return true; } return false; } public boolean tryLock(long time, TimeUnit unit){ String uuid = UUID.randomUUID().toString(); String ret = JedisUtis.set(LOCK_KEY, uuid, unit.toMillis(time)); if ("ok".equals(ret)) { LOCAL.set(uuid); //lock success return true; } return false; } 複製代碼
三、解鎖
解鎖的同時須要去檢查值是否與加鎖的值相同,不相同則不容許解鎖,這裏是經過ThreadLocal傳加鎖產生的uuid
/** * unlock * 執行lua腳本,保證原子性 */
public void unlock() {
release();
}
private void release() { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; JedisUtis.remove(LOCK_KEY, script, LOCAL.get()); } 複製代碼
12306售票是併發學習中經典案例,仍是拿這個舉例,好比有100 tickets,有多個售票窗口同時售票,怎麼保證不被重複賣
/** * 線程不安全示例 * * @author Qi.qingshan * @date 2020/5/1 */
public class SaleTicket implements Runnable {
private int tickets = 100; public void run() { for (; ; ) { sale(); if (tickets < 0) break; } } } /** * 售票 */ private void sale() { if (tickets > 0) { tickets--; System.out.println(Thread.currentThread().getName() + " - 在售第" + (100 - tickets) + "票 :: 剩餘" + (tickets)); } try { Thread.sleep(100); } catch (InterruptedException e) { } } } 複製代碼
測試類
/** * @author Qi.qingshan * @date 2020/5/1 */
public class SaleTicketTest {
BlockingDeque queue = new LinkedBlockingDeque(100); private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 50, 100, TimeUnit.SECONDS, queue); @Test public void testSaleTickets() throws IOException { SaleTicket saleTicket = new SaleTicket(); executor.execute(new Thread(saleTicket, "售票員001")); executor.execute(new Thread(saleTicket, "售票員002")); executor.execute(new Thread(saleTicket, "售票員003")); executor.execute(new Thread(saleTicket, "售票員004")); System.in.read(); } } 複製代碼
存在重複售票狀況,改用redisLock,調整核心代碼
public void run() {
for (; ; ) {
lock.lock();
try {
sale();
if (tickets < 0) break;
} finally {
lock.unlock();
}
}
}
複製代碼
執行結果以下
完整代碼已上傳github.com/qiqsa/distr…