我正在参加「掘金·启航计划」
日常开发中,有时候需要使用悲观锁来保证并发安全。
第一想法,是使用用 synchronized 关键字,锁住对应的 String对象 (intern)。
synchronized是依赖jvm虚拟机实现的,也就是说,只能在同一个进程中起作用,这样只能实现本地锁,只能应对单机场景。如果是分布式场景,就需要使用分布式锁。
分布式锁需要具备的特性:
- 多进程可见
- 互斥
- 高可用
- 高性能
- 安全性
技术选型
分布式锁的核心在于需要把锁存储到一个公共空间,使分布式系统中各个节点都在这块空间获取锁。
那么只要能提供公共存储空间的中间件都可以做分布式锁,常见的有mysql、zookeeper、redis。
| MySQL | Redis | |
|---|---|---|
| 互斥 | mysql本身的互斥锁 | setnx互斥指令 |
| 高可用 | 好 | 好 |
| 高性能 | 一般 | 好 |
| 安全 | 断开连接自动释放 | 使用锁超时自动释放 |
考虑到redis的高性能,最终采用redis,但是redis需要自己保证锁的安全性,编码难度较高,同时会有一些常见问题。
- 获取锁:setnx
- 释放锁:delete
锁误删
线程A申请锁,但是业务执行太久,被自动释放了,此时线程B拿到新锁,线程A执行完毕,把线程B的锁释放了。
解决方案:锁的value设置一个唯一标识,释放锁的时候校验一下,确认是自己的锁再释放。
锁校验和释放的原子性
为了解决锁误删的问题,在 delete key 之前需要校验value的值和当前线程的标识是否一致,但是这两个步骤不能保证原子性。
极端场景下可能导致:在校验成功,确定锁是自己的之后,释放锁之前被阻塞(GC)
阻塞过程中,锁超时释放,别的线程获得新的锁,等到阻塞结束后,再次出现锁误删。
因此需要保证校验和删锁的原子性,可以使用 lua 脚本实现校验和释放的原子性。
依然存在的问题
- 不可重入:同一个线程不能重复获取锁
- 不可重试:无法满足允许重试和等待锁的场景
- 超时释放:提前释放锁
- 主从一致性
Redisson
redisson是企业级的分布式锁解决方案,可以实现上述所说的所有问题
- 可重入:使用hash结构记录线程id和重入次数
- 获取锁:不存在,直接创建。存在,判断是不是自己的锁,不是则获取失败,是则重入+1
- 释放锁:判断是否是自己的锁,不是则退出。是则重入-1,为0则删除锁
- 可重试锁:订阅锁的释放消息,超时返回,在时间内获得消息才重试,防止忙等待的发生
- 如果 waitTime = -1,不等待重试,直接返回结果
- 超时续约:利用 watchDog (看门狗机制),每隔一段时间,延长过期时间
- 如果 leaseTime = -1,则第一次获取锁时会开启定时任务,每隔一段时间(leaseTime / 3)就延长过期时间
- 主从一致性:使用multiLock
- redisson使用复制的方式,对每一个节点都保存锁,每个节点可以配置自己的子节点,如果一个节点宕机了,其他线程可以从这个节点的子节点获取锁,但是却不能从其他节点获取锁,因此最终还是无法获取锁。
小结
本文简单交代了基于 redis 实现分布式锁的思路和一些常见问题。
更具体的内容还需要进一步的了解,比如 redisson 看门狗机制的细节,以及使用场景(无法确定业务key过期时间的场景都可以使用watchdog)