Redis分布式锁(源码篇)(待续)

前引

看看源码

上锁SetNX

Go 复制代码
func (c cmdable) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd {
	var cmd *BoolCmd
	switch expiration {
	case 0:
		cmd = NewBoolCmd(ctx, "set", key, value, "nx")          // ← 新版;老版是 "setnx"
	case KeepTTL:
		cmd = NewBoolCmd(ctx, "set", key, value, "keepttl", "nx")
	default:
		if usePrecise(expiration) {
			cmd = NewBoolCmd(ctx, "set", key, value, "px", formatMs(ctx, expiration), "nx")
		} else {
			cmd = NewBoolCmd(ctx, "set", key, value, "ex", formatSec(ctx, expiration), "nx")
		}
	}

	_ = c(ctx, cmd)
	return cmd
}

对于redis<2.6.12版本(早期)之前,上锁(set key value),和设置过期时间(expire time)是分开的两条命令,那么对于其他组件完全可能在中间穿插命令,从而导致并发问题。

于是redis>=2.6.12版本至今,上锁和设置过期时间等操作合为一条指令。并且将这些扩展为参数选择。

比如nx代表not exist,代表在之前没有加锁的情况,ex/px,分别代表秒级和毫秒级加锁。

一条命令就能原子完成「条件判断 + 设值 + 过期」 ,比如 SET lock val NX EX 30,从而有效处理了并发问题。

入参返回

以上代码中,注意的就是reciever是一个 cmdable类型,也就是任何支持cmdable接口的支持发送redis命令的组件,都可以调用。

返回值是一个叫 *boolcmd类型的【发送请求+结果容器】的结构体。大概简化如下

Go 复制代码
type BoolCmd struct {
    args []interface{} // 存命令参数,比如 ["set", "lock", "1", "nx", "ex", "30"] 
    err error // 存执行错误(网络错误、服务端错误等)
    val bool // 存最终的布尔结果 // 还有一些状态字段,标记是否执行完成 
}

是一个bool类型返回体的模版,比如最常见的返回一个bool值。如果是get请求,返回字符串,就用stringcmd来返回。

那么注意对于redis命令,并不是马上发出去,马上发回来的。遵循的是【批量发+批量回】的模式。比如攒了50个命令统一发放,这样只用一次网络i/o就可以,相比一个命令发一次节省时间,开销小 。原生 go-redis Pipeline 是手动显式触发 :由开发者主动调用 Exec() 来发送所有积攒的命令。

那么在发送命令后,会马上返回一个空壳cmd,而且命令实际上在本地积攒 ,到了一定量发送后,等待响应信息批量返回时。这时空壳cmd才负载了响应信息。

那么在中途如果去访问响应信息,就会陷入阻塞 ,比如方法叫叫 Result() / Get(),在结果没返回时调用,程序就会阻塞在这个方法上。

根据expire分流

  • 如果设置的过期时间为0,代表永久持有

  • keepttl(keep time to live),是一个定义的常量,值为-1,标志了保持【约定时间不变,仅修改value值这种情况】(对于redis的set命令是全量覆盖,不仅修改key还会抹掉过期时间,发生静默错误)

  • 一般情况下续约,先通过usePrecise()判断采用秒级还是毫秒级时间。代码如下
go 复制代码
func usePrecise(d time.Duration) bool {
	return d < time.Second || d%time.Second != 0
}

_ = c(ctx, cmd)忽略掉返回值,因为错误信息都会装到cmd这个结构体中。

context的运用

context 本质上是 Go 语言为「并发协程的生命周期管理」设计的一套标准化的取消信号广播机制 + 请求域元数据载体

context作为入参的作用主要有3点

  • 用context.withTimeout设定一个超时控制。

  • 在http请求或者链路中,一个节点一个节点连接,通过Done()方法,能实现当上游http请求取消后,链式的强制取消下游redis命令等操作(级联取消)。释放goroutine内存。

  • 存储一些原数据,trace id等,后续可以追踪链路。

错误处理

对于上锁操作,锁的key已经存在导致上锁失败,和因为网络等外部原因导致的失败要分开。以下接口方案。

GO 复制代码
ok, err := cmd.Result() 
if err != nil { 
    // 处理执行错误:重试、告警、降级 
    return fmt.Errorf("执行加锁命令失败: %w", err)
    } // 到这里 err 为 nil,ok 才是可信的业务结果
if ok {
          // 加锁成功 
} else {
    // 加锁失败:锁已被其他客户端持有
}

err代表外部错误,ok代表业务信息。

相关推荐
姜鱼问生2 小时前
Nginx 报错 Too many open files?一招 ulimit 解决高并发下的文件句柄耗尽
后端
挖掘狂人2 小时前
用 Python 从零搭一套 RAG 系统:40 行跑通,再谈优化
后端·程序员·ai编程
东创yu2 小时前
酒店弱电怎么验收?四个阶段与逐项检查清单
后端
QZQ541882 小时前
高并发系统如何保护自己:限流算法原理、实现与选型
后端
鶴哥只手遮天2 小时前
企业级 .NET 领域驱动框架深度解析-第 1 章 全景图:一套框架的七条脊柱
后端·架构·领域驱动设计
小满zs3 小时前
Go语言第十四章(文件操作)
后端·go
打工仔折腾 AI3 小时前
Linux 网络编程:从零掌握 TCP Socket 编程与 C++ 实战落地
linux·网络·人工智能·后端·python·tcp/ip·性能优化
沙蒿同学3 小时前
Wails v3 实战:一个桌面提醒客户端,从 v2 升到 v3 的全过程
前端·后端·github