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

参考文档

相关推荐
懒羊羊大王呀16 分钟前
Ubuntu20.04中 Redis 的安装和配置
linux·redis
John Song2 小时前
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
数据库·redis·哈希算法
吾日三省Java2 小时前
微服务体系下将环境流量路由到开发本机
微服务·系统架构·团队开发
海奥华24 小时前
go中的接口返回设计思想
开发语言·后端·golang
飞川撸码9 小时前
【LeetCode 热题100】网格路径类 DP 系列题:不同路径 & 最小路径和(力扣62 / 64 )(Go语言版)
算法·leetcode·golang·动态规划
Zfox_11 小时前
Redis:Hash数据类型
服务器·数据库·redis·缓存·微服务·哈希算法
呼拉拉呼拉11 小时前
Redis内存淘汰策略
redis·缓存
咖啡啡不加糖15 小时前
Redis大key产生、排查与优化实践
java·数据库·redis·后端·缓存
MickeyCV15 小时前
使用Docker部署MySQL&Redis容器与常见命令
redis·mysql·docker·容器·wsl·镜像
雪碧聊技术15 小时前
将单体架构项目拆分成微服务时的两种工程结构
微服务·架构·module·project·工程结构