深入理解Redis锁与Backoff重试机制在Go中的实现
文章目录
- 流程图
- Redis锁的深入实现
- Backoff重试策略的深入探讨
- 结合Redis锁与Backoff策略的高级应用
- 具体实现
- 结论
在构建分布式系统时,确保数据的一致性和操作的原子性是至关重要的。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] thenreturn redis.call("del", KEYS[1])elsereturn 0end`_, 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.Millisecondreturn 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 lockimport ("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.Millisecondreturn 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.Mutexticks []intjitter 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 = flagb.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 lockimport ("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.PoolexpireSecond uint32backoff 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] thenreturn redis.call("del", KEYS[1])elsereturn 0end;`script := redis.NewScript(1, luaScript)_, err := script.Do(conn, key, randVal)return err
}
- 重试
package lockimport "time"type mustSetOperation func() (string, error)type ErrNotify func(error)func mustSetRetryNotify1(operation mustSetOperation, b Backoff, notify ErrNotify) (string, error) {var err errorvar randVal stringvar wait time.Durationvar retry boolvar n intfor {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中的实现
文章目录 流程图Redis锁的深入实现Backoff重试策略的深入探讨结合Redis锁与Backoff策略的高级应用具体实现结论 在构建分布式系统时,确保数据的一致性和操作的原子性是至关重要的。Redis锁作为一种高效且广泛使用的分布式锁机制,能够帮助我们在多进程或分…...

uniapp-小程序开发0-1笔记大全
uniapp官网: https://uniapp.dcloud.net.cn/tutorial/syntax-js.html uniapp插件市场: https://ext.dcloud.net.cn/ uviewui类库: https://www.uviewui.com/ 柱状、扇形、仪表盘库: https://www.ucharts.cn/v2/#/ CSS样式&…...
Go语言数据库操作深入讲解
go操作MySQL 使用第三方开源的mysql库: github.com/go-sql-driver/mysql (mysql驱动)github.com/jmoiron/sqlx (基于mysql驱动的封装) 命令行输入 : go get github.com/go-sql-driver/mysqlgo get github.com/jmoiron/sqlx Insert操作 登录后复制 // 连接Mysql data…...
搜维尔科技:SenseGlove Nova 2触觉反馈手套开箱测评
SenseGlove Nova 2触觉反馈手套开箱测评 搜维尔科技:SenseGlove Nova 2触觉反馈手套开箱测评...

步步精科技诚邀您参加2024慕尼黑华南电子展
尊敬的客户: 我们诚挚地邀请您参加即将于2024年10月14日至10月16日在深圳国际会展中心 (宝安新馆)举办的慕尼黑华南电子展(electronica South China)。本届将聚焦人工智能、数据中心、新型储能、无线通信、硬件安全、新能源汽车、第三代半导…...

OPC UA与PostgreSQL如何实现无缝连接?
随着工业4.0的推进,数据交换和集成在智能制造中扮演着越来越重要的角色。OPC UA能够实现设备与设备、设备与系统之间的高效数据交换。而PostgreSQL则是一种强大的开源关系型数据库管理系统,广泛应用于数据存储和管理。如何将OPC UA与PostgreSQL结合起来&…...
C语言[斐波那契数列2]
本篇文章讲述前一篇文章的细节,方便大家进行代码的运算。 本次代码题为: 输出斐波那契数列的前20位数,每行4位数。 详细解释: 在 main 函数中,首先定义了循环变量 i 和用于存储斐波那契数列项的三个长整型变量 f1 、 f2 和 temp 。其…...

八、Linux之实用指令
1、指定运行级别 1.1 基本介绍 运行级别说明 0 :关机 1 :单用户【找回丢失密码】 2:多用户状态没有网络服务(用的非常少) 3:多用户状态有网络服务(用的最多) 4:系统未使…...

2024_E_100_连续字母长度
连续字母长度 题目描述 给定一个字符串,只包含大写字母,求在包含同一字母的子串中,长度第 k 长的子串的长度,相同字母只取最长的那个子串。 输入描述 第一行有一个子串(1<长度<100),只包含大写字母。 第二行为…...

清空redo导致oracle故障恢复---惜分飞
客户由于空间不足,使用> redo命令清空了oracle的redo文件 数据库挂掉之后,启动报错 Fri Oct 04 10:32:57 2024 alter database open Beginning crash recovery of 1 threads parallel recovery started with 31 processes Started redo scan Errors in file /home/oracle…...
VAE(与GAN)
VAE 1. VAE 模型概述 变分自编码器(Variational Autoencoder, VAE)是一种生成模型,主要用于学习数据的潜在表示并生成新样本。它由两个主要部分组成:编码器和解码器。 编码器:将输入数据映射到潜在空间,…...
【高等数学】多元微分学(二)
隐函数的偏导数 二元方程的隐函数 F ( x , y ) 0 F(x,y)0 F(x,y)0 推出隐函数形式 y y ( x ) yy(x) yy(x). 欲求 d y d x \frac{d y}{d x} dxdy 需要对 F 0 F0 F0 两边同时对 x x x 求全导 0 d d x F ( x , y ( x ) ) ∂ F ∂ x d x d x ∂ F ∂ y d y d x ∂ F…...
.NET 中的 Web服务(Web Services)和WCF(Windows Communication Foundation)
一、引言 在当今数字化时代,不同的软件系统和应用程序之间需要进行高效、可靠的通信与数据交换。.NET 框架中的 Web 服务和 WCF(Windows Communication Foundation)为此提供了强大的技术支持。它们在构建分布式应用程序、实现跨平台通信以及…...
Linux小知识2 系统的启动
我们在上文中介绍了文件系统,提到了Linux的文件系统存在一个块的概念,其中有一个特殊的块:引导块。这和我们这里要讲的系统启动有关。 BIOS 基本输入输出系统,基本上是一个操作系统最早实现也是最早运行的第一个程序。是一个比较…...

Oracle-19g数据库的安装
简介 Oracle是一家全球领先的数据库和云解决方案提供商。他们提供了一套完整的技术和产品,包括数据库管理系统、企业级应用程序、人工智能和机器学习工具等。Oracle的数据库管理系统是业界最受欢迎和广泛使用的数据库之一,它可以管理和存储大量结构化和…...

Dubbo快速入门(二):第一个Dubbo程序(附源码)
文章目录 一、生产者工程0.目录结构1.依赖2.配置文件3.启动类4.生产者服务 二、消费者工程0.目录结构1.依赖2.配置文件3.启动类4.服务接口5.controller接口 三、测试代码 本博客配套源码:gitlab仓库 首先,在服务器上部署zookeeper并运行,可以…...
不同数据类型转换与转义的对比差异
(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 在C和C语言中,类型转换与转义是有点像的,有时可能被误解,这块需要仔细辨别。 类型转换形如,把不同字节数或相同字节数的类型值进行转换,强调的是数值转换过去&…...

Kylin系统安装VMwareTools工具
如下图所示,安装好Kylin系统之后,还未安装VMwareTools工具,导致系统画面无法填充虚拟机 正常安装了VMwareTools工具后的系统画面 所以,接下来我们介绍一下如何在Kylin系统下安装VMwareTools工具 首先,点击VMware工具栏…...
uni-app 拍照图片添加水印
获取图片信息 uni.chooseImage({count: 6, //默认9sizeType: ["original", "compressed"], //可以指定是原图还是压缩图,默认二者都有sourceType: ["camera"], //从相册选择success: async function (result: any) {if (!props.isMar…...
Docker-registry私有镜像仓库的安装
Docker-registry私有镜像仓库的安装 我在这里的镜像仓库搭建在ip为192.168.3.23的虚机中。 安装docker-registry 1.拉取镜像 # docker pull registry 2.查看镜像 # docker images REPOSITORY TAG IMAGE ID CREATE…...

国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...

idea大量爆红问题解决
问题描述 在学习和工作中,idea是程序员不可缺少的一个工具,但是突然在有些时候就会出现大量爆红的问题,发现无法跳转,无论是关机重启或者是替换root都无法解决 就是如上所展示的问题,但是程序依然可以启动。 问题解决…...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...

Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...