Go主题月 | 分布式锁redis的坑

642 阅读5分钟

前 言

谈到分布式应用那必然离不开分布式锁🔐的问题,分布式锁在分布式应用中应用广泛,本文就讲讲基于redis实现的分布式锁的一些问题。

可能各位coder接触最多的还是在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问情况,例如下图:

多线程下的锁

对于单进程的并发场景,可以使用编程语言及相应的类库提供的锁,如Java中的 synchronized 语法以及 ReentrantLockGolang中的sync包下面的mutexRust中的async_std::sync::Mutex,避免并发问题,这实际上是本地锁的方式。

分布式锁

但是现在流行的分布式架构,在分布式环境下,如何保证不同节点的线程同步执行呢?或者共享资源怎么上锁呢???

在将应用拆分为分布式应用之前的单机系统中,对一些并发场景读取公共资源时如扣库存,卖车票之类的需求可以简单的使用同步或者是加锁就可以实现,但是应用分布式了之后系统由以前的单进程多线程的程序变为了多进程多线程,这时使用以上的解决方案明显就不够了。

分布式架构下

一般业界有几种解决方:

  • 基于 DB 的唯一索引
  • 基于 Memcached add 命令
  • 基于 Zookeeper 的临时有序节点
  • 基于 RedisNX EX
  • 基于Chubby粗粒度分布式锁服务

Redis的坑你填了几个?

如果在分布式场景中,实现不同客户端的线程对代码和资源的同步访问,保证在多线程下处理共享数据的安全性,就需要用到分布式锁技术,我就来写写基于Redis的一些坑😁。

在分布式时,在程序中修改已有数据时,需要先读取,然后进行修改保存,此时很容易遇到并发问题。由于修改和保存不是原子操作,在并发场景下,部分对数据的操作可能会丢失,本地锁无法在多个服务器之间生效,这时候保证数据的一致性就需要分布式锁来实现。

并行操作Redis示意图

Redis 锁主要利用 Redissetnx 命令实现,

  • 加锁命令:SETNX key value,当键不存在时,对键进行设置操作并返回成功,否则返回失败,KEY 是锁的唯一标识,一般按业务来决定命名。
  • 解锁命令:DEL key,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。
  • 锁超时:EXPIRE key timeout, 设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。
// 伪代码实现
fn main() {
    let key: &'static str = "sync_lock";
    if up_lock(key, 1) == 1 {
        // 设置超时
        expire(key, 30)
        // .....业务逻辑
    }
}

// 基于redis SETNX 和 EXPIRE 的实现,问题代码
fn up_lock(key: &'static str, num: i8) -> i8 {
    // ..... 上锁逻辑
    return 1;
}

fn expire(key: &'static str, num: i8) {
    // ... 自定义超时
}

写完这么一看还没有什么问题,其实上面🕳坑大着呢!!!如果你是这么去实现的,那笔者恭喜你,你掉坑里了😜(PS:这里不是代码问题导致的,而是SETNXEXPIRE 非原子性导致的)。


  1. SETNXEXPIRE 非原子性

过程

如果 SETNX 成功,在设置锁超时时间后,服务器挂掉、重启或网络问题等,导致 EXPIRE 命令没有执行,锁没有设置超时时间变成死锁。


  1. 锁误解除

如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程A实际释放的线程是 B 加的锁,从而导致锁混乱,然后导致实际逻辑代码混乱和乃至关键数据丢失。


  1. 超时解锁导致并发

如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行,那就没有分布式锁存在的意义了🤷‍。

  • 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
  • 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。

  1. 不可重入

当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的,如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。

小 结

Redis是个高性能的中间件,但是如果用在分布式锁上实现依然存在问题,我在一些网络文章看到很多人大部分都是用redis来解决分布式锁问题,希望这篇文章能帮助到你,记得点个关注!!