分布式锁

为什么需要分布式锁?

为了保证共享资源被安全地访问,需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。这样可以避免数据竞争和脏数据问题,保证程序的正确性和稳定性。

如何才能实现共享资源的互斥访问呢?锁是一个比较通用的解决方案,更准确点来说是悲观锁。悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题,所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。

对于单机多线程来说,在 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 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

设置过期时间导致的问题的解决办法:

  1. 应用程序每隔半分钟使用自己的 watch dog 监测当前 key 的 value,如果仍然是自己,TTL再续一分钟;
  2. 应用程序在删除锁的时候,需要比较 value 的值是否和自己设置的相同

性能提升

使用分段锁,将资源分段

集群的问题

主从同步有延时,程序访问不同主机,会有问题。

解决方案:红锁
服务器之间不同步数据,服务器数量为奇数,应用程序需要在超过一半的机器上加锁成功。

Zookeeper分布式锁

ZooKeeper 分布式锁是基于 临时顺序节点 和 Watcher(事件监听器) 实现的。

参考文献

分布式锁视频教程
分布式锁实现原理与最佳实践

posted @ 2024-10-30 10:09  千千菌  阅读(4)  评论(0编辑  收藏  举报