分布式锁的实现

背景

在JAVA程序中,我们为了保证多线程的安全性,我们引入了sychronized和Lock的概念,通过锁来保证多个线程可以访问同一个共享变量,且同时只有一个线程可以修改这个共享变量。
但上面的情况都是针对单机情况而言,随着互联网的发展,现在各大公司都已经是分布式架构,即一个应用要部署到多个服务器上,然后做负载均衡。可以理解成多个线程跑在了不同的JVM中,如果想要访问同一个共享的资源,传统的JVM锁是无法满足要求的,因此提出了分布式锁的概念,来解决多一种跨JVM的互斥机制来控制共享资源的访问。

分布式锁的条件

既然是锁,那肯定必须包含最基本的功能

  1. 加锁
  2. 解锁
  3. 锁超时机制。解决常见的死锁问题。
  4. 可重入性
  5. 阻塞机制

我们在锁的实现的时候,要带着这些问题去考虑分布式锁的性能和架构。

实现

在开始之前,我们先来了解一下分布式的CAP原则和一致性概念。

CAP原则指的是在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),最多只能满足两个,三者不能兼得。

强一致性:任何时候查询任何节点都要保证数据是最新的状态,如zookeeper。
最终一致性:允许任何时间查询的数据不一致,但是要保证最终数据一致,如reids的主从复制

在分布式系统中, CAP这三者很难做出取舍,在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

为了保证最终一致性,现有很多的方案,常用的方案有分布式锁、分布式事务等。

在本篇文章中,主要给大家介绍分布式锁的集中实现方式

基于数据库实现

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

DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
  `desc` varchar(255) NOT NULL COMMENT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

第二步、获取锁

INSERT INTO method_lock (method_name, desc) VALUES ('test', '测试');

因为我们对method_name这个字段加了唯一索引,所以当多个请求同事请求数据库时,只有一条Sql语句可以执行成功,获得锁。
第三步、释放锁

delete from method_lock where method_name ='test';

当执行完成后,删除对应方法的数据行即可释放锁。

这种实现的缺点

  • 锁对数据库时强依赖。分布式锁的性能和可用性由数据库的性能决定。
  • 不可重复锁。一但获取锁,获得锁的该机器也许要等待其自己的锁释放掉才可以再次获取锁。
  • 没有失效机制。因为应用程序是不可靠,万一应用程序挂掉或卡住,或者由于网络原因阻塞,那么锁永远不会释放,
  • 不具备阻塞机制。一但获取不到锁,则直接返回失败。

当然,如果现实中真的要使用数据库作为分布式锁,上面的问题也不是别看可以解决。比如:

  • 数据库分布式部署来保证锁的性能和可用性
  • 没有失效机制。锁表中再加一列失效时间,通过定时任务来清理无效的数据,实现失效机制
  • 不可重入。锁表添加一列来记录当前机器和线程,如果来自同一机器的同一线程,则直接获取锁。

为了解决这些问题,分布式锁的实现变的越来越复杂,性能和开销也越来越大。

基于redis的实现

redis是基于内存型的key-value数据库,有如下优点

  1. 基于内存的,性能高,支持10W+的tps(官方说的)
  2. redis的key可设置过期时间,天然的直接解决死锁。
  3. redis有setnx命令,天然满足锁的实现需求。当且仅当key不存在时,将value赋值给Key,返回1,不存在时,返回0.

获取锁的方式:

SET 资源名 value NX PX 过期时间

这种实现的缺点:

  1. 会存在竞争问题。在主从模式中,如果主挂掉,主的锁信息还没有同步到从,把主切换到从的时候,会导致这个锁的信息丢失,从而引起锁失效的问题。

当然,这些缺点也可以通过控制主从同步的模型以及同步的时间来减少这种损失。

zookeeper

zookeeper是一个种分布式文件系统,它内部是一个分层的文件系统目录树结构。如下图
在这里插入图片描述

有序节点:假如当前有一个父节点为 /动物,我们可以在这个父节点下面创建子节点,ZK 提供了一个可选的有序特性。狗为1,猫为2.
临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,ZK 会自动删除该节点。
事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,ZK 会通知客户端。

第一步、创建资源,即创建目录。
创建一个持久结点mylock,用于表示锁资源。
第二步、获取锁。
(1)假设clientA请求来获取锁,则在mylock目录下创建临时结点a。
(2)假设clientB和clientC依赖来访问同一块资源,则在mylock下创建临时b和临时结点c。如下图所示,为了方便理解,持久结点用实线表示,临时结点用虚线表示。
clientB会监听靠前的结点a,clientC会监听靠前的结点b。
这样,当a释放锁时,clientB会收到通知,去获取锁。当b释放锁时,clientC会受到通知,去获取锁。
在这里插入图片描述
第三步、释放锁
这里释放锁会有多种情况。

1、正常释放锁。clientA任务完成,删除结点a.
2、客户端崩溃或网络异常,clientA与zookeeper断开连接,a结点也会自动删除。

优点:

  1. 具有高可用性
  2. 具有锁重入机制,同一客户端用同一个临时结点。
  3. 具备锁阻塞机制。因为临时结点有序
  4. 可解决死锁问题,临时结点会自动删除。

缺点:
1、性能相比redis较低。需要频繁的创建和删除结点,且redis为内存型的,性能更好。
2、也会存在并发问题。因为客户端与zookeeper断开连接,可能会是网络阻塞的问题,如果结点被立刻删除,那么clientB获取了资源,就存在了并发的问题。但这个问题出现几率很低,因zookeeper有重试机制。

总结

就跟分布式系统的CAP原理一样,我们无法同时满足CAP三个,最多只能满足两个。所以具体使用哪种方案去实现分布所还要根据我们具体的业务场景去分析。

就个人而言的话,我比较推荐ZK 实现的分布式锁:因为 Redis 是有可能存在隐患的,可能会导致数据不对的情况。

如果公司里面有 ZK 集群条件,优先选用 ZK 实现,除非说公司里面只有 Redis 集群,没有条件搭建 ZK 集群。

参考:https://www.cnblogs.com/liuqingzheng/p/11080501.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值