欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Redis實(shí)現(xiàn)分布式鎖的原理

 更新時(shí)間:2023年09月06日 11:44:05   作者:uccs  
分布式鎖,即分布式系統(tǒng)中的鎖,在單體應(yīng)用中我們通過鎖解決的是控制共享資源訪問的問題,而分布式鎖,就是解決了分布式系統(tǒng)中控制共享資源訪問的問題,本文講給大家詳細(xì)介紹一下Redis實(shí)現(xiàn)分布式鎖的原理,需要的朋友可以參考下

通過原子操作實(shí)現(xiàn) redis 鎖

redis 內(nèi)部是通過 key/value 的形式存儲(chǔ)的,核心原理是設(shè)置一個(gè)唯一的 key,如果這個(gè) key 存在,說明有服務(wù)在使用

具體實(shí)現(xiàn)方式:

  • 首先判斷 redis 中是否存在某個(gè) key,并且為某個(gè)值
  • 如果這個(gè) key 不存在,說明當(dāng)前沒有服務(wù)在使用,設(shè)置 key
  • 如果這個(gè) key 存在,說明當(dāng)前有服務(wù)在使用,就等待一段時(shí)間,然后再次判斷這個(gè) key 是否存在

如下圖所示

這種情況有沒有問題呢?

如果在單體應(yīng)用的場(chǎng)景下,這種方式是可行的;但是在分布式場(chǎng)景下,這種方式就不可行了

因?yàn)樵诜植际綀?chǎng)景下,redis 是多個(gè)服務(wù)共享的,如果多個(gè)服務(wù)同時(shí)判斷 key 不存在,那么就會(huì)同時(shí)設(shè)置 key,就會(huì)導(dǎo)致多個(gè)服務(wù)同時(shí)執(zhí)行,這不是我們想要的結(jié)果

為什么這樣做會(huì)有問題?

因?yàn)?getset 操作不是原子操作,你先要做操作 get,然后在操作 set,這個(gè)過程中

這就會(huì)導(dǎo)致當(dāng)?shù)谝慌_(tái)服務(wù)在執(zhí)行 get 時(shí),發(fā)現(xiàn) key 不存在,然后進(jìn)行 set,這個(gè)時(shí)候 set 可能還沒有完成,第二臺(tái)服務(wù)執(zhí)行了 get,發(fā)現(xiàn) key 不存在,然后進(jìn)行 set,這個(gè)時(shí)候就會(huì)導(dǎo)致多個(gè)服務(wù)同時(shí)執(zhí)行,這就不是原子操作了

原子操作的意思是:一次性執(zhí)行,不會(huì)被打斷

這個(gè)怎么做呢?

redis 提供了一個(gè) setnx 的方法,作用是如果 key 不存在,就設(shè)置 key,設(shè)置成功返回 1,設(shè)置失敗返回 0

這就將 getset 的邏輯合二為一了,保證原子性了

如下圖所示:

當(dāng)我們了解了原理之后,看下人家是不是這樣實(shí)現(xiàn)的,以 redsync 為例,先來看它使用,從入口函數(shù)一步步往下追

rs := redsync.New(pool)
mutexname := "my-global-mutex"
mutex := rs.NewMutex(mutexname)
if err := mutex.Lock(); err != nil {
  panic(err)
}
if ok, err := mutex.Unlock(); !ok || err != nil {
  panic("unlock failed")
}

從上面代碼可以看到,它先調(diào)用 NewMutex 創(chuàng)建了一個(gè) mutex,然后調(diào)用 mutex.Lock() 方法

NewMutex 是初始化函數(shù),用來初始化一系列的參數(shù),

比較重要的有:

  • nameredis 中的 key
  • genValueFunc:生成 key 的函數(shù),保證唯一性
  • expirykey 過期的時(shí)間
  • tries:嘗試的次數(shù),可能會(huì)拿不到鎖,所以要嘗試多次
  • delayFunc:延遲時(shí)間(睡眠時(shí)間),可能會(huì)拿不到鎖,就需要等一會(huì)再嘗試
  • quorum:大多數(shù)節(jié)點(diǎn),這個(gè)是用來做分布式鎖的,如果有 5 個(gè)節(jié)點(diǎn),那么這里的大多數(shù)是 3 個(gè)節(jié)點(diǎn)
m := &Mutex{
  name:   name,
  expiry: 8 * time.Second,
  tries:  32,
  delayFunc: func(tries int) time.Duration {
    return time.Duration(rand.Intn(maxRetryDelayMilliSec-minRetryDelayMilliSec)+minRetryDelayMilliSec) * time.Millisecond
  },
  genValueFunc:  genValue,
  driftFactor:   0.01,
  timeoutFactor: 0.05,
  quorum:        len(r.pools)/2 + 1,
  pools:         r.pools,
}

初始化結(jié)束之后,調(diào)用 m.Lock() 上鎖,m.Lock() 方法中調(diào)用 m.LockContext() 方法,

LockContext 是核心方法,里面會(huì)做很多事情,這一步我們關(guān)心它是怎么上鎖的,通過搜索發(fā)現(xiàn),上鎖的方法是 m.acquire(),其源碼是:

func (m *Mutex) acquire(ctx context.Context, pool redis.Pool, value string) (bool, error) {
  conn, err := pool.Get(ctx)
  if err != nil {
    return false, err
  }
  defer conn.Close()
  reply, err := conn.SetNX(m.name, value, m.expiry)
  if err != nil {
    return false, err
  }
  return reply, nil
}

在這里我們清晰的看到調(diào)用 SetNX 方法

通過過期時(shí)間防止死鎖

這樣做完之后,還有一個(gè)問題需要解決

如果正在操作 redis 的服務(wù)掛了,那么這個(gè) key 就會(huì)一直存在,其他服務(wù)就會(huì)等待,這樣就造成了死鎖

解決這個(gè)問題就是設(shè)置過期時(shí)間,如果服務(wù)掛了,過期時(shí)間到了,key 就會(huì)自動(dòng)刪除,其他服務(wù)就可以繼續(xù)使用了

通過源代碼我們可以看到它設(shè)置了一個(gè)過期時(shí)間 expiry

reply, err := conn.SetNX(m.name, value, m.expiry)

這個(gè)過期時(shí)間是怎么來的呢?

剛剛在入口函數(shù)中,我們看到了 NewMutex 函數(shù),它初始化了一個(gè) expiry,這個(gè) expiry 就是過期時(shí)間:expiry: 8 * time.Second,它默認(rèn)設(shè)置的是 8

到這里就有疑問了,如果我的服務(wù)執(zhí)行時(shí)間超過 8 秒怎么辦?,不就達(dá)不到鎖的效果了?

我們很快就會(huì)想到,在過期前刷新下過期時(shí)間不就行了?

確實(shí) redsync 也考慮到了這個(gè)問題,它提供了一個(gè) Extend 方法,用來刷新過期時(shí)間

m.Extent() 方法調(diào)用 m.ExtendContext() 方法,在 m.ExtendContext() 方法中調(diào)用 m.touch() 方法

func (m *Mutex) Extend() (bool, error) {
  return m.ExtendContext(nil)
}
func (m *Mutex) ExtendContext(ctx context.Context) (bool, error) {
  // ... 省略其他代碼
  m.touch(ctx, pool, m.value, int(m.expiry/time.Millisecond))
  // ... 省略其他代碼
}
func (m *Mutex) touch(ctx context.Context, pool redis.Pool, value string, expiry int) (bool, error) {
  // ... 省略其他代碼
	conn, err := pool.Get(ctx)
  conn.Eval(touchScript, m.name, value, expiry)
}

在 m.touch() 方法中我們看到它調(diào)用 redis 提供的 Eval 方法,可以執(zhí)行一段 lua 腳本,腳本的內(nèi)容如下:

var touchScript = redis.NewScript(1, `
  if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("PEXPIRE", KEYS[1], ARGV[2])
  else
    return 0
  end
`)

它為什么要這樣做呢?

不就是把過期時(shí)間刷新下嗎?為什么要寫 lua

這里我們需要了解下 redislua 腳本,redislua 腳本是原子性的,它可以保證一段腳本的執(zhí)行是原子性的

這樣就可以保證刷新過期時(shí)間的操作是原子性的,不會(huì)出現(xiàn)刷新過期時(shí)間失敗的情況

如果我們用 go 語言去續(xù)期的需要三步:

  • 先獲取到 key 的值
  • 判斷 redis 中的值是不是你傳進(jìn)來的值
  • 如果是的話,續(xù)期

這樣的話,這樣的話就不具備原子性了,任何一步都有失敗的可能,所以 redsync 選擇了 lua 腳本

我們?cè)谑褂?m.Extend() 續(xù)期時(shí),需要用協(xié)程去做

redsync 為什么不自動(dòng)續(xù)期呢?

如果做自動(dòng)續(xù)期的話,當(dāng)前正在操作的服務(wù)如果 hung 住了,那么就會(huì)不停的續(xù)期,造成其他服務(wù)無法進(jìn)來,所以 redsync 將續(xù)期的功能交給了使用者

防止被其他服務(wù)刪除

鎖只能被持有該鎖的服務(wù)刪除,不能被其他服務(wù)刪除

如果保證鎖只能被持有該鎖的服務(wù)刪除,那么就需要在 setnx 的時(shí)候,給 key 設(shè)置一個(gè)唯一的值,這個(gè)值可以是 uuid,這樣就可以保證鎖只能被持有該鎖的服務(wù)刪除

我們看下 redsync 源碼是如何做的,初始化時(shí)就生成了一個(gè)唯一的值,它是使用 base64 編碼的

func genValue() (string, error) {
  b := make([]byte, 16)
  _, err := rand.Read(b)
  if err != nil {
    return "", err
  }
  return base64.StdEncoding.EncodeToString(b), nil
}

刪除的時(shí)候,調(diào)用 m.Unlock() 方法,m.Unlock() 方法調(diào)用 m.UnlockContext() 方法,在在 m.release() 方法

func (m *Mutex) release(ctx context.Context, pool redis.Pool, value string) (bool, error) {
  // ... 省略其他代碼
  conn, err := pool.Get(ctx)
  conn.Eval(deleteScript, m.name, value)
}

在 m.release() 方法中我們看的也是在執(zhí)行 lua 腳本,腳本的內(nèi)容如下:

var deleteScript = redis.NewScript(1, `
  if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
  else
    return 0
  end
`)

這也是為了保證在刪除鎖的時(shí)候,保證原子性

redlock

通過我們上面講解的已經(jīng)能滿足一般的使用場(chǎng)景,但是在大型項(xiàng)目中,不會(huì)只搭建一個(gè) redis,而是搭建 redis 集群

這樣又會(huì)出現(xiàn)一個(gè)新的問題:redlock

redlock 是什么呢?我們先來看下 redis 集群

一般 redis 集群有一個(gè) master 節(jié)點(diǎn),多個(gè) slave 節(jié)點(diǎn)

如下圖所示:

當(dāng)我在加鎖時(shí),如果 master 節(jié)點(diǎn)會(huì)自動(dòng)同步到 slave 節(jié)點(diǎn),那么就不會(huì)有問題

如果這時(shí) master 節(jié)點(diǎn)出問題了(或者說在同步過程中出問題,還沒有同步完),slave 節(jié)點(diǎn)會(huì)選舉出一個(gè) master 節(jié)點(diǎn),這個(gè)過程中會(huì)有一段時(shí)間,這時(shí)如果有一個(gè)服務(wù)進(jìn)來寫,發(fā)現(xiàn)是能寫入的,這就出現(xiàn)了問題

如下圖所示:

面對(duì)這種問題如何解決,引入了 redlock 的這個(gè)概念

redlock 的核心思想是:在 redis 集群中,大多數(shù)節(jié)點(diǎn)都能寫入成功,那么就認(rèn)為寫入成功,而不是只向一臺(tái) redis 寫入

當(dāng)?shù)谝粋€(gè)服務(wù)寫入時(shí),同時(shí)向 5 臺(tái) redis 寫入,這時(shí)如果第二個(gè)服務(wù)寫入,寫同時(shí)向 5 臺(tái) redis 寫入,誰先成功寫入大多數(shù) redis,誰就認(rèn)為寫入成功,鎖就交給誰

這里的大多數(shù)就是比一半多 1 臺(tái),也就是 n / 2 + 1,所以 redis 應(yīng)該準(zhǔn)備奇數(shù)臺(tái),同時(shí)也無需關(guān)心這 5 臺(tái) redis 的主從關(guān)系了

如下圖所示:

我們通過 redsync 源碼來學(xué)習(xí) redlock,是如何實(shí)現(xiàn)的:

  • 通過 select 實(shí)現(xiàn)超時(shí)控制
  • 核心代碼是 actOnPoolsAsync 方法
    • pools:表示向多臺(tái) redis 寫入
    • async:表示異步寫入多臺(tái) redis,同步寫入的話,效率偏低,使用 goroutine(具體可以查看下面 actOnPoolsAsync 方法的分析)
  • 判斷是否拿到鎖
    • 如果拿到鎖,更新 m.valuem.until
    • 如果沒有拿到鎖,需要釋放已經(jīng)寫入的 rediskey
func (m *Mutex) LockContext(ctx context.Context) error {
  if ctx == nil {
    ctx = context.Background()
  }
  value, err := m.genValueFunc()
  if err != nil {
    return err
  }
  // 如果沒有拿到鎖,等待一段時(shí)間在去拿
  for i := 0; i < m.tries; i++ {
    if i != 0 {
      // 使用 select 實(shí)現(xiàn)超時(shí)控制
      select {
      case <-ctx.Done():
        return ErrFailed
      case <-time.After(m.delayFunc(i)):
      }
    }
    // 記錄拿鎖開始時(shí)間
    start := time.Now()
    n, err := func() (int, error) {
      ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))
      defer cancel()
      // 異步寫入多臺(tái) redis
      return m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
        return m.acquire(ctx, pool, value)
      })
    }()
    // 記錄拿鎖結(jié)束時(shí)間
    now := time.Now()
    // 計(jì)算還剩多少時(shí)間:過期時(shí)間 - 拿鎖花費(fèi)的時(shí)間 - 時(shí)間偏移
    // 這段代碼是為了防止 `redis` 節(jié)點(diǎn)時(shí)間不同步,導(dǎo)致鎖過期時(shí)間不準(zhǔn)確,所以在過期時(shí)間上加上一個(gè) `driftFactor`,這個(gè)值是 `0.01`,也就是 `1%` 的誤差
    until := now.Add(m.expiry - now.Sub(start) - time.Duration(int64(float64(m.expiry)*m.driftFactor)))
    // 判斷是否競(jìng)爭(zhēng)成功
    if n >= m.quorum && now.Before(until) {
      m.value = value
      m.until = until
      return nil
    }
    // 如果競(jìng)爭(zhēng)失敗,釋放已經(jīng)寫入的 redis 的 key
    func() (int, error) {
      ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))
      defer cancel()
      return m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
        return m.release(ctx, pool, value)
      })
    }()
    if i == m.tries-1 && err != nil {
      return err
    }
  }
  return ErrFailed
}

為什么要使用異步寫入多臺(tái) redis 呢?

如果采用同步寫入的多臺(tái)的話,如果寫入的 redis 比較多,就會(huì)很耗時(shí),可能寫到最后一臺(tái) redis 時(shí),前面的 redis 已經(jīng)過期了,這樣就會(huì)出現(xiàn)問題

啟用 goroutine 去寫入的話,可以一瞬間都拿到 lock,調(diào)用 setnx 方法去寫入

然后再統(tǒng)計(jì)成功寫入的臺(tái)數(shù),返回出去

func (m *Mutex) actOnPoolsAsync(actFn func(redis.Pool) (bool, error)) (int, error) {
  type result struct {
    Node   int
    Status bool  // 成功寫入的臺(tái)數(shù)
    Err    error // 未成功寫入的錯(cuò)誤
  }
  // 啟用 goroutine 去調(diào)用 setnx 寫入
  // 用 channel 來接收結(jié)果
  ch := make(chan result)
  for node, pool := range m.pools {
    go func(node int, pool redis.Pool) {
      r := result{Node: node}
      r.Status, r.Err = actFn(pool)
      ch <- r
    }(node, pool)
  }
  n := 0
  var taken []int
  var err error
  for range m.pools {
    r := <-ch
    // 寫入成功,n++;寫入失敗,記錄錯(cuò)誤
    if r.Status {
      n++
    } else if r.Err != nil {
      err = multierror.Append(err, &RedisError{Node: r.Node, Err: r.Err})
    } else {
      taken = append(taken, r.Node)
      err = multierror.Append(err, &ErrNodeTaken{Node: r.Node})
    }
  }
  // 將寫入的臺(tái)數(shù)和錯(cuò)誤返回出去
  if len(taken) >= m.quorum {
    return n, &ErrTaken{Nodes: taken}
  }
  return n, err
}

總結(jié)

分布式鎖的實(shí)現(xiàn)需要考慮的問題:

  • 原子性(互斥性):鎖只能被一個(gè)服務(wù)持有
    • 使用 setnx 命令,將 setget 變成原子性
    • 使用 lua 腳本
  • 死鎖:設(shè)置過期時(shí)間,防止服務(wù)掛了變成死鎖
    • 續(xù)期操作需要保證原子性,使用 lua 腳本
  • 安全性:鎖只能被持有該鎖的服務(wù)刪除,不能被其他服務(wù)刪除
    • setnx 的時(shí)候,給 key 設(shè)置一個(gè)唯一的值
  • redlock:解決多臺(tái) redis 同步問題
    • 一個(gè)服務(wù)同時(shí)向多臺(tái) redis 設(shè)置 lock
    • 哪個(gè)服務(wù)向大多數(shù) redis 寫入成功,控制權(quán)就交給哪個(gè)服務(wù)

以上就是詳解Redis實(shí)現(xiàn)分布式鎖的原理的詳細(xì)內(nèi)容,更多關(guān)于Redis分布式鎖原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Redis使用SETNX命令實(shí)現(xiàn)分布式鎖

    Redis使用SETNX命令實(shí)現(xiàn)分布式鎖

    分布式鎖是一種用于在分布式系統(tǒng)中控制多個(gè)節(jié)點(diǎn)對(duì)共享資源進(jìn)行訪問的機(jī)制,本文主要為大家詳細(xì)介紹了Redis如何使用SETNX命令實(shí)現(xiàn)分布式鎖,需要的可以參考下
    2025-01-01
  • redis緩存的簡單操作(get、put)

    redis緩存的簡單操作(get、put)

    這篇文章主要介紹了redis緩存的簡單操作,包括引入jedisjar包、配置redis、RedisDao需要的一些工具等,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-09-09
  • 使用Redis存儲(chǔ)SpringBoot項(xiàng)目中Session的詳細(xì)步驟

    使用Redis存儲(chǔ)SpringBoot項(xiàng)目中Session的詳細(xì)步驟

    在開發(fā)Spring Boot項(xiàng)目時(shí),我們通常會(huì)遇到如何高效管理Session的問題,默認(rèn)情況下,Spring Boot會(huì)將Session存儲(chǔ)在內(nèi)存中,今天,我們將學(xué)習(xí)如何將Session存儲(chǔ)從內(nèi)存切換到Redis,并驗(yàn)證配置是否成功,需要的朋友可以參考下
    2024-06-06
  • Redis SETNX的實(shí)現(xiàn)示例

    Redis SETNX的實(shí)現(xiàn)示例

    SETNX是Redis提供的原子操作,用于在指定鍵不存在時(shí)設(shè)置鍵值,并返回操作結(jié)果,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-12-12
  • 了解redis中RDB結(jié)構(gòu)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理

    了解redis中RDB結(jié)構(gòu)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理

    這篇文章主要為大家詳細(xì)介紹了redis中RDB結(jié)構(gòu),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-08-08
  • 基于Redis結(jié)合SpringBoot的秒殺案例詳解

    基于Redis結(jié)合SpringBoot的秒殺案例詳解

    這篇文章主要介紹了Redis結(jié)合SpringBoot的秒殺案例,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-09-09
  • Linux下Redis安裝教程詳解

    Linux下Redis安裝教程詳解

    這篇文章主要為大家詳細(xì)介紹了Linux下Redis安裝教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-09-09
  • Redis+自定義注解+AOP實(shí)現(xiàn)聲明式注解緩存查詢的示例

    Redis+自定義注解+AOP實(shí)現(xiàn)聲明式注解緩存查詢的示例

    實(shí)際項(xiàng)目中,會(huì)遇到很多查詢數(shù)據(jù)的場(chǎng)景,這些數(shù)據(jù)更新頻率也不是很高,一般我們?cè)跇I(yè)務(wù)處理時(shí),會(huì)對(duì)這些數(shù)據(jù)進(jìn)行緩存,本文主要介紹了Redis+自定義注解+AOP實(shí)現(xiàn)聲明式注解緩存查詢的示例,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2025-04-04
  • redis群集簡單部署過程

    redis群集簡單部署過程

    文章介紹了Redis,一個(gè)高性能的鍵值存儲(chǔ)系統(tǒng),其支持多種數(shù)據(jù)結(jié)構(gòu)和命令,它還討論了Redis的服務(wù)器端架構(gòu)、數(shù)據(jù)存儲(chǔ)和獲取、協(xié)議和命令、高可用性方案、緩存機(jī)制以及監(jiān)控和日志功能,文章還提供了一個(gè)部署Redis群集的簡要指南,感興趣的朋友一起看看吧
    2025-02-02
  • 詳解Redis分布式鎖的原理與實(shí)現(xiàn)

    詳解Redis分布式鎖的原理與實(shí)現(xiàn)

    在單體應(yīng)用中,如果我們對(duì)共享數(shù)據(jù)不進(jìn)行加鎖操作,會(huì)出現(xiàn)數(shù)據(jù)一致性問題,我們的解決辦法通常是加鎖。下面我們一起聊聊使用redis來實(shí)現(xiàn)分布式鎖
    2022-06-06

最新評(píng)論