Redis分布式锁原理及go的实现

2023-10-27

业务背景: 后台定时任务刷新Redis的数据到数据库中,有多台机器开启了此定时同步的任务,但是需要其中一台工作,其他的作为备用,提高可用性。使用Redis分布式锁进行限制,拿到锁的机器去执行具体业务,拿不到锁的继续轮询。

分布式锁原理

分布式锁:当多个进程不在同一个系统中,多个进程共同竞争同一个资源,用分布式锁控制多个进程对资源的互斥访问。采用Redis服务器存储锁信息(即SET一个Key表示已加锁),可以实现多进程的并发读锁的状态,如果没有锁,则只允许一个进程加锁。
Redis分布式锁实现的关键点:

问题 问题描述 解决方案
互斥性 保证只有一个client可以获取资源 加锁
原子性 如果锁不存在则执行加锁操作,必须是原子性操作 原子性命令或者执行Lua脚本
避免死锁 当拿到锁的Client因宕机或网络原因断线后,如果锁不能释放就会产生死锁 为锁加超时时间
锁超时时间设定 锁超时时间到了,业务没执行完问题 心跳线程,不断更新锁超时时间
锁的所属权 解铃还须系铃人,加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。 Client与锁进行一一对应,使用UUID作为锁的值
自动重连 网络故障导致Client连接Redis失败的情况,网络恢复后可以自动重连 轮询

实现方案

方案一:采用Redis的原子性命令“SET key value EX expire-time NX”可以实现分布式锁的基本功能,其中的NX(Not Exist)即判断是否已存在锁,如果不存在key则可进行操作,SET key value 等同于加锁,EX expire-time即设置超时时间,可以避免死锁,但是超时时间的设置需要根据具体业务设置一个合理的经验值,避免锁超时时间到了,业务没执行完的问题。

方案二:采用Lua脚本实现,Redis会将整个脚本作为一个整体执行,因此Lua脚本可以实现原子性操作。相较于方案一,此处增加了心跳线程,不断更新锁超时时间,解决锁超时时间设置不合理的问题;生成UUID(或者是随机数字符串)作为锁的值,用于保证锁与Client的一一对应;采用轮询来实现断线自动重连。
Talk is cheap. Show me the code.

实现方案1:SET EX NX

加锁流程图:
在这里插入图片描述
定义锁的变量名为lock,那么对应Redis命令:
判断是否加锁的命令:GET lock
加锁的命令:SET lock
设置超时时间的命令:EXPIRE expire-time
三条命令分开执行是不具有原子性的,比如可能会出现一个进程执行GET lock得到的结果为nil即尚未加锁,在其执行SET lock前另一个进程也执行了SET lock,导致两个进程都认为是可以加锁的,失去互斥性。
因此判断尚未加锁、加锁、设置超时时间必须原子操作,使用Redis的命令“SET key value EX expire-time NX”可以实现该原子操作。

package main

import (
 "fmt"
 "time"
 "github.com/gomodule/redigo/redis"
)

func main() {
    rds, err := redis.Dial("tcp", "127.0.0.1:6379")
    if err != nil {
        fmt.Println("Connect to redis error", err)
        return
    }   
    defer rds.Close()
    
    for true {
    	//检查是否有所与加锁必须是原子性操作
        result, err := rds.Do("SET", "lock", 1, "EX", 5, "NX")	
        if err != nil {
            fmt.Println("redis set error.", err)
            return
        }
        result, err = redis.String(result, err)
        // 加锁失败,继续轮询
		if result != "OK" {
            fmt.Println("SET lock failed.")
            time.Sleep(5 * time.Second)
            continue
        }

		// 加锁成功
        fmt.Println("work begining...")
        // 此处处理业务
        fmt.Println("work end");
        
        // 业务处理结束后释放锁
        result, err := rds.Do("del", "lock")
        break;
    }
}

此方法弊端是对超时时间的设置有要求,需要根据具体业务设置一个合理的经验值,避免锁超时时间到了,业务没执行完的问题。

实现方案2:Lua脚本

在这里插入图片描述

package main

import (
 "fmt"
 "time"
 "github.com/satori/go.uuid"
 "github.com/gomodule/redigo/redis"
)

var uuidClient uuid.UUID

const (
    SCRIPT_LOCK = ` 
    local res=redis.call('GET', KEYS[1])
    if res then
        return 0
    else
        redis.call('SET',KEYS[1],ARGV[1]);
        redis.call('EXPIRE',KEYS[1],ARGV[2])
        return 1
    end 
    `   

    SCRIPT_EXPIRE = ` 
    local res=redis.call('GET', KEYS[1])
    if not res then
        return -1
    end 
    if res==ARGV[1] then
        redis.call('EXPIRE', KEYS[1], ARGV[2])
        return 1
    else
        return 0
    end 
    `   

    SCRIPT_DEL = ` 
    local res=redis.call('GET', KEYS[1])
    if not res then 
        return -1
    end 
    if res==ARGV[1] then
        redis.call('DEL', KEYS[1])
    else
        return 0
    end 
    `   
)

func ResetExpire() {
    fmt.Println("Reset expire begin...")
    rds, err := redis.Dial("tcp", "127.0.0.1:6379")
    if err != nil {
        fmt.Println("Connect to redis server error.", err)
        return
    }

    for true {
        luaExpire := redis.NewScript(1, SCRIPT_EXPIRE)
        result, err := redis.Int(luaExpire.Do(rds, "lock", uuidClient.String(), 5))
        if err != nil {
            fmt.Println("luaExpire exec error", err)
            break
        }
        if result != 1 {
            fmt.Println("Reset expire failed.")
            break
        } else {
            fmt.Println("Reset expire succeed.")
        }
        time.Sleep(3 * time.Second)
    }
    fmt.Println("Reset expire end.")
}

func main() {
    for true {
        rds, err := redis.Dial("tcp", "127.0.0.1:6379")
        if err != nil {
            fmt.Println("Connect to redis server error.", err)
            time.Sleep(5 * time.Second)
            continue
        }
        defer rds.Close()
	
		// 生成UUID标识锁与Client的对应关系
        uuidClient,err = uuid.NewV4()	//也可以生成随机数字符串来代替
        if err != nil {
            fmt.Println("New uuid error.", err)
            return
        }

        luaLock := redis.NewScript(1, SCRIPT_LOCK)
        luaDel:= redis.NewScript(1, SCRIPT_DEL)
        for true {
            result, err := redis.Int(luaLock.Do(rds, "lock", uuidClient.String(), 5))
            if err != nil {
                fmt.Println("luaLock exec error.", err)
                break
            }

            if result == 0 {
                fmt.Println("Set lock failed.")
                time.Sleep(5 * time.Second)
                continue
            }
            fmt.Println("Set lock succeed.")

            go ResetExpire()
            // 加锁成功
        	fmt.Println("work begining...")
	        // 此处处理业务
	        fmt.Println("work end");
	        
	        // 业务处理结束后释放锁
	        result, err = redis.Int(luaDel.Do(rds, "lock", uuidClient.String()))
	        return
        }
    }
}

Redis采用Lua脚本可以执行更多的个性化的原子操作,在我项目中就采用这种容错性更高的方式。

参考文献

[1] Redis分布式锁的正确实现方式
[2] 解决redis分布式锁过期时间到了业务没执行完问题

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis分布式锁原理及go的实现 的相关文章

  • Redis Docker compose无法处理RDB格式版本10

    我无法在 docker compose 文件中启动 redis 容器 我知道docker compose文件没问题 因为我的同事可以成功启动项目 我读到有一个删除 dump rdb 文件的解决方案 但我找不到它 我使用Windows机器 任
  • Lua中按字符分割字符串

    我有像这样的字符串 ABC DEF 我需要将它们分开 字符并将两个部分分别分配给一个变量 在 Ruby 中 我会这样做 a b ABC DEF split 显然Lua没有这么简单的方法 经过一番挖掘后 我找不到一种简短的方法来实现我所追求的
  • Redis Cluster 与 Pub/Sub 中的 ZeroMQ,用于水平扩展的分布式系统

    如果我要设计一个巨大的分布式系统 其吞吐量应随系统中的订阅者数量和通道数量线性扩展 哪个会更好 1 Redis集群 仅适用于Redis 3 0 alpha 如果是集群模式 您可以在一个节点上发布并在另一个完全不同的节点上订阅 消息将传播并到
  • 为什么 Redis TimeSeries 不捕获聚合中的最后一个元素?

    我试图了解 Redis 的时间序列规则创建的工作原理 但我很困惑为什么 Redis 会忽略聚合中的最后一项 并想知道这是否是预期的行为 我在中创建了示例代码redis cli为了显示 127 0 0 1 6379 gt FLUSHALL O
  • Redis、会话过期和反向查找

    我目前正在构建一个网络应用程序 并想使用 Redis 来存储会话 登录时 会话会使用相应的用户 ID 插入到 Redis 中 并且过期时间设置为 15 分钟 我现在想实现会话的反向查找 获取具有特定用户 ID 的会话 这里的问题是 由于我无
  • Laravel 所有会话 ID 与 Redis 驱动程序

    在我的应用程序中 我希望允许某些用户能够注销除他 她之外的所有其他用户 当会话驱动程序设置为文件时 我已经完成了此功能 但现在我使用 redis 作为会话驱动程序 并且我无法找到任何方法来列出所有当前会话 就像我在文件时所做的那样司机 问题
  • Redis是如何实现高吞吐量和高性能的?

    我知道这是一个非常普遍的问题 但是 我想了解允许 Redis 或 MemCached Cassandra 等缓存 以惊人的性能极限工作的主要架构决策是什么 如何维持连接 连接是 TCP 还是 HTTP 我知道它完全是用C写的 内存是如何管理
  • 使用 Celery 通过 Gevent 进行实时、同步的外部 API 查询

    我正在开发一个 Web 应用程序 该应用程序将接收用户的请求 并且必须调用许多外部 API 来编写对该请求的答案 这可以直接从主 Web 线程使用 gevent 之类的东西来扇出请求来完成 或者 我在想 我可以将传入的请求放入队列中 并使用
  • StackExchange.Redis的正确使用方法

    这个想法是使用更少的连接和更好的性能 连接会随时过期吗 对于另一个问题 redis GetDatabase 打开新连接 private static ConnectionMultiplexer redis private static ID
  • Redis+Docker+Django - 错误 111 连接被拒绝

    我正在尝试使用 Redis 作为使用 Docker Compose 的 Django 项目的 Celery 代理 我无法弄清楚我到底做错了什么 但尽管控制台日志消息告诉我 Redis 正在运行并接受连接 事实上 当我这样做时 docker
  • Laravel 异常队列最大尝试次数超出

    我创建了一个应用程序来向多个用户发送电子邮件 但在处理大量收件人时遇到问题 该错误出现在failed jobs table Illuminate Queue MaxAttemptsExceededException App Jobs ESe
  • 如何延长 django-redis 中的缓存 ttl(生存时间)?

    我正在使用 django 1 5 4 和 django redis 3 7 1 我想延长缓存的 ttl 生存时间 当我取回它时 这是示例代码 from django core cache import cache foo cache get
  • 如何配置Lettuce Redis集群异步连接池

    我正在配置我的生菜重新分配池 当我按照官方文档配置时 连接池无法正常初始化 无法获取连接 官方文档指出 RedisClusterClient clusterClient RedisClusterClient create RedisURI
  • ServiceStack PooledRedisClientManager 故障转移如何工作?

    根据 git commit 消息 ServiceStack 最近添加了故障转移支持 我最初认为这意味着我可以关闭我的一个 Redis 实例 并且我的池客户端管理器将优雅地处理故障转移并尝试与我的备用 Redis 实例之一连接 不幸的是 我的
  • 无法使用 ASP.NET 会话状态提供程序连接到 Redis 服务器

    一段时间以来 我一直在尝试用 Redis 替换 ASP NET Session 多个小时与适用于 Redis 的 Microsoft ASP NET 会话状态提供程序 http blogs msdn com b webdev archive
  • redis能完全取代mysql吗?

    简单的问题 我是否可以使用 redis 而不是 mysql 来处理各种 Web 应用程序 社交网络 地理位置服务等 IT 领域没有什么是不可能的 但有些事情可能会变得极其复杂 将键值存储用于全文搜索之类的事情可能会非常痛苦 另外 据我所知
  • memcache、redis 和 ehcache 作为分布式缓存框架的比较 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我需要做出的决定之一是在我的系统中使用什么缓存框架 有这么多可供选择 我目前正在研究 redis ehcache 和 memcached
  • 如何高效地将数十亿数据插入Redis?

    我有大约 20 亿个键值对 我想将它们有效地加载到 Redis 中 我目前正在使用 Python 并使用 Pipe 如redis py https redis py readthedocs io en latest redis Redis
  • Microsoft.Extensions.Caching.Redis 选择与 db0 不同的数据库

    一个关于了解使用哪个redis数据库以及如何配置它的问题 我有一个默认值ASP NET Core Web 应用程序和默认配置的本地redis服务器 含15个数据库 通过包管理控制台我已经安装了 Install Package Microso
  • 如何按键中的值对 Redis 哈希进行排序

    Redis 有没有一种好方法来获取按值排序的哈希中的键 我查看了文档 但没有找到直接的方法 另外有人可以解释一下redis中的排序是如何实现的 以及什么吗 本文档 http redis io commands SORT using hash

随机推荐

  • CMake 简介

    cmake是kitware公司以及一些开源开发者在开发几个工具套件 VTK 的过程中所产生的衍生品 后来经过发展 最终形成体系 在2001年成为一个独立的开放源代码项目 其官方网站是www cmake org 可以通过访问官方网站来获得更多
  • (杭电多校)2023“钉耙编程”中国大学生算法设计超级联赛(8)

    1005 0 vs 1 双端队列暴力模拟 时间复杂度为O n T 首先预处理0的右边第一个0的下标 1的右边第一个1的下标 0的左边第一个0的下标 1的左边第一个1的下标 然后进行模拟 如果当前是zero的轮次 那么就看双端队列的两端 如果
  • 数据结构与算法基础知识(1)

    文章概述 数据结构的定义与分类 逻辑结构 物理结构 数据结构的定义 数据结构就是关系 是数据元素之间存在的一种或者多种特定关系的集合 数据结构分为两类 a 逻辑结构 b 物理结构 逻辑结构 数据对象中数据元素之间的相互关系 物理结构 数据的
  • 前端防止按钮被多次重复点击

    多次重复点击会造成前端显示出bug 需要判断去过滤掉重复多次的点击 这个有好多种方法 逻辑是不管点几次 间隔一定时间才会去触发一次 只产生一次的记录 也就是弄个定时器 最直接的方法就是等每次点击过后等所有操作结束后释放变量 但是这个太麻烦了
  • 音频应用处理器性能benchmark

    我的书 购买链接 京东购买链接 淘宝购买链接 当当购买链接 处理器类别 1 Analog Devices SHARC Blackfin SigmaDSP 2 TI c55 c67x c66x 3 ARM cortex M4 M7 corte
  • python 时间和日期[time 和 calender模块]

    Python 程序能用很多方式处理日期和时间 转换日期格式是一个常见的功能 Python 提供了一个 time 模块可以用于格式化日期和时间 时间间隔是以秒为单位 每个时间戳都以自从1970年1月1日0 0 0 开始计时的 Python 的
  • Android 实现点击输入框以外的区域隐藏软键盘

    博主前些天发现了一个巨牛的人工智能学习网站 通俗易懂 风趣幽默 忍不住也分享一下给大家 点击跳转到网站 效果图如下 代码实现如下 首先创建一个工具类InputMethodUtil public class InputMethodUtil 隐
  • 【ML】机器学习模型的 10 个评估指标

    大家好 我是Sonhhxg 柒 希望你看完之后 能对你有所帮助 不足请指正 共同学习交流 个人主页 Sonhhxg 柒的博客 CSDN博客 欢迎各位 点赞 收藏 留言 系列专栏 机器学习 ML 自然语言处理 NLP 深度学习 DL fore
  • Ant Design Pro学习记录—ModalForm的使用(二)

    目录 一 ModalForm高度设置 二 ModalForm点击阴影背景 不隐藏弹框 三 ProFormSelect联动 四 ProFormText关联赋值 一 ModalForm高度设置 在modalProps中设置bodyStyle h
  • 华为OD机试 Python 【平均值最大子数组】

    题目 任务 你需要从一个有N个正数的列表里面找一个子列表 这个子列表的长度应该至少为L 而且它里面的数字要使几何平均值尽量大 我们需要你告诉我们这个子列表是从哪个位置开始的 以及它的长度 怎么判断哪个子列表最好 首先看几何平均值谁大 谁就好
  • springboot war打包步骤

    packaging的设置
  • centos7-elk之安装kibana

    下载解压安装包 安装elasticsearch6 0 1之后 下载kibana6 0 1 tar包 存放地址 opt elk 解压tar包 tar zxvf kibana6 0 1 修改配置文件 vim opt elk kibana6 0
  • BDD、KITTI、Cityscapes和Foggy Cityscapes百度云链接

    BDD 链接 https pan baidu com s 1gtUZGV 8X4L3Fe3PtCAxjw 提取码 vfoj KITTI 链接 https pan baidu com s 1EPEV z185GV8t RE48lROA 提取码
  • 为老人和残障人士“铺路搭桥”,这家银行是认真的

    一场疫情改写了银行业的规则 街道被封闭 城市空无一人 那个肃杀的冬季已经离我们远去 但对刚成立不久的小型银行来说 这是一场近乎致命的打击 裕民银行正是其中一员 这是江西省第一家 全国第18家民营银行 根据监管要求 民营银行只能采取 一行一店
  • kd树

    参考 1 统计学习方法 李航 2 https baike baidu com item kd tree 2302515 fr aladdin 3 http www jianshu com p ffe52db3e12b 4 http blog
  • error: device unauthorized.This adb server's $ADB_VENDOR_KEYS is not set

    以为是个复杂问题 百度之后将自己的手机屏幕解锁打开之后就成功连接上了 同样出问题的小伙伴可以看看是不是接口的问题 或者开发者模式没有打开
  • python中global用法实例分析

    lobal语句是适用于当前整个代码块的声明 它是全局变量的标识符 如果某名字在局部名字空间中没有定义 就自动使用相应的全局名字 没有global是不可能手动指定一个名字是全局的 在 global 中出现的名字不能在global 之前的代码中
  • 包裹快递

    包裹快递 背景 小K成功地破解了密文 但是乘车到X国的时候 发现钱包被偷了 于是无奈之下只好作快递员来攒足路费去Orz教主 描述 一个快递公司要将n个包裹分别送到n个地方 并分配给邮递员小K一个事先设定好的路线 小K需要开车按照路线给的地点
  • 记录几款vue的Tree树形控件

    一 Vue Beauty url https fe driver github io vue beauty components tree 特色 找了很多个框架 只有Vue Beauty是带连接线的 刚好项目能用上 二 iView url
  • Redis分布式锁原理及go的实现

    业务背景 后台定时任务刷新Redis的数据到数据库中 有多台机器开启了此定时同步的任务 但是需要其中一台工作 其他的作为备用 提高可用性 使用Redis分布式锁进行限制 拿到锁的机器去执行具体业务 拿不到锁的继续轮询 分布式锁原理 分布式锁