第一次写技术文章,内心还是有点忐忑,因为不知道自己是否真正的理解了分布式锁,还望看到本篇文章的朋友遇到文中解释不对的地方能够联系作者,感谢,废话不多说,进入正题吧!
首先我们要清楚什么是锁?
锁可以理解为在多线程并发更新某些共享资源时,我们为了解决多线程并发修改共享资源而带来的安全问题引入的一种同步机制。 锁可以分为本地锁与分布式锁,我是这么理解的。
什么是本地锁?
我相信大部分Java开发的同学对这个应该多多少少都涉及过,例如我们最常见的Syncronized锁,基于AQS实现的ReentryLock锁,后边我会对这两种锁的实现原理以及优缺点做一次分享。
什么是分布式锁?
这也是大部分开发人员在平时的业务迭代中很少涉及到的,也是本篇文章将要分享的内容。分布式锁主要要理解分布式这个概念,什么是分布式系统,也就是有多个不同的服务或者集群构建而成的系统。对比与本地锁来说,可以说本地锁是相对于单进程多线程而言的,而分布式锁是相对于多进程多线程而言的,分布式锁是为了控制不同进程之间对共享资源的访问。
为什么需要分布式锁?
举一个很简单的例子,我们常常听说的秒杀系统,就是一种基于分布式锁实现的典型场景。在大型秒杀系统的场景下,面临着大量的并发请求,单体项目是无法支撑这么大并发量的,所以我们会通过搭建微服务集群来分散压力,在不同的服务之间的进程又是不一样的,多个进程同时访问共享资源的情况下不加锁就会面临严重的问题,这也就是为什么需要分布式锁的原因。
如何实现分布式锁?
如何使用redis来实现分布式锁?
通过redis命令setnx来实现分布式锁(悲观锁)
setnx key value命令是key不存在则可以设置成功,通过它来实现分布式锁的话,那也就是设置成功便是获得了锁,相信有一定了解的同学肯定发现了这种方式的不足之处,如果一个线程在获得锁之后服务器宕机了,可能会导致锁一直被占用而无法释放的问题。因此,我们在写分布式锁的时候都会添加一个锁过期时间,但是setnx命令并不支持加锁的同时设置过期时间,这就导致了两个命令不是原子性的,这也可能会导致锁无法释放,也就是在获得锁与设置过期时间之间服务器宕机了。那么该如何解决这个问题呢?我们可以通过另一个命令来加锁,set key value nx px 在设置key的时候同时设置过期时间。
通过redis的事务机制来实现分布式锁(乐观锁)
watch key 命令为监听一个key,当key的值发生变化时,其它设置该key的线程都会失败,这样就保证了一次只有一个线程能设置成功。当然光靠watch命令是不够的,redis的单个命令都是原子性的,事务需要保证命令的集合都是原子性的,所以必须结合multi开启事务功能,将一个事务内的所有命令放入一个队列中串行执行才行。
- 具体实现步骤
- 利用redis的watch功能,监控rediskey的状态
- 获取rediskey的值
- 开启事务
- 给这个key的值加一
- 然后通过事务的exec命令执行
通过redis+lua脚本实现分布式锁
lua脚本的命令是原子性的
以上三种实现方式面临的问题
在单机的情况下,上述方式实现分布式锁是没有问题的,但是单机无法保证高可用。在主从的情况下就面临一些问题。
- redis是基于AP模型的,无法保证数据的强一致性。 因为它的持久化机制就存在数据丢失的情况,rdb会丢失最后一次快照以后的数据,aof每秒同步会丢失最多两秒的数据。这样就可能存在一个线程获得锁之后,主服务器宕机后,新主没有同步过来锁信息,这样就可能会导致多个线程同时获得锁。为了解决这个问题,redis作者提出了一个redLock方案,那就是加锁必须同时在半数以上的机器上加锁成功才算加锁成功了,否则释放所有已经加的锁,这样能解决大部分场景,如果是某个进程通过半数+1的机器上加锁成功了,这个时候某台机器宕机又恢复后,加锁的信息丢失了,那么进程2去加锁,也同时给半数+1台机器加锁成功,这样就导致了同时两个进程获得锁。关于红锁的详细解释请看大佬blog
- 锁无法续租的问题。 我们在设置分布式锁的时候都会添加一个过期时间,但是在过期时间之内业务还是没有执行完成就可能会导致其它线程也获取到了锁
通过Redission来实现分布式锁
Redission是采用Lua脚本的方式进行加锁,默认会给锁加上30s的有效期,同时他还会开启一个线程,俗称看门狗(watch dog),每隔10s去检查是否锁还存在,如果存在就续租10s,如果其它客户端请求加锁,如果锁已经被占有的情况下,它会返回锁还有多久的失效时间,那么这个线程就会陷入自旋加锁,同时Redission是支持锁重入的。
如何使用zookeeper实现分布式锁?
zookeeper能够保证多个线程同时创建一个节点时只有一个线程能创建成功而且他有节点的监听机制,基于此我们便可以通过这个实现排它锁,我们可以向zookeeper发送一条创建临时节点的命令,如果创建成功了,那么就表明该线程获取了锁,当使用完将这个节点删除即可,当节点发生变化,监听这个节点的客户端都能接收到通知,那么就会进行下一轮的锁竞争。
zookeeper还可以通过共享锁来实现分布式锁,具体实现是,每个线程都去zk上创建一个临时顺序节点,如果是读请求则判断比自己小的节点是否都是读请求或者自己就是最小的节点,那么这个线程就能获得锁,如果是写请求,就必须是顺序最小的节点。当锁释放的时候,所有监听的客户端又要进行新一轮的判断是否自己能够获得锁,在客户端数量非常多的情况下,这样是非常消耗性能的,也就是会造成羊群效应。其实我们每个线程只要监听自己关心的节点就可以,就跟排队一样,我们只要关注我们前面那一个。如果是读请求,就监听上一个最大顺序号的写请求,如果是写请求,就监听比自己小的最后一个节点即可。