分布式锁

分布式锁

字面上,就是在分布式系统架构里所用到的锁,一种用于并发控制的机制,确保在多个节点上对共享资源的互斥访问,从而避免数据竞争和冲突。与单体架构中的锁,最大的区别就是,细粒度的划分:从线程间的资源共享到进程间的资源共享。

分布式锁与普通锁(本地锁)的区别:

1.作用范围

  • 普通锁:
    单个进程或单个计算机内的多个线程之间的同步。当多个线程尝试访问同一资源时,普通锁可以确保只有一个线程可以访问该资源,其他线程需要等待锁的释放。
  • 分布式锁:
    分布式锁用于跨多个进程或多个计算机之间的同步。允许不同的进程或计算机协调对共享资源的访问,以避免冲突和数据不一致性。

2.锁的获取方式

  • 普通锁:
    普通锁通常是基于本地内存的互斥量或自旋锁实现的,可以通过在内存中的标记或计数器来判断锁的状态,并通过执行CPU自旋等待来获取锁。
  • 分布式锁:
    分布式锁通常使用基于分布式系统的外部组件或服务,如分布式缓存系统(如Redis)或分布式协调服务(如ZooKeeper)实现。进程或计算机通过与这些组件进行通信来获取和释放锁。

3.可靠性和容错性

  • 普通锁:
    普通锁在单个计算机上运行,受限于该计算机的可靠性和容错性。如果计算机故障或程序崩溃,可能会导致锁被永久占用或意外释放。
  • 分布式锁:
    分布式锁通过将锁状态存储在外部组件中,可以提供更高的可靠性和容错性。即使其中一个计算机或进程崩溃,其他进程仍然可以通过与外部组件通信来获取锁。

分布式锁的一些概念和特点:

1.互斥性:分布式锁能够确保同一时刻只有一个节点能够获取锁,从而避免多个节点同时对共享资源进行修改或访问。

2.可重入性:指同一个线程或进程在已经持有锁的情况下,可以再次获取同一把锁,而不会被自己阻塞。(不是必须的,比如非常简单的操作中,执行路径明确)。

  • 可重入性例子:
    可以通过在锁的value中记录当前客户端的标识和计数器信息来实现。当客户端第一次获取锁时,它的标识和计数器(通常初始化为1)被记录在锁的value中。如果同一个客户端再次请求锁,Redis会检查value中的标识是否与当前客户端的标识匹配,如果匹配,则增加计数器而不是重新设置锁。释放锁时,计数器会减少,只有当计数器减到0时,锁才真正被释放。

3.锁超时:分布式锁通常支持设置锁的超时时间,以防止因节点故障或其他原因导致锁无法释放而引起系统阻塞。

4.锁的实现方式:常见的分布式锁实现方式包括基于数据库的实现(使用行级锁或乐观锁)、基于缓存的实现(使用Redis、Memcached等分布式缓存)、基于ZooKeeper、etcd等分布式协调服务的实现,以及基于分布式锁算法的自定义实现等。

5.容错性:分布式锁需要考虑节点故障或网络分区等异常情况下的容错处理,确保锁的可靠性和稳定性。

6.性能和成本:选择合适的分布式锁实现需要考虑其性能开销和成本,尽量减少对系统性能的影响,并兼顾系统的可扩展性和可维护性。

分布式锁应该具备哪些条件:

1
2
3
4
5
6
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性:指获得锁的线程可以再次进入到相同的锁的代码块中,意义在于防止死锁
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

分布式锁的实现

常见的一共有三种:

1.使用关系型mysql数据库实现分布式锁。

实现方法:

  1. 在 MySQL 中实现分布式锁通常涉及到使用数据库表。可以创建一个专用的锁表,并利用行的唯一性(例如利用唯一索引)来实现锁机制。
  2. 使用基于事务的 FOR UPDATE 语句或 GET_LOCK() 函数来获取锁。

实操1方法,建一个锁表:

1
2
3
4
5
6
>create table test(
id int(4) primary key auto_increment comment '主键',
method_name varchar(255) not null comment '方法名',
datetime timestamp not null comment '过期时间',
unique key unique_method_name ( method_name )
)ENGINE=InnoDB default charset=utf8;

定义一个自增的主键、一个拥有唯一索引的方法名,以及一个过期时间。接下来,当有一个请求进行访问时,往数据库中插入一个数据,代码如下:

1
>insert into test values(null,'当前执行的方法名',current_timestamp + 想要加的秒数);

由于数据库中的第二列method_name添加了唯一约束,同一时间只会有一个请求访问当前方法,知道方法执行完成之后再删除掉对应的数据,代码如下:

1
>delete from test where method_name = '当前执行的方法名';

这样就可以保证分布式锁的作用。

注意:加一个过期时间的原因,当一个请求过来的时候,向数据库中添加了数据之后,服务器突然宕机,此时数据库中的数据还在,当服务器恢复之后,由于这个方法的数据一直在,所以导致无法有请求来执行这个方法。所以总体来说,过期时间再加上一个周期性检查数据是否过期的任务,来防止服务器突然宕机等情况,导致数据库中的数据没有被删除

优点:

  • 对于已经使用 MySQL 的系统,使用数据库来实现分布式锁很方便,无需额外的技术栈。
  • 利用事务和锁机制,保证了一致性

缺点:

  • 性能问题:相较于其他专用的锁服务,数据库操作通常性能较低。
  • 可能会因为数据库锁的冲突导致行锁升级为表锁,影响整个表的性能。
  • 增加数据库的负担,尤其是在高并发场景下。

2.使用redis非关系型数据库实现分布式锁。

特点:

A.Redis是单线程的,速度非常快。

B.Redis所能承受的压力,比mysql数据库高出很多。

可以使用原生redis中的setnx这个命令

优点:

  • 性能高:Redis 是内存数据库,获取锁和释放锁的操作非常快。
  • 支持锁的自动过期,降低死锁的风险。
  • 实现简单,客户端支持广泛。

缺点:

  • 不是正真意义的公平锁,无法保证请求锁的顺序。
  • 在 Redis 集群模式下,没有内置的分布式锁支持,需要更为复杂的实现来保证锁的一致性

3.使用zookeeper注册中心来实现分布式锁。

实现方法:

  1. 利用 Zookeeper 的节点(Znode)作为锁。客户端创建一个顺序临时节点,如果该节点是最小的节点,则获取锁。
  2. 客户端监听前一个顺序节点的删除事件来实现锁的等待。

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,他的内部是一个文件系统目录的树结构,和Linux系统比较相似。

可以采用创建临时节点的方法来进行加锁,当所有请求同时创建临时节点的时候,只会有一个请求能够成功创建临时节点,此时其他的请求都会直接报错,返回,我们使用服务降级来解决这一问题。

当服务器宕机之后,zookeeper也会随之下线,服务器重连之后,由于zookeeper也会进行重新连接,所以临时节点也会随之消亡,不必担心没有删除临时节点突然宕机的情况。

缺点:过于频繁创建和删除节点,在性能这一方面远远不如redis,并且只要有一处不可用,整个zookeeper服务就不可用,因为zookeeper满足CAP定理中的CP。

优点:

  • 公平性:因为 Zookeeper 的顺序节点保证了请求锁的顺序。
  • 可靠性高:Zookeeper 保证了状态的一致性。
  • 具备强一致性和容错性:适用于对一致性要求较高的场景

缺点:

  • 相较于 Redis,性能较低。
  • 实现复杂,需要处理 Znode 的创建和监听。
  • 对Zookeeper集群的依赖较大,要求集群本身高可用