大厂面试题扩展版
2021-03-15 12:52:01 3 举报
AI智能生成
加油
作者其他创作
大纲/内容
0、JAVA基础
集合
Map的实现类
Map:双列数据,存储key value数据
HashMap:作为map主要实现类,线程不安全的,效率高,可存储null的key和value
LinkedHashMap:线程安全的,对于频繁遍历操作,此类执行效率高于HashMap
TreeMap:保证按照添加的key value对进行排序,实现排序遍历
Hashtable:作为古老的实现类,线程安全,效率低,不可存储null的key和value
相关面试题
1.hashMap的底层实现原理:
先以JDK7为例说明:
1、HashMap map = new HashMap();
2、在实例化以后,底层创建了长度是16的一维数组,这个数组的类型是Entry[],数组名字是table
3、map.put(key1,value1);
4、首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry[]数组中的存放位置
5、如果此位置上的数据为空,此时的key1-value1添加成功 ---情况1
6、如果此位置上的数据不为空,意味着此位置上存在一个或多个数据(以链表的形式存在),比较key1和已经存在的一个或多个数据的哈希值:
7、如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功 ---- 情况2
8、如果key1的哈希值和已经存在的某个数据(key2-value2)的哈希值相同,继续比较,调用key1所在类的equals(key2)
9、 如果equals()返回false:此时key1-value1添加成功 -----情况3
10 如果equals()返回true:使用value1的值来替换value2的值
11、关于情况2和情况3,此时key1-value1和原来的数据以链表的方式存储数据
12、在不断的添加过程中,会涉及到扩容问题,默认的扩容方式是:扩容为原来容量的2倍,并扩容完以后,将原有的数据复制过来。
JDK8
jdk8相较于jdk7在底层实现方面的不同:
1.一开始当我们new HashMap的时候,底层并没有帮我们创建一个长度为16的数组
2.jdk8底层的数组不再叫Entry[]的数组,而是叫Node[]数组
3.首次调用put()方式时,底层创建长度为16的数组
4.jdk7的底层结构只有数组+链表,在jdk8当中的底层结构是数组+链表+红黑树
1、什么时候涉及到红黑树?答:当数组的某一个索引位置上的元素以链表形式存在的数据个数大于8且当前数组的长度大于64时
2、此时此索引位置上的所有数据改为使用红黑树来存储。
2、谈谈你对HashMap中put/get方法的认识,如果了解再谈谈HashMap的扩容机制,默认大小是多少?什么是负载因子(或填充比)?
1、JUC多线程及并发包
0.API中JUC并发的三个相应包和解释
1.java.util.concurrent。 concurrent是并发的意思
2.java.util.concurrent.atomic。 atomic 是原子性的意思
3.java.util.concurrent.locks。 locks是锁的意思
1.谈谈你对volatile的理解
1.volatile是Java虚拟机提供的轻量级的同步机制
1.1.保证可见性
1.2.不保证原子性
1.3.禁止指令重排
volatile面试题:
1.volatile是Java虚拟机提供的轻量级的同步机制,是基本上遵守了JMM的规范,主要是保证可见性和禁止指令重排,但是它并不保证原子性
2.JMM你谈谈
2.1.可见性
答:1.volatile是java虚拟机提供的轻量级的同步机制
答:2.volatile主要有以下三大特性:
1.1.保证可见性
1.2.不保证原子性
1.3.禁止指令重排
3.JMM你谈谈
详细解释JMM
较为官方的解释
可见性面试题
那你写一个Demo来证明一下什么叫保证了可见性?
2.2.原子性
JAVA内存模型(JMM)要求保证原子性,但是volatile是不保证原子性的
number++在多线程下是非线程安全的,如何不加synchronized解决?
原子性的案例代码
如何解决原子性?
1.可以加synchronized来解决不保证原子性问题,但是不推荐使用
2.可以使用java.util.concurrent.atomic包下的AtomicInteger(带原子性包装的整型类)来解决不保证原子性问题
解决方案的具体代码
原子性面试题:
volatile为什么不能保证原子性?而且为什么每次一运行的话,它的值都低于2w呢?
答:因为有很多值在putfield这步写回去的时候可能线程的调度被挂起了,刚好也没有收到最新值的通知,有这么一个纳秒级别的时间差,一写就出现了写覆盖,就把人家的值覆盖掉了
凭啥加了AtomicInteger这个就能解决不能保证原子性问题?
AtomicInteger这个玩意它的底层原理知道吗?
答:知道,AtomicInteger底层是CAS
那再跟我解释一下什么是CAS?
2.3.VolatileDemo代码演示可见性+原子性代码
2.4.有序性
重排1
重排2
案例
禁止指令重排小总结(了解)
3.你在哪些地方用到过volatile?
3.1 单例模式DCL代码
3.2代理模式volatile分析
3.3单例模式volatile代码
加上volatile来禁止指令重排,就能保证多线程间的语义一致性,若不加,那么就会在某一次就会导致线程不安全的问题
如果在高并发多线程的版本里面,那么此时的单例模式最终的写法就是:
1.加入双端检锁机制(也即加入同步代码块)
2.在需要单例的这个对象前面加入volatile来禁止指令重排。
2.CAS你知道吗
0.CAS的两句话概括:
1.如果线程的期望值跟物理内存的真实值一样,就更新值到物理内存当中,并返回true
2.如果线程的期望值跟物理内存的真实值不一样,返回是false,那么本次修改失败,那么此时需要重新获得主物理内存的新值
1.比较并交换
CASDemo代码
2.CAS底层原理?如果知道,谈谈你对UnSafe的理解
atomicInteger.getAndIncrement();
UnSafe
CAS是什么
unSafe.getAndIncrement
底层汇编
简单版小总结
底层原理详解
1.AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存.
2.线程A通过getIntVolatile(var1,var2) 拿到value值3,这是线程A被挂起.
3.线程B也通过getIntVolatile(var1,var2) 拿到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3 成功修改内存的值为4 线程B打完收工 一切OK.
4.这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了.
5.线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功
原子整形之所以在i++这种多线程的环境下面,不用加synchronized,就凭着底层的Unsafe类也能来保证原子性,来保证线程安全,是因为Unsafe是CAS的核心类,CAS是比较并交换
因为Unsafe类是根据内存偏移地址来获取数据的
3.CAS缺点
多次比较循环时间长开销很大
只能保证一个共享变量的原子性
引出来ABA问题???
二句话讲述:
1、synchronized加锁,一致性保证,并发性下降
2、CAS不加锁,保证一致性,但是它需要多次比较,耗时时间长,开销很大。
3.原子类AtomicInteger的ABA问题谈谈?原子更新引用知道吗
ABA问题的产生
原子引用
AtomicReferenceDemo
时间戳原子引用
AtomicStampedReference
ABADemo
规避ABA,解决ABA问题
就是新增一种机制,那就是增加时间戳,当时间戳跟要对比的时间戳不一致的话,就说明这个数据在中间被修改过
4.我们知道ArrayList是线程不安全,请编写一个不安全的案例并给出解决方案
解决方案1
ContainerNotSafeDemo
限制不可以使用vector和Collections工具类解决方案2
List线程copyOnWriteArrayList
set线程CopyOnwriteHashSet
map线程ConcurrentHashMap
集合的全部讲解代码
5.公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁
公平锁和非公平锁
是什么
两者的区别
题外话
可重入锁(又名递归锁)
是什么
ReentrantLock/synchronized就是一个典型的可重入锁
可重入锁最大的作用就是避免死锁
ReenterLockDemo
参考1
参考2
自旋锁
SpinLockDemo
独占锁(写)/共享锁(读)/互斥锁
ReadWriteLockDemo
读写锁
6.CountDownLatch/CyclicBarrier/Semaphore使用过吗?
CountDownLatch
让一些线程阻塞直到另外一些完成后才被唤醒
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞.其他线程调用countDown方法计数器减1(调用countDown方法时线程不会阻塞),当计数器的值变为0,因调用await方法被阻塞的线程会被唤醒,继续执行
CountDownLatchDemo
关门案例
枚举的使用
秦灭六国
CyclicBarrier
CyclicBarrier的字面意思是可循环(Cyclic) 使用的屏障(barrier).它要做的事情是,让一组线程到达一个屏障(也可以叫做同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法.
CyclicBarrierDemo
集齐7颗龙珠就能召唤神龙
代码
Semaphore
信号量的主要用户两个目的,一个是用于多喝共享资源的相互排斥使用,另一个用于并发资源数的控制.
SemaphoreDemo
抢车位
代码
7.阻塞队列知道吗?
队列+阻塞队列
为什么用?有什么好处?
BlockingQueue的核心方法
架构梳理+种类分析
架构介绍
种类分析
ArrayBlockingQueue: 由数组结构组成的有界阻塞队列.
LinkedBlockingQueue: 由链表结构组成的有界(但大小默认值Integer>MAX_VALUE)阻塞队列.
PriorityBlockingQueue:支持优先级排序的无界阻塞队列.
DelayQueue: 使用优先级队列实现的延迟无界阻塞队列.
SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列.
理论
SynchronousQueueDemo
LinkedTransferQueue:由链表结构组成的无界阻塞队列.
LinkedBlockingDeque:由了解结构组成的双向阻塞队列.
用在哪里
生产者消费者模式
传统版
ProdConsumerTraditionDemo
阻塞队列版
ProdConsumerBlockQueueDemo
线程池
消息中间件
8.请您说说synchronized和Lock的区别?用lock有什么好处?
synchronized
1、synchronized是JVM层面,它是JAVA的关键字
2、synchronized是不需要手动释放锁,当synchronized代码执行完以后,系统会自动让线程释放对锁的占用
3、synchronized不能中断,除非是抛出了异常或者是正常执行完成
4、synchronized是非公平锁
5、synchronized不支持精确唤醒,只能随机唤醒或者是唤醒全部线程
Lock
1、Lock是API层面的具体类,它是java5以后新出的一个类
2、lock就需要手动去释放锁,若没有主动的去释放锁,就可能导致死锁的现象
3、lock是可以中断的,主要是设置超时的方法,
4、lock默认是非公平锁,但是也支持公平锁
5、lock可支持精确唤醒
代码
用lock的好处就是:
lock可以支持锁绑定多个Condition,进行精确唤醒,并且还可中断lock
9.已经有Runnable接口,为什么还要出现Callable接口?请你谈谈它的诞生的前身背景?
1、因为并发,异步导致Callable接口的出现
2、主要是用Callable,能够实现当多个任务执行当中,若有一个任务完成的耗时时间比较长
3、那么可以先将其他任务先完成,然后等待这个耗时比较长的任务结束以后一起进行总的计算
10.线程池用过吗?ThreadPoolExecutor谈谈你的理解?
为什么使用线程池,优势
线程池如何使用?
架构实现
编码实现
了解
Executors.newCachedThreadPool();
java8新出
Executors.newWorkStealingPool(int);
java8新增,使用目前机器上可以的处理器作为他的并行级别
重点
Executors.newFixedThreadPool(int)
固定线程数的线程池,执行一个长期的任务,性能好很多
Executors.newSingleThreadExecutor()
一池一线程,一个任务一个线程执行的任务场景
Executors.newCachedThreadPool()
一池多线程,可扩容,带缓冲缓存的,适用:执行很多短期异步的小程序或者负载较轻的服务器
ThreadPoolExecutor
线程池几个重要参数介绍?
7大参数
1.corePoolSize:线程池中的常驻核心线程数
2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1
3.keepAliveTime:多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止
4.unit:keepAliveTime的单位
5.workQueue:任务队列,被提交但尚未被执行的任务.
6.threadFactory:表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可
7.handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示 数(maxnumPoolSize)时如何来拒绝.
说说线程池的底层工作原理?
更加容易理解的工作原理解释
1.假设一开始只有两个核心线程(既corePoolSize),请求的数量也只能两个,但在后面请求的数量越来越多,在队列(既BlockingQueue)这里等待的人爆满了
2.那么maximumPool就会开启最大非核心线程数来进行处理请求的数量,但是若在BlockingQueue这里等待的人已经爆满了,最大线程数和队列都爆满了,
3.那么handler就会开始拒绝其他正在大量的请求进来。
4.如果后期慢慢的请求量越来越少,也即请求量的数量开始少于目前线程的数量,那么此时线程池就会开始对目前已经空余的线程进行一段时间的等待,
若此时这等待的时间中,无再有更多更大量的请求量进来,也即现在来的请求数量里只需核心线程就能够处理的话,那么就会把多余的线程进行销毁,直至剩下两个核心线程(既corePoolSize)。
11.线程池用过吗?生产上你是如何设置合理参数
线程池的拒绝策略请你谈谈
是什么
JDK内置的拒绝策略
AbortPolicy(默认):直接抛出RejectedException异常阻止系统正常运行
CallerRunPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的一种拒绝策略方案
以上内置策略均实现了RejectExecutionHandler接口
你在工作中单一的/固定数的/可变你的三种创建线程池的方法,你用哪个多?超级大坑
答案是一个都不用,我们生产上只能使用自定义的线程池,也即手写线程池
Executors中JDK给你提供了,为什么不用?
你在工作中是如何创建线程池的,是否自定义过线程池使用
Case
合理配置线程池你是如何考虑的?
CPU密集型
IO密集型
1
2
1、看公司业务是CPU密集型还是IO密集型的,这两种不一样,来决定线程池线程数的最佳合理配置数
2、先查看服务器是几核的,调用Runtime.getRuntime().availableProcessors()这个方法来查看核数。
12.死锁编码及定位分析
是什么
产生死锁的主要原因
系统资源不足
进程运行推进的顺序不合适
资源分配不当
代码
解决
jps命令定位进程编号
jstack找到死锁查看
13.多线程常问面试题
创建多线程的方式有多少种?
答:创建多线程的方式以前有两种,现在有四种。
.start()以后是不是马上就启动这个线程?
答: 不是 因为多线程是跟操作系统有关系的,一旦.start()以后这个线程就变成了就绪状态
那什么时候运行这个.start()方法?
答:不知道的,这个要等待CPU操作系统的底层调度通知
JAVA多线程有几种状态?
NEW
新建
RUNNABLE,
就绪,可运行
BLOCKED,
阻塞
什么时候进入到这个阻塞状态?
Thread.sleep();和t1.wait();都会导致阻塞,那么请说说他们之间的区别?
WAITING,
一直等
TIMED_WAITING
设置时间的等
TIMED_WAITING和WAITING,的区别:
答:在于一个没有设置时间,一个设置了时间
TERMINATED
分支主题
接口能不能new?
可以new,创建匿名内部类
接口里面能不能有方法的实现?
在java8以前是不可以的,但是在java8以后就支持了
java8之前规定接口里面只允许有方法的声明,不能有方法的实现,但是java8以后允许有部分的方法实现
说说你对lock的理解?
答:Lock是跟Condition配合起来用的,精准通知,精准唤醒
多线程的8锁
0.8锁的代码
1.标准访问:请问先打印邮件还是短信?
解答:先打印邮件
2.邮件方法暂停4秒钟 请问先打印邮件还是短信?
解答:先打印邮件
3.新增一个普通方法hello(),请问先打印邮件还是hello?
答:先打印hello,因为加个普通方法后发现和同步锁无关
4.有两部手机,请问先打印邮件还是短信?
答:先打印短信 因为换成两个对象后,不是同一把锁了,情况就立刻发生变化
5.两个静态同步方法,同一部手机,请问先打印邮件还是短信?
答:先打印邮件,因为都换成静态同步方法后,情况又变化
6.两个静态同步方法,两部手机,请问先打印邮件还是短信?
答:先打印邮件
7.一个普通同步方法,一个静态同步方法,一部手机,请问先打印邮件还是短信?
答:先打印短信
8.一个普通同步方法,一个静态同步方法,两部手机,请问先打印邮件还是短信?
答:先打印短信
创建多线程的区别
Runnable和Callable接口的区别在哪?
答;方法不同,一个叫run方法无泛型并无返回值并且不带异常,一个叫call带有泛型并有返回值且带异常
为啥Runnable好端端的不用,用Callable接口?
为什么要用CAS,而不是用synchronized?
1、synchronized加锁,同一时间段只允许有一个线程来访问,一致性确实得到了保障,但是并发性下降
2、用CAS的话,是没有加锁的,可以反复的通过CAS进行比较,直到比较成功为止,这样既保证了一致性又提高了并发性
14.其他集合等的常问面试题
集合类安全与不安全
ArrayList是线程不安全的
如何解决?
答:使用CopyOnWriteArrayList来解决
扩容
ArrayList扩容到原来的一半
Vector是线程安全的
它是线程安全的,能保证数据的一致性,但是性能慢
HashMap是线程不安全
如何解决?
答:使用ConcurrentHashMap来解决
扩容
ArrayList扩容到原来的一倍
怎么优化HashMap?
可以把它的hash值设大一点来避免原始的扩容
HashSet是线程不安全
如何解决?
答:使用CopyOnWriteArraySet解决
请说一下你遇过的印象深刻的故障
java.util.ConcurrentModificationException 俗称:并发修改异常
导致的原因?
因为ArrayList.add线程是不安全的,并发的时候,没加锁
解决方案:
3.1 、用 Vector来解决 它是线程安全的,能保证数据的一致性,但是性能慢
3.2、 Collections.synchronizedList(new ArrayList<>()); 把线程不安全的ArrayList转换成线程安全的 小数据量的时候可以用这种解决方案
3.3、 用CopyOnWriteArrayList来解决, 性能很高(优先推荐)
请告诉我五个常见的java异常
我主要在高并发多线程的电商系统里面遇到过java.util.ConcurrentModificationException这种并发修改异常
请问HashSet底层数据结构是啥?
答:HashSet底层是HashMap。
那么请你回答我,为啥HashSet只需要填一个数据进去,而HashMap是要填两个数据进去的,但HashMap是键值对这种的,这根本不匹配啊?
答:HashSet底层一定是HashMap,这个我看过源码,不可能错。 因为HashSet底层的add添加方法调的就是HashMap的put方法
HashSet填一个数据进去,而HashMap是要填两个数据进去的,这是因为HashSet当中添加进去的一个元素就是HashMap的key,那么value永远就是一个object的常量固定写死的
请问HashMap的底层结构是啥?
答:HashMap<>()存储的不是键值对类型的,是存储一个个的node类型的节点; 这个node里面存键值对,底层是node的数组+node的链表+node的红黑树
HashMap<>的初始值数组长度是:16,负载因子是0.75。
负载因子能不能改?
答:可以改,但一般很少人去改这个负载因子,因为够用
2、JUC多线程之异步编排
1、创建线程
无法拿到返回值
继承Thread
实现Runable
可以拿到返回值
Callable
Future
2、CompletableFuture
1、创建CompletableFuture对象
public static CompletableFuture runAsync(Runnable runnable)
public static CompletableFuture runAsync(Runnable runnable, Executor executor)
public static CompletableFuture supplyAsync(Supplier supplier)
public static CompletableFuture supplyAsync(Supplier supplier, Executor executor)
总结
以Async结尾并且没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。
runAsync方法:它以Runnabel函数式接口类型为参数,所以CompletableFuture的计算结果为空。
supplyAsync方法以Supplier函数式接口类型为参数,CompletableFuture的计算结果类型为U。
这些线程都是Daemon线程,主线程结束Daemon线程不结束,只有JVM关闭时,生命周期终止。
示例
2、计算结果完成时的处理
public CompletableFuture whenComplete(BiConsumer super T,? super Throwable> action)
public CompletableFuture whenCompleteAsync(BiConsumer super T,? super Throwable> action)
public CompletableFuture whenCompleteAsync(BiConsumer super T,? super Throwable> action, Executor executor)
public CompletableFuture exceptionally(Function fn)
可以看到Action的类型是BiConsumer super T,? super Throwable>它可以处理正常的计算结果,或者异常情况。
方法不以Async结尾,意味着Action使用相同的线程执行,而Async可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)
示例
3、thenApply
当前阶段正常完成以后执行,而且当前阶段的执行的结果会作为下一阶段的输入参数。thenApplyAsync默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。
分支主题
4、thenAccept与thenRun
分支主题
thenAccept和thenRun都是无返回值的。thenApply生产,那么thenAccept和thenRun是在消费。它们是整个计算的最后两个阶段。
同样是执行指定的动作,同样是消耗,二者也有区别:
thenAccept接收上一阶段的输出作为本阶段的输入
thenRun根本不关心前一阶段的输出,根本不不关心前一阶段的计算结果,因为它不需要输入参数
5、thenCombine整合两个计算结果
分支主题
例如此阶段与其它阶段一起完成,进而触发下一阶段:
分支主题
6、异常处理completeExceptionally
为了能获取任务线程内发生的异常,需要使用 CompletableFuture的completeExceptionally方法将导致CompletableFuture内发生问题的异常抛出。
这样,当执行任务发生异常时,调用get()方法的线程将会收到一个 ExecutionException异常,该异常接收了一个包含失败原因的Exception 参数。
示例
7、异步编排(多任务组合方法allOf和anyOf)
allOf是等待所有任务完成
anyOf是只要有一个任务完成
3、全部代码
3、JVM+GC解析
前提复习
JVM内存结构
jVM体系概述
Java8以后的JVM
GC作用域
常见的垃圾回收算法
引用计数
复制
标记清除
标记整理
题目
1、JVM垃圾回收的时候如何确定垃圾?是否知道什么是 GC Roots
什么是垃圾
简单的说就是内存中已经不再被使用到的空间就是垃圾
要进行垃圾回收,如何判断一个对象是否可以被回收
引用计数法
枚举根节点做可达性分析(根搜索路径)
case
Java 可以做GCRoots的对象
虚拟机栈(栈帧中的局部变量区,也叫做局部变量表
方法区中的类静态属性引用的对象。
方法区中常量引用的对象
本地方法栈中N( Native方法)引用的对象
什么是GCRoots ?
答:是一组四种对象(虚拟机栈(栈帧中的局部变量区,也叫做局部变量表,方法区中的类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中N( Native方法)引用的对象)的根集合体,从这四种对象作为垃圾回收扫描的起始点
2、你说你做过JVM调优和参数配置,请问如何盘点查看MM系统默认值
JVM的参数类型
标配参数
-verison
-help
java -showversion
X参数(了解)
-Xint
解释执行
-Xcomp
第一次使用就编译成本地代码
-Xmixed
混合模式
XX参数
Boolean类型
公式
-XX:+或者- 某个属性值
+表示开启
-表示关闭
Case
是否打印GC收集细节
-XX:+PrintGCDetails
-XX:-PrintGCDetails
是否使用串行垃圾收集器
-XX:-UseSerialGC
-XX:+UseSerialGC
KV设值类型
公式
-XX:属性key=属性值value
Case
-XX:MetaspaceSize=128m
-XX:MaxTenuringThreshold=15
jinfo举例,如何查看当前运行程序的配置
公式
jinfo -flag 配置项 进程编号
Case1
Case2
Case3
题外话(坑题)
两个经典参数:-Xms和-Xmx
这个你如何解释
-Xms
等价于 -XX:InitialHeapSize 初始化堆内存
-Xmx
等价于-XX:MaxHeapSize 最大堆内存
查看JVM默认值
-XX:+PrintFlagsInitial
查看初始默认值
公式
java -XX:+PrintFlagsInitial -version
java -XX:+PrintFlagsInitial
Case
-XX:+PrintFlagsFinal
主要查看修改更新
公式
java -XX:+PirntFlagsFinal
java -XX:+PirntFlagsFinal -version
Case
PrintFlagsFinal举例,运行Java命令的同时打印出参数
分支主题
-XX:+PrintCommandLineFlags
分支主题
3、你平时工作用过的JVM常用基本配置参数有哪些?
基础知识复习
Case
常用参数
-Xms
初始大小内存,默认为物理内存1/64
等价于-XX:InitialHeapSize
-Xmx
最大分配内存,默认为物理内存1/4
等价于-XX:MaxHeapSize
-Xss
设置单个线程的大小,一般默认为512K~1024K
等价于-XX:ThreadStackSize
原理:
系统出厂默认值是跟平台有关,一般生产环境都是部署到linux系统(也即1024 KB)
-Xmn
设置年轻代大小,一般都不用设置,一般都是用默认的即可
-XX:MetaspaceSize
设置元空间大小
分支主题
-Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal
不管是几个G的内存,元空间默认都只占用20多M
典型设置案例
-XX:+PrintGCDetails
输出详细GC收集日志信息
GC
FullGC
-XX:SurvivoRatio
分支主题
-XX:NewRatio
分支主题
-XX:MaxTenuringThreshold
设置垃圾最大年龄
4、强引用、软引用、弱引用、虚引用分别是什么?
整体架构
强引用(默认支持模式)
Case
软引用
Case
弱引用
Case
软引用和弱应用的适用场景
你知道弱引用的话,能谈谈WeakHashMap吗?
虚引用
引用队列
Case
Case
GCRoots和四大引用的小总结
5、请谈谈你对OOM的认识
Java.lang.StackOverflowError
栈溢出错误
为什么会有StackOverflowError这种错误?
答 深度的方法调用导致出不来栈,栈爆了
Java.lang.OutOfMemoryError:Java heap space
堆内存不够用
为什么会有Java heap space这种错误?
答 堆干爆了
Java.lang.OutOfMemeoryError:GC overhead limit exceeded
Java.lang.OutOfMemeoryError:Direct buffer memory
内存挂了
Java.lang.OutOfMemeoryError:unable to create new native thread
非root用户登录Linux系统测试
服务器级别参数调优
Java.lang.OutOfMemeoryError:Metaspace
使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:MetaspaceSize为21810376B(约20M)
元空间溢出
6、G垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈
GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现
因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集
4种主要垃圾收集器
串行垃圾回收器(Serial)
它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境
并行垃圾回收器(Parallel)
多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景
并发垃圾回收器(CMS)
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程
互联网公司多用它,适用于对响应时间有要求的场景
G1垃圾回收器
G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
7、怎么查看服务器默认的垃圾收集器是那个?
生产上如何配置垃圾收集器的?
谈谈你对垃圾收集器的理解?
怎么查看默认的垃圾收集器是哪个?
默认的垃圾收集器有哪些
垃圾收集器
部分参数预先说明
DefNew
Default New Generation
Tenured
Old
ParNew
Parallel New Generation
PSYoungGen
Parallel Scavenge
ParOldGen
Parallel Old Generation
Server/Client模式分别是什么意思
新生代
串行GC(Serial)/(Serial Coping)
并行GC(ParNew)
并行回收GC(Parallel)/(Parallel Scavenge)
老年代
串行回收GC(Serial Old)/(Serial MSC)
并行GC(Parallel Old)/(Parallel MSC)
并发标记清除GC(CMS)
4步过程
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)和用户线程一起
重新标记(CMS remark)
并发清除(CMS concurrent sweep)和用户线程一起
优缺点
优
并发收集低停顿
缺
并发执行,对CPU资源压力大
采用的标记清除算法会导致大量碎片
垃圾收集器配置代码总结
底层代码
实际代码
如何选择垃圾收集器
8、G1垃圾收集器
以前收集器特点
年轻代和老年代是各自独立且连续的内存块
年轻代收集使用单eden+S0 +S进行复制算法
老年代收集必须扫描整个老年代区域
都是以尽可能少而快速地执行GC为设计原则
G1是什么
特点
底层原理
Region区域化垃圾收集器
最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可
回收步骤
4步过程
case案例
常用配置参数(了解)
-XX:+UseG1GC
-XX:G1HeapRegionSize=n : 设置G1区域的大小。值是2的幂,范围是1M到32M。目标是根据最小的Java堆大小划分出约2048个区域
-XX:MaxGCPauseMillis=n : 最大停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿时间小于这个时间
-XX:InitiatingHeapOccupancyPercent=n 堆占用了多少的时候就触发GC,默认是45
-XX:ConcGCThreads=n 并发GC使用的线程数
-XX:G1ReservePercent=n 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
和CMS相比的优势
小总结
9、生产环境服务器变慢,诊断思路和性能评估谈谈?
整机:top
uptime,系统性能命令的精简版
CPU:vmstat
查看CPU(包含不限于)
查看额外
查看所有CPU核信息
mpstat -P ALL 2
每个进程使用cpu的用量分解信息
pidstat -u 1 -p 进程编号
内存:free
应用程序可用内存数
查看额外
pidstat -p 进程号 -r 采样间隔秒数
硬盘:df
查看磁盘剩余空闲数
磁盘IO:iostat
磁盘I/O性能评估
查看额外
pidstat -d 采样间隔秒数 -p 进程号
网络IO:ifstat
默认本地没有,下载ifstat
查看网络IO
10、假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位
结合Linux和JDK命令一块分析
案例步骤
1. 先用top命令找出CPU占比最高的
2. ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序
3. 定位到具体线程或者代码
ps -mp 进程 -o THREAD,tid,time
参数解释
-m 显示所有线程
-p pid进程使用cpu的时间
-o 该参数后是用户自定义格式
4. 将需要的线程ID转换为16进制格式(英文小写格式)
printf "%x\n" 有问题的线程ID
5. jstack 进程ID | grep tid(16进制线程ID小写英文) -A60
11、对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的?
是什么
性能监控工具
jps(虚拟机进程状况工具)
jinfo(Java配置信息工具)
jmap(内存映像工具)
jstat(统计信息监控工具)
12、JVMGC结合SpringBoot微服务的调参优化
在实际的工作中,结合SpringBoot进行JVM的调优
JVMGC对微服务的生产部署调参的优化方案
JVM常问面试题
JVM基础
什么是JVM?
答:Java虚拟机。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。
请谈谈你对JVM的理解?JAVA8的虚拟机有什么更新?
错误的回答:是JAVA虚拟机的意思
正确的回答:我对JVM的理解是JVM的内部体系结构分为三部分:类装载器(ClassLoader)子系统和运行时数据区以及执行引擎
java8在虚拟机方面的更新是撤销了java7的永久带,引入了JAVA8的元空间。
什么是类加载器?
答:比如是一个Car.class二进制字节码文件被类装载ClassLoader装载进JVM以后,模版就有一份,这个模版就变成了一个大的Car Class文件,锁也是锁这个模版
通过这一个模版,可以创建多个实例对象。 也就是锁的是模版,不是对象。
类装载器的作用就是把读取硬盘上的小class文件,通过类装载器装载进JVM里面,充当快递员的角色。
类加载器有几种?
答:粗分就有三种类装载器,细分就有四种类装载器。
java虚拟机自带的加载器有三种(其实细分有四种,但是最后一种在下面进行详解):
启动类加载器:是用c++语言编写的,就加载java出厂默认的这些类 比如List,Object,String这些都是属于java出厂默认的,是启动类加载器以后自动加载进来的,所以我们能直接使用
扩展类加载器:是用JAVA语言编写的,
应用程序类加载器:用户自己可以定义的,java也叫系统类加载器,加载当前应用的classpath的所有类
什么是双亲委派机制?
一句话概括:就是出了事情,往上捅,我爸是李刚,有事找我爹。
详细解释:我要加载一个类,不是从本类开始加载,而是先去启动类加载器去加载去寻找看看有没有,找到了就直接用
找不到就再去扩展类加载器找,扩展类加载器里面找到了直接用,找不到才到应用程序类加载器,这样的好处就是保证大家使用的类是同一套体系,不会错,作用就是保证java源代码不受污染,保证源码干净一致,这也叫沙箱安全
较为官方解释:当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。 采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。
java虚拟机自带的加载器的第四种解释:
说明:如果启动类加载器和扩展类加载器以及应用程序类加载器都不好,那么可以自己自定义加载器。
用户自定义加载器:java.lang.ClassLoader的子类,用户可以定制类的加载方式。
如何使用?
只需要继承ClassLoader就可以自己定制类的加载方式。(但是这部分很少会有人自己去定制,除非你去阿里做基础架构,专做JAVAJDK订制开发)
什么是Native?
Native在Native本地方法栈主要是调底层的C语言的函数库。
什么叫PC寄存器?(也即程序计数器)
一句话概括:就是排版值日表的顺序
详细解释:程序计数器就是记录了程序内部的运行流程和跳转顺序。
较为官方的解释:每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
方法区
一句话概括:就是存放模版的地方,比如什么常量池啊这些静态共有的都存在了方法区
较为官方解释:供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)
实例变量存在堆内存中,和方法区无关
Stack栈
一句话概括:也即程序要运行,需要栈空间,它随程序的申请开辟,程序运行结束,释放。
8种基本类型的变量+对象的引用变量+实例方法都是存放在栈里面
堆
请你谈谈对象的生命周期?
对象的生命周期我的理解:
1、先从伊甸区有100个对象,第一次经过GC垃圾回收以后,会把活着的对象放入到幸存0区。
2、然后第二次会对伊甸区和幸存0区进行GC回收,第二次GC回收后存活的对象,将复制到幸存1区。
3、然后第三次GC垃圾回收前,幸存0区和幸存1区进行互换位置,然后对伊甸区和原先的幸存1区(也即已经成功与幸存0区交换位置的幸存1区)进行第三次GC的垃圾回收。
4、第三次GC后,会把第三次存活下来的对象,存入互换后的幸存0区。若这样GC垃圾回收15次以后,最终成功逃过15次的GC垃圾回收的对象,将存到养老区。
堆内存在JAVA8是由新生区+养老区+元空间所构成
堆内存调优01
用代码测试自己的电脑还能用多少内存等
堆内存调优:
-Xms 设置初始分配大小,默认为物理内存的1/64
-Xmx 最大分配内存,默认为物理内存的1/4
VM参数: -Xms1024m -Xmx1024m -XX:+PrintGCDetails
-XX:+PrintGCDetails 输出详细的GC处理日志
注意:参数调优一定要初始大小和最大分配内存的大小要一致
堆内存调优02
OutOfMemoryError异常
VM参数:-Xms8m -Xmx8m -XX:+PrintGCDetails
能够抛出OutOfMemoryError异常的代码测试
运行代码后的结果
Full GC都没有养老区的空间了,所以报java.lang.OutOfMemoryError: Java heap space这个异常
GC
GC分代收集概念
JVM它是由新生代,养老代,元空间三代构成,每一代有对应着不同的垃圾回收算法,所以叫分代收集。
GC四大算法
1、引用计数法(了解就行,已经不用)
2、复制算法
新生区/伊甸区都是用复制算法
意思也即,对象死的收尸,活的要复制到幸存02区
相关原理
具体解释:
年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
复制算法的优缺点:
答:优点是不会产生内存碎片
答:缺点是会有空间的浪费
具体解释
3、标记清除
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
相关原理
标记清除的优缺点
优点:
不需要额外的空间
缺点
两次扫描,耗时严重
会产生内存碎片
具体优缺点解释
1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。
2、其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。
而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
4、标记压缩
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
相关原理
标记压缩的优缺点
优点
减少移动对象的成本
缺点
GC面试题
请谈谈GC是什么?主要是什么样?
答:GC是垃圾回收,主要是分代收集算法的体现
请再说说GC?
次数上频繁收集Young区
次数上较少收集Old区
基本不动元空间
GC四大算法的优缺点请你谈谈?
复制算法的优缺点:
答:优点是不会产生内存碎片
答:缺点是会有空间的浪费
具体解释
标记清除的优缺点
优点:
不需要额外的空间
缺点
两次扫描,耗时严重
会产生内存碎片
具体优缺点解释
1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。
2、其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。
而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
标记压缩的优缺点
优点
减少移动对象的成本
缺点
GC四大算法怎么用?哪个代用哪个算法?
新生代用复制算法
养老代用标记清除或者标记压缩
4、Github的骚操作
常用词含义
watch: 会持续收到该项目的动态
fork,复制某个项目到自己的github仓库中
star,可以理解为点赞
clone,将项目下载至本地
follow,关注你感兴趣的作者,会收到他们的动态
in关键词限制搜索范围
公式
xxx关键词 in:name或description或readme
xxx in:name项目名包含xxx的
xxx in:description项目描述包含xxx的
xxx in:readme项目的readme文件中包含xxx的
组合使用
搜索项目名或者readme中包含秒杀的项目
seckill in:name,readme
stars或fork数量关键词去查找
公式
xxx关键词 stars 通配符
:> 或者 :>=
区间范围数字
数字1..数字2
查找stars数大于等于5000的springboot项目
springboot stars:>=5000
查找forks数大于500的springcloud项目
springcloud forks:>500
组合使用
查找ifork在100到200之间并且stars数在80到100之间的springboot项目
springboot forks:100..200 stars:80..100
awesome加强搜索
公式
awesome 关键字
awesome 系列 一般是用来收集学习,工具,书籍类相关的项目
搜索优秀的redis相关的项目,包括框架,教程等
高亮显示某一行代码
公式
1行
地址后面紧跟#L数字
多行
地址后面紧跟#L数字-L数字2
项目内搜索
英文t
https://help.github.com/en/articles/using-keyboard-shortcuts
搜索某个地区内的大佬
公式
location:地区
language:语言
地区北京的java方向的用户
location:beijing language:java
5、消息中间件之MQ
1、作为技术选型的时候,你们是怎么判断的?还是说你进去公司以后,公司要你用这个你就用这个?请问你有没有自己的独立见解和思考?
我刚入行的时候,人为年轻,也没有这个能力这个权限去做我们公司项目的技术选型,经理让我们用什么我们就用什么,但是我现在深度了解过ActiveMQ。我在公司用ActiveMQ的时候都接触过api发送和接收,MQ的高可用性,集群容错,持久化等功能,如果贵公司用ActiveMQ这款产品最好,如果不用,我也相信其他MQ也会支持像ActiveMQ的高可用,集群容错等功能,因为技术维度都是相通的。
2、对于这个消息里面,两大经典的Queue队列和Topic主题,这两者有什么区别?请谈谈你的理解?
1.1、Queue队列是以数据默认存储在MQ服务器上文件形式保存起来,也可以用数据库进行存储。它采用的是负载均衡的模式,若当前没有消费者,消息也不会被丢弃。如果有多个消费者,那一个消息就只会发送给其中一个消费者。
1.2、Topic它是无状态的发布订阅模式,若没有订阅者,那么消息就直接被丢弃,如果有多个订阅者,那么这些订阅者就都会收到消息,但是性能会降低,因为它是按照订阅者数量进行复制数据来发送的。
3、ActiveMQ的默认端口是什么?
ActiveMQ两大经典默认的端口是61616和8161
4、消息队列的主要作用是什么?
削锋
解耦
异步
它能对多个模块进行解耦,对相关高并发场景进行削峰以及消息的异步处理
5、你生产上的链接协议如何配置的?是使用tcp吗?
答:还用过NIO,它类似于TCP传输协议,但是它是用的是非阻塞型NIO,它有比TCP更好的性能
6、默认的61616端口以及web页面的端口在哪修改
答:可以在ActiveMQ的conf文件夹下的ActiveMQ.xml当中进行修改61616进程端口,以及在conf文件夹下的jetty.xml进行修改web页面的端口
7、kafka activemq,rabbitmq,rocketmq都有什么区别?
1、我就说说我用过的ActiveMQ,ActiveMQ是Apache下的产品,它支持JAVA,它自身是JAVA开发出来的产品,而Apache跟我们JAVA程序员是耍不掉的,我们用的很多产品也都是Apache下的,比如Tomcat这些产品,所以它对于我们的入门学习的提升非常有帮助。它有较低的概念数据丢失,但是这个可以控制的。可能等到ActiveMQ6.0出来了以后,可能会更完善了一些,但现在如果比起阿里巴巴的RocketMQ,这个ActiveMQ没有RocketMQ好。2、因为RocketMQ是阿里的产品,它模仿了kafka的精华,进行改造出来的产品,也是得到阿里双十一验证的较为成熟的产品。
3、而kafka是大数据场景下用到的产品,它是支持十万级别的吞吐量,但是它似乎会丢失一些数据的概率大一些吧
4、而RabbitMQ是其他语言开发的,若改它源码可能使我们JAVA程序员较为麻烦。扩展性可能就不是很好。
8、你们为什么不用其他的MQ,最终选择了用ActiveMQ?
因为ActiveMQ是Apache下的产品,它支持JAVA,它自身是JAVA开发出来的产品,而Apache 跟我们JAVA程序员是耍不掉的,我们用的很多
产品也都是Apache的,比如Tomcat这些产品,所以它对于我们的入门学习的提升非常有帮助并且扩展性较好。它有较低的概念数据丢失,但是这个可以控制的。可能等到下一代的ActiveMQ6.0出来了以后,可能会更加完善了一些
9、ActiveMQ的持久化机制请你谈谈?
答:MQ自身也带有持久化机制,并且MQ默认的就是KahaDB以日志文件进行存储的,但是这种自带的持久化机制也不可靠,因为若一旦MQ宕机了,就算自带可持久化机制也需要重启恢复,所以为了保证高可用,为了保证可靠性。MQ一般都会采用数据库存储等方式进行更为保障的持久化。
10、请你谈谈ActiveMQ默认存储机制KahaDB的原理是什么?
1、KahaDB里面有四类文件一把锁,其中一个db-1.log是日志文件,当这个日志文件满了以后,会新增一个新的日志文件,并在日志文件名称中按照数字进行编号,若不再引用该日志文件了,该日志文件就会被删除或者是归档。
2、db.data是索引,他用来存储日志文件记录的具体索引位置的。
3、db.free是看db.data索引文件当中是有有空闲
4、lock是读取该KahaDB的一把权限锁。
11、你是如何保证生产端发送消息比较快,并且要保证消息能发送成功?
1、若生产端发送的大量消息,消费端消费的比较慢,可以开启异步投递的方式,
有三种方法可以开启生产端的异步投递,使其性能提高,发送消息会更快
2、但是同时也会导致丢失数据的风险,因为生产端直接生产了消息就直接丢给了MQ,也不管MQ收到没收到,生产端这边就自认为自己已经完成了工作。但MQ一旦宕机等了,那么数据可能会丢失。
3、想保证消息能发送成功,要加入异步投递的同时,再加入异步接收回调方法,使其再判断是否都发送成功,若失败了就需要人工干预,继续重发。
12、请你谈谈MQ当中具体哪些情况会引发消息的重发?
1、消费端使用事务,但是在session中调用了rollback回滚,没有真正的提交数据
2、消费端使用事务,但是没有提交或者是之前关闭了
3、消费端在使用签收的模式下,在session中调用了recover()重试的方法
这以上三种情况都会导致消息的重发。
13、请说说消息重发时间间隔和重发次数?
默认是间隔每一秒钟,消息会重发6次
14、MQ的有毒消息谈谈你的理解?
我的理解是:这个消息重发了6次以后,还是发送不出去,消费端就会自动给MQ一个标识,表明这个消息是有毒的消息,告诉MQ实例(broker),不要再发这个消息了。MQ实例(broker)就会把这个消息放到死信队列当中。
15、谈谈你对死信队列的理解?
若MQ发送的消息,发送了6次都未能发送成功,会将该消息放入到死信队列当中,相关工程师可以在这个MQ的网页端的Queue队列中查看出错的消息,然后进行人工的干预处理
16、你在项目中是如何保证消息队列的高可用?
答:是用事务,签收,持久化和zookeeper+replicated-leveldb-store的主从集群来保证MQ的高可用
17、消费者消费消息,如何保证MQ幂等性?
1、若MQ的消息是可以落盘到数据库的,可以在数据库当中设置唯一的ID主键,这样就算出现了重复消费的情况,也会导致主键的冲突。避免数据库会出现脏数据。不过这个并不推荐
2、推荐用redis缓存来做,给消息分配一个全局的id,只要是消费过的消息,就放入redis当中,那么消费者在开始消费前,先去redis中查询有没有消费的记录。
18、为什么要在系统里引入消息中间件?
1、它能够解决系统耦合调用的问题
2、它能够解决系统RPC同步调用的问题异步模型
3、它能够抵御洪峰流量,达到保护主业务的目的。
19、何种场景下使用了消息中间件?
1、只要是消息发送,尽量都是异步,所以可以引入消息中间件
2、只要是系统解耦,尽量都可以引入消息中间件
20、如果消息中间件的消息大量积压了,你是如何处理的?
为什么会导致消息大量的积压?
什么情况下会导致消息的积压?
怎么解决消息积压的问题?
解决方案1:
可以限制生产者的发送流量,但是要限制流量,就要限制业务,只要业务不执行,就不发这个消息出去
解决方案2:
更可以在消费者来解决消息积压的问题,既然MQ服务器里面的消息积压了,就可以上线更多的消费者来消费消息
解决方案3:
由于可能数据量太大,如果上线更多的消费者的话,那么这个消费者如果是正常来消费消息,还要来处理,那么这个处理可能还要一段时间,就会有点慢,那么就可以上线一个专门处理消息的消费者,假设MQ有百万的消息积压,可以直接从这个专门处理消息的消费者把百万积压的消息全部批量拿出来存到数据库,然后存到数据库以后,再来编写一个离线处理消息的业务功能,从数据库里面,慢慢取出一条一条的数据进行处理。
21、MQ在高并发情况下假设队列满了如何防止消息的丢失?
为什么会导致消息的丢失?
什么情况下会导致消息的丢失?
1、消息发出去,但由于网络的问题没有抵达到MQ服务器
那么如何保证消息一定发出去呢?
解决方案1:可以做好容错方案,也即发送消息可能会导致网络原因而发不出去,那么就将没发送出去的消息进行重试发送
解决方案2:做好日志记录,只要每发一个消息,都做好相应的日志记录,可以给每一个业务的数据库里面创建一张MQ的表,这个表主要保存每一个消息的详细信息,只要发送失败了,就可以定期去扫描这个MQ日志表的数据库,把这些失败的消息拿出来再发一遍
2、消息发送到MQ服务器,MQ服务器要将消息写入到磁盘才能算成功,但此时MQ服务器还没持久化这个数据也即可能还没处理这个数据,就宕机了,MQ服务器一旦宕机了以后再次启动,那么这个没有处理的数据就丢失了。
解决方案1:使用生产者发送消息的确认机制,每一个确认成功的消息,都去数据库MQ表里面修改一下状态,改为:已收到的状态
3、比如生产者发送一个消息给MQ服务器,确认抵达机制已经到了MQ服务器,那么消费者此时就在消费消息,消息者刚把消息拿到,还没来得及消费,消费者却宕机了,宕机以后,此时这个消息,如果是自动确认(也即自动ACK机制的)情况下,相当于消费者已上线拿到消息就默认回复给MQ服务器:消费者已经收到了,但是其实最终却没有消费成功这个消息,那么这个消息就相当于走了一般过场而已
解决方案1: 一定要开启手动确认机制(也即手动开启ACK机制),消费真正成功才移除,失败或者没来得及处理就让消息重新入消息队列
怎么解决消息丢失的问题?
那么如何保证消息一定发出去呢?
解决方案1:
可以做好容错方案,也即发送消息可能会导致网络原因而发不出去,那么就将没发送出去的消息进行重试发送
解决方案2:
做好日志记录,只要每发一个消息,都做好相应的日志记录,可以给每一个业务的数据库里面创建一张MQ的表,这个表主要保存每一个消息的详细信息,只要发送失败了,就可以定期去扫描这个MQ日志表的数据库,把这些失败的消息拿出来再发一遍
解决方案3:
一定要开启手动确认机制(也即手动开启ACK机制),消费真正成功才移除,失败或者没来得及处理就让消息重新入消息队列
解决方案4:
使用生产者发送消息的确认机制,每一个确认成功的消息,都去数据库MQ表里面修改一下状态,改为:已收到的状态
总结:
做好消息的确认机制,要做两端的确认(也即消费者和生产者) 特别是消费者一定要开启手动确认机制(也即手动开启ACK机制),否则只要一收到消息,但还没有处理消息就宕机了,那么这个消息就直接被删除了。一旦做了确认机制,那么每一个发送的消息都在数据库做好记录,然后将定期所有失败的消息再次发送一遍
22、MQ在高并发下若消息重复了,你是如何处理的?
为什么会导致消息的重复?
什么情况下会导致消息的重复?
怎么解决消息重复的问题?
解决方案1:
将业务逻辑方法设计成幂等性的。
解决方案2:
也可以使用防重表,每一个消息由于都有一个唯一的id,只要它被处理过了,就可以去防重表里面记录一下。
解决方案3:
也可以使用RabbitMQ的消息的属性字段,来看看是不是消息是重新派发过来的,那么就可以不处理了,但是这样做太暴力了,万一上一次是失败的,没有消费成功,那么再派送过来也丢失了
总结:
如果消息重复了,把业务设计成幂等性的就行了,即使消息发上一万遍,那都是最终执行一遍的结果。
6、MySQL相关(待更新)
1、mysql5.5以上的存储引擎是什么
答:默认的是InnoDB存储引擎,
2、为什么mysql5.5以后的存储引擎会选择这个作为存储引擎呢?
答:因为它支持事务,行锁,支持外键,InnoDB它的高并发支持的比较好,因为要锁也只是锁一行,不是锁整张表.所以InnoDB的功能和性能会更强一些。
3、请谈谈InnoDB和MyISAM优点和缺点,并谈谈你的理解?
答:现在最常用的存储引擎是InnoDB,它从Mysql5.5.5版本开始以后就成为了默认的存储引擎
InnoDB支持主外键,事务,行表锁,缓存,表的空间,而MyISAM是不支持主外键以及事务的,它在锁方面也仅仅只是支持表锁,它并不适合做高并发的操作,因为它操作一个记录的时候,会锁住整个表。
MyISAM的关注点是在性能,它偏向于查询,而InnoDB的关注点是在事务方面,它偏向于数据的稳定和持久
4、请问当你写了一个sql以后,在Mysql数据库底层是如何加载并运行的?
答:Mysql默认是从FROM 先开始加载表,再从ON。然后就是JOIN,WHERE,GROUP BY, HAVING, SELECT,DISTINCT, ORDER BY ,LIMIT等其他这种顺序运行的
5、请问Join是有几种?
答:粗分是有四种,左连接,右连接,内连接,外连接
6、索引是什么?
Mysql官方对索引的定义是:索引(Index)是帮助Mysql高效获取数据的数据结构。
为什么要建索引?
索引的目的在于提高查询效率。
索引有两大功能:查找快,排好序。也即建的索引将会影响到Sql的两部分。
第一部分:就是where条件后面这部分的条件约束是否用到索引,这部分就是负责查找的条件过滤。
第二部分:索引会影响到where后面的查找以及order by后面的排序。
哪些情况下适合建索引?
1.主键自动建立唯一索引。
2.频繁作为查询条件的字段应该建索引。
3.查询中与其他表关联的字段,外键关系建立索引。
4.频繁更新的字段不适合创建索引:因为每次更新不单单只是更新了记录,还会更新索引,加重了IO负担。
5.Where条件里用不到的字段不创建索引。
6.单键/组合索引的选择问题(高并发下倾向创建组合索引)。
7.查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度。
8.查询中统计或者分组字段。
哪些情况下不适合建索引?
1.表记录太少。
2.经常增删改的表:提高了查询效率,但是同时却会降低了更新表的速度,如对表进行INSERT,UPDATE以及DELETE。因为更新表时,MySql不仅要保存数据,还要保存索引文件。
3.数据重复并且分布平均的表字段,因此应该只给最经常查询和最经常排序的数据列建立索引,但如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果。
7、NoSQL数据库Redis实现分布式锁
1、查询缓存
为了系统性能的提升,我们一般都会将数据放入缓存中,加速访问。而db承担数据落盘工作
整合jedis
2、缓存问题
1、缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,
将去查询数据库,但是数据库也无此记录,并且处于容错考虑,
我们没有将这次查询的null写入缓存,
这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决:空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
2、缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3、缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
与缓存雪崩的区别:
u 击穿是一个热点key失效
u 雪崩是很多key集体失效
解决
分布式锁
3、分布式锁
1、我们想要的效果
2、分布式锁第一阶段&出现的问题
3、分布式锁第二阶段&出现的问题
4、分布式锁第三阶段&出现的问题
5、分布式锁第四阶段&出现的问题
6、分布式锁核心代码
4、缓存使用模式
Cache-Aside
即业务代码围绕Cache编写,由业务代码维护缓存;
读场景
写场景
双写模式
失效模式
并发更新问题与解决
(多个缓存实例,同时更新自己里面的同样数据)
canal
自定义数据分片规则,实现一致性hash
Cache As SoR
即把Cache当做SoR,所有操作都是对Cache进行,然后Cache委托SoR进行数据的真实读写。
即业务代码只看到Cache的操作,看不到关于SoR的代码;三种实现模式:read-through、
write-through、write-behind
read-through
业务代码首先调用Cache,Cache不命中,由Cache回源到SoR(类似回调的方式或者Cache配置的方式),
而不是业务代码。【Guava、Ehcache均支持这种模式】
优点:业务代码整洁;
MyBatis缓存机制;userMapper.get(1);
write-through
称为穿透写模式/直写模式。业务代码调用Cache写数据,然后由Cache负责写缓存和写SoR,而不是业务代码。
MyBatis缓存机制:userMapper.update(1);
1)、直接把缓存删了或者改了
2)、再去改数据
write-behind(write-back)
也叫write-back;回写模式。不同于write-through的是,write-through是同步写,而write-back是异步写,异步成功后可以实现批量写、合并写、延时写等
Copy-Pattern(缓存数据复制方式)
名词
SoR(system-of-record):记录系统,或者可以叫数据源,实际存储原始数据的系统
Cache:缓存,是SoR的快照数据,Cache的访问速度比SoR要快,放入Cache的目的是提升系统速度,减少回源到SoR的次数
回源:回到数据源头检索数据,Cache没有命中需要回到SoR读取数据,这叫做回源
5、分布式锁&集合落地实现----Redisson
1、简介
2、整合
1、导入依赖
2、配置
1、配置redis
2、配置redisson
3、文档
4、分布式锁
5、分布式集合
6、各种锁
锁的基本问题
1、锁是什么?干什么用?
2、知道下面这些锁吗?
自旋锁
阻塞锁
可重入锁
读写锁
互斥锁
悲观锁
乐观锁
公平锁
偏向锁
对象锁
线程锁
锁粗化
锁消除
轻量级锁
重量级锁
信号量
独享锁
共享锁
分段锁
闭锁
锁分类
常见(kao)的锁
Synchronized
Lock
ReentrantLock与synchronized 的区别
ReentrantLock(可重入锁)
中断等待
ReentrantLock获取锁定有三种方式
lock(),
如果获取了锁立即返回,如果别的线程持有锁, 当前线程则一直处于休眠状态,直到获取锁
tryLock(),
如果获取了锁立即返回true, 如果别的线程正持有锁,立即返回false
tryLock(long timeout,TimeUnit unit)
如果获取了锁定立即返回true, 如果别的线程正持有锁, 会等待参数给定的时间, 在等待的过程中,如果获取了锁定,就返回true, 如果等待超时,返回false;
lockInterruptibly:
如果获取了锁定立即返回, 如果没有获取锁定,当前线程处于休眠状态, 直到获取锁定,或者当前线程被别的线程中断
可实现公平锁
对于ReentrantLock而言, 通过构造函数指定该锁是否是公平锁, 默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
锁绑定多个条件
公平锁
synchronized(也可重入)
优势
synchronized是在JVM层面上实现的, 不但可以通过一些监控工具监控synchronized的锁定, 而且在代码执行时出现异常,JVM会自动释放锁定, 但是使用Lock则不行,lock是通过代码实现的, 要保证锁定一定会被释放,就必须将unLock()放到finally{}中
场景
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock, 但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态; 实际上,我推荐大家以压力测试为准
按照性质分类
公平锁/非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序, 有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁
非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。
ReentrantLock是通过AQS的来实现线程调度,实现公平锁(AbstractQueuedSynchronizer)
乐观锁/悲观锁
悲观锁
悲观锁认为对于同一个数据的并发操作, 一定是会发生修改的,哪怕没有修改,也会认为修改。 因此对于同一个数据的并发操作,悲观锁采取加锁的形式
悲观锁适合写操作非常多的场景
悲观锁在Java中的使用,就是利用各种锁。
乐观锁
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。 在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。 乐观的认为,不加锁的并发操作是没有事情的
乐观锁适合读操作非常多的场景
不加锁会带来大量的性能提升
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法, 典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
独享锁/共享锁
独享锁
独享锁是指该锁一次只能被一个线程所持有。
ReentrantLock是独享锁。
Synchronized是独享锁
共享锁
共享锁是指该锁可被多个线程所持有
ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的, 通过实现不同的方法,来实现独享或者共享。
互斥锁/读写锁
独享锁/共享锁是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁
ReentrantLock
读写锁
读写锁在Java中的具体实现就是ReentrantReadWriteLock
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层加锁方法会自动获取锁。
ReentrantLock、Synchronized都是可重入锁,可重入锁的一个好处是可一定程度避免死锁。
如果锁是不具有可重入性特点的话,那么线程在调用同步方法、含有锁的方法时就会产生死锁。
所以所有的锁都应该被设计成可重入的
按照设计分类
自旋锁/自适应自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞, 而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。 非阻塞方式获取锁
自适应自旋
在JDK1.6中引入了自适应的自旋锁。
自旋的时间不固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
什么是阻塞方式获取锁
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)
sleep():睡眠
阻塞而不释放锁
wait():等待
阻塞并释放锁
yield():礼让
暂停当前线程,主动让出自己的CPU时间
join():插队
当前线程等待join进来的执行完,再继续
suspend()和resume():暂停/恢复
有死锁倾向
线程类常用方法
锁粗化/锁消除
锁消除
锁粗化
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问, 那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁
是指当锁是偏向锁的时候,被另一个线程所访问, 偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去, 当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。
重量级锁会让其他申请的线程进入阻塞,性能降低。
分段锁
分段锁是一种锁的设计,并不是具体的一种锁
ConcurrentHashMap并发的实现就是通过分段锁的形式来实现高效的并发操作
分布式锁
Redisson
数据库锁
表锁
行锁
间隙锁
悲观锁
乐观锁
共享锁(读锁)
排他锁(写锁)
8、分布式协调技术Zookeeper实现分布式锁
zookeeper实现过分布式锁吗?
分布式情况下,怎么解决订单号生成的重复问题?
问题产生
真分布式/伪分布式式
分布式优缺点
解决
1.使用分布式锁
1、mysql数据库的乐观锁实现
2、redis--redission
3、zookeeper
2.提前生成好订单号,存放在内存取。获取订单号,直接从内存中取
实现思路
设计思想
回顾我们的zookeeper临时节点的创建
zkClient端的事件监控通知demo
步骤方案
启动Linux系统下的zk服务器并设置好防火墙
pom
log4j.xml
模拟订单的工具类
ZK接口ZKLock
模版模式抽象类ZkAbstractTemplateLock
模版模式
ZkAbstractTemplateLock
实现分布式锁的类ZkDistributedLock
业务实现类OrderZkService
最终效果图
分支主题
9、SpringCloud Alibaba系列
1、Nacos[spring cloud alibaba]
简介
文档
https://nacos.io/zh-cn/docs/what-is-nacos.html
安装
1、windows
2、linux
3、docker化
使用
1、核心概念
namespace(命名空间)
namespace 的设计是 nacos 基于此做多环境以及多租户数据(配置和服务)隔离的
用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
配置
在系统开发过程中,开发者通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成。配置变更是调整系统运行时的行为的有效手段。
配置管理
系统配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动。
配置项
一个具体的可配置的参数与其值域,通常以 param-key=param-value 的形式存在。例如我们常配置系统的日志输出级别(logLevel=INFO|WARN|ERROR) 就是一个配置项。
配置集
一组相关或者不相关的配置项的集合称为配置集。在系统中,一个配置文件通常就是一个配置集,包含了系统各个方面的配置。例如,一个配置集可能包含了数据源、线程池、日志级别等配置项。
配置集 ID(data-id)
Nacos 中的某个配置集的 ID。配置集 ID 是组织划分配置的维度之一。Data ID 通常用于组织划分系统的配置集。一个系统或者应用可以包含多个配置集,每个配置集都可以被一个有意义的名称标识。Data ID 通常采用类 Java 包(如 com.taobao.tc.refund.log.level)的命名规则保证全局唯一性。此命名规则非强制。
配置分组
Nacos 中的一组配置集,是组织配置的维度之一。通过一个有意义的字符串(如 Buy 或 Trade )对配置集进行分组,从而区分 Data ID 相同的配置集。当您在 Nacos 上创建一个配置时,如果未填写配置分组的名称,则配置分组的名称默认采用 DEFAULT_GROUP 。配置分组的常见场景:不同的应用或组件使用了相同的配置类型,如 database_url 配置和 MQ_topic 配置。
endpoint
当 nacos server 集群需要扩缩容时,客户端需要有一种能力能够及时感知到集群发生变化。及时感知到集群的变化是通过 endpoint来实现的。也即客户端会定时的向endpoint发送请求来更新客户端内存中的集群列表。
其他查看官方文档
分支主题
2、nacos-discovery服务注册、发现
1、创建provider应用(cloud选择ribbon,一会儿要测试调用)
2、引入nacos-discovery
3、修改application.properties
4、启用服务注册发现功能
@EnableDiscoveryClient
5、在nacos控制台查看注册的服务
6、创建consumer应用(cloud选择ribbon,测试调用)
1、引入nacos-discovery依赖
2、修改application.properties指定nacos地址
3、启用服务注册发现
4、利用ribbon测试远程调用
3、nacos-config配置管理(配置的动态变更)
1、引入nacos-config
2、创建bootstrap.properties文件,指定nacos配置
3、我们需要了解的默认规则
1、spring.application.name很重要 ,是因为它是构成 Nacos 配置管理 dataId字段的一部分。
2、dataId 的完整格式
${prefix}-${spring.profile.active}.${file-extension}
prefix
默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置。
spring.profile.active
当前环境对应的 profile。当 spring.profile.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
file-exetension
为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。
4、通过 Spring Cloud 原生注解 @RefreshScope 实现配置自动更新:
5、修改配置文件,查看是否实时变更
6、高级:nacos,指定namespace&多data-id加载
7、当一个配置在本地文件和nacos中都有时,优先使用nacos的,如果nacos中娶不到值,则用本地配置的
2、SpringCloud Gateway
简介
注意
Webflux
特性
文档
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.1.0.RELEASE/single/spring-cloud-gateway.html#_cors_configuration
概念
Route 路由
gateway的基本构建模块。它由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则匹配到该路由。
Predicate 断言
这是一个Java 8 Function Predicate。输入类型是 Spring Framework ServerWebExchange。这允许开发人员可以匹配来自HTTP请求的任何内容,例如Header或参数。
Filter 过滤器
这些是使用特定工厂构建的 Spring FrameworkGatewayFilter实例。所以可以在返回请求之前或之后修改请求和响应的内容。
入门示例
核心
Predicate 断言
RoutePredicateFactory
Filter 过滤器
GatewayFilterFactory
GlobalFilter
应用
1、网关项目的pom
2、整合各个项目的统一swagger接口(后来做)
GulimallSwaggerResourceProvider
SwaggerResourceController
3、路由各个请求到相应的服务配置
4、整合Hystrix进行容错
0、导入Hystrix依赖,并开启断路保护@EnableHystrix或者@EnableCircuitBreaker
1、Gateway配置各个微服务服务容错
2、Gateway配置全局容错
3.你是怎么解决分布式事务?
答:我是用SpringCloudAlibaba开源的Seata
4.那请你说说你对Seata的理解?
答:seata它是由1+3的套件所组成,所谓的1+3的1就是全局唯一事务的id,只要在同一ID下不管几个库,都能证明是一套的全局下面的统一体,3就是3大组件,主要是指TC,TM,RM三个概念。
5.那TC,TM,RM是怎么协作的?
答:请你谈谈什么是服务的降级,服务的熔断,服务的限流?
6.SpringCloud Alibaba和Spring团队研发的SpringCloud有什么区别?
答:SpringCloud原生的很多组件已经不维护了,而springcloudalibaba是借鉴了原生的springcloud技术,开发出的新产品。而且更加的方便易用,springcloudalibaba已能提供微服务治理的整套解决方案并经过了双十一的验证
7.SpringCloud和SpringBoot的区别和关系?
SpringBoot专注于快速,方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。
什么是服务的熔断?
什么是服务的降级?
10、本地事务&&分布式事务
本地事务
1.本地事务的简介
2.ACID的特性
ACID
3.事务的传播行为
4.事务的隔离级别
4.1.隔离级别产生的原因:是因为数据库同时存在即读又写,所以得防止在写的过程中,读这条数据是要读到,还是怎么去读,隔离级别就是为了保证同时对一个数据进行读写该要怎么处理。
4.2.读未提交【read uncommitted】(可以读到没有提交的数据)
4.3.读已提交【read committed】(只能读到提交了的数据) Oracle数据库默认的隔离级别
4.4.可重复读【Repeatable read】(同一个事务内,之前读到的数据是多少,以后还是多少,只要事物不结束的情况下) Mysql数据库默认的隔离级别
4.5.串行化【Serializable】(已经不用)
4.6.以上不同隔离级别可能会导致以下现象
1.脏读:若把隔离级别调为读未提交,那么读到没有生效的数据(读未提交)就会发生脏读,这是不允许发生的
2.幻读:同一个事物内下次读到的和上次读到的不一样(读未提交,读已提交)是允许发生的
3.可重复读:可重复多次读取数据,多次读取到的都一样,就不会有幻读的问题
4.不可重复读:不能多次重复读取数据,多次读取会读到不一样的,这会有幻读的问题。
5.事务的回滚策略
rollbackFor:指定的异常必须回滚
noRollbackFor:发生指定的异常不用回滚
异常
运行时异常(不受检查异常,没有强制要求try-catch的):都会回滚
ArrayOutofIndex
Math...
OutofMemory
NullPointException
编译异常(受检查异常)【必须进行处理,要么try-catch要么throws】的:都不回滚
FileNotFoundException
6.SpringBoot本地事务之大坑
5.1.SpringBoot在做事务的时候,在某些情况下有可能会不起作用,特别是使用@Transactional(propagation = Propagation.REQUIRES_NEW)的隔离级别的时候,有可能不起事务的作用。
5.2. 因为 @Transactional的底层是AOP,那么事务想要生效必须是使用代理对象来调用才行:
如果你的业务代码中,有使用事务的隔离级别,尽量不要使用this来调用,防止事务的失效。
因为this并不是代理对象,就相当于代码粘到了大方法里面,this.方法()是跟外层用的同一个事务,所以事务失效了。
5.3.解决方案:开启AOP来进行代理,导入aop-starter,暴露代理对象来解决
1.开启自动代理:@EnableAspectJAutoProxy
2.暴露代理对象:@EnableAspectJAutoProxy(exposeProxy=true)
3.获取代理对象:AopContext.getCurrent()
分布式事务
7.什么是分布式事务
8.BASE【柔性的事务: 最终一致,基本可用,软状态】
分布式事务的相关解决方案
基于XA协议的两阶段提交
TCC编程模式
消息事务+最终一致性
9.分布式事务解决方案Seata
1.核心概念
Distributed Transaction:分布式事务
Global Transaction:全局事务
Branch Transaction:分支事务
Local Transaction:本地事务
以上四个的关系
Transaction Coordinator(TC):事务协调器
Transaction Manager(TM):事务管理器
Resource Manager(RM): 资源管理器
2.示例工程的代码
https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata
3.如何使用Seata
1.每一个微服务的数据库,都建立一个seata的undo_log日志表。来记录各个事务的记录
2.下载并启动seata服务器
https://github.com/seata/seata/
3.调整自己的微服务
4.seata其他相关文档
5、整合到业务
1.每一个微服的数据库必须有undo_log表
2.导包
seata的starter
seata-all
3.写配置
1.我们原来的DataSource要用seata的
2.file.conf
定义seata客户端核心工作规则信息
事务日志
当前微服务在seata服务器中注册的信息配置
客户端相关工作的机制
3.registry.conf
定义让seata知道微服务在其他注册中心的一些配置。
1、指定注册中心信息
2、seata客户端的配置【这些也是可以放在配置中心中】
4.每一个微服务原来自己的数据源都必须使用DataSourceProxy来进行代理
5.若我们使用注册中心,进行服务的发现,seata服务器也得配置放在注册中心,去seata服务器配置registry.conf
11、Spring等高级框架复习
1、SpringMVC
1、SpringMVC的图解原理分析
1、先来一张完整的原理图:
分支主题
2、简单先分析一下上图的流程是怎么样的
1、首先发了一个请求过来
分支主题
2、那么就要看这个请求能不能发给在web.xml配置的SpringDispatcherServlet,若能够发过来到SpringDispatcherServlet,那么此时这个请求就跟SpringDispatcherServlet的url-pattern相对应。
分支主题
3、然后再看SpringMVC里面有没有对应的映射?也即有没有使用@RequestMapping注解映射过,如果要是没有映射过的话,那么就还要来看有没有配置
分支主题
4、如果也没有配置 的话,控制台就会有一个报错,报错将提示没有对应的映射,同时将会给一个404页面。
5、如果有配置 的话,就会去找目标资源,若目标资源没有的话可能也会给一个404页面,而控制台不会显示:没有对应的映射这样的报错信息。
分支主题
6、如果此时要是有对应的@RequestMapping注解映射过,那么这个时候会由HandlerMapping获取一个HandlerExecutionChain对象。
分支主题
7、然后再获取一个HandlerAdapter对象。
8、再调拦截器的PreHandle方法
分支主题
9、再调用目标Handler的目标方法得到ModelAndView对象,再调用拦截器的PostHandle方法。
分支主题
10、这个时候查看在调用目标方法的过程中是否存在异常,要是有异常的话,将使用异常解析器(HandlerExceptionResolver)去解析这个异常,得到一个新的ModelAndView对象。
分支主题
11、然后再由ViewResolver组件根据ModelAndView对象得到实际的View对象
12、然后去渲染视图。
13、渲染视图后响应就会得到,然后再调用拦截器的afterCompletion方法。
分支主题
以上就是整个SpringMVC的运行流程。
2、SpringBoot
1.SpringBoot自动配置原理:
1、当SpringBoot应用启动的时候,就从主方法里面进行启动的。
它主要加载了@SpringBootApplication注解主配置类,这个@SpringBootApplication注解主配置类里边最主要的功能就是SpringBoot开启了一个@EnableAutoConfiguration注解的自动配置功能。
2、@EnableAutoConfiguration作用:
它主要利用了一个EnableAutoConfigurationImportSelector选择器给Spring容器中来导入一些组件。
3、那导入了什么组件呢?
1、来看EnableAutoConfigurationImportSelector这个类的父类selectImports。
2、父类里面规定了一个方法叫selectImports这个方法,查看了selectImports这个方法里面的代码内容就能知道导入了哪些组件了。
3、在selectImports这个方法里面主要有个configurations,并且这个configurations最终会被返回。
4、这个configurations它是获取候选的配置。
5、这个configurations方法的作用就是利用SpringFactoriesLoader.loadFactoryNames从类路径下得到一个资源。
4、那得到了哪些资源呢?
1、它是扫描javajar包类路径下的“META-INF/spring.factories”这个文件。
2、那么扫描到的这些文件作用:是把这个文件的urls拿到之后并把这些urls每一个遍历,最终把这些文件整成一个properties对象。
3、然后它从properties对象里边获取一些值,把这些获取到的值来加载我们最终要返回的这个结果,这个结果就是我们要交给Spring容器中的所有组件,这相当于这factoryClassName就是我们传过来的Class的这个类名。
4、而传过来的Class是调用这个getSpringFactoriesLoaderFactoryClass()这个方法得到从properties中获取到EnableAutoConfiguration.class类名对应的值。
5、然后把它们添加在容器中
5、按照它的这个意思,来到第二个Springjar包的META-INF下的spring.factories这个文件找到配置所有EnableAutoConfiguration的值加入到Spring容器中
1、所以说我们容器中最终会添加很多的类
2、比如:
3、每一个xxxAutoConfiguration类都是容器中的一个组件,并都加入到容器中。
4、加入到容器中之后的作用就是用它们来做自动配置
5、这就是Springboot自动配置之源,也就是自动配置的开始
6、只有这些自动配置类进入到容器中以后,接下来这个自动配置类才开始进行启动
6、每一个自动配置类进行自动配置功能
1、以一个自动配置类HttpEncodingAutoConfiguration(HTTP的编码自动配置)为例子来解释SpringBoot的自动配置之原理:
1、这个HttpEncodingAutoConfiguration类上面标注了一大堆的注解:
2、点进去HttpEncodingProperties这个类,发现这个HttpEncodingProperties类上面标注了@ConfigurationProperties注解。
3、所以说配置文件中该配置什么,我们就按照它的这个旨意,它要配spring.http.encoding这个属性,这个属性里边能配置什么值,就对应HttpEncodingProperties这个类来配置,所有的配置文件中能配置的属性都是在xxx.Properties类中封装着。
4、所以说配置文件能配置什么就可以参照某一个功能对应的这个属性类
7、这个HttpEncodingProperties类就是根据当前不同的条件判断,决定这个配置类是否生效。
1、如果一旦生效了,所有的配置类都成功了,就给容器中添加各种组件,这些组件的属性是从对应的properties类中获取的,而这properties类里边的每一个属性又是和配置文件绑定的。
2、我们再深入的看一下properties。
3、我们看到properties是HttpEncodingProperties,也就是说HttpEncodingProperties这个对象的值它是获取配置文件的值的,所以我们在配置这个filter到底要用什么编码的时候是从properties获取的。
4、而且值得注意的是:
5、这个HttpEncodingAutoConfiguration只有一个有参构造器,在只有一个有参构造器的情况下,参数的值就会从容器中拿
8、而容器中它怎么去拿到呢?
1、相当于是前面的这个@EnableConfigurationProperties(HttpEncodingProperties.class) 注解,这个@EnableConfigurationProperties注解的作用就是把HttpEncodingProperties.class和配置文件进行绑定起来并把HttpEncodingProperties加入到容器中。
2、接下来这个自动配置类,通过一个有参构造器把这个属性拿到,而这个属性已经和SpringBoot映射了,接下来要用什么编码,就是拿到HttpEncodingProperties这个类里边的属性。
3、所以SpringBoot能配置什么,它要设置编码,它是获取properties里边getCharset里边的name值。
4、所以就以此类推,配置一个Spring配置,就可以照着HttpEncodingProperties这里边的来配置。
5、比如在application.properties配置文件下配置一个http.encoding.enabled属性:
6、还能配置其他的一些属性。比如:
7、所以我们能够配置哪些属性,都是来源于这个功能的properties类
8、有了这个自动配置类,自动配置类就给容器中添加这个filter,然后这个filter就会起作用了。
9、用好SpringBoot只要把握这几点:
1).SpringBoot启动会加载大量的自动配置类
2).所要做的就是我们需要的功能SpringBoot有没有帮我们写好的自动配置类:
3).如果有就再来看这个自动配置类中到底配置了哪些组件,Springboot自动配置类里边只要我们要用的组件有,我们就不需要再来配置了,但是如果说没有我们所需要的组件,那么我们就需要自己来写一个配置类来把我们相应的组件配置起来。
4).给容器中自动配置类添加组件的时候,会从properties类中获取某些属性,而这些属性我们就可以在配置文件指定这些属性的值
10、以上内容就是SpringBoot自动配置原理的整个精髓,只要掌握了SpringBoot的原理,我们才能随心所欲的运用。
2.SpringBoot创建及运行原理:
12、2020谷粒商城项目复习
1、分布式Session不共享不同步的问题
1、你是如何在这个电商系统当中解决Session不共享不同步的问题的?
1、如何解决多台同个服务在不同机器上运行的session不同步之问题
第一种解决方案:可以使用session复制来解决Session不同步的问题
其优点是:Tomcat原生是支持的,只需要修改一下配置文件即可
其缺点是:session复制需要数据的传输,可能会有延迟问题,也会占用大量的网络带宽,降级了服务器群的业务处理的能力。在比较大型的分布式集群下,每一个tomcat可能都会全量保存相应的session数据,所以此方案是不可取的。
第二种解决方案:可以让客户端进行存储session
优点:服务器不需存储session,浏览器自己保存session信息到cookie当中,这样能够节省服务器端的资源
缺点:每次的HTTP请求,携带用户在cookie中的完整信息,会浪费网络的带宽。而且全部session数据都放在cookie当中,cookie只能限制保存4K,不能保存大量的信息,并且session放在cookie当中也会有不安全的隐患。所以这个方案也不可用
第三种解决方案:可以使用ip的hash一致性,主要是来源于同一个IP访问的,那么就给他永远访问一台服务器就行了
优点:只需要改nginx的相应配置,不需要去修改应用的代码,也可以支持web-server的水平扩展,但session同步就不行了,因为受内存的限制
缺点:session其实还是存在了web-server中,如果web-server重启,也就会导致大量的session丢失。如果web-server进行水平的扩展,hash后session重新分布,也会导致一部分用户路由不到正确的session。
不过以上缺点问题都不是很大,因为session本来都是有效期的
第四种解决方案:统一存储session
全部session都存到数据库或者是缓存当中
优点:没有安全的隐患,还可以水平扩展,web-server重启也都不会丢失数据
缺点:又增加了一次网络的调用,并且可能还需要修改后端的代码
不过以上统一存储session的缺点,都可以使用SpringSession进行解决
2、如何不同域名的情况下,session不共享的问题
解决方案
放大session的作用域,不能只是属于某个服务的,将其放大,使其能够在不同子域都能获得这个session
3、使用SpringSession和redis来解决Session的共享问题
4、SpringSession的官网:https://docs.spring.io/spring-session/docs/2.2.1.RELEASE/reference/html5/#httpsession-redis-jc
2、电商登录后session共享的整个过程
1、所有登录后的状态信息是存到session里面的,而任何一个服务都整合了Spring Session,并且将Session统一存储到redis里面
2、让session第一次存储数据的时候,会给浏览器进行发卡,这个卡就标识了session的id是什么,而发的这个卡也放大了整个作用域,让某一个服务发的卡,这个卡可以让全系统服务通用,也即夸整个父子域,不论是父域系统旗下的所有域名,都可以全使用这个session的id,这就是整合Spring Session达到的效果
3、但是在更大的系统里面也会出现一些问题
1、一家公司旗下的多个不同网站系统的session共享同步问题,该如何解决?也即在多个系统里面,一旦一个用户已经登录一个网站,那么其他不同系统的网站是否还需要重新登录或者注册?
2、在多系统里面,能够希望达到一个效果,只要是一家公司旗下的不同网站系统,在某个网站系统注册和登录过了,就其他网站系统直接能全部自动登录并显示该用户的登录信息,而无需到了其他网站系统还需要重新登录注册。
3、解决这问题:可以抽取一个登录注册的认证中心,专门处理不同系统的登录以及注册请求的。一旦用户想要在某个网站系统登录成功了,那么任何其他的网站系统都能使用。但是SpringSession是实现不了这个功能的
4、举个例子,在某个电商系统里面登录,如果都用session,那么在发卡的时候,放大了该卡的作用域,也最多放大到该卡的一级域名。但不能放大到更大的域名,比如.com,所以只能放大到一个系统里面服务的域名,那么相当于如果浏览器要访问其他网站,就带不上在电商系统里面发的卡,所以就不能通过简单的SpringSession来解决多系统的单点登录问题
分支主题
2、单点登录。怎么实现在不同域名当中其一次登录,处处可用?
有一个核心服务器
1.中央认证服务器
所有的登录请求都应该发给中央认证服务器
其他客户端
2.想要登录,都会先去中央认证服务器进行登录,而且登录成功以后跳转回来客户端系统
3.客户端系统只要有一个登录了,那么其他的都不用登录,会自动登录成功
4.在全局任何系统,都统一了一个cookie的唯一标识,这样才能标识哪个用户登录成功的了,这样的话无论是其他系统还是中央认证服务器都不是相同域名旗下的,所以所有系统域名都不相同
怎么解决所有系统域名都不相同,还能达到一处登录,处处自动登录呢?
单点登录流程
1、浏览器第一次访问到客户端中受保护的请求资源以后
2、客户端就来判断浏览器是否已经登录过
1、按照以往常规的流程也即判断session里面有没有这个用户的会话信息,如果有就说明登录了,没有就说明没有登录,没有登录就去登录中心认证服务器登录
2、如果客户端去中心认证服务器登录成功了就要跳转回来,也即从哪跳过来的就从哪跳回去
3、如果当前系统没有保存当前登录的用户,也即客户端没有人登录,如果没有登录就要去访问登陆服务器进行登录
4、由于没登录就给浏览器重定向到登录服务器的登录页面进行登录
5、如果在登录的认证服务器中登录页面登录成功了,就得跳回去
6、那么一旦登录成功以后,怎么跳回去?
1、可以跳到登录页的时候,在页面路径后面加一个原来的地址
这样相当于重定向到登录页的时候,也告诉登录服务器之前原来的地址是哪个地址,这样的话登录服务器,登录成功也就知道跳回哪了
2、所以为了能登录成功后,能跳回原来的页面,可以在客户端的跳转登录页的控制器请求那加一个重定向原来的地址
3、这样登录服务器登录成功以后。想要重新跳回客户端以前的页面,就把请求地址那后面的参数地址拿过来就行了,也即客户端得告诉登录服务器,我登录以后想要跳转到哪里
4、此时登录服务器就能感知到客户端发过来的重定向地址
3、登录认证服务器开始处理登录功能
1、首先登录服务器展示它的登录页面,而且输入相应的帐号密码登录成功以后还要跳回去
2、为了能跳回去,在方面登录页的时候,登录页是能取到客户端给的登录成功后的URL地址
3、但是我们再来登录,那么上一步的地址就会丢失,所以为了防止这个客户端给的跳转地址丢失,在登录的控制器中将URL地址放到请求域中,可以在登录页面里面可以添加一个隐藏的input输入框,让它自己取出请求域的地址就行了
4、用户在浏览器的登录页面中输入帐号密码进行登录,并且这个登录页也有重定向的地址,也即登录成功以后跳转到客户端给的地址,当登录以后,就会提交登录请求(含用户名,密码,客户端给的登录成功后要重定向跳转的url地址)给登录认证服务器
5、登录认证服务器,就开始处理登录请求
6、登录成功后,就会跳转到客户端在登录请求发来的URL地址
7、登录成功后,无法跳转到客户端指定的URL地址
1、访问客户端指定的URL地址,又要遵循上面的逻辑,也即访问这个客户端的时候,又要看有没有登录,因为客户端总是显示没登录,所以这块就算登录成功后也无法跳转到指定的页面,这就导致了死循环
2、如何解决上面这个问题:在认证服务器登录成功以后,重定向跳转回去以后的时候客户端指定地址的时候,客户端要能感知到是已经登录成功以后跳转回来的,而不是直接访问的
3、那么能让客户端感知已经登录的业务代码怎么写?
1、用redis或者session来存储已经登录的用户信息,在本系统当中我们就用了redis来存储的登录成功的用户信息
2、那么如果是用session来存储已经登录的用户信息,那么就要发一个卡,类似于银行卡的卡号。
3、若是在redis存的话key,因为redis会存好多登录的用户,所以要为每一个用户生成一个唯一的key来进行存储,比如可以用UUID来存储key,值就存用户信息
4、把登录成功后的用户信息存到redis,但是得让其他客户端知道这个用户已经登录过的了
5、在登录页,登录成功后返回客户端给的重定向URL地址后面再加上一个用户的URL参数(类似于token令牌,token令牌就是UUID)使其能够告诉客户端这个用户已经登录过的了,然后跳转到客户端指定的URL地址
6、只要登录成功后,带了token令牌,就认为它是已经登录了的。
7、那么在客户端,只要你登录成功了,返回了客户端指定的URL地址,这URL地址当中还带了token令牌,那么就给你跳转回客户端指定的页面地址
8、但是token是不一定带的,因为第一次进来访问,就是没有带token的,只有去登录服务器登录回来以后才会有token
9、登录成功不成功,有两个判断,第一个是先看看session里面有没有用户信息如果有就说明已经登录成功,第二个只要是登录成功后跳回来客户端指定的页面的还带了token的,就说明也是登录成功
10、有了token,那么登录成功的用户再放到session里面,session的值放的是认证服务器登录后的用户信息,也即在客户端获取认证服务器当前token真正对应的用户信息,拿到了用户信息了,才能说明是成功的。
11、就成功跳转到客户端指定的URL页面地址
12、那么在下一次再访问客户端指定的URL页面地址就不用再跳转到认证服务器进行登录了,也是直接能访问到想要访问的资源了
13、不用登录的原因就是由于在认证服务器登录成功以后跳回去,带了这个token,有了这个token以后,客户端就会去拿到这个token,来证明是登录过的用户
14、最大的一个问题就是,如果访问其他的客户端域名,会有域名不同的情况,就说明一个客户端登录了,不代表另外一个客户端域名就直接不用登录了
15、就应该一个客户端登录了,其他客户端就不用再登录了。
8、一个客户端已经成功登录了,但是怎么能让其他客户端就不用再登录,而直接去访问想要访问的受保护资源?
1、由于某一个客户端已经在认证服务器登录过了,那么其他的客户端是互相授信的系统,彼此是信任的,那么就希望第二个客户端系统不用去登录就直接能够访问想要访问的受保护的资源
2、访问第二个客户端,默认访问第二个客户端是需要重新登录的,但是第一个客户端已经登录了,就想希望第二个客户端能通过第一个客户端已经登录完成的状态进行免登录
3、第二个客户端需要重新登录的原因就是在认证服务器这一块,认证服务器它并没有记住哪些用户已经登录了。
4、认证服务器需要保存哪些用户已经登录了的
1、只要登录成功以后,就会去客户端提供的页面地址重定向过去,还会把当前用户保存过来,也就是重定向的时候带上这个令牌信息
2、为了下一次别的客户端系统想要登录,得知道上一个客户端系统已经登录过了,就只有一种做法
1、客户端访问认证服务器会带上认证服务器的所有的cookie
2、假设只要有了登录了,就搞一个cookie,来保存上一个用户登录的用户令牌信息
3、所以无论是第一个客户端还是其他多个客户端没有登录,只要能跳到认证服务器的登录页面,浏览器就会带上这个认证服务器下的所有cookie。
4、也就是说为了能记住登录的状态信息,即使是上一个人登录,就应该给我们登录服务器留一个痕迹,这个痕迹就是留给客户端浏览器的,告诉客户端浏览器之前这个在认证服务器已经登录过了,痕迹就是cookie
5、所以唯一要做的一件事就是处理登录请求和用户状态信息的token返回出去外,还要给当前系统留一个记号,这个记号可以是一个cookie来表示曾经登录过
3、单点登录的整个令牌是在登录认证服务器保存的
1、登录服务器只要有任何的客户端浏览器登录成功了,那么就给这个客户端浏览器留一个令牌
2、登录后的用户信息是在redis当中存起来的,但是为了有标识,认证服务器就命令客户端浏览器保存一个cookie,这个cookie的键是自己定义,值是用户信息的唯一id。
3、此时客户端浏览器就应该将认证服务器发过来的cookie保存在认证服务器当中
4、只要访问认证服务器,那么认证服务器就会有相应的cookie
5、也即某个客户端浏览器没有登录,就会跳转到登录认证服务器进行登录,只要在登录服务器做了登录请求,那么登录服务器关键的一步就是
1、只要进行登录的请求,它会做两件事,把用户信息存到redis里面,并且给当前服务器留一个cookie标识
2、登录成功就会做两件事
1、不止要执行浏览器指定的重定向到地址,并且重定向的时候,还带了token
2、登录成功以后,还给当前认证服务器保存了一个用户信息的cookie
3、此时又重定向到受保护的资源页面
6、客户端浏览器去认证服务器登录,登录后不仅重定向到浏览器指定的页面,但在重定向之前,客户端浏览器要给认证服务器的域名下保存一个cookie。因为这个登录请求是发给认证服务器的
7、客户端浏览器在认证服务器登录后,还留了一个cookie的在认证服务器,这个cookie的作用?
1、保存cookie的作用就是,客户端浏览器只要以后访问认证服务器的域名,都会带上这个域名下的所有cookie。
8、目前看到的效果就是:不仅登录成功,跳回客户端浏览器指定的页面地址,还在请求路径上带了一个token,并在域名下面留了一个cookie,cookie和token的值都是一样的
9、接下来第二个客户端浏览器去访问,显示没有登录,就会重定向跳转到了认证服务器的登录页进行登录
1、但是已经有一个客户端浏览器登录过了,登录了以后还给认证服务器留了一个用户的cookie信息
2、所以其他客户端浏览器下一次再来访问登录页的时候,就显示没有登录,浏览器就命令去访问登录页。
3、但是在访问登录页的时候,这个请求还带上了之前保存的Cookie,这个Cookie就是上一个客户端浏览器登录成功后在认证服务器留下的cookie,所以其他客户端浏览器请求的时候都会带上这个cookie
4、登录页的重新处理应该就是:如果客户端浏览器带了这个Cookie,就说明是上一个客户端浏览器留下来的痕迹,那么就可以说明新来的客户端浏览器可以不用登录了
5、也即在后台代码的登录页的控制器当中拿到这个Cookie的值
6、核心就是:在展示登录页面的时候判断这个客户端浏览器是否已经登录过,判断是否有关键的cookie,如果没有这个cookie,那就展示登录页,如果有就直接免登录,跳回受保护的资源页面
10、为了让客户端浏览器知道,访问认证服务器登录成功以后,那么重定向跳回来的时候就会加一个token,如果有token就认为是登录回来的,就把登录后的信息放到session里面,否则就是直接访问的,那么就要拒绝,并要求它跳回到认证服务器的登录页进行登录
5、此时已经达到了一个客户端浏览器去认证服务器登录后,其他客户端浏览器就已经不用再登录了,直接免登录跳回指定的页面资源地址
1、其他客户端访问资源地址,先判断是否已经登录过,此时客户端浏览器会重定向到认证服务器的登录页面去登录,当然还带着一个cookie。
2、客户端浏览器就去访问认证服务器的登录页面
3、其他客户端浏览器访问登录页面的时候,结果登录页面,其他客户端浏览器只要带了这个cookie,因为可能上一次有其他客户端浏览器登录过,那么其他客户端浏览器访问认证服务器就会带上以前浏览器留下的cookie。
4、如果客户端浏览器带了这个cookie就拿着,直接可以跳回客户端浏览器指定的页面。
5、那么一旦跳回到客户端浏览器指定的页面
4、整个单点登录流程图
分支主题
3、购物车
1、购物车数据存储在redis中
2、购物车数据存在redis的好处是什么?
1、购物车是一个读写都是高并发的操作,那么如果使用Mysql数据库,那么读写都高并发的情况下,就会让mysql数据库都承担非常大的压力
2、所以可以选择一些nosql来存储购物车的数据,比如可以使用mongodb,但是使用mongodb的话,它的性能也并不能带来很大的提升
3、所以选择了Nosql的redis来存储购物车的数据
4、放在redis的好处就是
1、数据结构好组织
2、redis拥有极高的读写并发性能
5、不过存在redis里面也会出现一个问题
1、也就是登录以后的购物车是需要持久化保存的
2、但是redis默认是内存数据库,所有数据是存在内存的,一旦redis宕机了,那么所有的数据就丢失了
3、但是mysql就不一样了,只要保存了,即使宕机了也还在
4、不过这种问题也可以得到解决
1、在安装redis的时候,可以指定好redis的持久化策略
2、那么在redis的每一条数据,都能持久化到磁盘里面,即使redis宕机,下次重新启动,数据还会在。
3、这样虽然能损失一定的性能,就相当于没有那么高的吞吐量了,但是即使redis的数据存在磁盘里面,也会比mysql快很多。
4、所以用户登录以后,那么在购物车当中的数据存在redis里面
5、如果用户没登录,没有登录的购物车,最大的特点就是一个临时的购物车,其关闭了浏览器,下次打开浏览器还是会有以前保存的临时购物车的数据
6、那么临时购物车,也用redis来存储
7、当然临时购物车也可以存到很多的地方。比如放到cookie当中
1、但是如果放在cookie当中,那么也就是浏览器存储,后台不存,那么这样的话。在大数据的时代下,想统计一些用户购买了哪些商品,甚至是做一些用户购买的热度高的商品推荐,都是没有办法做到的
8、最终无论用户登录了的购物车还是没有登录的购物车,都统一存在redis当中
6、购物车存在redis的数据应该以什么数据类型来进行存储
1、一个用户等于两个购物车,一个登录后的购物车,一个是没登录的临时购物车
2、但是不管是哪个购物车,购物车里面数据应该都是有非常多的购物项,也就是添加的那些商品
3、每一个购物项都应该有当期商品的id,因为添加商品都是按照sku销售属性来进行添加的
4、包括每一个购物项是被选中还是没有选中,这个状态也要进行存储
5、保存商品的标题信息和商品的默认图片等信息也要进行存储
6、购物车有很多的购物项,那么使用redis的五种数据类型的哪一种进行一个购物车的存储呢?
1、一个购物车里面就应该是一个数组,数组里面都是一个个的购物项,也就是一个个的对象
2、但是redis里面保存的数据都是以键值对的结构进行存储
3、那么假设用redis的list类型来存储的话,key应该是存用户的标识,代表哪个用户的购物车
1、这个用户的购物车应该是一个数组类型,数组里面放一个个的购物项
2、也即value值存购物车里面的购物项,就是存具体的商品信息
3、如果按照list的数据类型来存储购物车的信息,会有缺点
1、如果来修改某个购物车里面的购物项
2、那么就相当于redis里面存储的购物项也得修改
3、那么就得去redis里面在list数据类型下找这个页面被修改的购物项数据
4、因为是用list来存储购物项,那么问题可能会添加了很多的购物项,在某个购物项修改了相关的信息,那么怎么知道要改购物车里面的哪个商品?
5、可能会有两种办法:
1、得先知道选的是第几个购物项修改的商品信息,在list中来找相应被修改的元素,前提是页面跟后台redis存储的数据顺序是一模一样的,那么就比较容易找到
2、但是会非常的麻烦。所以为了后来的方便快速找购物项的数据并且进行修改
3、可以把购物车里面的list类型存储的购物项改为hash进行存储
4、使用redis的hash进行存储购物车当中的购物项
1、key还是代表某个用户的购物车,这样方便定位到具体的用户的购物车
2、那么hash的value值是有两个值
1、第一个值存储商品的id
2、第二个值存储商品的具体信息
3、那么hash结构的数据类型最终的存储结构应该是这样
1号用户的购物车里面,存了每一个购物项,数据值是某个商品的id,它的信息是具体的商品信息
4、那么在修改购物项就直接去redis里面找到相应用户的购物车,也不用挨个去遍历来去修改,也可以不用按照其索引来找,因为这样很麻烦,只需要类似于map的方式,按照某个商品的id来修改具体的商品信息。
5、所以在redis里面存购物车的商品不能直接用list方式来存储,因为list来找商品得遍历循环,看是哪个商品
6、但是用于redis的hash结构来存储购物车的商品,也就是map的方式,那么找商品,直接拿key商品的id就能找到
7、最终就应该是以类似于Map 这样的方式来进行存储购物车里面的数据
数据格式:Map> 有两个key,这个最前面的key是String类型,是存储每一个用户的购物车标识,而Map就是存储整个购物车,这个购物车里面存储每一个的购物项,为了方便找购物项,这个Map的key也是String类型,这个String类型用来存储购物项的商品id,而Map的CartItem对象类型就是存储购物车的详情信息
所以真正的购物车信息应该是以Map>这样的Hash方式进行存储的
3、离线购物车
分支主题
4、在线购物车
4、目前整个2020谷粒商城我已经学习和开发完毕,但是我只分享以上三个技术点的笔记,剩下的暂不分享,谢谢理解
0 条评论
下一页