基于分布式原理解析Redis分布式锁的实现

栏目: 数据库 · 发布时间: 5年前

  • Redis 作为分布式锁是个轻量级的解决方案, 但很多同学在使用过程中并未弄明白其中的优劣, 知其然不知其所以然, 反而引入了一些那一排查的线上故障.
  • 这里针对Redis分布式锁常用的几种方式, 从原理触发, 分析其适用场景及潜在缺陷

先回顾一下LESLIE LAMPORT大神在其1977年论文 Proving the Correctness of Multiprocess Programs 中对于分布式系统正确性的定义

Correctness == Safety and liveness

  1. Safety (安全性)
    safety properties informally require that "something bad will never happen" in a distributed system or distributed algorithm
  2. Liveness (活性)
    liveness properties refers to a set of properties of concurrent systems, that require a system to make progress despite the fact that its concurrently executing components ("processes") may have to "take turns" in critical sections , parts of the program that cannot be simultaneously run by multiple processes
    知乎大神翻译了数学证明, 有兴趣的同学可以自取

对于使用分布式锁的正确性, 我们不妨如下界定

  1. Safety
    同一时间只有一个进程可以获得锁
    没有获得锁的其他进程应该被正确的置为获取锁失败的状态
  2. Liveness
    获得锁的进程最终应该释放锁, 让其他进程可以再次尝试获取

下面从三个阶段来分析Reids作为分布式锁的用法

  1. 锁的获取;
  2. 锁的持有;
  3. 锁的释放;

先看锁的获取

// redis version < 2.6.12 
SETNX lock_key lock_value
EXPIRE lock_key ttl_with_seconds
// redis version >= 2.6.12 
SET lock_key lock_value NX PX ttl_with_seconds

Redis的SETNX可以保证只有第一次设置可以成功, 那么获取锁的Safety是可以保证的

但是在2.6.12之前SETNX和EXPIRE是两条命令, 这样会存在如下情况:

Command Status
SETNX lock_key lock_value 执行成功
EXPIRE lock_key ttl_with_seconds 发送失败&Client Crash

会导致lock_key无法正确释放, 从而不能满足Liveness

问题有了, 如何解决?

  1. Redis2.6.12之前版本可以采用 lua 脚本将命令一次提交, 保证操作原子性
  2. 升级到2.6.12之后版本

锁的持有比较复杂, 我们先来看锁的释放

野狐禅版本, golang示例

func AcquireLock(lock_key string, lock_value string, timeout uint32) bool {
     // SET lock_key lock_value NX PX ttl_with_seconds 
     // return is_success
}

func ReleaseLock(lock_key string) {
     // DELETE lock_key 
}

func Process(lock_key string, lock_value string, timeout uint32) {
    if  AcquireLock(lock_key, lock_value, timeout) {
        // 无论业务逻辑执行是否成功, 一定释放锁
        defer func() {
          ReleaseLock(lock_key)
        }()

        // do something
        // maybe process over lock TTL
    }
}

看上去很完美的实现. But Really Good Job?

基于分布式原理解析Redis分布式锁的实现

惨案是如何发生的

这里完全没考虑如果业务执行超过TTL时间, 导致锁被自动释放的情况 >_< !!!!!!!!!!!!

来看个正规军版本

func ReleaseLock(lock_key string, lock_value string) bool {
     // 在释放锁的时候加入乐观锁校验, 并通过lua脚本保证原子性
     // return_val = eval (
     //           if redis.call("get",lock_key) == lock_value then
     //                return redis.call("del",lock_key)
     //           else
     //                return 0
     //           end
     // )
     return return_val!=0
}

func Process(lock_key string, lock_value string, timeout uint32) {
    if  AcquireLock(lock_key, lock_value, timeout) {
        // 无论业务逻辑执行是否成功, 一定释放锁
        defer func() {
            release_success := ReleaseLock(lock_key)
            if !release_success {
                // 如果锁释放失败, 说明锁超时, 其他人已经获取了锁, 需要根据业务决定是否rollback刚刚的操作
                // maybe rollback?? 
            }
        }()

        // do something
        // maybe process over lock TTL
    }
}

最后需要注意的事情

由于使用了lock_value作为释放锁时的乐观校验, 那么lock_value的选择就需要一定的技巧

值的生成 优点 缺点
系统时间戳 程序实现简单 分布式环境下ntp时钟并不同步, 有概率碰撞
ID服务获取 全局保证唯一 引入了外部服务依赖, 降低健壮性
分布式ID算法 全局保证唯一 有一定技术门槛

这里推荐第三种方案, 可参考twitter的snowflake算法

顺便鄙视一下某些无良码农, 仅仅是对snowflake算法的位进行调整, 就人模狗样的跑出来说实现了nb的分布式ID算法>_<


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Shallows

The Shallows

Nicholas Carr / W. W. Norton & Company / 2011-6-6 / USD 15.95

"Is Google making us stupid?" When Nicholas Carr posed that question, in a celebrated Atlantic Monthly cover story, he tapped into a well of anxiety about how the Internet is changing us. He also crys......一起来看看 《The Shallows》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具