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 的分布式锁

参考文档

相关推荐
王彬泽4 小时前
【微服务】服务注册与发现、分布式配置管理 - Nacos
微服务·服务注册与发现·分布式配置管理
wclass-zhengge4 小时前
Redis篇(最佳实践)(持续更新迭代)
redis·缓存·bootstrap
Dylanioucn4 小时前
【分布式微服务云原生】探索Redis:数据结构的艺术与科学
数据结构·redis·分布式·缓存·中间件
Code成立5 小时前
1、深入理解Redis线程模型
数据库·redis·bootstrap
__AtYou__13 小时前
Golang | Leetcode Golang题解之第448题找到所有数组中消失的数字
leetcode·golang·题解
攸攸太上13 小时前
Spring Gateway学习
java·后端·学习·spring·微服务·gateway
千年死缓14 小时前
go+redis基于tcp实现聊天室
redis·tcp/ip·golang
小小娥子16 小时前
Redis的基础认识与在ubuntu上的安装教程
java·数据库·redis·缓存
DieSnowK16 小时前
[Redis][集群][下]详细讲解
数据库·redis·分布式·缓存·集群·高可用·新手向
一直在进步的派大星17 小时前
Docker 从安装到实战
java·运维·docker·微服务·容器