前引
看看源码
上锁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代表业务信息。