Go微服务: redis分布式锁

概述

  • 在分布式系统中,并发控制和数据一致性是至关重要的问题
  • 当多个服务或进程需要访问和修改共享资源时,我们必须确保在同一时间只有一个服务或进程能够执行操作,以防止数据竞争和不一致,这就是分布式锁要解决的问题
  • Redis 作为一个高性能的键值存储系统,经常被用作实现分布式锁的工具。
  • Redis 的 SETNX、EXPIRE、DEL 等命令可以组合起来实现一个简单的分布式锁,但是,直接使用这些命令可能会引入一些复杂的逻辑和潜在的错误
  • 因此,许多开发者选择使用现成的库来简化分布式锁的实现
  • 在 Go 语言中,github.com/go-redsync/redsync 是一个流行的库,它基于 Redis 提供了分布式锁的抽象和实现

Redis 分布式锁的基本原理

Redis 分布式锁通常基于以下原理

  • 加锁:使用 SETNX 命令尝试设置一个键值对,如果键不存在则设置成功(返回 1),否则设置失败(返回 0)。设置成功的进程获得了锁
  • 设置过期时间:为了防止死锁,通常会为锁设置一个过期时间,使用 EXPIRE 命令
  • 解锁:当进程完成操作后,需要删除之前设置的键值对来释放锁,使用 DEL 命令
  • 处理锁过期:如果持有锁的进程在锁过期之前未能完成操作并释放锁,其他进程可以重新获取锁

示例程序

go 复制代码
package main

import (
	"fmt"
	"time"

	"github.com/go-redsync/redsync/v4"
	"github.com/go-redsync/redsync/v4/redis/goredis/v9"
	goredislib "github.com/redis/go-redis/v9"

	"sync"
)

func RedisLock(wg *sync.WaitGroup) {
	// 初始化锁客户端
	client := goredislib.NewClient(&goredislib.Options{
		Addr:     "127.0.0.1:6380",
		Password: "123456_redis",
		Username: "root",
		DB:       0,
	})
	pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)
	rs := redsync.New(pool)

	// 初始化锁
	mutexname := "my-global-mutex"
	mutex := rs.NewMutex(mutexname, redsync.WithExpiry(30*time.Second))

	// 开始锁定
	fmt.Println("Lock()....")
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
	// 自己的一些业务逻辑
	fmt.Println("Get Lock!!!")
	time.Sleep(time.Second * 1)
	// 开始解锁
	fmt.Println("UnLock()")
	if ok, err := mutex.Unlock(); !ok || err != nil {
		panic("unlock failed")
	}
	fmt.Println("Released Lock!!!")
}

func main() {
	// 测试程序
	var wg sync.WaitGroup
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go RedisLock(&wg)
	}
	wg.Wait()
}
  • 程序运行,输出

    Lock()....
    Lock()....
    Lock()....
    Get Lock!!!
    UnLock()
    Released Lock!!!
    Get Lock!!!
    UnLock()
    Released Lock!!!
    Get Lock!!!
    UnLock()
    Released Lock!!!
    
  • 前三个Lock() 代表程序在排队阶段

  • 后面接着三次输出,每次都有: Get Lock!!!, UnLock(), Released Lock!!!

  • 每个过程都是 获取锁,解锁,释放锁

  • 所以在高并发的时候,使用redis的分布式锁可以解决资源数据的竞争

  • 特别注意的是: mutexname := "my-global-mutex" 这个锁的名称在 redis 中使用的时候会生成,当锁被全部释放后,就会自动释放,而且这个锁的名称最好也自行规范下

    go 复制代码
    // 以下是 docker 内的redis容器cli
    127.0.0.1:6379> keys *
    1) "my-global-mutex"
    // 稍后再次查询后,锁的key和value消失
    127.0.0.1:6379> keys *
    (empty array)
  • 另外,如果锁在规定的时间内没有完成工作,那么在设定的过期时间后就会自动结束,如果不设置过期时间,走系统内的过期时间

使用 redsync 实现 Redis 分布式锁

  • redsync 库提供了一个高级接口,使得在 Go 语言中使用 Redis 分布式锁变得更加简单
  • 下面是如何使用 redsync 实现分布式锁的步骤:
    • 初始化 Redis 客户端:首先,你需要初始化一个 Redis 客户端。在上述代码中,使用了 github.com/redis/go-redis/v9 库来创建 Redis 客户端,并为其设置了地址、密码、用户名和数据库编号
    • 创建锁池:redsync 需要一个锁池(Pool)来管理锁,使用 goredis.NewPool 方法来创建一个基于 go-redis 客户端的锁池
    • 创建分布式锁:使用 redsync.New 方法来创建一个 Redsync 实例,并使用 NewMutex 方法来创建一个分布式锁,可以为锁指定一个名称,并设置锁的过期时间。
    • 加锁:调用 Lock 方法来尝试获取锁,如果加锁失败(例如,其他进程已经持有锁),则该方法会返回一个错误
    • 执行业务逻辑:在成功获取锁之后,执行您的业务逻辑
    • 解锁:完成业务逻辑后,调用 Unlock 方法来释放锁,请注意,Unlock 方法会返回一个布尔值和一个错误,布尔值表示是否成功释放了锁,而错误则表示在解锁过程中是否发生了错误

注意事项

  • 死锁:务必为锁设置合理的过期时间,以防止死锁
  • 重试机制:当加锁失败时,您可能需要实现一个重试机制来等待一段时间后再次尝试加锁
  • 解锁失败:虽然 Unlock 方法会尝试释放锁,但在某些情况下(例如,Redis 实例崩溃或网络问题),解锁可能会失败。因此,您应该始终检查 Unlock 方法的返回值,并在必要时处理解锁失败的情况
  • 并发控制:在分布式系统中,除了使用分布式锁之外,您还需要考虑其他并发控制策略,例如消息队列、事件驱动架构等
  • 安全性:确保 Redis 实例的安全性,包括使用强密码、限制访问权限等。此外,如果您的系统对安全性有更高的要求,您可能需要考虑使用更复杂的分布式锁实现,例如基于 ZooKeeper 或 etcd 的分布式锁

参考文档

相关推荐
Hello.Reader4 分钟前
Redis大Key问题全解析
数据库·redis·bootstrap
B1nna3 小时前
Redis学习(三)缓存
redis·学习·缓存
网络风云3 小时前
【魅力golang】之-反射
开发语言·后端·golang
time_silence8 小时前
微服务——数据管理与一致性
微服务·云原生·架构
A22749 小时前
Redis——缓存雪崩
java·redis·缓存
weisian1519 小时前
Redis篇--应用篇3--数据统计(排行榜,计数器)
数据库·redis·缓存
言之。9 小时前
Redis单线程快的原因
数据库·redis·缓存
LYX369313 小时前
Docker 安装mysql ,redis,nacos
redis·mysql·docker
计算机毕设定制辅导-无忧学长13 小时前
Redis 持久化机制详解
redis
奋斗的老史15 小时前
Spring Retry + Redis Watch实现高并发乐观锁
java·redis·spring