分布式锁
字面上,就是在分布式系统架构里所用到的锁,一种用于并发控制的机制,确保在多个节点上对共享资源的互斥访问,从而避免数据竞争和冲突。与单体架构中的锁,最大的区别就是,细粒度的划分:从线程间的资源共享到进程间的资源共享。
分布式锁与普通锁(本地锁)的区别:
1.作用范围
- 普通锁:
单个进程或单个计算机内的多个线程之间的同步。当多个线程尝试访问同一资源时,普通锁可以确保只有一个线程可以访问该资源,其他线程需要等待锁的释放。 - 分布式锁:
分布式锁用于跨多个进程或多个计算机之间的同步。允许不同的进程或计算机协调对共享资源的访问,以避免冲突和数据不一致性。
2.锁的获取方式
- 普通锁:
普通锁通常是基于本地内存的互斥量或自旋锁实现的,可以通过在内存中的标记或计数器来判断锁的状态,并通过执行CPU自旋等待来获取锁。 - 分布式锁:
分布式锁通常使用基于分布式系统的外部组件或服务,如分布式缓存系统(如Redis)或分布式协调服务(如ZooKeeper)实现。进程或计算机通过与这些组件进行通信来获取和释放锁。
3.可靠性和容错性
- 普通锁:
普通锁在单个计算机上运行,受限于该计算机的可靠性和容错性。如果计算机故障或程序崩溃,可能会导致锁被永久占用或意外释放。 - 分布式锁:
分布式锁通过将锁状态存储在外部组件中,可以提供更高的可靠性和容错性。即使其中一个计算机或进程崩溃,其他进程仍然可以通过与外部组件通信来获取锁。
分布式锁的一些概念和特点:
1.互斥性:分布式锁能够确保同一时刻只有一个节点能够获取锁,从而避免多个节点同时对共享资源进行修改或访问。
2.可重入性:指同一个线程或进程在已经持有锁的情况下,可以再次获取同一把锁,而不会被自己阻塞。(不是必须的,比如非常简单的操作中,执行路径明确)。
- 可重入性例子:
可以通过在锁的value中记录当前客户端的标识和计数器信息来实现。当客户端第一次获取锁时,它的标识和计数器(通常初始化为1)被记录在锁的value中。如果同一个客户端再次请求锁,Redis会检查value中的标识是否与当前客户端的标识匹配,如果匹配,则增加计数器而不是重新设置锁。释放锁时,计数器会减少,只有当计数器减到0时,锁才真正被释放。
3.锁超时:分布式锁通常支持设置锁的超时时间,以防止因节点故障或其他原因导致锁无法释放而引起系统阻塞。
4.锁的实现方式:常见的分布式锁实现方式包括基于数据库的实现(使用行级锁或乐观锁)、基于缓存的实现(使用Redis、Memcached等分布式缓存)、基于ZooKeeper、etcd等分布式协调服务的实现,以及基于分布式锁算法的自定义实现等。
5.容错性:分布式锁需要考虑节点故障或网络分区等异常情况下的容错处理,确保锁的可靠性和稳定性。
6.性能和成本:选择合适的分布式锁实现需要考虑其性能开销和成本,尽量减少对系统性能的影响,并兼顾系统的可扩展性和可维护性。
分布式锁应该具备哪些条件:
1 | 1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; |
分布式锁的实现
常见的一共有三种:
1.使用关系型mysql数据库实现分布式锁。
实现方法:
- 在 MySQL 中实现分布式锁通常涉及到使用数据库表。可以创建一个专用的锁表,并利用行的唯一性(例如利用唯一索引)来实现锁机制。
- 使用基于事务的 FOR UPDATE 语句或 GET_LOCK() 函数来获取锁。
实操1方法,建一个锁表:
1 | >create table test( |
定义一个自增的主键、一个拥有唯一索引的方法名,以及一个过期时间。接下来,当有一个请求进行访问时,往数据库中插入一个数据,代码如下:
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注册中心来实现分布式锁。
实现方法:
- 利用 Zookeeper 的节点(Znode)作为锁。客户端创建一个顺序临时节点,如果该节点是最小的节点,则获取锁。
- 客户端监听前一个顺序节点的删除事件来实现锁的等待。
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,他的内部是一个文件系统目录的树结构,和Linux系统比较相似。
可以采用创建临时节点的方法来进行加锁,当所有请求同时创建临时节点的时候,只会有一个请求能够成功创建临时节点,此时其他的请求都会直接报错,返回,我们使用服务降级来解决这一问题。
当服务器宕机之后,zookeeper也会随之下线,服务器重连之后,由于zookeeper也会进行重新连接,所以临时节点也会随之消亡,不必担心没有删除临时节点突然宕机的情况。
缺点:过于频繁创建和删除节点,在性能这一方面远远不如redis,并且只要有一处不可用,整个zookeeper服务就不可用,因为zookeeper满足CAP定理中的CP。
优点:
- 公平性:因为 Zookeeper 的顺序节点保证了请求锁的顺序。
- 可靠性高:Zookeeper 保证了状态的一致性。
- 具备强一致性和容错性:适用于对一致性要求较高的场景
缺点:
- 相较于 Redis,性能较低。
- 实现复杂,需要处理 Znode 的创建和监听。
- 对Zookeeper集群的依赖较大,要求集群本身高可用