千恋*万花

  • 首页
  • 个人简历
  • 文章分类
    • 后端开发
    • 运维
    • 基础知识
    • 笔记
    • 工作运维记录
    • 随笔
    • 未分类文章
  • 管理
    • 后台管理
    • 登出
萌奈の笔记簿
分享我的努力,希望为你助力
  1. 首页
  2. 后端开发
  3. golang
  4. 正文

基于 go-redis 与 Lua 实现简易且高效的分布式锁

2023-01-03 194点热度 0人点赞 0条评论

分布式锁的应用场景

在分布式业务中,若涉及到共享的资源,未防止多个进程同时访问、处理该资源,那么则需要一个分布式锁 来保证该共享资源在同一时刻只能被一个对象进行处理。

设计理论

  1. 为了实现锁互斥能力,则一定涉及到 Redis 的SETNX指令(SET if Not eXist)
  2. 锁只能由创建锁的对象解开,所以需要记录锁是由哪个对象创建的,解锁时需要验证锁的所有权
  3. 为了防止死锁,第一 需要允许强制解除锁,第二 每个锁必须要有 超时时间

综上,便可以编写出一个锁的定义了

type lock struct {
    context context.Context
    name    string // 锁名称
    owner   string // 锁标识
    seconds int64  // 有效期
}

type LockInterface interface {
    Lock() bool
    Block(seconds int64) bool       // 持续获取锁
    Release() bool
    ForceRelease()
}

这里的lock是私有属性,主要考虑到防止使用者随意修改lock的属性,导致无法解锁的问题。

代码

生成一个锁

// GetLock 生成锁
func GetLock(name string, seconds int64) LockInterface {
    return &lock{
        context.Background(),
        LOCK_PREFIX + name,
        utils.RandString(16),
        seconds,
    }
}

尝试锁定

// Lock 获取锁
func (l *lock) Lock() bool {
    // SetNX:SET if Not eXists
    return Redis.SetNX(l.context, l.name, l.owner, time.Duration(l.seconds)*time.Second).Val()
}

若锁(key)不存在,则添加key-value,并返回 true,代表已获取到锁
若锁(key)已存在,则不进行任何操作,并返回 false,代表未获取到锁

尝试获取锁,若没有获取到,则持续尝试获取锁

// Block 阻塞1秒后,尝试重新获取锁,持续seconds秒。
func (l *lock) Block(seconds int64) bool {
    starting := time.Now().Unix()
    for {
        if !l.Lock() {
            time.Sleep(time.Duration(1) * time.Second)
            if time.Now().Unix()-seconds >= starting {
                return false
            }
        } else {
            return true
        }
    }
}

释放锁

// 释放锁 Lua 脚本,防止任何对象都能解锁
// KEYS[1]: lock.name
// ARGV[1]: lock.owner
const releaseLockLuaScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
`

// Release 释放锁
func (l *lock) Release() bool {
    luaScript := redis.NewScript(releaseLockLuaScript)

    result := luaScript.Run(
        l.context, Redis,
        []string{l.name}, // []string{l.name}   KEYS[1]
        l.owner,          // l.owner            ARGV[1]
    ).Val().(int64)
    return result != 0
}

在这里不用担心 LuaScript 会占用网络流量,go-redis 在向服务器发送请求时,发送的是 lua 脚本的sha1,
redis服务器收到lua哈希值时,会根据sha1先判断自己是否已经缓存了这个lua脚本,缓存了那就直接用,若没有缓存,go-redis才会向redis服务器发送完整的lua脚本。

强制释放锁

// ForceRelease 强制释放锁
func (l *lock) ForceRelease() {
    Redis.Del(l.context, l.name).Val()
}

若上一个脚本发生了错误,导致 锁 无法被正常释放,那么则需要人手工去释放这个锁。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: go golang lock lua redis 分布式锁
最后更新:2023-02-05

初音萌奈

我是练习时长 一年半 的后端开发程序员 谢谢你参观我的博客! 本网站现已支持IPv6 ☞ 个人简历 ☜

点赞
< 上一篇
下一篇 >

文章评论

取消回复
文章目录
  • 分布式锁的应用场景
  • 设计理论
  • 代码

COPYRIGHT © 2023 HatsuneMona ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

晋ICP备17007130号-4