30岁之后搞Java已经没有前途,Java基础常问面试题

2021年09月15日 阅读数:4
这篇文章主要向大家介绍30岁之后搞Java已经没有前途,Java基础常问面试题,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

1.为何要使用分布式锁

使用分布式锁的目的,无外乎就是保证同一时间只有一个客户端能够对共享资源进行操做。java

1.1举一个很长的例子

系统 A 是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,可是用户下订单以前必定要去检查一下库存,确保库存足够了才会给用户下单。因为系统有必定的并发,因此会预先将商品的库存保存在 Redis 中,用户下单的时候会更新 Redis 的库存。此时系统架构以下:git

30岁之后搞Java已经没有前途,Java基础常问面试题

可是这样一来会产生一个问题:假如某个时刻,Redis 里面的某个商品库存为 1。面试

此时两个请求同时到来,其中一个请求执行到上图的第 3 步,更新数据库的库存为 0,可是第 4 步尚未执行。redis

而另一个请求执行到了第 2 步,发现库存仍是 1,就继续执行第 3 步。这样的结果,是致使卖出了 2 个商品,然而其实库存只有 1 个。算法

很明显不对啊!这就是典型的库存超卖问题。此时,咱们很容易想到解决方案:用锁把 二、三、4 步锁住,让他们执行完以后,另外一个线程才能进来执行第 2 步。数据库

30岁之后搞Java已经没有前途,Java基础常问面试题

按照上面的图,在执行第 2 步时,使用 Java 提供的 Synchronized 或者 ReentrantLock 来锁住,而后在第 4 步执行完以后才释放锁。安全

这样一来,二、三、4 这 3 个步骤就被“锁”住了,多个线程之间只能串行化执行服务器

当整个系统的并发飙升,一台机器扛不住了。如今要增长一台机器,以下图:markdown

30岁之后搞Java已经没有前途,Java基础常问面试题

增长机器以后,系统变成上图所示,假设此时两个用户的请求同时到来,可是落在了不一样的机器上,那么这两个请求是能够同时执行了,仍是会出现库存超卖的问题。多线程

由于上图中的两个 A 系统,运行在两个不一样的 JVM 里面,他们加的锁只对属于本身 JVM 里面的线程有效,对于其余 JVM 的线程是无效的。

所以,这里的问题是:Java 提供的原生锁机制在多机部署场景下失效了,这是由于两台机器加的锁不是同一个锁(两个锁在不一样的 JVM 里面)。

那么,咱们只要保证两台机器加的锁是同一个锁,问题不就解决了吗?此时,就该分布式锁隆重登场了。

分布式锁的思路是:在整个系统提供一个全局、惟一的获取锁的“东西”,而后每一个系统在须要加锁时,都去问这个“东西”拿到一把锁,这样不一样的系统拿到的就能够认为是同一把锁。

至于这个“东西”,能够是 Redis、Zookeeper,也能够是数据库。此时的架构如图:

30岁之后搞Java已经没有前途,Java基础常问面试题

经过上面的分析,咱们知道了库存超卖场景在分布式部署系统的状况下使用 Java 原生的锁机制没法保证线程安全,因此咱们须要用到分布式锁的方案。

2.高效的分布式锁

在设计分布式锁的时候,应该考虑分布式锁至少要知足的一些条件,同时考虑如何高效的设计分布式锁,如下几点是必需要考虑的:

(1) 互斥

在分布式高并发的条件下,最须要保证在同一时刻只能有一个线程得到锁,这是最基本的一点。

(2) 防止死锁

在分布式高并发的条件下,好比有个线程得到锁的同时,尚未来得及去释放锁,就由于系统故障或者其它缘由使它没法执行释放锁的命令,致使其它线程都没法得到锁,形成死锁。因此分布式很是有必要设置锁的有效时间,确保系统出现故障后,在必定时间内可以主动去释放锁,避免形成死锁的状况。

(3) 性能

对于访问量大的共享资源,须要考虑减小锁等待的时间,避免致使大量线程阻塞。

因此在锁的设计时,须要考虑两点。

一、 锁的颗粒度要尽可能小。好比你要经过锁来减库存,那这个锁的名称你能够设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。

二、 锁的范围尽可能要小。好比只要锁2行代码就能够解决问题的,那就不要去锁10行代码了。

(4) 重入

咱们知道ReentrantLock是可重入锁,那它的特色就是:同一个线程能够重复拿到同一个资源的锁。重入锁很是有利于资源的高效利用。关于这点以后会作演示。

3.基于Redis实现分布式锁

3.1 使用Redis命令实现分布式锁

3.1.1加锁

加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过时时间。

使用的命令:SET lock_key random_value NX PX 5000

值得注意的是:

random_value 是客户端生成的惟一的字符串。

NX 表明只在键不存在时,才对键进行设置操做。

PX 5000 设置键的过时时间为5000毫秒。

也可使用另一条命令:SETNX key value

只不过过时时间没法设置。

这样,若是上面的命令执行成功,则证实客户端获取到了锁。

3.1.2解锁

解锁的过程就是将Key键删除,但要保证安全性,举个例子:客户端1的请求不能将客户端2的锁给删除掉。

释放锁涉及到两条指令,这两条指令不是原子性的,须要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的。脚本以下:

if redis.call('get',KEYS[1]) == ARGV[1] then 
  return redis.call('del',KEYS[1]) 
else
  return 0 
end

这种方式比较简单,可是也有一个最重要的问题:锁不具备可重入性

3.2使用Redisson实现分布式锁

3.2.1Redisson介绍

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优点,基于Java实用工具包中经常使用接口,为使用者提供了一系列具备分布式特性的经常使用工具类。使得本来做为协调单机多线程并发程序的工具包得到了协调分布式多机多线程并发系统的能力,大大下降了设计和研发大规模分布式系统的难度。同时结合各富特点的分布式服务,更进一步简化了分布式环境中程序相互之间的协做。

3.2.2Redisson简单使用

Config config = new Config(); 
config.useClusterServers() 
.addNodeAddress("redis://192.168.31.101:7001") 
.addNodeAddress("redis://192.168.31.101:7002") 
.addNodeAddress("redis://192.168.31.101:7003") 
.addNodeAddress("redis://192.168.31.102:7001") 
.addNodeAddress("redis://192.168.31.102:7002") 
.addNodeAddress("redis://192.168.31.102:7003"); 

RedissonClient redisson = Redisson.create(config); 

RLock lock = redisson.getLock("anyLock"); 

lock.lock(); 

lock.unlock(); 

只须要经过它的 API 中的 Lock 和 Unlock 便可完成分布式锁,并且考虑了不少细节:

l Redisson 全部指令都经过 Lua 脚本执行,Redis 支持 Lua 脚本原子性执行

l Redisson 设置一个 Key 的默认过时时间为 30s,可是若是获取锁以后,会有一个WatchDog每隔10s将key的超时时间设置为30s。

另外,Redisson 还提供了对 Redlock 算法的支持,它的用法也很简单:

RedissonClient redisson = Redisson.create(config); 
RLock lock1 = redisson.getFairLock("lock1"); 
RLock lock2 = redisson.getFairLock("lock2"); 
RLock lock3 = redisson.getFairLock("lock3"); 
RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3); 
multiLock.lock(); 

multiLock.unlock(); 

3.2.3Redisson原理分析

30岁之后搞Java已经没有前途,Java基础常问面试题

(1) 加锁机制

线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。

线程去获取锁,获取失败: 一直经过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。

(2) WatchDog自动延期机制

在一个分布式环境下,假如一个线程得到锁后,忽然服务器宕机了,那么这个时候在必定时间后这个锁会自动释放,也能够设置锁的有效时间(不设置默认30秒),这样的目的主要是防止死锁的发生。可是在实际状况中会有一种状况,业务处理的时间可能会大于锁过时的时间,这样就可能致使解锁和加锁不是同一个线程。因此WatchDog做用就是Redisson实例关闭前,不断延长锁的有效期。

若是程序调用加锁方法显式地给了有效期,是不会开启后台线程(也就是watch dog)进行延期的,若是没有给有效期或者给的是-1,redisson会默认设置30s有效期而且会开启后台线程(watch dog)进行延期

多久进行一次延期:(默认有效期/3),默认有效期能够设置修改的,即默认状况下每隔10s设置有效期为30s

(3) 可重入加锁机制

Redisson能够实现可重入加锁机制的缘由:

l Redis存储锁的数据类型是Hash类型

l Hash数据类型的key值包含了当前线程的信息

下面是redis存储的数据

30岁之后搞Java已经没有前途,Java基础常问面试题

这里表面数据类型是Hash类型,Hash类型至关于咱们java的 <key,<key1,value>> 类型,这里key是指 'redisson'

它的有效期还有9秒,咱们再来看里们的key1值为078e44a3-5f95-4e24-b6aa-80684655a15a:45它的组成是:

guid + 当前线程的ID。后面的value是就和可重入加锁有关。value表明同一客户端调用lock方法的次数,便可重入计数统计。

举图说明

30岁之后搞Java已经没有前途,Java基础常问面试题

上面这图的意思就是可重入锁的机制,它最大的优势就是相同线程不须要在等待锁,而是能够直接进行相应操做。

3.2.4 获取锁的流程

30岁之后搞Java已经没有前途,Java基础常问面试题

其中的指定字段也就是hash结构中的field值(构成是uuid+线程id),即判断锁是不是当前线程

3.2.5 加锁的流程

30岁之后搞Java已经没有前途,Java基础常问面试题

3.2.6 释放锁的流程

30岁之后搞Java已经没有前途,Java基础常问面试题

4. 使用Redis作分布式锁的缺点

Redis有三种部署方式

l 单机模式

l Master-Slave+Sentienl选举模式

l Redis Cluster模式

若是采用单机部署模式,会存在单点问题,只要 Redis 故障了。加锁就不行了

采用 Master-Slave 模式,加锁的时候只对一个节点加锁,即使经过 Sentinel 作了高可用,可是若是 Master 节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。

基于以上的考虑,Redis 的做者也考虑到这个问题,他提出了一个 RedLock 的算法。

这个算法的意思大概是这样的:假设 Redis 的部署模式是 Redis Cluster,总共有 5 个 Master 节点。

经过如下步骤获取一把锁:

  • 获取当前时间戳,单位是毫秒。
  • 轮流尝试在每一个 Master 节点上建立锁,过时时间设置较短,通常就几十毫秒。
  • 尝试在大多数节点上创建一个锁,好比 5 个节点就要求是 3 个节点(n / 2 +1)。
  • 客户端计算创建好锁的时间,若是创建锁的时间小于超时时间,就算创建成功了。
  • 要是锁创建失败了,那么就依次删除这个锁。
  • 只要别人创建了一把分布式锁,你就得不断轮询去尝试获取锁。

可是这样的这种算法,可能会出现节点崩溃重启,多个客户端持有锁等其余问题,没法保证加锁的过程必定正确。例如:

假设一共有5个Redis节点:A, B, C, D, E。设想发生了以下的事件序列:

(1)客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。

(2)节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。

(3)节点C重启后,客户端2锁住了C, D, E,获取锁成功。

这样,客户端1和客户端2同时得到了锁(针对同一资源)。

最后

因为篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】

MySQL全家桶笔记

还有更多面试复习笔记分享以下

Java架构专题面试复习