Rumah >pangkalan data >Redis >Cara menggunakan Go dan Redis untuk melaksanakan kunci mutex dan kunci merah yang diedarkan
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
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 endBerikan 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
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 pengawasKunci 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 pengawasMulakan utas apabila kunci berjaya dan teruskan memanjangkan masa tamat tempoh kunci; Proses pengawas adalah seperti berikut:
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!