【分布式】分布式锁

本文为分布式学习笔记,参考了JavaGuide


各种锁概念介绍:

  1. 可重入锁:允许线程在持有锁的情况下多次获取同一个锁,而不会被自己所持有的锁所阻塞,这种特性也被称为锁的可重入性。

  2. 自旋锁:与传统的互斥锁不同,自旋锁不会将线程挂起(进入阻塞状态),而是在获取锁时不断循环尝试获取,直到成功为止,因此称为“自旋”。

自旋锁优缺点:

  • 优点:自旋锁适用于短期内资源占用的情况,不涉及线程上下文切换,避免了上下文切换开销。
  • 缺点:不适用长期资源占用情况,长时间自旋时会导致其他线程无法获取CPU时间片,导致CPU资源浪费。
  1. 公平锁:公平锁是一种锁的获取机制,它确保线程按照请求锁的顺序来获取锁,即先到先得的原则。在使用公平锁时,如果有多个线程在等待同一个锁,那么锁会被分配给等待时间最长的线程。

特点:按顺序获取锁避免线程饥饿(即某个线程一直无法获取到锁)、性能相对较低

  1. 多重锁:多重锁通常指的是一种锁的嵌套使用或者多个锁的同时持有。
  2. 红锁:红锁是一种分布式锁的算法,旨在解决在分布式环境下的锁竞争和故障恢复问题。

红锁步骤如下:

  1. 客户端尝试在 N 个 Redis 实例上获取锁,每个实例使用相同的锁名称和唯一的随机值作为锁的值,同时设置相同的过期时间(TTL)。
  2. 客户端在大多数(大于等于 N/2+1) Redis 实例上成功获取到锁时,认为获取锁成功;否则认为获取锁失败。
  3. 如果客户端获取锁失败,会在所有 Redis 实例上释放已经获取到的锁。
  4. 当客户端释放锁时,会在所有 Redis 实例上释放锁。

特点:高可用性容错性存在一定性能开销

  • 高可用性:通过在多个 Redis 实例上获取锁,提高了锁的可用性和可靠性,即使部分 Redis 实例宕机或者网络分区,也能够继续提供服务。
  • 容错性:红锁算法能够容忍部分 Redis 实例获取锁失败的情况,只要大多数实例成功获取到锁即可认为获取锁成功。
  • 性能开销:尽管红锁算法提高了锁的可用性和可靠性,但是在获取锁和释放锁的过程中需要访问多个 Redis 实例,可能会带来一定的性能开销。
  1. 读写锁:与传统的互斥锁不同,读写锁允许多个线程同时读取共享资源,但在写操作时需要独占访问。

读写锁特点:

  • 读取共享资源的并发性:读取操作是非互斥的,多个线程可以同时持有读锁并读取共享资源,这样可以提高并发性能。
  • 写入共享资源的互斥性:写入操作是互斥的,只有当没有其他线程持有读锁或写锁时,写入操作才能够执行。
  • 写锁优先级高于读锁:当一个线程持有写锁时,其他线程无法获取读锁或写锁,确保写入操作的一致性。

分布式锁

在多线程访问共享资源时,会发生数据竞争,这时就需要锁。

常规的锁,比如ReetrantLock类、synchronized关键字等本地锁都是在同一个JVM虚拟机中,用来控制对共享资源的访问。

但是在分布式的情况下,不同的线程不在同一个JVM中,甚至不在同一台机器上,那么在对共享资源进行访问控制时,就需要分布式锁。

一个分布式锁应具备以下基本要求:

  • 互斥:任意时刻,锁只能被同一线程持有
  • 高可用:当一个锁出现问题时,能够自动切换到另外一个锁服务。并且客户端锁释放出现问题时,锁最终也能够被释放(一般通过超时机制完成)
  • 可重入:一个节点获取该锁后,可以再次获取该锁

除了上述基本条件,一个优秀的锁还应该:

  • 高性能:锁的获取和释放应该在短时间完成,不会对系统有太大影响
  • 非阻塞:如果获取不到锁,不能无限等待

常见实现方式

分布式锁一般有以下几种常见实现:

  • 基于关系型数据库,例如MySQL实现分。
  • 基于分布式协调服务,例如ZooKeeper实现。
  • 基于分布式键值存储中间件,例如Redis、Etcd实现。

数据库

关系型数据库一般通过唯一索引或者排他锁实现,不过一般不会使用这种方式,因为性能太差、不具备锁失效机制。

Redis

不论是本地锁还是分布式锁,核心都在于互斥。

Redis中,SETNX可以实现互斥(SET if Not eXists),类似Java中的setIfAbsent,如果key不存在就设置,并返回1,存在则直接返回0。

本地:db0> setnx lock_key lock_value
1
本地:db0> setnx lock_key lock_value
0
本地:db0> 

释放锁只需要使用DEL命令直接删除对应的key即可:

本地:db0> del lock_key
1
本地:db0> 

为了防止误删到其他锁,建议使用lua脚本根据key对应的value值去删除。
使用lua脚本是因为redis在执行lua脚本时,可以以原子性方式执行:

-- 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

这种方式比较简单高效,但是可能存在锁无法释放的问题(比如客户端线程突然挂掉)。

给锁设置过期时间

为了避免锁无法被释放,可以给锁设置一个过期时间:

本地:db0> SET lock_key lock_value EX 30 NX
OK
本地:db0> SET lock_key lock_value EX 30 NX

本地:db0> 

EX代表过期时间,示例为30秒,NX代表不存在才设置。

一定要保证设置指定 key 的值和过期时间是一个原子操作!!!不然仍旧会存在锁无法被释放的情况。

这种方式虽然避免了锁无法被释放,但是可能会出现锁提前被释放的问题,而且过期时间也不好判断。

如果对共享资源的操作未完成时,锁能够自动续期就好了。

锁的续期

Java中常用的是Redisson,其他语言也可以在Redis官方文档中找到对应方案。

Redisson中的分布式锁自带续期功能,原理就是提供了一个专门用来监控和续期锁的Watch Dog(看门狗),如果操作共享资源的线程还没有结束,看门狗会自动续期。

Redisson默认创建一个30秒的锁,每次续期10秒钟

这里以 Redisson 的分布式可重入锁 RLock 为例来说明如何使用 Redisson 实现分布式锁:

// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();

只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。

// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);

锁的可重入

可重入的意思就是,持有锁的线程再次获取锁时仍然可以获取该锁。
Java中的ReentrantLocksynchronized都是可重入的,并且lock几次,同时也需要unlock几次(重入计数器)。

项目中的分布式锁推荐使用Redisson,它内置了多种类型的锁:可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。

Redis如何解决集群情况下分布式锁的可靠性

为了避免单点故障,生产环境中Redis一般都是集群部署。

集群情况下分布式锁一般使用红锁RedLock,这种方式直接操作Redis节点,但是实现复杂,性能较差,一般也不推荐使用。

ZooKeeper也可以实现分布式锁

……

posted @ 2024-03-22 10:33  code-blog  阅读(15)  评论(0编辑  收藏  举报