Go微服务: redis分布式锁在集群中可能遇到的问题及其解决方案

2024-06-22 00:12

本文主要是介绍Go微服务: redis分布式锁在集群中可能遇到的问题及其解决方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

概述

  • 我们的 redis 一般都是集群来给我们程序提供服务的,单体的redis现在也不多见
  • 看到上面是主节点redis和下面是6个重节点redis,主节点和重节点的通讯都是畅通没问题的
  • 这个时候,我们有 gorouting 写我们的数据,那它就会用到我们的setNX
  • 写完数据内部是自动同步的,就是你的这个数据通过主节点同步到这些从节点了
  • 下面又有我们的 gorouting 去读我们的从节点,但是,我们是在高并发和网络不确定的情况下可能会遇到一些问题

可能会遇到的一些问题


1 )集群方面

  • 如果,我们上面是一个gorouting,在主节点上,它用setNX写数据,如果主节点挂了
  • 集群就从我们的所有子节点中抽取一个节点,当成主节点顶上去,集群又可以正常工作了
  • 这个时候,有一个gorouting在右下角,它又来读数据了,由于我们上面刚写了数据
  • 还没有来得及同步到最后一个 redis 这个节点上, 但是面临着新的gorouting读取数据或操作
  • 这个时候最后这一台redis它是拿不到那个锁的,是没有同步到的
  • 最后来的 gorouting,就认为你没有锁, 或者说我要的资源,你没锁住
  • 那其他 gorouting 就认为它是无主的, 就可以锁, 这个时候就会造成一些问题

2 )网络方面

  • 正常的时候,写数据还有下面的从节点去获取数据,获取锁,都是没问题的
  • 如果是网络抖动或不通会有一些问题,由于redis它是网络传输的
  • 如果说我们右边的这网络络不通了,相当于右边的 redis 没有和主节点通讯
  • 这个时候我们的一个gorouting就来获取锁进行数据的操作
  • 如果这个时候,我要操作的资源没有上锁,那这个gorouting就认为它是还没有被加锁,就把这个锁锁上了
  • 所以这个地方也是有可能出问题的风险

解决方案


1 )使用 redLock

  • 锁不住资源,有可能因为节点挂了或网络抖动, 我们现在尝试使用 redLock 来解决这一个问题
  • redLock它没有master节点,也没有这个slave从节点,都是独立的
  • 每一个redis,都是有一个 SetNx 这么一个锁, 现在有两个协程来申请锁
  • redis集群的一般是7个,而不是说双数的, 如果双数的那我左边的 gorouting 获得3个
  • 右边的 gorouting 获得3个,他就要重新再做选举投票之类的东西
  • 基于redLock, 当左边的 gorouting 抢到了4个,那右边的只有3个就应该释放掉
  • 为下一次再运行做准备,右边这个锁就消失了

2 ) 源码

  • redlock把原来的master,slave这种模式改成了平等的模式,最终解决了问题

2.1 ) NewMutex

  • 在源码的 NewMutex 函数中
    // NewMutex returns a new distributed mutex with given name.
    func (r *Redsync) NewMutex(name string, options ...Option) *Mutex {m := &Mutex{name:   name,expiry: 8 * time.Second,tries:  32, // 重试 32 次delayFunc: func(tries int) time.Duration {return time.Duration(rand.Intn(maxRetryDelayMilliSec-minRetryDelayMilliSec)+minRetryDelayMilliSec) * time.Millisecond},genValueFunc:  genValue,driftFactor:   0.01, // 漂移因子timeoutFactor: 0.05, // 超时因子quorum:        len(r.pools)/2 + 1, // 法定数,找一半+1,大多数pools:         r.pools,}for _, o := range options {o.Apply(m)}if m.shuffle {randomPools(m.pools)}return m
    }
    
  • 上面 driftFactor 是说我们的服务器的时钟漂移
    • 比如说我们的A服务器是中午12点,但是B服务器是中午11点59分30秒
    • C服务器是中午的12点0分30秒,相当于它们每台服务器相差30秒
    • 这就是服务器的时间漂移,它不准,那这会导致什么呢?
  • 假如说我们的这个过期时间是8秒,那你差了30秒,肯定就是有的服务器会先释放锁
  • 那先释放锁,其他人就可以拿到锁,所以他就设置了一个因子
  • 关于 quorum 就如同上面的例子,7台服务器拿到了4台就是成功的

2.2 ) Lock

  • 回到锁定的函数 Lock 中,进入其 lockContext
    // lockContext locks m. In case it returns an error on failure, you may retry to acquire the lock by calling this method again.
    func (m *Mutex) lockContext(ctx context.Context, tries int) error {if ctx == nil {ctx = context.Background()}// 获取 base64 的值value, err := m.genValueFunc()if err != nil {return err}var timer *time.Timer// 对默认32次循环的操作for i := 0; i < tries; i++ {if i != 0 {if timer == nil {timer = time.NewTimer(m.delayFunc(i))} else {timer.Reset(m.delayFunc(i))}select {// 如果 完成 状态,则返回 ErrFailedcase <-ctx.Done():timer.Stop()// Exit early if the context is done.return ErrFailed// 没有完成,则不动case <-timer.C:// Fall-through when the delay timer completes.}}start := time.Now()// 拿到计数器和错误信息n, err := func() (int, error) {ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))defer cancel()/// 注意这里,最终的函数就是执行的这里return m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {return m.acquire(ctx, pool, value)})}()now := time.Now()// 下面是核心算法: 过期时间 - 拿锁的时间 - 漂移因子可能的时间until := now.Add(m.expiry - now.Sub(start) - time.Duration(int64(float64(m.expiry)*m.driftFactor)))// 判断是否是大多数并且没有过期,则直接进行赋值if n >= m.quorum && now.Before(until) {m.value = valuem.until = untilreturn nil}// 否则 release 进行释放_, _ = func() (int, error) {ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))defer cancel()return m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {return m.release(ctx, pool, value)})}()if i == tries-1 && err != nil {return err}}return ErrFailed
    }
    
  • 进入 actOnPoolsAsync 这里参数是一个函数
    func (m *Mutex) actOnPoolsAsync(actFn func(redis.Pool) (bool, error)) (int, error) {type result struct {node     intstatusOK boolerr      error}// 创建 channelch := make(chan result, len(m.pools))// 循环 poolsfor node, pool := range m.pools {// 开协程提速go func(node int, pool redis.Pool) {r := result{node: node}r.statusOK, r.err = actFn(pool)ch <- r}(node, pool)}var (n     = 0 // 计数器taken []interr   error // 错误)// 循环 poolsfor range m.pools {// 从 channel 中拿到结果r := <-chif r.statusOK {n++ // 计数器加加} else if r.err == ErrLockAlreadyExpired {err = multierror.Append(err, ErrLockAlreadyExpired)} else if r.err != nil {err = multierror.Append(err, &RedisError{Node: r.node, Err: r.err})} else {taken = append(taken, r.node)err = multierror.Append(err, &ErrNodeTaken{Node: r.node})}if m.failFast {// fast retrunif n >= m.quorum {return n, err}// fail fastif len(taken) >= m.quorum {return n, &ErrTaken{Nodes: taken}}}}if len(taken) >= m.quorum {return n, &ErrTaken{Nodes: taken}}return n, err
    }
    
  • 以上就是 redLock 源码锁的机制,通过源代码可以更好的理解框架
  • 即使上面一些细节点看不懂,跳过即可,前期可以先看大的实现脉络帮助我们理解

这篇关于Go微服务: redis分布式锁在集群中可能遇到的问题及其解决方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1082735

相关文章

linux生产者,消费者问题

pthread_cond_wait() :用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。 pthread_cond_wait() 必须与pthread_mutex 配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread

问题:第一次世界大战的起止时间是 #其他#学习方法#微信

问题:第一次世界大战的起止时间是 A.1913 ~1918 年 B.1913 ~1918 年 C.1914 ~1918 年 D.1914 ~1919 年 参考答案如图所示

2024.6.24 IDEA中文乱码问题(服务器 控制台 TOMcat)实测已解决

1.问题产生原因: 1.文件编码不一致:如果文件的编码方式与IDEA设置的编码方式不一致,就会产生乱码。确保文件和IDEA使用相同的编码,通常是UTF-8。2.IDEA设置问题:检查IDEA的全局编码设置和项目编码设置是否正确。3.终端或控制台编码问题:如果你在终端或控制台看到乱码,可能是终端的编码设置问题。确保终端使用的是支持你的文件的编码方式。 2.解决方案: 1.File -> S

vcpkg安装opencv中的特殊问题记录(无法找到opencv_corexd.dll)

我是按照网上的vcpkg安装opencv方法进行的(比如这篇:从0开始在visual studio上安装opencv(超详细,针对小白)),但是中间出现了一些别人没有遇到的问题,虽然原因没有找到,但是本人给出一些暂时的解决办法: 问题1: 我在安装库命令行使用的是 .\vcpkg.exe install opencv 我的电脑是x64,vcpkg在这条命令后默认下载的也是opencv2:x6

问题-windows-VPN不正确关闭导致网页打不开

为什么会发生这类事情呢? 主要原因是关机之前vpn没有关掉导致的。 至于为什么没关掉vpn会导致网页打不开,我猜测是因为vpn建立的链接没被更改。 正确关掉vpn的时候,会把ip链接断掉,如果你不正确关掉,ip链接没有断掉,此时你vpn又是没启动的,没有域名解析,所以就打不开网站。 你可以在打不开网页的时候,把vpn打开,你会发现网络又可以登录了。 方法一 注意:方法一虽然方便,但是可能会有

Eureka高可用注册中心registered-replicas没有分布式注册中心

自己在学习过程中发现,如果Eureka挂掉了,其他的Client就跑不起来了,那既然是商业项目,还是要处理好这个问题,所以决定用《Spring Cloud微服务实战》(PDF版在全栈技术交流群中自行获取)中说的“高可用注册中心”。 一开始我yml的配置是这样的 server:port: 8761eureka:instance:hostname: 127.0.0.1client:fetch-r

Windows/macOS/Linux 安装 Redis 和 Redis Desktop Manager 可视化工具

本文所有安装都在macOS High Sierra 10.13.4进行,Windows安装相对容易些,Linux安装与macOS类似,文中会做区分讲解 1. Redis安装 1.下载Redis https://redis.io/download 把下载的源码更名为redis-4.0.9-source,我喜欢跟maven、Tomcat放在一起,就放到/Users/zhan/Documents

springboot家政服务管理平台 LW +PPT+源码+讲解

3系统的可行性研究及需求分析 3.1可行性研究 3.1.1技术可行性分析 经过大学四年的学习,已经掌握了JAVA、Mysql数据库等方面的编程技巧和方法,对于这些技术该有的软硬件配置也是齐全的,能够满足开发的需要。 本家政服务管理平台采用的是Mysql作为数据库,可以绝对地保证用户数据的安全;可以与Mysql数据库进行无缝连接。 所以,家政服务管理平台在技术上是可以实施的。 3.1

vue同页面多路由懒加载-及可能存在问题的解决方式

先上图,再解释 图一是多路由页面,图二是路由文件。从图一可以看出每个router-view对应的name都不一样。从图二可以看出层路由对应的组件加载方式要跟图一中的name相对应,并且图二的路由层在跟图一对应的页面中要加上components层,多一个s结尾,里面的的方法名就是图一路由的name值,里面还可以照样用懒加载的方式。 页面上其他的路由在路由文件中也跟图二是一样的写法。 附送可能存在

vue+elementui--$message提示框被dialog遮罩层挡住问题解决

最近碰到一个先执行this.$message提示内容,然后接着弹出dialog带遮罩层弹框。那么问题来了,message提示框会默认被dialog遮罩层挡住,现在就是要解决这个问题。 由于都是弹框,问题肯定是出在z-index比重问题。由于用$message方式是写在js中而不是写在html中所以不是很好直接去改样式。 不过好在message组件中提供了customClass 属性,我们可以利用