掘金 后端 ( ) • 2024-05-07 18:11

前言

  由于在平时的工作中,线上服务器是分布式多台部署的,经常会面临解决分布式场景下数据一致性的问题,那么就要利用分布式锁来解决这些问题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性、可用性和分区容错性,最多只能同时满足两项”,故此很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

  在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。所以自己结合实际工作中的一些经验和网上看到的一些资料,做一个讲解和总结。希望这篇文章可以方便自己以后查阅,同时要是能帮助到他人那也是很好的。

一、基础概念

1.1 什么是锁

在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量,而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记,这个标记可以理解为锁。

我们都知道,在业务开发中,为了保证在多线程下处理共享数据的安全性,需要保证同一时刻只有一个线程能处理共享数据。Java 给我们提供了线程锁,开放了处理锁机制的 API,比如 Synchronized、Lock 等。当一个锁被某个线程持有的时候,另一个线程尝试去获取这个锁会失败或者阻塞,直到持有锁的线程释放了该锁。在单台服务器内部,可以通过线程加锁的方式来同步,避免并发问题。如下图所示:

20240503201643.png

在多线程环境下,为了保证数据的线程安全,锁保证同一时刻,只有一个可以访问和更新共享数据。在单机系统中,我们可以使用 synchronized 或者 Lock 保证线程安全。synchronized 是 Java 提供的一种内置锁,在单个 JVM 进程中提供线程之间的锁定机制,控制多线程并发,但只适用于单机环境下的并发控制。

1.2 什么是分布式锁

分布式锁是一种在分布式系统中用于控制并发访问的机制。在分布式系统中,多个客户端同时对同一个资源进行访问时,容易出现数据不一致的问题。分布式锁的作用就是确保同一时刻只有一个客户端能够对某个资源进行访问,以避免数据不一致的问题。

在传统单体应用单机部署的情况下,可以使用并发处理相关的功能(如ReentrantLcok或synchronized)进行互斥控制来解决。但是,随着业务的发展,系统架构也会逐步优化升级,原本单体单机部署的系统被演化成分布式集群系统,由于分布式系统多线程、多进程并且分布在多个不同机器上,这将使原单机部署情况下的并发控制锁策略无法满足,并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式所要解决的难题!

分布式场景下解决并发问题,需要应用分布式锁技术。如上图所示,分布式锁的目的是保证在分布式部署的应用集群中,多个服务在请求同一个方法或者同一个业务操作的情况下,对应业务逻辑只能被一台机器上的一个线程执行,避免出现并发问题。如下图所示:

20240503202153.png

1.3 锁和事务的区别

  单进程的系统中,存在多线程同时操作一个公共变量,此时需要加锁对变量进行同步操作,保证多线程的操作线性执行消除并发修改。解决的是单进程中的多线程并发问题。

  • 分布式锁

  只要的应用场景是在集群模式的多个相同服务,可能会部署在不同机器上,解决进程间安全问题,防止多进程同时操作一个变量或者数据库。解决的是多进程的并发问题。

  • 事务

  解决一个会话过程中,上下文的修改对所有数据库表的操作要么全部成功,要不全部失败。所以应用在service层。解决的是一个会话中的操作的数据一致性。

  • 分布式事务

  解决一个联动操作,比如一个商品的买卖分为添加商品到购物车、修改商品库存,此时购物车服务和商品库存服务可能部署在两台电脑,这时候需要保证对两个服务的操作都全部成功或者全部回退。

二、分布式锁基础理论

2.1 为什么要使用分布式锁

为什么需要分布式锁呢?在很久以前,用户群体不大的时候,单体应用就可以足够满足用户的所有请求,当用户增加的时候,出现了一定的并发度,可以使用简单的锁机制来协调并发的共享资源的获取。但是,随着业务的增大,用户数量的增加,为了满足业务的高效性、集群的出现,简单的锁机制已经不能够满足协调多个应用之间的共享资源了,于是就出现了分布式锁。由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

微服务的架构下,多个应用服务要同时对同一条数据做修改,那么要确保数据的正确性,就只能有一个应用修改成功。可以把锁看成房门外的一把锁,所有并发线程比作人,他们都想进入房间,房间内只能有一个人进入。当有人进入后,将门反锁,其他人必须等待,直到进去的人出来。server1、server2、server3 这三个服务都要修改amount这个数据,每个服务更新的值不同,为了保证数据的正确性,三个服务都向lock server服务申请修改权限,最终server2拿到了修改权限,即server2将amount更新为2,其他服务由于没有获取到修改权限则返回更新失败。如下图所示:

20240504022138.png

2.2 分布式锁特性

分布式锁需要解决的问题包括互斥性、安全性、死锁和容错。互斥性是指任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。安全性是指锁只能被持有该锁的客户端删除,不能由其它客户端删除。死锁是指获取锁的客户端因为某些原因未能释放锁,其它客户端再也无法获取到该锁。容错是指当部分节点down机时,客户端仍然能够获取锁和释放锁。

那么对于一个分布式系统中分布式锁应该满足什么条件呢?也就是它应该具备怎样的约束、规则,下面是我总结的分布式锁至少拥有的几个规则。

graph TD
    A[分布式锁特性] --> A1[互斥性]
    A --> A2[防止死锁]
    A --> A3[高可用性]
    A --> A4[可重入性]
    A --> A5[唯一标识]
  • 互斥性:分布式锁最基本的特性。任意时刻,共享资源的锁在同一时间只能有一个客户端才能获取。当有节点获取锁之后,其他节点无法获取锁,不同节点之间具有互斥性。
  • 防止死锁:有时也称之为超时机制。分布式锁应该设计成在锁的持有者异常退出或崩溃时能够自动释放,其他请求能正常获取锁,以防止死锁的发生。
  • 高可用性:查询快,且在节点故障时也能正常工作,确保锁的可靠性。
  • 可重入性:允许同一个线程或客户端在持有锁的情况下多次获取同一个锁,而不会出现死锁或阻塞的情况。
  • 唯一标识:分布式锁应该具备唯一的标识,以便客户端可以识别和管理不同的锁。锁只能被持有的用户删除,不能被其他客户端删除。

一个分布式锁能够具备上面的几种条件,应该来说是比较好的分布式锁了,但是现实中没有十全十美的锁,对于不同的分布式锁,没有最好,只能说那种场景更加适合。例如,在分布式缓存中,多个节点同时对同一个缓存进行操作时,可能会出现数据不一致的问题。使用分布式锁可以确保同一时刻只有一个节点能够对该缓存进行操作,以避免数据不一致的问题。

2.3 分布式锁的实现方式

分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。分布式锁通常基于某种共享存储机制实现,分布式锁的实现方式主要有基于数据库、基于缓存(如Redis)和基于 Zookeeper 等第三方服务系统来设计,如下所示:

  • 基于数据库的分布式锁

基于数据库的分布式锁是最常见的一种实现方式,其基本原理是利用数据库的行锁或表锁来实现分布式锁。具体的实现方式是在数据库中创建一个专门用于锁定的表,该表中包含一个字段用于标识锁的状态(锁定或未锁定),以及一个字段用于记录锁定该资源的进程ID。当需要锁定资源的进程获取到锁后,会在表中更新自己的进程ID,而其他进程则无法获取到锁。当锁定资源的进程释放锁后,其他进程才可以再次尝试获取锁。

  • 基于Redis的分布式锁

Redis 是一个单独的非业务服务,不会受到其他业务服务的限制,所有的业务服务都可以向 Redis 发送写入命令,且只有一个业务服务可以写入命令成功,那么这个写入命令成功的服务即获得了锁,可以进行后续对资源的操作,其他未写入成功的服务,则进行其他处理。基于 Redis 的分布式锁利用 Redis 的 setnx 命令来实现。具体的实现方式是当某个节点需要锁定资源时,通过 setnx 命令将一个指定的key设置为value(该value通常是资源的唯一标识符),如果设置成功则表示获取到了锁。其他节点在尝试获取锁时,会先检查该key是否存在,如果不存在则表示锁已经被获取,等待锁的释放即可。当锁定资源的节点释放锁时,通过delete命令删除该key即可。流程如下:

20240503194942.png

  • 基于Zookeeper的分布式锁

Zookeeper 是一个高性能的、开源的、为分布式应用所设计的协调服务,ZooKeeper 有四种节点类型,包括持久节点、持久顺序节点、临时节点和临时顺序节点,利用 ZooKeeper 支持临时顺序节点的特性,可以实现分布式锁。具体的实现方式是在 Zookeeper中 创建一个临时节点作为锁标志,当某个节点需要锁定资源时,通过 Zookeeper 的临时节点实现等待锁的释放。当锁定资源的节点释放锁时,删除临时节点即可。Zookeeper 创建临时会话顺序节点时,谁创建的节点序号最小,谁就获得了锁,并且其他节点就会监听序号比自己小的节点,一旦序号比自己小的节点被删除了,其他节点就会得到相应的事件,然后查看自己是否为序号最小的节点,如果是,则获取锁。流程如下:

20240503195956.png

总结

把今天最好的表现当作明天最新的起点…...~

  投身于天地这熔炉,一个人可以被毁灭,但绝不会被打败!一旦决定了心中所想,便绝无动摇。迈向光明之路,注定荆棘丛生,自己选择的路,即使再荒谬、再艰难,跪着也要走下去!放弃,曾令人想要逃离,但绝境重生方为宿命。若结果并非所愿,那就在尘埃落定前奋力一搏!

划重点.gif