自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

面試官:Redis 分布式鎖如何實現(xiàn),存在哪些問題?Redlock 算法的核心思想是什么?Redis 事務(wù)原理怎樣,是否支持回滾

數(shù)據(jù)庫 Redis
Redis分布式鎖的實現(xiàn)原理主要依賴于Redis的原子指令來確保在同一時刻只有一個進(jìn)程(或線程)能夠獲取鎖。

面試官:Redis分布式鎖如何實現(xiàn),為什么推薦使用 SET命令而不是 SETNX命令?使用過程中還存在哪些問題?

Redis分布式鎖的實現(xiàn)原理主要依賴于Redis的原子指令來確保在同一時刻只有一個進(jìn)程(或線程)能夠獲取鎖。

1. Redis分布式鎖的實現(xiàn)原理

Redis分布式鎖的實現(xiàn)通常涉及以下幾個關(guān)鍵步驟:

  • 嘗試獲取鎖:客戶端使用Redis的某個命令嘗試設(shè)置一個特定的鍵值對,這個鍵通常代表鎖,而值可以是客戶端的唯一標(biāo)識符或隨機生成的字符串。如果設(shè)置成功,則表示客戶端獲取到了鎖;如果設(shè)置失敗(因為鍵已存在),則表示鎖已被其他客戶端占用。
  • 處理業(yè)務(wù)邏輯:在獲取到鎖之后,客戶端可以安全地處理需要同步的業(yè)務(wù)邏輯。
  • 釋放鎖:處理完業(yè)務(wù)邏輯后,客戶端需要釋放鎖,這通常是通過刪除之前設(shè)置的鍵值對來實現(xiàn)的。

在實現(xiàn)過程中,為了確保鎖的安全性和可靠性,需要考慮以下幾點:

  • 鎖的過期時間:為了防止死鎖,必須為鎖設(shè)置一個合理的過期時間。這樣,即使持有鎖的客戶端出現(xiàn)異?;虮罎?,鎖也不會被永久占用。
  • 鎖的原子性釋放:釋放鎖時,必須確保操作的原子性,以防止在檢查鎖持有者和刪除鎖之間發(fā)生競態(tài)條件。這通常通過使用Lua腳本來實現(xiàn)。

偽代碼如下:

try{
  String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
      return true;
  }
  return false;
} finally {
    unlock(lockKey);
}

2. 使用SET命令而非SETNX實現(xiàn)分布式鎖的原因

雖然SETNX命令也可以用于實現(xiàn)分布式鎖(當(dāng)鍵不存在時設(shè)置鍵值對),但推薦使用SET命令的原因如下:

SET命令支持NX(Not Exist,僅當(dāng)鍵不存在時設(shè)置)和PX(expire time in milliseconds,設(shè)置鍵的過期時間,單位為毫秒)選項,這使得客戶端可以在一個原子操作中完成獲取鎖和設(shè)置過期時間的兩個步驟。而SETNX命令則需要與EXPIRE命令配合使用,這增加了操作的復(fù)雜性和出錯的可能性。

if (jedis.setnx(lockKey, val) == 1) {
   jedis.expire(lockKey, timeout);
}

上述代碼使用SETNX命令時,如果客戶端在獲取鎖后崩潰或網(wǎng)絡(luò)中斷,而未能及時執(zhí)行EXPIRE命令設(shè)置過期時間,那么鎖可能會被永久占用,導(dǎo)致死鎖問題。

假如在高并發(fā)場景中,有大量的lockKey加鎖成功了,但不會失效,有可能直接導(dǎo)致redis內(nèi)存空間不足。而使用SET命令的NX和PX選項則可以在獲取鎖的同時設(shè)置過期時間,從而避免了這一問題。

使用redis分布式鎖的過程中還可能存在如下問題:

(1) A線程釋放了B線程的鎖

假如線程A加鎖成功了,鎖的名字叫做lockKey,但是由于業(yè)務(wù)功能耗時時間很長,超過了設(shè)置的超時時間,這時候redis會自動釋放鎖。此時,并發(fā)的線程B就會給lockKey加鎖成功了,執(zhí)行它的業(yè)務(wù)操作。恰好這個時候,線程A執(zhí)行完了業(yè)務(wù)功能,并且A在finally方法中釋放了鎖lockKey,導(dǎo)致線程B的鎖,被線程A釋放了。

這個時候如果線程C再獲取鎖就會獲取成功,而實際上B還沒有結(jié)束它在臨界區(qū)的任務(wù)。

解決方法:

① 使用唯一標(biāo)識

在獲取鎖時,每個進(jìn)程或線程都應(yīng)該使用一個唯一標(biāo)識(如UUID)來標(biāo)記鎖。這個唯一標(biāo)識將存儲在Redis中作為鎖的value值。在釋放鎖時,進(jìn)程或線程需要先檢查Redis中鎖的值是否與自己的唯一標(biāo)識匹配,如果匹配則釋放鎖。

if (jedis.get(lockKey).equals(requestId)) {
    jedis.del(lockKey);
    return true;
}
return false;

② 使用Lua腳本

Lua腳本是Redis提供的一種在服務(wù)器端執(zhí)行多個命令的方式。通過將“檢查鎖值”和“刪除鎖”這兩個操作封裝在一個Lua腳本中,可以確保這兩個操作是原子的,即它們要么同時成功,要么同時失敗。這樣可以避免在并發(fā)情況下出現(xiàn)釋放了別人的鎖的問題。

③ 合理設(shè)置鎖的過期時間

鎖的過期時間應(yīng)該根據(jù)任務(wù)的預(yù)期執(zhí)行時間來合理設(shè)置。如果任務(wù)執(zhí)行時間過長,而鎖的過期時間設(shè)置得過短,那么鎖可能會在任務(wù)完成之前自動失效。

(2) 并發(fā)時出現(xiàn)大量失敗請求

問題描述:

在并發(fā)環(huán)境下,多個進(jìn)程或線程可能會同時嘗試獲取同一個鎖來訪問共享資源。如果有1萬的請求同時去競爭那把鎖,可能只有一個請求是成功的,其余的9999個請求都會失敗。

解決方法:

引入重試機制:在并發(fā)環(huán)境下,為了處理這些失敗請求,可以引入重試機制,即進(jìn)程或線程當(dāng)獲取鎖失敗時,可以等待一段時間后再嘗試獲取鎖。通過合理設(shè)置重試次數(shù)和重試間隔,可以在一定程度上提高并發(fā)請求的成功率,而且不同線程或進(jìn)程之間的等待時間應(yīng)該使用隨機時間,以免線程同時被喚醒并競爭鎖。

try {
  Long start = System.currentTimeMillis();
  while(true) {
     String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
     if ("OK".equals(result)) {
        // do something
        return true;
     }
     long time = System.currentTimeMillis() - start;
      if (time>=timeout) {
          return false;
      }
      try {
          int randomSleepTime = 10 + random.nextInt(90);
          Thread.sleep(randomSleepTime);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  }
} finally{
    unlock(lockKey,requestId);
}  
return false;

面試官:使用過 Redisson 么?它是如何實現(xiàn)分布式鎖的?解釋一下Redisson的watch dog機制?你還知道Redisson的哪些鎖?

Redisson是一個在Redis基礎(chǔ)上實現(xiàn)的Java駐內(nèi)存數(shù)據(jù)網(wǎng)格(In-Memory Data Grid,IMDG),它不僅提供了對分布式和可伸縮數(shù)據(jù)結(jié)構(gòu)的支持,還提供了多種分布式服務(wù),包括但不限于分布式鎖、集合、映射、計數(shù)器、發(fā)布/訂閱消息等。

Redisson實現(xiàn)分布式鎖主要依賴于Redis的相關(guān)命令和Lua腳本的原子性操作。以下是Redisson分布式鎖實現(xiàn)的關(guān)鍵點:

  • Redis的SETNX命令:Redisson的分布式鎖實現(xiàn)基于Redis的SETNX(SET if Not eXists)命令。當(dāng)一個節(jié)點試圖獲取鎖時,它會使用SETNX命令在Redis中設(shè)置一個特定的鍵值對。如果鍵不存在(表示鎖未被占用),則設(shè)置成功,該節(jié)點獲取鎖;如果鍵已經(jīng)存在(表示鎖已被其他節(jié)點占用),則設(shè)置失敗,該節(jié)點無法獲取鎖。
  • 加鎖機制:通過Lua腳本實現(xiàn)原子性加鎖操作,確保在多個客戶端同時請求鎖時,只有一個客戶端能夠成功獲取鎖。Lua腳本的原子性保證了在執(zhí)行過程中不會被其他命令打斷,從而避免了競態(tài)條件。
  • 鎖互斥機制:利用Redis的數(shù)據(jù)結(jié)構(gòu)(如哈希表)和唯一性標(biāo)識(如UUID+threadId)來確保鎖的互斥性。每個獲取鎖的客戶端都會生成一個唯一的標(biāo)識,并將其與鎖關(guān)聯(lián)起來。在釋放鎖時,只有持有該唯一標(biāo)識的客戶端才能成功釋放鎖。
  • 鎖續(xù)期機制:通過看門狗(Watchdog)定時任務(wù)自動續(xù)鎖,防止鎖因超時而被其他客戶端獲取??撮T狗會定期檢查鎖的狀態(tài),如果鎖仍然被持有,則將其有效期重置為一個較長的值。這樣,即使業(yè)務(wù)操作需要較長時間,鎖也不會因為超時而被釋放。需要注意的是,當(dāng)業(yè)務(wù)操作完成后,需要手動釋放鎖以避免資源泄露。
  • 可重入加鎖機制:允許同一個客戶端在同一個線程中多次獲取同一個鎖,而不會導(dǎo)致死鎖。Redisson使用Hash結(jié)構(gòu)來存儲線程信息和重入次數(shù),從而實現(xiàn)了可重入鎖的功能。
  • 鎖釋放機制:在釋放鎖時,需要驗證鎖的持有者身份,確保只有鎖的持有者才能釋放鎖。這是通過比較釋放鎖的客戶端的唯一標(biāo)識與鎖關(guān)聯(lián)的唯一標(biāo)識來實現(xiàn)的。如果匹配成功,則釋放鎖;否則,釋放操作將失敗。

1. Watch Dog機制

只要線程一加鎖成功,就會啟動一個 watch dog 看門狗,它是一個后臺線程,會每隔 10 秒檢查一下,如果線程 1 還持有鎖,那么就會不斷的延長鎖 key 的生存時間。因此,Redisson 就是使用 watch dog機制解決了鎖過期釋放,業(yè)務(wù)沒執(zhí)行完問題。

2. Redisson可重入鎖

Redisson分布式鎖默認(rèn)是可重入鎖,它允許同一個線程在獲得鎖之后,如果再次嘗試獲得該鎖,能夠成功獲取而不會進(jìn)入死鎖狀態(tài)。這種特性使得同一個線程可以安全地在多個方法或代碼塊中使用鎖。

以下是對Redisson可重入鎖的詳細(xì)介紹以及代碼示例:

可重入性:

  • 當(dāng)一個線程已經(jīng)持有鎖時,它可以多次重入該鎖,而不會被阻塞。Redisson通過遞增鎖的持有計數(shù)來實現(xiàn)這一功能。
  • 第一次加鎖時,Redisson會在Redis中設(shè)置一個鍵(例如鎖的標(biāo)識),并將該線程的唯一標(biāo)識符(如UUID)作為鎖的值。
  • 如果同一個線程再次請求同一個鎖,Redisson會檢查Redis中該鎖的值。如果值與當(dāng)前線程的UUID相同,即當(dāng)前線程已經(jīng)持有該鎖,那么Redisson會遞增持有計數(shù)。

釋放鎖:

  • 當(dāng)線程釋放鎖時,Redisson會檢查當(dāng)前線程的持有計數(shù)。
  • 如果持有計數(shù)大于1,則將計數(shù)遞減而不是刪除鎖。
  • 當(dāng)持有計數(shù)減為0時,鎖會被刪除,表示該線程完全釋放了鎖。

自動過期解鎖:

  • Redisson的可重入鎖還支持自動過期解鎖功能,即可以在加鎖時指定一個過期時間。如果超過這個時間鎖還沒有被釋放,那么鎖會自動被刪除,從而避免死鎖的發(fā)生。

以下是一個使用Redisson可重入鎖的Java代碼示例:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonReentrantLockExample {
    public static void main(String[] args) {
        // 配置Redisson客戶端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redissonClient = Redisson.create(config);
        // 獲取可重入鎖對象
        RLock lock = redissonClient.getLock("myLock");
        try {
            // 第一次加鎖
            lock.lock();
            System.out.println("第1次加鎖成功");
            // 執(zhí)行需要同步的代碼塊
            // ...
            // 第二次加鎖(同一線程)
            lock.lock();
            System.out.println("第2次加鎖成功");
            // 執(zhí)行需要同步的代碼塊
            // ...
            // 第一次釋放鎖
            lock.unlock();
            System.out.println("第1次釋放鎖");
            // 執(zhí)行其他代碼
            // ...
            // 第二次釋放鎖
            lock.unlock();
            System.out.println("第2次釋放鎖,鎖被完全釋放");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 確保在程序結(jié)束時關(guān)閉Redisson客戶端
            redissonClient.shutdown();
        }
    }
}

3. Redisson公平鎖

Redisson的公平鎖是一種基于Redis實現(xiàn)的分布式公平鎖,它確保所有線程按照請求鎖的順序來獲得鎖,從而避免了某些線程長時間占用資源而導(dǎo)致其他線程一直無法獲得鎖的問題。與Redisson的可重入鎖相比,公平鎖增加了一個公平的機制,使得鎖的分配更加公平和有序。

  • 請求隊列:Redisson的公平鎖在Redis中維護(hù)了一個請求隊列,所有請求鎖的線程都會在這個隊列中排隊。
  • 鎖分配:當(dāng)鎖被釋放時,Redisson會檢查請求隊列中的線程,并按照請求的順序?qū)㈡i分配給隊列中的第一個線程。

以下是一個使用Redisson公平鎖的Java代碼示例:

import org.redisson.Redisson;
import org.redisson.api.RFairLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonFairLockExample {
    public static void main(String[] args) {
        // 配置Redisson客戶端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redissonClient = Redisson.create(config);
        // 獲取公平鎖對象
        RFairLock fairLock = redissonClient.getFairLock("myFairLock");
        // 模擬多個線程競爭鎖
        Runnable task = () -> {
            try {
                // 請求鎖
                fairLock.lock();
                try {
                    // 獲得鎖后執(zhí)行的代碼
                    System.out.println(Thread.currentThread().getName() + " 獲得了鎖");
                    // 模擬任務(wù)執(zhí)行時間
                    Thread.sleep(2000);
                } finally {
                    // 釋放鎖
                    fairLock.unlock();
                    System.out.println(Thread.currentThread().getName() + " 釋放了鎖");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        };
        // 創(chuàng)建并啟動多個線程
        for (int i = 0; i < 5; i++) {
            new Thread(task, "Thread-" + i).start();
        }
        // 確保在程序結(jié)束時關(guān)閉Redisson客戶端
        Runtime.getRuntime().addShutdownHook(new Thread(() -> redissonClient.shutdown()));
    }
}

4. Redisson讀寫鎖

Redisson的讀寫鎖(ReadWriteLock)是一種允許多個讀線程同時訪問共享資源,但寫線程獨占訪問的鎖機制。這種鎖在讀取操作遠(yuǎn)多于寫入操作的場景下特別有用,因為它可以提高并發(fā)性能,同時確保數(shù)據(jù)的一致性。

Redisson的讀寫鎖實現(xiàn)了java.util.concurrent.locks.ReadWriteLock接口,因此它提供了與Java標(biāo)準(zhǔn)庫中的讀寫鎖相似的API。Redisson的讀寫鎖是基于Redis的分布式實現(xiàn),這意味著它可以在分布式系統(tǒng)中工作,確保多個服務(wù)實例之間的讀寫同步。

以下是一個使用Redisson讀寫鎖的示例代碼:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.RWLock;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedissonReadWriteLockExample {
    public static void main(String[] args) {
        // 配置Redisson客戶端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        // 創(chuàng)建Redisson客戶端
        RedissonClient redissonClient = Redisson.create(config);
        // 獲取讀寫鎖
        RWLock rwLock = redissonClient.getReadWriteLock("myReadWriteLock");
        // 讀取操作示例
        new Thread(() -> {
            RLock readLock = rwLock.readLock();
            try {
                // 獲取讀鎖
                readLock.lock();
                // 執(zhí)行讀取操作
                System.out.println("Read operation started");
                // 模擬讀取操作耗時
                TimeUnit.SECONDS.sleep(2);
                System.out.println("Read operation completed");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                // 釋放讀鎖
                readLock.unlock();
            }
        }).start();
        // 寫入操作示例
        new Thread(() -> {
            RLock writeLock = rwLock.writeLock();
            try {
                // 獲取寫鎖
                writeLock.lock();
                // 執(zhí)行寫入操作
                System.out.println("Write operation started");
                // 模擬寫入操作耗時
                TimeUnit.SECONDS.sleep(2);
                System.out.println("Write operation completed");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                // 釋放寫鎖
                writeLock.unlock();
            }
        }).start();
    }
}

5. Redisson閉鎖

Redisson的閉鎖(CountDownLatch)是一種同步輔助類,在完成一組正在其他線程中執(zhí)行的操作之前,它允許一個或多個線程等待。這個機制特別適用于等待直到一組并行操作全部完成時才繼續(xù)執(zhí)行的場景。Redisson的閉鎖實現(xiàn)了java.util.concurrent.CountDownLatch接口,但它是在分布式環(huán)境中工作的,這意味著它可以在多個服務(wù)實例之間協(xié)調(diào)等待/通知操作。

以下是一個使用Redisson閉鎖的示例代碼:

import org.redisson.Redisson;
import org.redisson.api.RCountDownLatch;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonCountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        // 配置Redisson客戶端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        // 創(chuàng)建Redisson客戶端
        RedissonClient redissonClient = Redisson.create(config);
        // 創(chuàng)建一個計數(shù)為3的閉鎖
        RCountDownLatch latch = redissonClient.getCountDownLatch("myCountDownLatch");
        latch.trySetCount(3);
        // 啟動三個線程,每個線程在完成工作時減少閉鎖的計數(shù)
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    // 模擬工作耗時
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " - Work done, count down.");
                    // 減少閉鎖的計數(shù)
                    latch.countDown();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }
        // 等待直到閉鎖的計數(shù)達(dá)到0
        System.out.println("Main thread - Waiting for all threads to complete.");
        latch.await();
        System.out.println("Main thread - All threads have completed.");
        // 關(guān)閉Redisson客戶端
        redissonClient.shutdown();
    }
}

面試官:什么是Redlock算法,說說它的核心思想?

Redis 一般都是集群部署的,假設(shè)數(shù)據(jù)在主從同步過程,主節(jié)點掛了,Redis分布式鎖就可能會失效。

如果線程一在 Redis 的 master 節(jié)點上拿到了鎖,但是加鎖的 key 還沒同步到slave 節(jié)點。恰好這時,master 節(jié)點發(fā)生故障,一個 slave 節(jié)點就會升級為master 節(jié)點。線程二就可以獲取同個 key 的鎖啦,但線程一也已經(jīng)拿到鎖了,鎖的安全性就沒了。

為了解決這個問題,Redis 作者 antirez 提出一種高級的分布式鎖算法:Redlock。

Redlock 核心思想是這樣的:搞多個 Redis master 部署,以保證它們不會同時宕掉。并且這些 master 節(jié)點是完全相互獨立的,相互之間不存在數(shù)據(jù)同步。同時,需要確保在這多個master 實例上,是與在 Redis 單實例使用相同方法來獲取和釋放鎖。

我們假設(shè)當(dāng)前有 5 個 Redis master 節(jié)點,在 5 臺服務(wù)器上面運行這些 Redis實例。

RedLock 的實現(xiàn)步驟如下:

  • 獲取當(dāng)前時間,以毫秒為單位。
  • 按順序向 5 個 master 節(jié)點請求加鎖??蛻舳嗽O(shè)置網(wǎng)絡(luò)連接和響應(yīng)超時時間,并且超時時間要小于鎖的失效時間。(假設(shè)鎖自動失效時間為 10 秒,則超時時間一般在 5-50 毫秒之間,我們就假設(shè)超時時間是 50ms 吧)。如果超時,跳過該 master 節(jié)點,盡快去嘗試下一個 master 節(jié)點。
  • 客戶端使用當(dāng)前時間減去開始獲取鎖時間(即步驟 1 記錄的時間),得到獲取鎖使用的時間。當(dāng)且僅當(dāng)超過一半(N/2+1,這里是 5/2+1=3 個節(jié)點)的Redis master 節(jié)點都獲得鎖,并且使用的時間小于鎖失效時間時,鎖才算獲取成功。(如上圖,10s> 30ms+40ms+50ms+4m0s+50ms)

如果取到了鎖,key 的真正有效時間就變啦,需要減去獲取鎖所使用的時間。如果獲取鎖失?。]有在至少 N/2+1 個 master 實例取到鎖,有或者獲取鎖時間已經(jīng)超過了有效時間),客戶端要在所有的 master 節(jié)點上解鎖(即便有些 master 節(jié)點根本就沒有加鎖成功,也需要解鎖,以防止有些漏網(wǎng)之魚)。

Redlock算法的優(yōu)勢包括:

  • 安全性:通過在多個Redis節(jié)點上嘗試獲取鎖來提高安全性。
  • 高可用性:即使在部分Redis實例不可用或網(wǎng)絡(luò)分割的情況下,也能保證鎖的正確性和系統(tǒng)的可用性。
  • 防止單點故障:大大降低了單點故障對分布式系統(tǒng)的影響。

Redlock算法的劣勢:

  • 部署成本:需要維護(hù)多個Redis實例,增加了部署成本。
  • 網(wǎng)絡(luò)延遲:加鎖和釋放鎖的操作需要同時與多個Redis實例通信,可能導(dǎo)致較高的網(wǎng)絡(luò)延遲。
  • 時鐘漂移:Redlock算法假設(shè)Redis節(jié)點的時鐘一致。如果節(jié)點時鐘漂移較大,可能導(dǎo)致鎖的有效期計算錯誤。

面試官:Redis事務(wù)的原理是怎樣的,如何使用?Redis事務(wù)是否支持回滾?Redis事務(wù)的隔離性、原子性、持久性和一致性是如何實現(xiàn)的?

Redis事務(wù)主要包含以下幾個命令和階段:

  • MULTI:該命令用于開啟一個事務(wù),它標(biāo)志著執(zhí)行該命令的客戶端從非事務(wù)狀態(tài)切換至事務(wù)狀態(tài)。在MULTI命令之后,客戶端發(fā)送的命令不會被立即執(zhí)行,而是被放入一個事務(wù)隊列中等待執(zhí)行。
  • 命令入隊:在MULTI命令開啟事務(wù)后,客戶端發(fā)送的命令會被放入事務(wù)隊列中,而不是立即執(zhí)行。這些命令在隊列中保持其發(fā)送的順序。
  • EXEC:該命令用于執(zhí)行事務(wù)隊列中的所有命令。當(dāng)客戶端發(fā)送EXEC命令時,Redis會按照事務(wù)隊列中命令的順序依次執(zhí)行它們,并返回每個命令的執(zhí)行結(jié)果。
  • DISCARD:該命令用于取消事務(wù),即清空事務(wù)隊列并放棄執(zhí)行事務(wù)中的所有命令。

1. 事務(wù)的原子性

Redis事務(wù)在一定程度上保證了原子性,但有一定的條件限制:

當(dāng)命令入隊時報錯(如語法錯誤),Redis會放棄事務(wù)執(zhí)行,從而保證原子性。

redis> MULTI
  OK
  redis> SET msg "other msg"
  QUEUED
  redis> wrongcommand  ### 故意寫錯誤的命令
  (error) ERR unknown command 'wrongcommand' 
  redis> EXEC
  (error) EXECABORT Transaction discarded because of previous errors.
  redis> GET msg
  "hello world"

當(dāng)命令入隊時正常,但執(zhí)行EXEC命令后報錯(如運行時錯誤,如操作了錯誤類型的鍵),Redis不會回滾已經(jīng)執(zhí)行的命令,而是繼續(xù)執(zhí)行事務(wù)隊列中的剩余命令。這意味著在這種情況下,Redis事務(wù)的原子性無法得到保證。

redis> MULTI  
  OK
  redis> SET msg "other msg"
  QUEUED
  redis> SET mystring "I am a string"
  QUEUED
  redis> HMSET mystring name  "test"
  QUEUED
  redis> SET msg "after"
  QUEUED
  redis> EXEC
  1) OK
  2) OK
  3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
  4) OK
  redis> GET msg
  "after"

2. 事務(wù)的隔離性

Redis并沒有傳統(tǒng)數(shù)據(jù)庫中的事務(wù)隔離級別概念。然而,在并發(fā)場景下,Redis事務(wù)之間可以做到一定程度的互不干擾:

  • 在EXEC命令執(zhí)行前,Redis key仍然可以被其他客戶端修改。此時,可以使用WATCH機制來實現(xiàn)樂觀鎖的效果。WATCH命令可以監(jiān)控一個或多個鍵,在事務(wù)執(zhí)行前如果這些鍵被修改了,那么事務(wù)將被中斷。
  • 由于Redis是單線程執(zhí)行操作命令的,所以在EXEC命令執(zhí)行后,Redis會保證命令隊列中的所有命令按順序執(zhí)行完。這樣可以保證在當(dāng)前事務(wù)執(zhí)行期間,不會有其他客戶端的命令插入到事務(wù)命令序列中。

3. 事務(wù)的持久性與一致性

  • 持久性:Redis事務(wù)的持久性取決于Redis的持久化配置模式。如果沒有配置RDB或AOF,事務(wù)的持久性無法保證。即使使用了RDB或AOF模式,也可能存在數(shù)據(jù)丟失的情況。因此,Redis事務(wù)的持久性并不能得到完全保證。
  • 一致性:一致性是指事務(wù)只能將數(shù)據(jù)庫從一個有效狀態(tài)帶到另一個有效狀態(tài),同時保持?jǐn)?shù)據(jù)庫的完整性約束。在Redis事務(wù)中,如果某個命令執(zhí)行失敗,它不會影響其他命令的執(zhí)行(除非使用了WATCH機制)。因此,這可能導(dǎo)致數(shù)據(jù)庫狀態(tài)的不一致。因此,開發(fā)者需要在事務(wù)執(zhí)行出錯后自行處理,以恢復(fù)數(shù)據(jù)庫到一致狀態(tài)。

面試官:Redis中的WATCH命令有什么作用?它是如何實現(xiàn)樂觀鎖的?Redis事務(wù)與Lua腳本的關(guān)系是什么?它們之間有哪些異同點?

Redis中的WATCH命令具有關(guān)鍵作用,主要用于實現(xiàn)樂觀鎖機制。以下是關(guān)于WATCH命令的作用及其如何實現(xiàn)樂觀鎖的詳細(xì)解釋:

1. WATCH命令的作用

WATCH命令的主要作用是對Redis中的某個或某些鍵進(jìn)行監(jiān)視。當(dāng)這些被監(jiān)視的鍵在事務(wù)執(zhí)行之前被其他客戶端修改時,事務(wù)將會被中斷,從而避免臟數(shù)據(jù)的問題。這種機制確保了事務(wù)的一致性。

2. WATCH命令實現(xiàn)樂觀鎖的原理

監(jiān)視鍵的變化:

  • 客戶端使用WATCH命令指定需要監(jiān)視的一個或多個鍵。
  • 被WATCH的鍵會被記錄在內(nèi)存中,與當(dāng)前客戶端關(guān)聯(lián)起來,直到事務(wù)執(zhí)行完成或者取消。

事務(wù)的執(zhí)行與檢查:

  • 如果沒有變化,Redis會按順序執(zhí)行事務(wù)隊列中的所有命令。
  • 如果發(fā)生了變化,Redis會中斷事務(wù)的執(zhí)行,不執(zhí)行事務(wù)隊列中的任何命令,并返回一個空結(jié)果給客戶端。
  • 在事務(wù)開始(通過MULTI命令)之后,客戶端可以執(zhí)行一系列Redis命令,但這些命令會被放入事務(wù)隊列中,并不會立即執(zhí)行。
  • 當(dāng)客戶端發(fā)送EXEC命令嘗試執(zhí)行事務(wù)時,Redis會首先檢查被WATCH的鍵自WATCH命令執(zhí)行后是否發(fā)生了變化。

樂觀鎖的實現(xiàn):

  • 樂觀鎖基于“假設(shè)最好的情況會發(fā)生”的思想,即假設(shè)在事務(wù)執(zhí)行期間,被監(jiān)視的鍵不會被其他客戶端修改。
  • 當(dāng)使用WATCH命令監(jiān)視鍵時,就相當(dāng)于設(shè)置了一個檢查點。如果在事務(wù)提交(EXEC命令)之前,這些鍵被其他客戶端修改了,那么事務(wù)就會因為檢查失敗而被中斷,從而實現(xiàn)了樂觀鎖的效果。

事務(wù)的取消與重新監(jiān)視:

  • 如果客戶端在事務(wù)提交之前決定取消事務(wù),可以使用DISCARD命令。此時,WATCH的效果也會失效。
  • 如果事務(wù)因為被監(jiān)視的鍵發(fā)生變化而被中斷,客戶端可以選擇重新監(jiān)視這些鍵并嘗試重新執(zhí)行事務(wù)。

3. WATCH命令的注意事項

  • WATCH命令的執(zhí)行是立即的,但具體的監(jiān)視行為是延遲到EXEC命令被執(zhí)行時才會起作用。
  • 可以使用UNWATCH命令手動取消對所有被WATCH鍵的監(jiān)視。
  • 在Redis的Cluster模式中,WATCH命令可能無效,因為Cluster模式涉及多個節(jié)點之間的數(shù)據(jù)同步和分片。

下面是watch的使用示例。

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.WatchMonitor;
public class RedisWatchExample {
    public static void main(String[] args) {
        // 連接到Redis服務(wù)器
        Jedis jedis = new Jedis("localhost", 6379);
        // 要監(jiān)視的鍵
        String watchedKey = "myKey";
        // 初始值設(shè)置(僅用于示例初始化)
        jedis.set(watchedKey, "initialValue");
        while (true) {
            try (WatchMonitor monitor = jedis.watch(watchedKey)) {
                // 開始事務(wù)
                Transaction tx = jedis.multi();
                // 獲取當(dāng)前值(注意:這個值在EXEC之前可能是過時的,如果其他客戶端修改了它)
                String currentValue = jedis.get(watchedKey);
                System.out.println("Current value before transaction: " + currentValue);
                // 假設(shè)我們要在值后面追加一些內(nèi)容
                String newValue = currentValue + "_appended";
                // 在事務(wù)中設(shè)置新值
                tx.set(watchedKey, newValue);
                // 嘗試執(zhí)行事務(wù)
                tx.exec();
                System.out.println("Transaction succeeded, new value set to: " + newValue);
                break; // 如果事務(wù)成功,跳出循環(huán)
            } catch (Exception e) {
                // 如果捕獲到異常(通常是jedis.exceptions.JedisDataException: WATCHED KEY MODIFIED),表示事務(wù)期間鍵被修改
                System.out.println("Transaction failed due to watched key being modified. Retrying...");
                // 這里可以選擇重試或者其他錯誤處理邏輯
            }
            // 注意:在try-with-resources語句中,monitor會自動unwatch,但在catch塊中退出循環(huán)時不會執(zhí)行到那里。
            // 在實際應(yīng)用中,你可能需要在finally塊中顯式調(diào)用jedis.unwatch()以確保資源釋放,但在這個簡單示例中不是必需的。
            // 因為jedis連接在整個main方法期間都保持打開狀態(tài),并且我們在每次循環(huán)迭代開始時都會重新調(diào)用watch。
        }
        // 關(guān)閉Jedis連接
        jedis.close();
    }
}

4. Redis事務(wù) 和 Lua腳本異同點

實現(xiàn)方式:

  • Redis事務(wù)是基于樂觀鎖的,它使用WATCH命令來監(jiān)視一個或多個鍵,如果在事務(wù)執(zhí)行之前這些鍵被其他客戶端修改了,那么事務(wù)將被中斷。事務(wù)的執(zhí)行是通過MULTI、EXEC等命令來實現(xiàn)的。
  • Lua腳本則是將一系列Redis命令嵌入到Lua語言中,然后在Redis服務(wù)器上一次性執(zhí)行。Lua腳本的執(zhí)行是通過EVAL或EVALSHA命令來實現(xiàn)的。

靈活性:

  • Redis事務(wù)相對簡單,它只能按照固定的順序執(zhí)行一系列預(yù)定義的命令,不能包含復(fù)雜的邏輯判斷或循環(huán)結(jié)構(gòu)。
  • Lua腳本則更加靈活,它可以使用Lua語言的全部特性,包括條件判斷、循環(huán)結(jié)構(gòu)、函數(shù)定義等。這使得Lua腳本能夠執(zhí)行更加復(fù)雜的操作,并能夠處理各種異常情況。

性能:

  • 在性能方面,Lua腳本通常比Redis事務(wù)更具優(yōu)勢。因為Lua腳本是在Redis服務(wù)器上執(zhí)行的,避免了客戶端與服務(wù)器之間的多次往返通信開銷。此外,Lua腳本可以利用Redis的單線程特性,確保一系列命令以極高的效率執(zhí)行。
  • 然而,需要注意的是,如果Lua腳本中包含大量復(fù)雜的邏輯或數(shù)據(jù)操作,可能會導(dǎo)致執(zhí)行時間過長,從而影響Redis服務(wù)器的性能。因此,在使用Lua腳本時,需要權(quán)衡其靈活性和性能之間的關(guān)系。

責(zé)任編輯:趙寧寧 來源: 程序員阿沛
相關(guān)推薦

2022-08-11 18:27:50

面試Redis分布式鎖

2019-06-19 15:40:06

分布式鎖RedisJava

2021-06-03 08:55:54

分布式事務(wù)ACID

2021-06-03 00:02:43

RedisRedlock算法

2021-07-30 00:09:21

Redlock算法Redis

2024-09-24 16:30:46

分布式鎖Redis數(shù)據(jù)中間件

2023-08-21 19:10:34

Redis分布式

2024-10-07 10:07:31

2024-04-01 05:10:00

Redis數(shù)據(jù)庫分布式鎖

2024-02-27 15:23:48

RedLock算法Redis

2022-01-06 10:58:07

Redis數(shù)據(jù)分布式鎖

2022-06-21 08:27:22

Seata分布式事務(wù)

2020-09-27 06:52:22

分布式存儲服務(wù)器

2020-07-09 13:30:03

RedisJava分布式鎖

2024-06-26 11:55:44

2023-03-01 08:07:51

2024-11-28 15:11:28

2023-08-17 14:42:54

Redis分布式鎖

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2024-04-09 10:40:04

點贊
收藏

51CTO技術(shù)棧公眾號