深入理解Redis锁与Backoff重试机制在Go中的实现
在构建分布式系统时,确保数据的一致性和操作的原子性是至关重要的。Redis锁作为一种高效且广泛使用的分布式锁机制,能够帮助我们在多进程或分布式环境中同步访问共享资源。本文将深入探讨如何在Go语言中实现Redis锁,并结合Backoff重试策略来优化锁的获取过程,确保系统的健壮性和可靠性。
Redis锁的深入实现
在Go语言中,我们使用github.com/gomodule/redigo/redis包来操作Redis。Redis锁的实现依赖于Redis的SET命令,该命令支持设置键值对,并且可以带有过期时间(EX选项)和仅当键不存在时才设置(NX选项)。以下是一个更详细的Redis锁实现示例:
func SetWithContext(ctx context.Context, redisPool *redis.Pool, key string, expireSecond uint32) (bool, string, error) {
// ...省略部分代码...
conn, err := redisPool.GetContext(ctx)
if err != nil {
return false, "", err
}
defer conn.Close()
randVal := generateRandVal() // 生成随机值
_, err = conn.Do("SET", key, randVal, "NX", "EX", int(expireSecond))
if err != nil {
return false, "", err
}
return true, randVal, nil
}
在上述代码中,generateRandVal()函数用于生成一个唯一的随机值,这个值在释放锁时用来验证是否是锁的持有者。expireSecond参数确保了即使客户端崩溃或网络问题发生,锁也会在一定时间后自动释放,避免死锁。
释放锁时,我们使用Lua脚本来确保只有持有锁的客户端才能删除键:
func ReleaseWithContext(ctx context.Context, redisPool *redis.Pool, key string, randVal string) error {
// ...省略部分代码...
conn, err := redisPool.GetContext(ctx)
if err != nil {
return err
}
defer conn.Close()
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
_, err = conn.Do("EVAL", script, 1, key, randVal)
return err
}
Backoff重试策略的深入探讨
在分布式系统中,获取锁可能会因为网络延迟、高负载或其他原因而失败。Backoff重试策略通过在重试之间引入等待时间来减轻这些问题的影响。在提供的代码中,我们定义了多种Backoff策略,每种策略都有其特定的使用场景和优势。
例如,指数退避策略ExponentialBackoff的实现如下:
func (b *ExponentialBackoff) Next(retry int) (time.Duration, bool) {
// ...省略部分代码...
m := math.Min(r*b.t*math.Pow(b.f, float64(retry)), b.m)
if m >= b.m {
return 0, false
}
d := time.Duration(int64(m)) * time.Millisecond
return d, true
}
在这个策略中,重试间隔随重试次数的增加而指数级增长,但有一个最大值限制。这有助于在遇到连续失败时,逐步增加等待时间,避免立即重载系统。
结合Redis锁与Backoff策略的高级应用
将Redis锁与Backoff策略结合起来,可以创建一个健壮的锁获取机制。例如,我们可以定义一个MustSetRetry方法,该方法会不断尝试获取锁,直到成功为止:
func (r *RedisLock) MustSetRetry(ctx context.Context, key string) (string, error) {
op := func() (string, error) {
return r.MustSet(ctx, key)
}
notifyFunc := func(err error) {
// ...错误处理逻辑...
}
return mustSetRetryNotify(op, r.backoff, notifyFunc)
}
在这个方法中,mustSetRetryNotify函数负责执行重试逻辑,直到MustSet方法成功获取锁或达到最大重试次数。通过这种方式,我们能够确保即使在高竞争环境下,也能以一种可控和安全的方式获取锁。
具体实现
- backoff
package lock
import (
"math"
"math/rand"
"sync"
"time"
)
// BackoffFunc specifies the signature of a function that returns the
// time to wait before the next call to a resource. To stop retrying
// return false in the 2nd return value.
type BackoffFunc func(retry int) (time.Duration, bool)
// Backoff allows callers to implement their own Backoff strategy.
type Backoff interface {
// Next implements a BackoffFunc.
Next(retry int) (time.Duration, bool)
}
// -- ZeroBackoff --
// ZeroBackoff is a fixed backoff policy whose backoff time is always zero,
// meaning that the operation is retried immediately without waiting,
// indefinitely.
type ZeroBackoff struct{}
// Next implements BackoffFunc for ZeroBackoff.
func (b ZeroBackoff) Next(retry int) (time.Duration, bool) {
return 0, true
}
// -- StopBackoff --
// StopBackoff is a fixed backoff policy that always returns false for
// Next(), meaning that the operation should never be retried.
type StopBackoff struct{}
// Next implements BackoffFunc for StopBackoff.
func (b StopBackoff) Next(retry int) (time.Duration, bool) {
return 0, false
}
// -- ConstantBackoff --
// ConstantBackoff is a backoff policy that always returns the same delay.
type ConstantBackoff struct {
interval time.Duration
}
// NewConstantBackoff returns a new ConstantBackoff.
func NewConstantBackoff(interval time.Duration) *ConstantBackoff {
return &ConstantBackoff{interval: interval}
}
// Next implements BackoffFunc for ConstantBackoff.
func (b *ConstantBackoff) Next(retry int) (time.Duration, bool) {
return b.interval, true
}
// -- Exponential --
// ExponentialBackoff implements the simple exponential backoff described by
// Douglas Thain at http://dthain.blogspot.de/2009/02/exponential-backoff-in-distributed.html.
type ExponentialBackoff struct {
t float64 // initial timeout (in msec)
f float64 // exponential factor (e.g. 2)
m float64 // maximum timeout (in msec)
}
// NewExponentialBackoff returns a ExponentialBackoff backoff policy.
// Use initialTimeout to set the first/minimal interval
// and maxTimeout to set the maximum wait interval.
func NewExponentialBackoff(initialTimeout, maxTimeout time.Duration) *ExponentialBackoff {
return &ExponentialBackoff{
t: float64(int64(initialTimeout / time.Millisecond)),
f: 2.0,
m: float64(int64(maxTimeout / time.Millisecond)),
}
}
// Next implements BackoffFunc for ExponentialBackoff.
func (b *ExponentialBackoff) Next(retry int) (time.Duration, bool) {
r := 1.0 + rand.Float64() // random number in [1..2]
m := math.Min(r*b.t*math.Pow(b.f, float64(retry)), b.m)
if m >= b.m {
return 0, false
}
d := time.Duration(int64(m)) * time.Millisecond
return d, true
}
// -- Simple Backoff --
// SimpleBackoff takes a list of fixed values for backoff intervals.
// Each call to Next returns the next value from that fixed list.
// After each value is returned, subsequent calls to Next will only return
// the last element. The values are optionally "jittered" (off by default).
type SimpleBackoff struct {
sync.Mutex
ticks []int
jitter bool
}
// NewSimpleBackoff creates a SimpleBackoff algorithm with the specified
// list of fixed intervals in milliseconds.
func NewSimpleBackoff(ticks ...int) *SimpleBackoff {
return &SimpleBackoff{
ticks: ticks,
jitter: false,
}
}
// Jitter enables or disables jittering values.
func (b *SimpleBackoff) Jitter(flag bool) *SimpleBackoff {
b.Lock()
b.jitter = flag
b.Unlock()
return b
}
// jitter randomizes the interval to return a value of [0.5*millis .. 1.5*millis].
func jitter(millis int) int {
if millis <= 0 {
return 0
}
return millis/2 + rand.Intn(millis)
}
// Next implements BackoffFunc for SimpleBackoff.
func (b *SimpleBackoff) Next(retry int) (time.Duration, bool) {
b.Lock()
defer b.Unlock()
if retry >= len(b.ticks) {
return 0, false
}
ms := b.ticks[retry]
if b.jitter {
ms = jitter(ms)
}
return time.Duration(ms) * time.Millisecond, true
}
关键Backoff策略:
- ZeroBackoff: 不等待,立即重试。
- StopBackoff: 从不重试。
- ConstantBackoff: 固定等待时间。
- ExponentialBackoff: 指数增长的等待时间。
- SimpleBackoff: 提供一组固定的等待时间,可选择是否添加随机抖动。
- 锁
package lock
import (
"context"
"errors"
"fmt"
"time"
"github.com/gomodule/redigo/redis"
)
var (
// 防止孤儿lock没release
// 目前expire过期时间的敏感度是考虑为一致的敏感度
defaultExpireSecond uint32 = 30
)
var (
ErrLockSet = errors.New("lock set err")
ErrLockRelease = errors.New("lock release err")
ErrLockFail = errors.New("lock fail")
)
// RedisLockIFace 在common redis上封一层浅封装
// 将redis pool 与expire second作为redis lock已知数据
type RedisLockIFace interface {
MustSet(ctx context.Context, k string) (string, error)
MustSetRetry(ctx context.Context, k string) (string, error) // 必须设置成功并有重试机制
Release(ctx context.Context, k string, randVal string) error
}
// RedisLock nil的实现默认为true
type RedisLock struct {
redisPool *redis.Pool
expireSecond uint32
backoff Backoff
}
// An Option configures a RedisLock.
type Option interface {
apply(*RedisLock)
}
// optionFunc wraps a func so it satisfies the Option interface.
type optionFunc func(*RedisLock)
func (f optionFunc) apply(log *RedisLock) {
f(log)
}
// WithBackoff backoff set
func WithBackoff(b Backoff) Option {
return optionFunc(func(r *RedisLock) {
r.backoff = b
})
}
func NewRedisLock(redisPool *redis.Pool, opts ...Option) *RedisLock {
r := &RedisLock{
redisPool: redisPool,
expireSecond: defaultExpireSecond,
backoff: NewExponentialBackoff(30*time.Millisecond, 500*time.Millisecond), // default backoff
}
for _, opt := range opts {
opt.apply(r)
}
return r
}
func (r *RedisLock) Set(ctx context.Context, key string) (bool, string, error) {
if r == nil {
return true, "", nil
}
isLock, randVal, err := SetWithContext(ctx, r.redisPool, key, r.expireSecond)
if err != nil {
return isLock, randVal, ErrLockSet
}
return isLock, randVal, err
}
// MustSetRetry 必须设置成功并带有重试功能
func (r *RedisLock) MustSetRetry(ctx context.Context, key string) (string, error) {
op := func() (string, error) {
return r.MustSet(ctx, key)
}
notifyFunc := func(err error) {
if err == ErrLockFail {
fmt.Printf("RedisLock.MustSetRetry redis must set err: %v", err)
} else {
fmt.Printf("RedisLock.MustSetRetry redis must set err: %v", err)
}
}
return mustSetRetryNotify(op, r.backoff, notifyFunc)
}
func (r *RedisLock) MustSet(ctx context.Context, key string) (string, error) {
isLock, randVal, err := r.Set(ctx, key)
if err != nil {
return "", err
}
if !isLock {
return "", ErrLockFail
}
return randVal, nil
}
func (r *RedisLock) Release(ctx context.Context, key string, randVal string) error {
if r == nil {
fmt.Printf("that the implementation of redis lock is nil")
return nil
}
err := ReleaseWithContext(ctx, r.redisPool, key, randVal)
if err != nil {
fmt.Printf("s.RedisLock.ReleaseWithContext fail, err: %v", err)
return ErrLockRelease
}
return nil
}
func SetWithContext(ctx context.Context, redisPool *redis.Pool, key string, expireSecond uint32) (bool, string, error) {
if expireSecond == 0 {
return false, "", fmt.Errorf("expireSecond参数必须大于0")
}
conn, _ := redisPool.GetContext(ctx)
defer conn.Close()
randVal := time.Now().Format("2006-01-02 15:04:05.000")
reply, err := conn.Do("SET", key, randVal, "NX", "PX", expireSecond*1000)
if err != nil {
return false, "", err
}
if reply == nil {
return false, "", nil
}
return true, randVal, nil
}
func ReleaseWithContext(ctx context.Context, redisPool *redis.Pool, key string, randVal string) error {
conn, _ := redisPool.GetContext(ctx)
defer conn.Close()
luaScript := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end;
`
script := redis.NewScript(1, luaScript)
_, err := script.Do(conn, key, randVal)
return err
}
- 重试
package lock
import "time"
type mustSetOperation func() (string, error)
type ErrNotify func(error)
func mustSetRetryNotify1(operation mustSetOperation, b Backoff, notify ErrNotify) (string, error) {
var err error
var randVal string
var wait time.Duration
var retry bool
var n int
for {
if randVal, err = operation(); err == nil {
return randVal, nil
}
if b == nil {
return "", err
}
n++
wait, retry = b.Next(n)
if !retry {
return "", err
}
if notify != nil {
notify(err)
}
time.Sleep(wait)
}
}
- 使用
func main() {
backoff := lock.NewExponentialBackoff(
time.Duration(20)*time.Millisecond,
time.Duration(1000)*time.Millisecond,
)
redisPool := &redis.Pool{
MaxIdle: 3,
IdleTimeout: 240 * time.Second,
// Dial or DialContext must be set. When both are set, DialContext takes precedence over Dial.
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp",
"redis host",
redis.DialPassword("redis password"),
)
},
}
redisLock := lock.NewRedisLock(redisPool, lock.WithBackoff(backoff))
ctx := context.Background()
s, err := redisLock.MustSetRetry(ctx, "lock_user")
if err != nil && err == lock.ErrLockFail {
fmt.Println(err)
return
}
time.Sleep(20 * time.Second)
defer func() {
_ = redisLock.Release(ctx, "lock_user", s)
}()
return
}
结论
通过深入理解Redis锁和Backoff重试策略的实现,我们可以构建出既能够保证资源访问的原子性,又能在面对网络波动或系统负载时保持稳定性的分布式锁机制。这不仅提高了系统的可用性,也增强了系统的容错能力。在实际开发中,合理选择和调整这些策略对于确保系统的高性能和高可靠性至关重要。通过精心设计的锁机制和重试策略,我们可以为分布式系统提供一个坚实的基础,以应对各种挑战和压力。
深入理解Redis锁与Backoff重试机制在Go中的实现的更多相关文章
- JAVA重试机制多种方式深入浅出
重试机制在分布式系统中,或者调用外部接口中,都是十分重要的. 重试机制可以保护系统减少因网络波动.依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制. 为了方便说明,先假设我们想要进行 ...
- 深入理解Redis主键失效原理及实现机制
http://blog.jobbole.com/71095/ 对于缓存失效,不同的缓存有不同的处理机制,可以说是大同中有小异,作者通过对Redis 文档与相关源码的仔细研读,为大家详细剖析了 Redi ...
- 深入理解Redis中的主键失效及其实现机制
参考:http://blog.sina.com.cn/s/articlelist_1221155353_0_1.html 作为一种定期清理无效数据的重要机制,主键失效存在于大多数缓存系统中,Reids ...
- redis锁机制介绍与实例
转自:https://m.jb51.net/article/154421.htm 今天小编就为大家分享一篇关于redis锁机制介绍与实例,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要 ...
- 深入理解Redis主键失效原理及实现机制(转)
原文:深入理解Redis主键失效原理及实现机制 作为一种定期清理无效数据的重要机制,主键失效存在于大多数缓存系统中,Redis 也不例外.在 Redis 提供的诸多命令中,EXPIRE.EXPIREA ...
- 源码级别理解 Redis 持久化机制
文章首发于公众号"蘑菇睡不着",欢迎来访~ 前言 大家都知道 Redis 是一个内存数据库,数据都存储在内存中,这也是 Redis 非常快的原因之一.虽然速度提上来了,但是如果数据 ...
- springboot系列——重试机制原理和应用,还有比这个讲的更好的吗(附完整源码)
1. 理解重试机制 2. 总结重试机制使用场景 3. spring-retry重试组件 4. 手写一个基于注解的重试组件 5. 重试机制下会出现的问题 6. 模板方法设计模式实现异步重试机制 如果有, ...
- Redis锁构造
单线程与隔离性 Redis是使用单线程的方式来执行事务的,事务以串行的方式运行,也就是说Redis中单个命令的执行和事务的执行都是线程安全的,不会相互影响,具有隔离性. 在多线程编程中,对于共享资源的 ...
- Kafka内核理解:消息的收集/消费机制
原文:https://www.cnblogs.com/daochong/p/6425762.html 一.Kafka数据收集机制 Kafka集群中由producer负责数据的产生,并发送到对应的Top ...
- 【原创】(求锤得锤的故事)Redis锁从面试连环炮聊到神仙打架。
这是why技术的第38篇原创文章 又到了一周一次的分享时间啦,老规矩,还是先荒腔走板的聊聊生活. 有上面的图是读大学的时候,一次自行车骑行途中队友抓拍的我的照片.拍照的地方,名字叫做牛背山,一个名字很 ...
随机推荐
- 备份服务器eBackup
目录 软件包方式安装eBackup备份软件 1.前景提要 2.创建虚拟机 3.安装备份软件. 4.安装 eBackup 补丁 5.配置 eBackup 服务器 6.访问web界 ...
- 通过程序名称kill掉所有的进程
通过程序名称kill掉所有的进程 今天差点把服务器搞崩了. 脚本的循环条件有问题,结果起了无数的nslookup,用pkill杀不掉,只能用kill一个个解决,服务器还被搞得慢得要命. 还好又黄队长, ...
- 【Java】Reflection 反射机制 01概述
Reflection 反射机制 反射允许程序在执行期间借助ReflectionAPI获取任何类的内部信息,直接操作任意对象的内部属性和方法 加载完类之后,堆内存的方法区产生了一个Class 类类型的对 ...
- 人形机器人sim2real —— 致使现实环境与仿真环境下的差距的因素 —— sim2real
下图引自:https://b2b.baidu.com/q/aland?q=7B7474317C2E72330F621B0F7D6F09247E747E610623742B&id=qid599a ...
- Google的Jax框架的JAX-Triton目前只能成功运行在TPU设备上(使用Pallas为jax编写kernel扩展)—— GPU上目前无法正常运行,目前正处于 experimental 阶段
使用Pallas为jax编写kernel扩展,需要使用JAX-Triton扩展包.由于Google的深度学习框架Jax主要是面向自己的TPU进行开发的,虽然也同时支持NVIDIA的GPU,但是支持力度 ...
- 记一次 .NET某智慧出行系统 CPU爆高分析
一:背景 1. 讲故事 前些天有位朋友找到我,说他们的系统出现了CPU 100%的情况,让你帮忙看一下怎么回事?dump也拿到了,本想着这种情况让他多抓几个,既然有了就拿现有的分析吧. 二:WinDb ...
- 电脑打不开CHM格式文件解决办法
如图所示 比如说jdk1.8的api 双击打开后,这个样子 就ok了
- 将 Rust 代码编译为 WASM
前言 在现代 Web 开发中,WebAssembly (WASM) 已成为一种强大的工具.它使得开发者可以在浏览器中运行高性能的代码,跨越传统的 JavaScript 性能限制.Rust 语言因其高效 ...
- OpenPCDet复现过程记录
0.前言 OpenPCDet项目之前我就复现过,一个很优秀的项目,这几天又需要用到这个项目,再次复现遇到了不少问题,特此记录复现的流程 1.环境准备 1.1.前置条件 以下是我安装的版本 CUDA 1 ...
- MFC中CString转int,double
CString str=L"123"; int n=_wtoi(str); //n=123 double d=_wtof(str); //d=123.0000 Vs2012中编译