首頁 > 軟體

使用Redis解決高並行方案及思路解讀

2023-04-02 06:02:49

NoSQL

Not Only SQL的簡稱。NoSQL是解決傳統的RDBMS在應對某些問題時比較乏力而提出的。

即非關係型資料庫,它們不保證關係資料的ACID特性,資料之間一般沒有關聯,在擴充套件上就非常容易實現,並且擁有較高的效能。

Redis

redis是nosql的典型代表,也是目前網際網路公司的必用技術。

redis是鍵值(Key-Value)儲存資料庫,主要會使用到雜湊表。大多數時候是直接以快取的形式被使用,使得請求不直接存取到磁碟,所以效率方面是很不錯的,完全能滿足中小型企業的使用需求。

常用資料型別

  • 字串string
  • 雜湊hash
  • 列表list
  • 集合sets
  • 有序集合sort set

使用頻率上string和hash會高一些,各個型別有各自的操作命令,無非增刪改查,具體的命令後面我會整理一份。

痛點

web應用在眾多請求同時發生時,可能會導致資料讀取、儲存上出現錯誤,即發生髒讀、髒資料生成。

在分散式專案下,會出現更多的問題。

思路

並行時,本質其實就是多個請求同時進來了,沒辦法正確的去進行處理。

可以將所有的請求放在 一個佇列,讓請求們按照一個順序,挨個進來執行業務邏輯。目前成熟的解決方案就是使用訊息佇列,下次我會整理一篇訊息佇列處理高並行的;

還有一個方法是直接將並行轉為序列,Java提供了synchronized,即同步,不過這個在效率要求比較苛刻的地方 或者 分散式專案下還是不太合適的方案,這裡就引出了使用redis來實現分散式鎖,從而解決並行問題。

分散式鎖

在分散式專案中,使用一個唯一、通用、效率高的標識,來表示上鎖和解鎖。

redis實現起來很簡單,即對一個key是否存在來表示是否上鎖、是否解鎖。

以string型別舉例:

Integer stock = goodsMapper.getStock();
if (stock > 0) {
    stock =- 1;
    goodsMapper.updateStock(stock);
}

以上是最簡單的秒殺虛擬碼,我們嘗試用redis實現分散式鎖。

// 這裡是錯誤程式碼,只是一個思考過程,請耐心看完哦
String key = "REDIS_DISTRIBUTION_LOCKER"; // 分散式鎖名稱
String value = jedisUtils.get(key);
if (value != null) { // 未上鎖
    // wingzingliu
    jedisUtils.set(key, 1); // 上鎖
    Integer stock = goodsMapper.getStock();
    if (stock > 0) {
        stock =- 1;
        goodsMapper.updateStock(stock);
        jedisUtils.del(key); // 釋放鎖
    }
}

以上程式碼可能會出現一個問題,就是當同時多個請求進來,某次多個請求都拿到value為空,執行緒A進入if 走到// wingzingliu這裡的時候,還未上鎖,其他請求也進來了,這樣就會出現髒資料了。

這裡的程式碼問題就是出在沒有考慮原子性問題。

所以我們要使用到redis的一個setNx命令,本質也是設定值,但是這是一個原子操作,執行之後會返回是否設定成功。

redis> SETNX job "programmer"    # job 設定成功
(integer) 1
 
redis> SETNX job "code-farmer"   # 嘗試覆蓋 job ,失敗
(integer) 0
 
redis> GET job                   # 沒有被覆蓋
"programmer"

重點關注 當有值時,會失敗,返回0。所以我們的程式碼會改造成以下這個樣子。

// 這裡是錯誤程式碼,只是一個思考過程,請耐心看完哦
String key = "REDIS_DISTRIBUTION_LOCKER"; // 分散式鎖名稱
Long result = jedisUtils.setNx(key, 1);
if (result > 0) { // 上鎖成功,進入邏輯
    // wingzingliu1
    Integer stock = goodsMapper.getStock();
    if (stock > 0) {
        stock =- 1;
        goodsMapper.updateStock(stock);
 
        System.out.println("購買成功!");
    } else {
        System.out.println("沒有庫存了!");
    }
    // wingzingliu2
    jedisUtils.del(key); // 釋放鎖
}

以上我們就可以保證原子性,能正確的按照順序去處理。

可是還有一個隱藏的問題,就是當某個執行緒執行上鎖成功後,在wingzingliu1到wingzingliu2之間時,程式拋異常了,那麼程式終止了,就無法釋放鎖,其他執行緒也都進不來了。

解決方案是加上try catch finally塊,在finally裡面去釋放鎖。

可是那如果是宕機呢?上鎖之後宕機了,finally裡面的依然不會執行,鎖沒有得到釋放,不手動處理的情況下,以後所有執行緒也無法進入。

所以引入了redis的過期時間,到了某個時間自動解鎖。

// 這裡是不夠完善的程式碼,請耐心看完哦
try {
    String key = "REDIS_DISTRIBUTION_LOCKER"; // 分散式鎖名稱
    Long result = jedisUtils.setNx(key, 1, 30); // 假設處理邏輯需要20s左右,設定了30秒自動過期
    if (result > 0) { // 上鎖成功,進入邏輯
        Integer stock = goodsMapper.getStock();
        if (stock > 0) {
            stock =- 1;
            goodsMapper.updateStock(stock);
 
            System.out.println("購買成功!");
        } else {
            System.out.println("沒有庫存了!");
        }
    }
} catch (Exception e) {
    
} finally {
    jedisUtils.del(key); // 釋放鎖
}

以上是比較完善的分散式鎖了,但是還有一個小瑕疵,就是假設某一次請求A處理的很慢,預計20s但是跑了35s,到了30s的時候鎖過期了,其他請求就自然進來了。

這不僅僅會導致一次並行,當請求A處理完時,依然會執行釋放鎖,這實際上是下一個執行緒上的鎖。以此類推,整個並行控制就亂了。

理論上可以設定一個更大的key過期時間,但是並不是最好的解決方案。這裡就引出一個概念:鎖續命。

鎖續命

如其名,給鎖續命。實現就是 當鎖快過期的時候,去延長鎖的時間。假設一個30s的鎖,每個10s去檢測一下,鎖是否還在 如果在就重新延長至30s。這樣就避免掉了上面的這個可能出現的問題。

這裡使用一個定時任務,週期性的呼叫即可。

擴充套件

剛剛對key設定的value是1,其實能使用請求ID來進行儲存,這樣就能知道鎖是由哪個請求上的,在解鎖的時候 也可以避免解鎖了其他執行緒上的鎖。具體由前端傳遞,或者由伺服器端以某種規則生成都可以。

結語

至此我們就使用redis,一步一步的解決了在分散式專案下的並行問題。redis不是唯一的解決方案,但是對於大部分網際網路公司來說,是一個很成熟、效能不錯、便捷的方案。

還可以使用synchronized(非分散式專案)、mq 、zookeeper等方案去實現分散式鎖 以 解決高並行問題。

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com