分布式锁实现方案
Posted codingjav
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式锁实现方案相关的知识,希望对你有一定的参考价值。
1、什么是锁
在单进程环境中,存在多个线程可以同时改变某个变量,就需要对变量或代码块做同步,使其修改这种变量时能够线性执行,防止并发带来不可控的结果。而这种同步的本质就是通过锁实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么就需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余线程发现已经有标记后则等待标记的线程结束同步代码块取消后再尝试标记。这个标记可以理解为锁。
不同地方实现锁不同,只要满足线程看到标记即可。Java 里面的synchronized 是在对象头设置标记、lock接口实现类是通过 volatile 修饰的 int 变量来实现、Linux内核则是通过信号量或互斥量来实现。
这些只能用来解决同一进程中,不同线程对同一资源使用的并发问题,而实际环境中则是分布式部署,是多进程之间资源的竞争,所以,上述提供的方式在分布式环境中显得无能为力。需要通过其他手段,来保证同一时刻操作数据的单一性。
2、分布式锁
从上文中就能归纳出来:分布式锁是控制分布式环境系统同步访问共享资源的一种方式。
2.1、为什么需要分布式锁?
Martin Kleppmann是英国剑桥大学的分布式系统的研究员,之前和Redis之父Antirez进行过关于RedLock(红锁)是否安全的激烈讨论。Martin认为一般我们使用分布式锁有两个场景:
-
效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。
-
正确性:加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。
2.2、分布式要满足的几个条件
-
锁排他性:同一时间只有一个客户端能上锁,其他无法同时获取
-
高可用性:获取与释放必须是高可用且高性能的
-
避免死锁:锁在一定时限内,一定会被释放(无论正常释放或异常释放)
-
具备可重入性
2.3、针对分布式锁实现的几种方案
-
基于数据库实现分布式锁
-
基于缓存实现分布式锁
-
基于zk实现分布式锁
3、分布式锁实现方案
3.1、基于数据库实现分布式锁
3.1.1、通过创建一个锁表来实现分布式锁:
1、创建一个锁表,记录锁定的资源名和时间,并对锁加上唯一性约束,这里使用方法名,建表语句如下:
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_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_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
2、想要锁住某个方法时,在表中插入一条记录,执行以下SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’);
因为我们对method_name做了唯一性约束,如果同时有多个请求提交到数据库的话,数据库会保证只有一个可以插入成功,其他的请求则会抛出异常,对于插入数据成功的线程,我认为他获得了后面方法的锁,可以执行之后的方法。
3、方法执行完毕需要释放锁
delete from methodLock where method_name = 'method_name';
3.1.1.2、优缺点
优点:
- 直接依赖数据库,易于理解
缺点:
- 单点问题:锁强依赖数据库的可用性,如果是单点数据库,一旦数据库挂掉,会导致业务系统不可用,解决方法:搭建数据库集群
- 锁释放问题:锁没有失效时间,当解锁操作失败或发生异常时,锁记录就会一直存在表中,其他线程无法再获得到锁,解决方法:启动一个定时任务,定时清理过期数据。
3.1.2 利用数据库的排他锁实现分布式锁:
基于mysql的InnoDB引擎,可以在查询语句后面增加for update,数据库会对查询结果中的每行都加排他锁,如下SQL:
select * from user_info where user_id = xx for update;
注意:
使用排他锁的时候,一定要有where条件保证检索的数据范围最小,通常需要给user_id列添加索引,保证InnoDB引擎在加锁的时候不用全表扫描,因为只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。
获得排它锁的线程即可获得分布式锁,使用完成之后再通过connection.commit()操作来释放锁。
3.1.2.1 排他锁问题:
- MySql会对查询进行优化,是否使用索引是通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。
- 一个排他锁长时间不提交,就会占用数据库连接,一旦类似的连接变得多了,就可能把数据库连接池撑爆。
3.1.3 通过乐观锁来实现分布式锁:
基于InnoDB存储引擎的表,通过给表中追加一个版本字段来实现,每次更新数据时,把版本作为条件。因为InnoDB默认会为insert、delete、update加上排他锁,所以,可以在更新数据时,使用如下SQL:
update user_info set value = 100 where id = 1 and version = #{version}
3.1.3.1 优缺点
优点:性能比悲观锁高
缺点:会存在大量更新失败异常
3.2、基于缓存实现分布式锁
大家网上搜索分布式锁实现,恐怕最多的就是通过redis来实现的,因为redis性能好,实现起来简单,而且也是大多数开发者掌握最多的嘛(毕竟面试高频),所以让很多人都对其十分青睐。
3.2.1、Redis分布式锁实现
熟悉Redis的同学那么肯定对setNx(set if not exist)方法不陌生,如果不存在则更新,其可以很好的用来实现我们的分布式锁。对于某个资源加锁我们只需要
setNx resourceName value
这里有个问题,加锁了之后如果机器宕机那么这个锁就不会得到释放所以会加入过期时间,加入过期时间需要和setNx同一个原子操作,在Redis2.8之前我们需要使用Lua脚本达到我们的目的,但是redis2.8之后redis支持nx和ex操作是同一原子操作。
set resourceName value ex 5 nx
3.2.2、Redission
Javaer都知道Jedis,Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redission也是Redis的客户端,相比于Jedis功能简单。Jedis简单使用阻塞的I/O和redis交互,Redission通过Netty支持非阻塞I/O。Jedis最新版本2.9.0是2016年的快3年了没有更新,而Redission最新版本是2018.10月更新。
Redission封装了锁的实现,其继承了java.util.concurrent.locks.Lock的接口,让我们像操作我们的本地Lock一样去操作Redission的Lock,具体操作则是需要大家下去自己分析了。
3.2.3、RedLock
我们想象一个这样的场景当机器A申请到一把锁之后,如果Redis主宕机了,这个时候从机并没有同步到这一把锁,那么机器B再次申请的时候就会再次申请到这把锁,为了解决这个问题Redis作者提出了RedLock红锁的算法,在Redission中也对RedLock进行了实现。具体操作也需要大家下去自己分析。
3.2.4、Redis小结
-
优点:对于Redis实现简单,性能对比Mysql较好。如果不需要特别复杂的要求,那么自己就可以利用setNx进行实现,如果自己需要复杂的需求的话那么可以利用或者借鉴Redission。对于一些要求比较严格的场景来说的话可以使用RedLock。
-
缺点:需要维护Redis集群,如果要实现RedLock那么需要维护更多的集群;
3.3、基于zk实现分布式锁
3.3.1、zk简介
zookeeper(简称zk):是一种提供配置管理、分布式协同以及命名的中心化服务
抽象模型:zk提供一个多层级的节点命名空间(znode),每个节点都用一个以斜杠(/)分隔的路径表示,而且每个节点都有父节点(根节点除外),类似文件系统。例如,/f1/d2这个表示一个znode,它的父节点为/f1,父父节点为/,而/为根节点没有父节点。这些节点都可以设置关联的数据,为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得ZK不能用于存放大量的数据,每个节点的存放数据上限为1M
有序节点:zk在生成子节点时会根据当前子节点的数量自动添加整数序号,例如:node-0000000000、 node-0000000001**
临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zk会自动删除该节点**
事件监听:创建节点、删除节点、节点数据修改、节点变更 **
3.3.2、锁过程
借助zookeeper 临时有序节点 来实现的分布式锁的过程如下:
当每个客户端对某个方法加锁时,在zookeeper上与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。
当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
3.3.3、优缺点
优点:
自动释放锁:在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。
可阻塞:客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,ZK会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
可重入:客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
高可用:ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
缺点 :
性能上不如使用缓存实现分布式锁。需要对ZK的原理有所了解。
4、小结
本文主要讲了多种分布式锁的实现方法,以及他们的一些优缺点。最后也说了一下有关于分布式锁的安全的问题,对于不同的业务需要的安全程度完全不同,我们需要根据自己的业务场景,通过不同的维度分析,选取最适合自己的方案。
从理解的难易程度角度(从低到高):数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高): Zookeeper >= 缓存 > 数据库
从性能角度(从高到低): 缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低): Zookeeper > 缓存 > 数据库
最后打个广告,如果你觉得这篇文章对你有文章,可以关注我的技术公众号。你的关注和转发是对我最大的支持,O(∩_∩)O。
以上是关于分布式锁实现方案的主要内容,如果未能解决你的问题,请参考以下文章