Rumah >pangkalan data >Redis >Cara menggunakan Go dan Redis untuk melaksanakan kunci mutex dan kunci merah yang diedarkan

Cara menggunakan Go dan Redis untuk melaksanakan kunci mutex dan kunci merah yang diedarkan

WBOY
WBOYke hadapan
2023-05-28 08:54:441350semak imbas

Kunci Mutex

Terdapat perintah 设置如果不存在 dalam Redis Kita boleh menggunakan perintah ini untuk melaksanakan fungsi kunci mutex Kaedah pelaksanaan standard yang disyorkan dalam dokumentasi Redis rasmi ialah perintah rentetan , di mana: SET resource_name my_random_value NX PX 30000

  • mewakili sumber yang akan dikunci resource_name

  • mewakili tetapan jika ia tidak wujud NX

  • bermaksud masa tamat tempoh ialah 30000 milisaat, iaitu 30 saatPX 30000

  • Nilai ini mestilah unik dalam semua pelanggan, semuanya sama Nilai pesaing kunci kunci tidak boleh sama. my_random_value

Nilai mestilah nombor rawak terutamanya untuk melepaskan kunci dengan lebih selamat Apabila melepaskan kunci, gunakan skrip untuk memberitahu Redis: Hanya kunci yang wujud dan nilai yang disimpan adalah sama. sebagai nilai yang saya nyatakan, saya berjaya memadamkannya dan mengelak daripada melepaskan kunci pesaing lain secara tidak sengaja.

Memandangkan dua operasi terlibat, kita perlu memastikan atomicity operasi melalui skrip Lua:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
Berikan contoh tanpa skrip Lua: Klien A memperoleh kunci sumber, tetapi kemudiannya disekat oleh operasi lain Apabila klien A ingin melepaskan kunci selepas menjalankan operasi lain, kunci asal telah tamat masa dan secara automatik dikeluarkan oleh Redis Dalam tempoh ini, kunci sumber telah diperoleh semula oleh klien B.

Oleh kerana penghakiman dan pemadaman adalah dua operasi, ada kemungkinan A akan melepaskan kunci secara automatik selepas ia tamat tempoh selepas menilainya, dan kemudian B akan memperoleh kunci itu, dan kemudian A akan memanggil Del, menyebabkan B kunci untuk dilepaskan.

Pelaksanaan TryLock dan Buka Kunci

sebenarnya menggunakan TryLock untuk mengunci Di sini, SET resource_name my_random_value NX PX 30000 digunakan sebagai nilai rawak dan nilai rawak dikembalikan apabila kunci berjaya . Nilai rawak ini Nilai akan digunakan apabila UUID; Unlock

logik buka kunci adalah untuk melaksanakan Unlock yang dinyatakan sebelum ini. lua脚本

func (l *Lock) TryLock(ctx context.Context) error {
   success, err := l.client.SetNX(ctx, l.resource, l.randomValue, ttl).Result()
   if err != nil {
      return err
   }
   // 加锁失败
   if !success {
      return ErrLockFailed
   }
   // 加锁成功
   l.randomValue = randomValue
   return nil
}

func (l *Lock) Unlock(ctx context.Context) error {
   return l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err()
}
Pelaksanaan kunci

ialah kunci pemerolehan menyekat, jadi apabila kunci gagal, anda perlu mencuba semula. Sudah tentu, situasi luar biasa lain juga mungkin berlaku (seperti masalah rangkaian, permintaan tamat masa, dll.), dan dalam situasi ini, Lock akan dikembalikan terus. error

Langkah-langkahnya adalah seperti berikut:

  • Cuba kunci, dan jika kunci berjaya, ia akan kembali terus

  • Jika kunci gagal, ia akan terus bergelung dan cuba menambah Kunci sehingga berjaya atau pengecualian berlaku

func (l *Lock) Lock(ctx context.Context) error {
  // 尝试加锁
  err := l.TryLock(ctx)
  if err == nil {
    return nil
  }
  if !errors.Is(err, ErrLockFailed) {
    return err
  }
  // 加锁失败,不断尝试
  ticker := time.NewTicker(l.tryLockInterval)
  defer ticker.Stop()
  for {
    select {
    case <-ctx.Done():
      // 超时
      return ErrTimeout
    case <-ticker.C:
      // 重新尝试加锁
      err := l.TryLock(ctx)
      if err == nil {
        return nil
      }
      if !errors.Is(err, ErrLockFailed) {
        return err
      }
    }
  }
}
Melaksanakan mekanisme pengawas

Kunci mutex yang disebut dalam contoh kami sebelum ini mempunyai masalah kecil, iaitu, jika ia dipegang Jika klien A dengan kunci disekat, maka kunci A mungkin dilepaskan secara automatik selepas tamat masa, menyebabkan klien B memperoleh kunci terlebih dahulu.

Untuk mengurangkan berlakunya situasi ini, kami boleh teruskan memanjangkan masa tamat tempoh kunci semasa A memegang kunci, dan mengurangkan situasi di mana pelanggan B memperoleh kunci itu terlebih dahulu. Ini adalah mekanisme pengawas .

Sudah tentu, tiada cara untuk mengelak sepenuhnya situasi di atas, kerana jika pelanggan A kebetulan menutup sambungan dengan Redis selepas memperoleh kunci, tiada cara untuk melanjutkan tamat masa.

Pelaksanaan pengawas

Mulakan utas apabila kunci berjaya dan teruskan memanjangkan masa tamat tempoh kunci;

Proses pengawas adalah seperti berikut:

  • Kunci berjaya dan pengawas dimulakan

  • Benang pengawas diteruskan untuk memanjangkan masa proses Kunci

  • Buka kunci, matikan pengawas

func (l *Lock) startWatchDog() {
  ticker := time.NewTicker(l.ttl / 3)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      // 延长锁的过期时间
      ctx, cancel := context.WithTimeout(context.Background(), l.ttl/3*2)
      ok, err := l.client.Expire(ctx, l.resource, l.ttl).Result()
      cancel()
      // 异常或锁已经不存在则不再续期
      if err != nil || !ok {
        return
      }
    case <-l.watchDog:
      // 已经解锁
      return
    }
  }
}
TryLock: Mulakan pengawas

func (l *Lock) TryLock(ctx context.Context) error {
  success, err := l.client.SetNX(ctx, l.resource, l.randomValue, l.ttl).Result()
  if err != nil {
    return err
  }
  // 加锁失败
  if !success {
    return ErrLockFailed
  }
  // 加锁成功,启动看门狗
  go l.startWatchDog()
  return nil
}
Buka Kunci: Matikan pengawas

func (l *Lock) Unlock(ctx context.Context) error {
  err := l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err()
  // 关闭看门狗
  close(l.watchDog)
  return err
}
Kunci merah

Memandangkan pelaksanaan di atas adalah berdasarkan satu contoh Redis, jika contoh ini sahaja hang, semua permintaan akan gagal kerana kunci tidak boleh diperolehi toleransi kesalahan, kita boleh menggunakan berbilang contoh Redis yang diedarkan pada mesin yang berbeza, dan selagi kita mendapat kunci kebanyakan nod, kita boleh berjaya mengunci Ini adalah algoritma kunci merah. Ia sebenarnya berdasarkan algoritma contoh tunggal di atas, kecuali kita perlu memperoleh kunci pada berbilang kejadian Redis pada masa yang sama.

加锁实现

在加锁逻辑里,我们主要是对每个Redis实例执行SET resource_name my_random_value NX PX 30000获取锁,然后把成功获取锁的客户端放到一个channel里(这里因为是多线程并发获取锁,使用slice可能有并发问题),同时使用sync.WaitGroup等待所有获取锁操作结束。

然后判断成功获取到的锁的数量是否大于一半,如果没有得到一半以上的锁,说明加锁失败,释放已经获得的锁。

如果加锁成功,则启动看门狗延长锁的过期时间。

func (l *RedLock) TryLock(ctx context.Context) error {
  randomValue := gofakeit.UUID()
  var wg sync.WaitGroup
  wg.Add(len(l.clients))
  // 成功获得锁的Redis实例的客户端
  successClients := make(chan *redis.Client, len(l.clients))
  for _, client := range l.clients {
    go func(client *redis.Client) {
      defer wg.Done()
      success, err := client.SetNX(ctx, l.resource, randomValue, ttl).Result()
      if err != nil {
        return
      }
      // 加锁失败
      if !success {
        return
      }
      // 加锁成功,启动看门狗
      go l.startWatchDog()
      successClients <- client
    }(client)
  }
  // 等待所有获取锁操作完成
  wg.Wait()
  close(successClients)
  // 如果成功加锁得客户端少于客户端数量的一半+1,表示加锁失败
  if len(successClients) < len(l.clients)/2+1 {
    // 就算加锁失败,也要把已经获得的锁给释放掉
    for client := range successClients {
      go func(client *redis.Client) {
        ctx, cancel := context.WithTimeout(context.Background(), ttl)
        l.script.Run(ctx, client, []string{l.resource}, randomValue)
        cancel()
      }(client)
    }
    return ErrLockFailed
  }

  // 加锁成功,启动看门狗
  l.randomValue = randomValue
  l.successClients = nil
  for successClient := range successClients {
    l.successClients = append(l.successClients, successClient)
  }

  return nil
}
看门狗实现

我们需要延长所有成功获取到的锁的过期时间。

func (l *RedLock) startWatchDog() {
  l.watchDog = make(chan struct{})
  ticker := time.NewTicker(resetTTLInterval)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      // 延长锁的过期时间
      for _, client := range l.successClients {
        go func(client *redis.Client) {
          ctx, cancel := context.WithTimeout(context.Background(), ttl-resetTTLInterval)
          client.Expire(ctx, l.resource, ttl)
          cancel()
        }(client)
      }
    case <-l.watchDog:
      // 已经解锁
      return
    }
  }
}
解锁实现

我们需要解锁所有成功获取到的锁。

func (l *RedLock) Unlock(ctx context.Context) error {
   for _, client := range l.successClients {
      go func(client *redis.Client) {
         l.script.Run(ctx, client, []string{l.resource}, l.randomValue)
      }(client)
   }
   // 关闭看门狗
   close(l.watchDog)
   return nil
}

Atas ialah kandungan terperinci Cara menggunakan Go dan Redis untuk melaksanakan kunci mutex dan kunci merah yang diedarkan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:yisu.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam