浅析基于 Redis 的分布式锁

72 阅读3分钟

我正在参加「掘金·启航计划」

日常开发中,有时候需要使用悲观锁来保证并发安全。

第一想法,是使用用 synchronized 关键字,锁住对应的 String对象 (intern)。

synchronized是依赖jvm虚拟机实现的,也就是说,只能在同一个进程中起作用,这样只能实现本地锁,只能应对单机场景。如果是分布式场景,就需要使用分布式锁。

分布式锁需要具备的特性:

  • 多进程可见
  • 互斥
  • 高可用
  • 高性能
  • 安全性

技术选型

分布式锁的核心在于需要把锁存储到一个公共空间,使分布式系统中各个节点都在这块空间获取锁。

那么只要能提供公共存储空间的中间件都可以做分布式锁,常见的有mysql、zookeeper、redis。

MySQLRedis
互斥mysql本身的互斥锁setnx互斥指令
高可用
高性能一般
安全断开连接自动释放使用锁超时自动释放

考虑到redis的高性能,最终采用redis,但是redis需要自己保证锁的安全性,编码难度较高,同时会有一些常见问题。

  • 获取锁:setnx
  • 释放锁:delete

锁误删

线程A申请锁,但是业务执行太久,被自动释放了,此时线程B拿到新锁,线程A执行完毕,把线程B的锁释放了。

解决方案:锁的value设置一个唯一标识,释放锁的时候校验一下,确认是自己的锁再释放。

锁校验和释放的原子性

为了解决锁误删的问题,在 delete key 之前需要校验value的值和当前线程的标识是否一致,但是这两个步骤不能保证原子性。

极端场景下可能导致:在校验成功,确定锁是自己的之后,释放锁之前被阻塞(GC)

阻塞过程中,锁超时释放,别的线程获得新的锁,等到阻塞结束后,再次出现锁误删。

因此需要保证校验和删锁的原子性,可以使用 lua 脚本实现校验和释放的原子性。

依然存在的问题

  • 不可重入:同一个线程不能重复获取锁
  • 不可重试:无法满足允许重试和等待锁的场景
  • 超时释放:提前释放锁
  • 主从一致性

Redisson

redisson是企业级的分布式锁解决方案,可以实现上述所说的所有问题

  1. 可重入:使用hash结构记录线程id和重入次数
    • 获取锁:不存在,直接创建。存在,判断是不是自己的锁,不是则获取失败,是则重入+1
    • 释放锁:判断是否是自己的锁,不是则退出。是则重入-1,为0则删除锁
  2. 可重试锁:订阅锁的释放消息,超时返回,在时间内获得消息才重试,防止忙等待的发生
    • 如果 waitTime = -1,不等待重试,直接返回结果
  3. 超时续约:利用 watchDog (看门狗机制),每隔一段时间,延长过期时间
    • 如果 leaseTime = -1,则第一次获取锁时会开启定时任务,每隔一段时间(leaseTime / 3)就延长过期时间
  4. 主从一致性:使用multiLock
    • redisson使用复制的方式,对每一个节点都保存锁,每个节点可以配置自己的子节点,如果一个节点宕机了,其他线程可以从这个节点的子节点获取锁,但是却不能从其他节点获取锁,因此最终还是无法获取锁。

小结

本文简单交代了基于 redis 实现分布式锁的思路和一些常见问题。

更具体的内容还需要进一步的了解,比如 redisson 看门狗机制的细节,以及使用场景(无法确定业务key过期时间的场景都可以使用watchdog)