分布式锁的各种实现,看完这篇你就懂了!

作者:微信小助手

发布时间:2023-11-24T12:35:38

今天我们讲讲分布式锁,网上相关的内容有很多,但是比较分散,刚好自己刚学习完总结下,分享给大家,文章内容会比较多,我们先从思维导图中了解要讲的内容。

什么是分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式,通过互斥来保持一致性。

了解分布式锁之前先了解下线程锁和进程锁:

线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如Synchronized、Lock等

进程锁:控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁

比如Golang语言中的sync包就提供了基本的同步基元,如互斥锁

但是以上两种适合在单体架构应用,但是分布式系统中多个服务节点,多个进程分散部署在不同节点机器中,此时对于资源的竞争,上诉两种对节点本地资源的锁就无效了。

这个时候就需要分布式锁来对分布式系统多进程访问资源进行控制,因此分布式锁是为了解决分布式互斥问题!

分布式锁的特性

互斥

互斥性很好理解,这也是最基本功能,就是在任意时刻,只能有一个客户端才能获取锁,不能同时有两个客户端获取到锁。

避免死锁

为什么会出现死锁,因为获取锁的客户端因为某些原因(如down机等)而未能释放锁,其它客户端再也无法获取到该锁,从而导致整个流程无法继续进行。

面对这种情况,当然有解决办法啦!

引入过期时间:通常情况下我们会设置一个 TTL(Time To Live,存活时间) 来避免死锁,但是这并不能完全避免。

  1. 1. 比如TTL为5秒,进程A获得锁

  2. 2. 问题是5秒内进程A并未释放锁,被系统自动释放,进程B获得锁

  3. 3. 刚好第6秒时进程A执行完,又会释放锁,也就是进程A释放了进程B的锁

仅仅加个过期时间会设计到两个问题:锁过期和释放别人的锁问题

锁附加唯一性:针对释放别人锁这种问题,我们可以给每个客户端进程设置【唯一ID】,这样我们就可以在应用层就进行检查唯一ID。

自动续期:锁过期问题的出现,是我们对持有锁的时间不好进行预估,设置较短的话会有【提前过期】风险,但是过期时间设置过长,可能锁长时间得不到释放。

这种情况同样有处理方式,可以开启一个守护进程(watch dog),检测失效时间进行续租,比如Java技术栈可以用Redisson来处理。

可重入:

一个线程获取了锁,但是在执行时,又再次尝试获取锁会发生什么情况?

是的,导致了重复获取锁,占用了锁资源,造成了死锁问题。

我们了解下什么是【可重入】:指的是同一个线程在持有锁的情况下,可以多次获取该锁而不会造成死锁,也就是一个线程可以在获取锁之后再次获取同一个锁,而不需要等待锁释放。

解决方式:比如实现Redis分布式锁的可重入,在实现时,需要借助Redis的Lua脚本语言,并使用引用计数器技术,保证同一线程可重入锁的正确性。

容错

容错性是为了当部分节点(redis节点等)宕机时,客户端仍然能够获取锁和释放锁,一般来说会有以下两种处理方式:

一种像etcd/zookeeper这种作为锁服务能够自动进行故障切换,因为它本身就是个集群,另一种可以提供多个独立的锁服务,客户端向多个独立锁服务进行请求,某个锁服务故障时,也可以从其他服务获取到锁信息,但是这种缺点很明显,客户端需要去请求多个锁服务。

分类

本文会讲述四种关于分布式锁的实现,按实现方式来看,可以分为两种:自旋、watch监听

自旋方式

基于数据库和基于Redis的实现就是需要在客户端未获得锁时,进入一个循环,不断的尝试请求是否能获得锁,直到成功或者超时过期为止。

监听方式

这种方式只需要客户端Watch监听某个key就可以了,锁可用的时候会通知客户端,客户端不需要反复请求,基于zooKeeper和基于Etcd实现分布式锁就是用这种方式。

实现方式

分布式锁的实现方式有数据库、基于Redis缓存、ZooKeeper、Etcd等,文章主要从这几种实现方式并结合问题的方式展开叙述!

基于MySQL

利用数据库表来实现实现分布式锁,是不是感觉有点疑惑,是的,我再写之前收集资料的时候也有点疑问,虽然这种方式我们并不推崇,但是我们也可以作为一个方案来进行了解,我们看看到底怎么做的:

比如在数据库中创建一个表,表中包含方法名等字段,并在方法名name字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入一条记录,成功插入则获取锁,删除对应的行就是锁释放。

//锁记录表
CREATE TABLE `lock_info` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(64) NOT NULL COMMENT '方法名',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_name` (`method_name`) 
) ENGINE=InnoD

这里主要是用name字段作为唯一索引来实现,唯一索引保证了该记录的唯一性,锁释放就直接删掉该条记录就行了。

缺点也很多:

  1. 1. 数据库是单点,非常依赖数据库的可用性

  2. 2. 需要额外自己维护TTL

  3. 3. 在高并发常见下数据库读写是非常缓慢

这里我们就不用过多的文字了,现实中我们更多的是用基于内存存储来实现分布式锁。

基于Redis

面试官问:你了解分布式锁吗?想必绝大部分面试者都会说关于Redis实现分布式锁的方式,OK,进入正题【基于Redis分布式锁】

Redis 的分布式锁, setnx 命令并设置过期时间就行吗?

setnx lkey lvalue expire lockKey 30

正常情况下是可以的,但是这里有个问题,虽然setnx是原子性的,但是setnx + expire就不是了,也就是说setnx和expire是分两步执行的,【加锁和超时】两个操作是分开的,如果expire执行失败了,那么锁同样得不到释放。

关于为什么要加锁和超时时间的设定在文章开头【避免死锁】有提到,不明白的可以多看看。

Redis正确的加锁命令是什么?

//保证原子性执行命令
SET lKey randId NX PX 30000

randId是由客户端生成的一个随机字符串,该客户端加锁时具有唯一性,主要是为了避免释放别人的锁。