疏漏总结(十四)——分布式锁

连续摸鱼一周了,想回归好青年好好学习了…就先从分布式锁开始着手进行学习吧。。。

首先,什么是分布式锁

分布式系统中多个进程之间是相互关联的, 但是也存在相互的干扰,为了能够让这些模块之间能够顺利进行调度,于是就可以通过分布式锁来实现这种想法。

我们为什么要用分布式锁

分布式环境下是多进程的,是否又在这个基础上进行多线程的开发,看个人,不过,我们至少需要的是让一个方法或者一个变量,同时存在于多个进程的jvm内存中去加载,处理,运算,那么就必然会导致一些关于至少是可见性的问题。

于是我们就可以通过分布式锁来解决相关的问题。

为了解决这个问题,分布式锁应该具备哪些基本条件

  1. 在分布式系统的环境下,一个方法在同一时间,只能被一个机器的一个线程所执行。
  2. 高可用且高性能的获取锁和释放锁
  3. 具备可重入锁的基本性质
  4. 具备锁失效机制,以防死锁
  5. 具备非阻塞锁的特性,也就是没有获取到锁直接返回一个锁失败。

我们可以通常见到哪些分布式锁

分布式锁一般有三种:
1.基于数据库(像下面说的mysql)
2.基于缓存(像redis memcached)
3.基于zk

在关系型数据库中,我们接触的mysql,就可以实现分布式锁的功能。方法是乐观锁,排他锁和唯一索引。(下面会细说)

而在缓存数据库中,最常见的其实就是redis了,redis利用基本的setnx来进行加锁的操作,并且保持原子操作,只有当key为null的时候才能加锁成功。

其次,我们还有见过zk实现的分布式锁,,可以利用zk的顺序临时节点来实现分布式锁和等待队列来实现。

我们可以在哪里使用到分布式锁呢?

提到分布式锁,就不得不提一嘴分布式事务,其实前几篇系列的文章,我倒是也提过分布式事务的几个种类,但是分布式事务,不管是要求强一致性的,还是走消息队列搞异步的,性能都不太理想,而且敏感度也不是很高,在这种情况下,我们就可以考虑使用分布式锁,不过说实话,我也是入门分布式锁,所以也不好说到底应该在哪里用,就从网上学习了一下。

先说个经典的使用场景:两个进程同时访问一片资源池里面一部分相同的资源,并且对其共同进行update操作(增删改)。

根据上面的案例,我们进行分别的举例

  • Mysql

(1) 基于mysql的数据库表

CREATE TABLE `LockTest` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁名',
  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_name` (`name`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

当希望锁住某个方法的时候,直接执行:

insert into LockTest(name,desc) values (${name},${desc});

因为我们这里做了唯一性约束,那么如果有多个请求同时提交到数据库的话,数据库只会保证一个操作成功,于是我们就可以认为操作成功的线程获取到了该方法的锁,可以执行方法体的内容。

当我们执行完了线程内的方法的时候,需要释放锁,就需要执行:

delete from LockTest where name=${name};

但是这种基于mysql数据库表的方法实现有几个问题:

  1. 锁强依赖于数据库本身,数据库挂了,锁也完蛋
  2. 这种锁没有失效的安全策略,一旦解锁失败,线程会永远锁在数据库里阻塞住
  3. 这种锁只能是非阻塞的,因为insert这种语句,如果失败会报错,这样没有获得锁的线程就不会进入队列获取锁(类似aqs),想要再次获得锁,就要再次触发事件。
  4. 这锁是非重入锁,所以同一个线程没释放之前无法重复获取锁,因为数据已经存在了。

但是这几个问题,可以这么解决:

  1. 数据库搞成分布式,给他来个HA
  2. 设置安全策略
  3. 手动写一个自旋操作,成功再退出
  4. 因为是非重入的,所以需要在数据库加一个记录线程信息的字段,让下次获取锁的时候优先查询数据库,如果线程信息存在,那么直接分配锁就可以了

(2) 基于mysql的数据库的排他锁

数据中自带的锁也可以用来实现分布式锁,基于InnoDB引擎,我们可以执行这句sql:
select * from LockTest where name = ${} for update;
我们知道这么写直接就加排他锁了(InnoDB默认加锁的原则都是表级锁,但是如果有索引驱动加锁,就会进行行锁的加锁,但这里更加特殊的一点是,我们还得在基础上使用唯一索引,否则会出现多个重载方法之间,无法同时被访问的情况

那么,在多进程访问资源的时候,获得到了排他锁,就相当于获得到了分布式锁。

相对于之前的数据库表进行分布式锁的加锁,优点就是:不用考虑无法释放锁和阻塞锁的问题了。

为什么不用考虑呢?

for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
而至于无法释放锁的问题,只要服务挂了,锁就自动释放了。

但是仍然有残留问题:可重入和单机数据库的问题

此外还有两个很重要的小概率事件:

虽然我们对name进行唯一索引,并且用for update加了排他锁,但mysql自带查询优化,你写了索引,但是InnoDB他不一定会用,这要根据他底层的不同执行计划的代价去决定,哪个效率高执行哪个,换句话说,虽然你用了索引,但是还是会有小概率导致进行了全表select最后导致InnoDB使用的是表锁。

还有一个问题,就是我们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆。

数据库实现分布式锁总结如下:

优点:基于sql实现简单
缺点:性能问题,意外事件,复杂业务不好写代码

  • Redis

相对于数据库实现分布式锁,Redis的性能更高,并且轻松解决单点的问题。

使用起来还是很简单的,我们使用 get <key> 检查被上了分布式锁的key,是否已经被上锁,返回nil就是没锁,返回string就是锁的name。

然后使用setnx < key > < lockname > 来设置锁住的key,和锁的名字。如果已经被锁了,就返回0,反之就返回1

在这里插入图片描述

既然设置了锁,那锁也总是得有失效时间的,总不能一直去锁着手动去释放,很麻烦,所以我们就需要设置锁的过期时间,操作如下:

假设我们给刚才的锁,设置10s的过期时间,然后再来检查锁的状态:

在这里插入图片描述
可以明显发现,设置之后,锁的状态有了明显转变。

实现的伪代码如下:
(图来自于自己博客)
在这里插入图片描述
但是这么写,有一个问题,如果我们用setnx设置了锁之后,设置过期时间之前就过期了,就会一直处于被锁的状态,其他的线程也就无法获得锁了。也就是说,也就是说,无法保证原子性。

于是,redis就又有了一种新方法,可以让setnx和expire揉在一起,创造出有过期时间的锁。

操作如下:

SET key value [EX seconds] [PX milliseconds] [NX|XX]

其中:

EX seconds:设置键的过期时间为second秒

PX milliseconds:设置键的过期时间为millisecond毫秒

NX:只在键不存在的时候,才对键进行设置操作

XX:只在键已经存在的时候,才对键进行设置操作

最后,在Set成功完成的时候,返回OK,否则返回nil。

但是,我们使用Redis进行分布式锁的实现也还是有一定缺点的:

1.锁只能是非阻塞的,无论成功还是失败都直接返回。
2.锁是非重入的,一个线程获得锁之后,在释放锁之前,无法再次获得该锁。

而至于修改的方法,也是可以参照之前数据库表的行为。

  • Zookeeper

大致的思路是:每一个客户端对应某个方法需要加锁的时候,zookeeper下管理的方法会和当前节点形成一个瞬时有序的序列,判断是否获取锁,只需要判断序列的最小值和当前节点目前的值是否是一样的就可以,释放锁,也只需要删除掉这个结点就可以了。同时,zk也可以防止服务端宕机导致锁无法释放就死锁了。

然后我们再看看zookeeper与众不同的地方

  1. 使用zk是可以解决锁无法释放的问题的,这个上面说了。
  2. zk也可以实现阻塞的锁,具体实现方法是,客户端通过在zk上创建顺序序列,并且序列绑定好一个监控器,一旦节点有变化,就去检查这个序列,如果所创建出的节点在序列中是最小的,就可以获取锁了。
  3. zk是分布式部署的,只要半数存活,就可以服务可用,所以也可以解决单点问题
  4. zk有一个很强的功能,在客户端创建节点的时候,会写入当前客户端的主机信息和线程信息,下次再想获取锁,就要和最小节点进行比对,一样的话就获取锁,不一样就创建临时顺序节点,参与排队等待获取锁。

那么Zookeeper是完美的吗?

之前我们列举出了zk功能的健全,但是zk最大的缺点就是,性能太差了,因为不管是创建锁还是释放锁,都需要动态创建和销毁节点来实现锁功能。而这一行为,zk默认都是让master服务器执行,再同步到slave,就很慢。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值