火花思惟三面:说说Redis分布式锁是如何实现的!

前言

近来,分布式的问题被普遍说起,好比分布式事务、分布式框架、ZooKeeper、SpringCloud等等。本文先回顾锁的概念,再介绍分布式锁,以及如何用Redis来实现分布式锁。面试

1、锁的基本了解

首先,回顾一下咱们工做学习中的锁的概念。数据库

为何要先讲锁再讲分布式锁呢?

咱们都清楚,锁的做用是要解决多线程对共享资源的访问而产生的线程安全问题,而在平时生活中用到锁的状况其实并很少,可能有些朋友对锁的概念和一些基本的使用不是很清楚,因此咱们先看锁,再深刻介绍分布式锁。缓存

Redis分布式锁面试题答案地址:Redis分布式面试题安全

经过一个卖票的小案例来看,好比你们去抢dota2 ti9门票,若是不加锁的话会出现什么问题?此时代码以下:性能优化

**代码分析:**这里有8张ti9门票,设置了10个线程(也就是模拟10我的)去并发抢票,若是抢成功了显示成功,抢失败的话显示失败。按理说应该有8我的抢成功了,2我的抢失败,下面来看运行结果:多线程

咱们发现运行结果和预期的状况不一致,竟然10我的都买到了票,也就是说出现了线程安全的问题,那么是什么缘由致使的呢?并发

缘由就是多个线程之间产生了时间差。框架

如图所示,只剩一张票了,可是两个线程都读到的票余量是1,也就是说线程B尚未等到线程A改库存就已经抢票成功了。分布式

怎么解决呢?想必你们都知道,加个synchronized关键字就能够了,在一个线程进行reduce方法的时候,其余线程则阻塞在等待队列中,这样就不会发生多个线程对共享变量的竞争问题。函数

举个例子

好比咱们去健身房健身,若是好多人同时用一台机器,同时在一台跑步机上跑步,就会发生很大的问题,你们会打得不可开交。若是咱们加一把锁在健身房门口,只有拿到锁的钥匙的人才能够进去锻炼,其余人在门外等候,这样就能够避免你们对健身器材的竞争。代码以下:

运行结果:

果不其然,结果有两我的没有成功抢到票,看来咱们的目地达成了。

2、锁的性能优化

2.1 缩短锁的持有时间

事实上,按照咱们对平常生活的理解,不可能整个健身房只有一我的在运动。因此咱们只须要对某一台机器加锁就能够了,好比一我的在跑步,另外一我的能够去作其余的运动。

对于票务系统来讲,咱们只须要对库存的修改操做的代码加锁就能够了,别的代码仍是能够并行进行,这样会大大减小锁的持有时间,代码修改以下:

这样作的目的是充分利用cpu的资源,提升代码的执行效率。

这里咱们对两种方式的时间作个打印:

果真,只对部分代码加锁会大大提供代码的执行效率。

因此,在解决了线程安全的问题后,咱们还要考虑到加锁以后的代码执行效率问题。

2.2 减小锁的粒度

举个例子,有两场电影,分别是最近刚上映的魔童哪吒和蜘蛛侠,咱们模拟一个支付购买的过程,让方法等待,加了一个CountDownLatch的await方法,运行结果以下:

执行结果:

魔童哪吒的剩余票数为:20

咱们发现买哪吒票的时候阻塞会影响蜘蛛侠票的购买,而实际上,这两场电影之间是相互独立的,因此咱们须要减小锁的粒度,将movie整个对象的锁变为两个全局变量的锁,修改代码以下:

执行结果:

魔童哪吒的剩余票数为:20

蜘蛛侠的剩余票数为:100

如今两场电影的购票不会互相影响了,这就是第二个优化锁的方式:减小锁的粒度。顺便提一句,Java并发包里的ConcurrentHashMap就是把一把大锁变成了16把小锁,经过分段锁的方式达到高效的并发安全。

2.3 锁分离

锁分离就是常说的读写分离,咱们把锁分红读锁和写锁,读的锁不须要阻塞,而写的锁要考虑并发问题。

3、锁的种类

  • 公平锁: ReentrantLock
  • 非公平锁: Synchronized、ReentrantLock、cas
  • 悲观锁: Synchronized
  • 乐观锁:cas
  • 独享锁:Synchronized、ReentrantLock
  • 共享锁:Semaphore

这里就不一一讲述每一种锁的概念了,你们能够本身学习,锁还能够按照偏向锁、轻量级锁、重量级锁来分类。

4、Redis分布式锁

了解了锁的基本概念和锁的优化后,重点介绍分布式锁的概念。

上图所示是咱们搭建的分布式环境,有三个购票项目,对应一个库存,每个系统会有多个线程,和上文同样,对库存的修改操做加上锁,能不能保证这6个线程的线程安全呢?

固然是不能的,由于每个购票系统都有各自的JVM进程,互相独立,因此加synchronized只能保证一个系统的线程安全,并不能保证分布式的线程安全。

因此须要对于三个系统都是公共的一个中间件来解决这个问题。

这里咱们选择Redis来做为分布式锁,多个系统在Redis中set同一个key,只有key不存在的时候,才能设置成功,而且该key会对应其中一个系统的惟一标识,当该系统访问资源结束后,将key删除,则达到了释放锁的目的。

4.1 分布式锁须要注意哪些点

1)互斥性

在任意时刻只有一个客户端能够获取锁。

这个很容易理解,全部的系统中只能有一个系统持有锁。

2)防死锁

假如一个客户端在持有锁的时候崩溃了,没有释放锁,那么别的客户端没法得到锁,则会形成死锁,因此要保证客户端必定会释放锁。

Redis中咱们能够设置锁的过时时间来保证不会发生死锁。

3)持锁人解锁

解铃还须系铃人,加锁和解锁必须是同一个客户端,客户端A的线程加的锁必须是客户端A的线程来解锁,客户端不能解开别的客户端的锁。

4)可重入

当一个客户端获取对象锁以后,这个客户端能够再次获取这个对象上的锁。

4.2 Redis分布式锁流程

Redis分布式锁的具体流程:

1)首先利用Redis缓存的性质在Redis中设置一个key-value形式的键值对,key就是锁的名称,而后客户端的多个线程去竞争锁,竞争成功的话将value设为客户端的惟一标识。Java学习圈子:14 201 9 080

2)竞争到锁的客户端要作两件事:

  • 设置锁的有效时间 目的是防死锁 (很是关键)

须要根据业务须要,不断的压力测试来决定有效期的长短。

  • 分配客户端的惟一标识,目的是保证持锁人解锁(很是重要)

因此这里的value就设置成惟一标识(好比uuid)。

3)访问共享资源

4)释放锁,释放锁有两种方式,第一种是有效期结束后自动释放锁,第二种是先根据惟一标识判断本身是否有释放锁的权限,若是标识正确则释放锁。

4.3 加锁和解锁

4.3.1 加锁

1)setnx命令加锁

set if not exists 咱们会用到Redis的命令setnx,setnx的含义就是只有锁不存在的状况下才会设置成功。

2)设置锁的有效时间,防止死锁 expire

加锁须要两步操做,思考一下会有什么问题吗?

假如咱们加锁完以后客户端忽然挂了呢?那么这个锁就会成为一个没有有效期的锁,接着就可能发生死锁。虽然这种状况发生的几率很小,可是一旦出现问题会很严重,因此咱们也要把这两步合为一步。

幸运的是,Redis3.0已经把这两个指令合在一块儿成为一个新的指令。

来看jedis的官方文档中的源码:

这就是咱们想要的!

4.3.2 解锁

  • 检查是否本身持有锁(判断惟一标识);
  • 删除锁。

解锁也是两步,一样也要保证解锁的原子性,把两步合为一步。

这就没法借助于Redis了,只能依靠Lua脚原本实现。

这就是一段判断是否本身持有锁并释放锁的Lua脚本。

为何Lua脚本是原子性呢?由于Lua脚本是jedis用eval()函数执行的,若是执行则会所有执行完成。

5、Redis分布式锁代码实现

  • 用一个上下文全局变量来记录持有锁的人的uuid,解锁的时候须要将该uuid做为参数传入Lua脚本中,来判断是否能够解锁。
  • 要记录当前线程,来实现分布式锁的重入性,若是是当前线程持有锁的话,也属于加锁成功。
  • 用eval函数来执行Lua脚本,保证解锁时的原子性。

6、分布式锁的对比

6.1 基于数据库的分布式锁

1)实现方式

获取锁的时候插入一条数据,解锁时删除数据。

2)缺点

  • 数据库若是挂掉会致使业务系统不可用。
  • 没法设置过时时间,会形成死锁。

6.2 基于zookeeper的分布式锁

1)实现方式

加锁时在指定节点的目录下建立一个新节点,释放锁的时候删除这个临时节点。由于有心跳检测的存在,因此不会发生死锁,更加安全。

2)缺点

性能通常,没有Redis高效。

因此:

  • 从性能角度: Redis > zookeeper > 数据库
  • 从可靠性(安全)性角度: zookeeper > Redis > 数据库

7、总结

本文从锁的基本概念出发,提出多线程访问共享资源会出现的线程安全问题,而后经过加锁的方式去解决线程安全的问题,这个方法会性能会降低,须要经过:缩短锁的持有时间、减少锁的粒度、锁分离三种方式去优化锁。

更多Java面试资料共享地址:面试题资料合集

以后介绍了分布式锁的4个特色:

  • 互斥性
  • 防死锁
  • 加锁人解锁
  • 可重入性

而后用Redis实现了分布式锁,加锁的时候用到了Redis的命令去加锁,解锁的时候则借助了Lua脚原本保证原子性。

最后对比了三种分布式锁的优缺点和使用场景。

相关文章
相关标签/搜索