分布式锁
为什么需要分布式锁?
为了保证共享资源被安全地访问,需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。这样可以避免数据竞争和脏数据问题,保证程序的正确性和稳定性。
如何才能实现共享资源的互斥访问呢?锁是一个比较通用的解决方案,更准确点来说是悲观锁。悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题,所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
对于单机多线程来说,在 Java 中,通常使用 ReentrantLock 类、synchronized 关键字这类 JDK 自带的本地锁来控制一个 JVM 进程内的多个线程对本地共享资源的访问。分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现共享资源的互斥访问了。于是,就需要使用分布式锁。
分布式锁应该具备的条件
互斥、高可用、可重入,最好还 高性能、非阻塞
- 互斥:任意一个时刻,锁只能被一个线程持有。
- 高可用:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。
- 可重入:一个节点获取了锁之后,还可以再次获取锁。
- 高性能:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。
- 非阻塞:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。
分布式锁常见实现方案
MySQL分布式锁
【不常用】利用MySQL的特性:主键或者唯一索引值是唯一的。
Redis分布式锁
原理
使用setnx key value命令实现互斥,setnx = set if not exists,也就是只有当key不存在时才set,key存在时不做任何操作。
获取锁:setnx key value
释放锁:del key
死锁
这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问,造成死锁。
死锁解决办法:设置key的过期时间 setnx key value ttl,value是请求的唯一标识。
设置过期时间导致的问题:程序还没有执行完,锁过期了。这时就会有其他程序获取到锁,删除锁的时候也可能会删除其他程序的锁。
如何实现锁的优雅续期?
对于Java,已经有了现成的解决方案:Redisson。Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
设置过期时间导致的问题的解决办法:
- 应用程序每隔半分钟使用自己的 watch dog 监测当前 key 的 value,如果仍然是自己,TTL再续一分钟;
- 应用程序在删除锁的时候,需要比较 value 的值是否和自己设置的相同
性能提升
使用分段锁,将资源分段
集群的问题
主从同步有延时,程序访问不同主机,会有问题。
解决方案:红锁
服务器之间不同步数据,服务器数量为奇数,应用程序需要在超过一半的机器上加锁成功。
Zookeeper分布式锁
ZooKeeper 分布式锁是基于 临时顺序节点 和 Watcher(事件监听器) 实现的。