要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。redis
1.线程锁算法
主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,由于线程锁的实如今根本上是依靠线程之间共享内存实现的,好比Synchronized、Lock等。数据库
2.进程锁缓存
为了控制同一操做系统中多个进程访问某个共享资源,由于进程具备独立性,各个进程没法访问其余进程的资源,所以没法经过synchronized等线程锁实现进程锁。安全
3.分布式锁服务器
当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。网络
在传统单机部署的状况下,可使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。session
可是在分布式系统后,因为分布式系统多线程、多进程而且分布在不一样机器上,这将使原单机并发控制锁策略失效,为了解决这个问题就须要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁的由来。多线程
当多个进程不在同一个系统中,就须要用分布式锁控制多个进程对资源的访问。架构
首先,为了确保分布式锁可用,咱们至少要确保锁的实现同时知足如下四个条件:
一、互斥性:任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
二、安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除。
三、死锁:获取锁的客户端由于某些缘由(如down机等)而未能释放锁,其它客户端再也没法获取到该锁。
四、容错:当部分节点(redis节点等)down机时,客户端仍然可以获取锁和释放锁。
1. 数据库乐观锁;
2. 基于ZooKeeper的分布式锁;
3.基于Redis的分布式锁;
基于Redis命令:SET key value NX EX max-lock-time
这里补充下: 从2.6.12版本后, 就可使用set来获取锁, Lua 脚原本释放锁。setnx是老黄历了,set命令nx,xx等参数, 是为了实现 setnx 的功能。
1.加锁
public class RedisTool {
private static final String LOCK_SUCCESS = “OK”;
private static final String SET_IF_NOT_EXIST = “NX”;
private static final String SET_WITH_EXPIRE_TIME = “PX”;
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {return true;}return false;}}
jedis.set(String key, String value, String nxxx, String expx, int time)
这个set()方法一共有五个形参:
第一个为key,咱们使用key来当锁,由于key是惟一的。
第二个为value,咱们传的是requestId,不少童鞋可能不明白,有key做为锁不就够了吗,为何还要用到value?缘由就是咱们在上面讲到可靠性时,分布式锁要知足第四个条件解铃还须系铃人,经过给value赋值为requestId,咱们就知道这把锁是哪一个请求加的了,在解锁的时候就能够有依据。requestId可使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数咱们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,咱们进行set操做;若key已经存在,则不作任何操做;
第四个为expx,这个参数咱们传的是PX,意思是咱们要给这个key加一个过时的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,表明key的过时时间。
总的来讲,执行上面的set()方法就只会致使两种结果:1. 当前没有锁(key不存在),那么就进行加锁操做,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不作任何操做。
2.解锁
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = “if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”;
Object
result = jedis.eval(script,
Collections.singletonList(lockKey),Collections.singletonList(requestId));if
(RELEASE_SUCCESS.equals(result)) {return true;}return false;}}
那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,若是相等则删除锁(解锁)。
以上就是redis实现分布式锁详解,除此以外,也可使用Redission(Redis 的客户端)集成进来实现分布式锁,也可使用数据库等,具体能够参考:
1.基于数据库表
要实现分布式锁,最简单的方式可能就是直接建立一张锁表,而后经过操做该表中的数据来实现了。
当咱们要锁住某个方法或资源时,咱们就在该表中增长一条记录,想要释放锁的时候就删除这条记录。
建立这样一张数据库表:
当咱们想要锁住某个方法时,执行如下SQL:
由于咱们对method_name作了惟一性约束,这里若是有多个请求同时提交到数据库的话,数据库会保证只有一个操做能够成功,那么咱们就能够认为操做成功的那个线程得到了该方法的锁,能够执行方法体内容。
当方法执行完毕以后,想要释放锁的话,须要执行如下Sql:
上面这种简单的实现有如下几个问题:
一、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会致使业务系统不可用。
二、这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在数据库中,其余线程没法再得到到锁。
三、这把锁只能是非阻塞的,由于数据的insert操做,一旦插入失败就会直接报错。没有得到锁的线程并不会进入排队队列,要想再次得到锁就要再次触发得到锁操做。
四、这把锁是非重入的,同一个线程在没有释放锁以前没法再次得到该锁。由于数据中数据已经存在了。
固然,咱们也能够有其余方式解决上面的问题。
2.基于数据库排他锁
除了能够经过增删操做数据表中的记录之外,其实还能够借助数据中自带的锁来实现分布式的锁。
咱们还用刚刚建立的那张数据库表。能够经过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎,可使用如下方法来实现加锁操做:
在查询语句后面增长for update,数据库会在查询过程当中给数据库表增长排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有经过索引进行检索的时候才会使用行级锁,不然会使用表级锁。这里咱们但愿使用行级锁,就要给method_name添加索引,值得注意的是,这个索引必定要建立成惟一索引,不然会出现多个重载方法之间没法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁以后,其余线程没法再在该行记录上增长排他锁。
咱们能够认为得到排它锁的线程便可得到分布式锁,当获取到锁以后,能够执行方法的业务逻辑,执行完方法以后,再经过如下方法解锁:
经过connection.commit()操做来释放锁。
这种方法能够有效的解决上面提到的没法释放锁和阻塞锁的问题。
可是仍是没法直接解决数据库单点和可重入问题。
这里还可能存在另一个问题,虽然咱们对method_name 使用了惟一索引,而且显示使用for update来使用行级锁。可是,MySql会对查询进行优化,即使在条件中使用了索引字段,可是否使用索引来检索数据是由 MySQL 经过判断不一样执行计划的代价来决定的,若是 MySQL 认为全表扫效率更高,好比对一些很小的表,它就不会使用索引,这种状况下 InnoDB 将使用表锁,而不是行锁。若是发生这种状况就悲剧了。。。
还有一个问题,就是咱们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库链接。一旦相似的链接变得多了,就可能把数据库链接池撑爆
总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是经过表中的记录的存在状况肯定当前是否有锁存在,另一种是经过数据库的排他锁来实现分布式锁。
数据库实现分布式锁的优势
数据库实现分布式锁的缺点
基于缓存实现分布式锁
相比较于基于数据库实现分布式锁的方案来讲,基于缓存来实如今性能方面会表现的更好一点。并且不少缓存是能够集群部署的,能够解决单点问题。
目前有不少成熟的缓存产品,包括Redis,memcached以及咱们公司内部的Tair。
这里以Tair为例来分析下使用缓存实现分布式锁的方案。关于Redis和memcached在网络上有不少相关的文章,而且也有一些成熟的框架及算法能够直接使用。
基于Tair的实现分布式锁其实和Redis相似,其中主要的实现方式是使用TairManager.put方法来实现。
以上实现方式一样存在几个问题:
一、这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在tair中,其余线程没法再得到到锁。
二、这把锁只能是非阻塞的,不管成功仍是失败都直接返回。
三、这把锁是非重入的,一个线程得到锁以后,在释放锁以前,没法再次得到该锁,由于使用到的key在tair中已经存在。没法再执行put操做。
固然,一样有方式能够解决。
可是,失效时间我设置多长时间为好?如何设置的失效时间过短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。若是设置的时间太长,其余获取锁的线程就可能要平白的多等一段时间。这个问题使用数据库实现分布式锁一样存在
缓存实现分布式锁总结
可使用缓存来代替数据库来实现分布式锁,这个能够提供更好的性能,同时,不少缓存服务都是集群部署的,能够避免单点问题。而且不少缓存服务都提供了能够用来实现分布式锁的方法,好比Tair的put方法,redis的setnx方法等。而且,这些缓存服务也都提供了对数据的过时自动删除的支持,能够直接设置超时时间来控制锁的释放。
缓存实现分布式锁的优势
缓存实现分布式锁的缺点
基于zookeeper临时有序节点能够实现的分布式锁。
大体思想即为:每一个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个惟一的瞬时有序节点。 判断是否获取锁的方式很简单,只须要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除便可。同时,其能够避免服务宕机致使的锁没法释放,而产生的死锁问题。
来看下Zookeeper能不能解决前面提到的问题。
能够直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。
使用ZK实现的分布式锁好像彻底符合了本文开头咱们对一个分布式锁的全部指望。可是,其实并非,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并无缓存服务那么高。由于每次在建立锁和释放锁的过程当中,都要动态建立、销毁瞬时节点来实现锁功能。ZK中建立和删除节点只能经过Leader服务器来执行,而后将数据同不到全部的Follower机器上。
其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的状况,因为网络抖动,客户端可ZK集群的session链接断了,那么zk觉得客户端挂了,就会删除临时节点,这时候其余客户端就能够获取到分布式锁了。就可能产生并发问题。这个问题不常见是由于zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。屡次重试以后还不行的话才会删除临时节点。(因此,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)
Zookeeper实现分布式锁总结
Zookeeper实现分布式锁的优势
Zookeeper实现分布式锁的缺点
上面几种方式,哪一种方式都没法作到完美。就像CAP同样,在复杂性、可靠性、性能等方面没法同时知足,因此,根据不一样的应用场景选择最适合本身的才是王道。
1.从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
2.从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
3.从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
4.从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库