SpringBoot + Redisson實現分散式鎖
一、什麼是分散式?
要想說什麼是分散式,那麼首先要知道分散式之前的系統是什麼樣的架構,之前的架構又存在什麼樣的問題?
單體架構
分散式之前就是單體架構,單體架構顧名思義就是將所有的業務功能打包在一個應用中,然後部署在伺服器上。如果我們把單體架構比作一個汽車工廠,那麼從汽車發動機到汽車上的一個螺絲釘都需要由它來負責完成,如果有一天這家工廠由於自然災害的原因導致業務量急劇下滑,甚至停止生產,那麼這個整個工廠無論是造發動機的,還是造螺絲釘的全部都得停工。落實到網際網路就是單體架構開發成本高、出現故障影響的範圍大,很難適應現在的網際網路專案。分散式
既然單體架構不足以解決現在存在的高併發、高效能、高可用的要求,那麼分散式就來了,原來我們把一個汽車所有的零件都放在一家工廠進行生產,結果會出現上面停工的情況,那麼分散式怎麼解決的?所謂分散式就是把所有的零件分開生產,每個工廠根據自己的特長來生產相應的汽車配件,最後統一組裝,即使因為一些其他原因,導致某個工廠停止了生產,但是依舊不影響其他工廠的生產進度,我們可以通過小的代價來尋找其他工廠來代替停工的工廠,確保我們的生產任務可以正常的運轉。
二、為什麼要有分散式鎖?
如果沒有分散式鎖
解釋這個問題之前,我們先來說下,如果沒有分散式鎖會存在什麼問題,舉個經典的例子,如果我們有一個購物網站,有一件商品只有10件可賣,此時使用者A進來後還有1個庫存,然後使用者A下單付款,扣減庫存,在使用者A付款的這個過程中,恰好使用者B進來了,也看到還有一個庫存,使用者B也開始下單付款,扣減庫存,那麼這個過程很明顯已經出了問題,出現了超賣的問題。分散式鎖
如果有了分散式鎖就不會出現超賣問題,舉個簡單的例子,分散式就像是銀行的ATM,你去ATM取錢,如果你去了恰巧裡面有人,那麼你肯定是進不去的,必須等裡面的人出來之後你才能進去,同樣的,如果你正在取錢,如果外面還有人等著取錢,那麼無論如何他都必須等你取完出來,他才能進去,要麼他不取了回家去。
三、Redis實現的分散式鎖
簡單的介紹了分散式和分散式鎖,之後我們現在來看看如何實現一個分散式鎖,先看我之前使用Redis寫個一個分散式鎖,看看有什麼問題?
public Boolean lock(String key,Long waitTime,Long expireTime) {
String value = UUID.randomUUID().toString().replaceAll("-","").toLowerCase();
Boolean flag = setNx(key,value,expireTime,TimeUnit.SECONDS);
// 嘗試獲取鎖 成功返回
if (flag) {
return flag;
} else {
// 獲取失敗
// 現在時間
long newTime = System.currentTimeMillis();
// 等待過期時間
long loseTime = newTime + waitTime;
// 不斷嘗試獲取鎖成功返回 11 < 11
while (System.currentTimeMillis() < loseTime) {
Boolean testFlag = setNx(key,TimeUnit.MILLISECONDS);
if (testFlag) {
return testFlag;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
return false;
}
複製程式碼
首先嚐試獲取鎖,如果獲取到了同時設定過期時間(原子操作,防止死鎖),獲取到了直接返回,簡單看沒什麼問題,但是如果說,我設定了10秒的過期時間,但是我的業務執行了12秒,那麼此時其他執行緒也進來了,那麼這個鎖根本就沒有起到任何作用,注意:大家千萬不要說那我把過期時間設定為1分鐘設定10分鐘不就好了,千萬不要這麼幹,治標不治本。
其實辦法肯定是有的,在獲取到鎖之後,然後開一個守護執行緒,判斷當前執行緒時候執行結束,如果沒有那就重置鎖的過期時間。但是我們這裡就不說了,而是直接用別人封裝好的框架。
四、Redisson分散式鎖
1、首先引入maven
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.5</version>
</dependency>
複製程式碼
2、構建Redisson例項(基於SpringBoot)
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
複製程式碼
3、使用
其實單從使用來說是非常簡單的,這裡需要跟大家說一點,就是lock()這個方法,我們先看原始碼,這個方法是可以設定過期時間的,但是它並不會去進行檢查任務是否執行結束,如果任務沒有執行結束,然後鎖的過期時間到了,執行緒中斷,就會出現異常。
public void lock(long leaseTime,TimeUnit unit) {
try {
this.lock(leaseTime,unit,false);
} catch (InterruptedException var5) {
throw new IllegalStateException();
}
}
複製程式碼
異常資訊
4、較為完善的方法,lock()
這個方法不需要傳遞任何引數,它的底層方法會通過我們剛才說的原理,去校驗當前任務是否執行結束,如果沒有執行結束,那麼相應的就會延長鎖的過期時間。
底層實現
private void lock(long leaseTime,TimeUnit unit,boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = this.tryAcquire(leaseTime,threadId);
if (ttl != null) {
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
this.commandExecutor.syncSubscription(future);
try {
// 校驗任務是否執行結束
while(true) {
ttl = this.tryAcquire(leaseTime,threadId);
if (ttl == null) {
return;
}
if (ttl >= 0L) {
try {
this.getEntry(threadId).getLatch().tryAcquire(ttl,TimeUnit.MILLISECONDS);
} catch (InterruptedException var13) {
if (interruptibly) {
throw var13;
}
this.getEntry(threadId).getLatch().tryAcquire(ttl,TimeUnit.MILLISECONDS);
}
} else if (interruptibly) {
this.getEntry(threadId).getLatch().acquire();
} else {
this.getEntry(threadId).getLatch().acquireUninterruptibly();
}
}
} finally {
this.unsubscribe(future,threadId);
}
}
}
複製程式碼
其實Redisson還有其他好多的方法來解決現在網際網路中的好多問題,但是我這裡就不一一介紹了,大家如果想了解更多的東西,可以去Redisson官網。