首页 > 图灵资讯 > 技术篇>正文

一文了解分布式锁

2023-04-13 16:33:24

  大多数互联网系统都是分布式部署的,确实可以提高性能和效率,但因此,我们需要解决分布式环境中的数据一致性问题。

  当一个资源在多系统之间共享时,为了确保每个人访问的资源数据都是一致的,那么必须要求只有一个客户端同时处理,不能并发执行,否则会有人写有人同时阅读,每个人访问的数据都不一致。

  为什么我们需要分布式锁?

  在单机时代,虽然不需要分布式锁,但也面临着类似的问题,但在单机情况下,如果有多个线程同时访问共享资源,我们可以使用线程锁机制,即当线程获得资源时,立即锁定资源,当使用资源时,解锁,其他线程可以继续使用。例如,在JAVA中,一些API甚至提供了特殊的锁处理机制(synchronize/Lock等)

  然而,在分布式系统时代,线程之间的锁定机制不起作用。系统可能有多个部件并部署在不同的机器上。这些资源不再在线程之间共享,而是在过程之间共享的资源。

  因此,为了解决这个问题,我们必须介绍它「分布式锁」。

  分布式锁是指在分布式部署环境下,通过锁机制访问多客户互斥的共享资源。

  分布式锁应满足哪些要求?

  排他性:只有一个客户端可以同时获得锁,其他客户端不能同时获得锁

  避免锁:锁在有限的时间内,会被释放(正常释放或异常释放)

  高可用性:获取或释放锁的机制必须高可用性和良好的性能

  在完成背景和理论之后,让我们来看看分布式锁的具体分类和实际应用。

  二、实现分布式锁的方法有哪些?

  目前主流有三种,从实现的复杂性来看,难度从上到下依次增加:

  基于数据库的实现

  基于Redis的实现

  实现基于Zookeper的实现

  不管怎样,其实都不完美,还是要根据我们业务的实际情况来选择。

  基于数据库的实现:

  通常有两种方法可以基于数据库进行分布式锁定:

  基于数据库的乐观锁

  基于数据库的悲观锁

  让我们来看看如何基于它「乐观锁」来实现:

  乐观锁机制实际上是在数据库表中引入一个版本号(version)实现字段。

  当我们想从数据库中读取数据时,我们也会读取这个version字段。如果我们想更新读取的数据并写回数据库,我们需要添加version1,并将新数据和新version更新到数据表中,我们必须在更新时检查当前数据库中的version值是否为以前的version。如果是,则正常更新。如果没有,则更新失败,表明在此过程中还有其他过程可以更新数据。

  下面找图例,

  如图所示,假设用户A和用户B必须在同一个账户中取款。账户原余额为2000,用户A取1500,用户B取1000。如果没有锁定机制,余额可能同时扣除1500和1000,导致最终余额不正确甚至负。但是,如果在这里使用乐观的锁定机制,当两个用户去数据库读取余额时,除了读取2000余额外,他们还读取了当前版本号version=1。当用户A或用户B修改数据库余额时,无论谁先操作,版本号都会增加1,即version=2,然后当另一个用户更新时,他们会发现版本号是错误的,已经变成2,当初读出来的时候不是1,那么这次更新失败了,就要重新读取最新的数据库余额。

  从上面的例子可以看出,使用「乐观锁」必须满足机制:

  (1)锁服务要有增加版本号version

  (2)每次更新数据时,必须先判断版本号是否正确,然后写入新版本号

  让我们来看看如何基于它「悲观锁」来实现:

  悲观锁又称排他锁,基于Mysql for update 实现加锁,例如:///锁定方法-伪代码 public boolean lock(){ connection.setAutoCommit(false) for(){ result = select * from user where id = 100 for update; if(result){ //结果不是空的, ///说明获得了锁 return true; } ///没有得到锁,继续获取 sleep(1000); } return false; } //释放锁-伪代码 connection.commit();

  在上面的例子中,id是user表中的主键,通过 for update 操作时,数据库会在查询此记录时添加排他锁。

  (需要注意的是,只有在InnoDB中添加索引的字段才是行级锁,否则是表级锁,所以这个id字段应该添加索引)

  当该记录加上排他锁时,其他线程无法操作此记录。

  然后,在这种情况下,我们可以认为获得排他锁的线程有一个分布式锁,然后我们可以执行我们想要做的业务逻辑。逻辑完成后,我们可以调用上述释放锁的句子。

  基于Redis的实现

  基于Redis实现的锁定机制主要依靠Redis自身的原子操作,如: SET user_key user_value NX PX 100

  redis从2.6开始.SET命令从12版开始支持这些参数:

  NX:只有当键不存在时,才能设置键,SET key value NX 效果等同于 SETNX key value

  PX millisecond:设置键的过期时间为millisecondm秒。当超过此时间时,设置键将自动失效

  上述代码示例是指当redis中没有user_key键时,将设置user_key键,并将键的值设置为 user_value,而且这个键的存活时间是100ms

  为什么这个命令可以帮助我们实现锁定机制?

  因为这个命令只有在key不存在的时候才能成功执行。然后,当多个过程同时设置相同的key时,总会有一个过程成功。

  当一个过程设置成功时,您可以执行业务逻辑,然后在业务逻辑完成后解锁。

  解锁很简单,只需删除这个key即可,但在删除之前需要判断,这个key对应的value是原来自己设置的那个。

  此外,redis集群模式的分布式锁可以采用redis的redlock机制。

  实现基于Zookeper的实现

  事实上,基于Zookeeper,是利用其临时有序节点实现的分布式锁。

  原理是:当客户端需要逻辑锁定时,在zookeeper上的指定节点目录下生成唯一的临时有序节点, 然后判断自己是否是这些有序节点中最小的序号。如果是这样,它将被视为获得锁。如果没有,则表示没有锁,则需要在序列中找到比自己小的节点,并调用exist()方法监控其注册事件。当监控器听到节点被删除时,再次判断其创建的节点是否成为序列中最小的节点。如果是,获得锁,如果没有,重复上述步骤。

  释放锁时,只需删除此临时节点即可。

  如图所示,locker是一个持久节点,node_1/node_2/…/node_n 由客户端client创建的临时节点。

  client_1/client_2/…/clien_n 都是想要获得锁的客户端。以client_1为例,要想获得分布式锁,需要跑到locker下创建临时节点(如果是node_1),看看自己的节点序号是否是locker下面最小的,如果是,就可以获得锁。如果没有,找到比自己小的节点(如果是node_2),找到后监控node_2,直到node_2被删除,然后开始判断你的node_1是否是序列中最小的。如果是,获得锁。如果没有,继续找节点。

  以上是为什么我们需要分布式锁技术,以及分布式锁中常见的三种机制。

上一篇 Spring Boot 应用程序启动流程分析
下一篇 Java基础面试题i = i++ 执行原理

文章素材均来源于网络,如有侵权,请联系管理员删除。