面试题
2020-03-16 15:09:18 1 举报
AI智能生成
面试题,月薪15K,正在完善中
作者其他创作
大纲/内容
java基础
java基础
JVM、JRE和JDK的关系?
JVM (Java Virtual Machine) 是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。
JRE (Java Runtime Environment) 包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
JDK(Java Development Kit)是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等
子主题
equals跟 == ?
==对于基本数据类型跟引用数据类型是不同的
基本数据类型比较的是值
引用数据类型比较的是引用
没有重写的equlas跟==一样(equals源码就是==)
d大多数类都重写了equals方法,比较的是值
java中基本的的数据类型
子主题
4. final 在 Java 中有什么作用?
fianl关键字修饰的类,方法,变量,表示最终的
final修饰的类不能被继承,final修饰的方法不能被重写,final修饰的变量不能被修改
抽象,继承,封装,多态
抽象
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象跟行为抽象
抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
抽取一类对象的共同特征,包括行为跟属性,不关注具体细节(猫类,狗类继承抽象动物类)
抽象类的特点
抽象类要用abstract 修饰
抽象类不一定有抽象方法,有抽象方法的类一定是抽象类
抽象类不能被实例化,通过子类对象实例化,这叫抽象类多态
抽象类的子类要么重写抽象类的所有抽象方法,要么是抽象类
抽象类可以有变量跟常量,有参,空参构建,抽象方法,普通方法
抽象跟接口的区别
成员区别:抽象类有变量,常量,有构造方法,普通方法,接口只能是常量跟抽象方法
关系区别:类之间可以实单继承,类与接口之间可以多实现,接口之间可以多继承
设计理念:抽象类是对事物的抽象,而接口是对行为的抽象
接口
接口的特点
接口定义的是规则
用关键字interface修饰
类实现用关键字implements
接口不能被实例化,只能通过实现类对象实例化,这叫接口多态
接口的子类要么重写所有的抽象方法,要么是抽象类
接口的成员变量只能说常量,默认被final,static修饰
没有构造方法,因为接口主要是扩展功能的,而没有具体存在
继承
继承是使用已有的类作为基础建立新的类,新类可以增加新的数据和功能,也可以使用父类的功能
继承可以提到代码的复用性,子类拥有父类的非私有化方法
封装
隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
多态
程序中引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,
即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
同一个对象,在不同时刻表现出来的不同形态
实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)
即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态的前提:
要有继承或实现关系
要有方法重写
要有父类引用指向子类对象(向上转型)
多态的好处
1,消除类型之间的耦合关系,实现低耦合。
2,灵活性。
3,可扩充性。
4,可替换性。
5,提高程序的扩展性
多态弊端
不能使用子类特有的功能
多态的成员特点
成员变量:编译看左边,运行看右边
成员方法:编译看左边,运行看右边
子主题
String和StringBuffer、StringBuilder的区别是什么?
1,String类的内容一旦声明后是不可改变的,改变的只是其内存的指向,而StringBuffer类的对象内容是可以改变的。
2,对于StringBuffer,不能像String那样直接通过赋值的方式完成对象实例化,必须通过构造方法的方式完成。
3,StringBuffer的在进行字符串处理时,不生成新的对象,在内存使用上要优于串类。所以在实际使用时,如果经常需要对一个字符串进行修改,例如插入,删除等操作,使用StringBuffer要更加适合一些。
stringBuffer是线程安全的
集合
ArrayList与LinkedList的区别
- ArrayList和LinkedList都是不同步的,也就是不保证线程安全
- ArrayList底层使用Object数组,LinkedList底层使用双向链表(jdk1.6之前是循环链表,1.7以后取消了循环)
- 插入和删除是否受到元素位置的影响:ArrayList采用数组存储,所以插入和删除元素时的时间复杂度受元素位置影响,LinkedList使用链表存储,所以,插入和删除元素时不受元素位置的影响,都是以O(1),而数组是O(n).
- 是否支持快速随机访问:LinkedList不支持快速随机访问,ArryList支持。
- 内存空间占用:ArraryList的空间浪费主要体现在在list的链表的结尾会预留空间,LinkedList空间体现在它的每一个元素需要比ArraryList更多的空间(因为要存放直接后继和直接前驱以及数据).
- 数组查询快,增删慢,链表增删快,查询慢
ArraryList和vector的区别?为什么要用ArraryList代替Vector
- Vector的所有方法都是同步的,可以多线程安全访问vector对象,但是如果单线程访问vector对象,会比较耗费时间。
- ArraryList是不同步的,所以在不需要保证线程安全时使用它。
HashMap和HashTable的区别
- 线程是否安全:HashMap是非线程安全的,HashTable是线程安全的,HashTable内部的方法都经过synchronized修饰。
- HashMap的效率比HashTable要高,因为线程安全的问题。在代码中,尽量不要使用HashTable.
- HashMap中,null值可以作为键,这样的键只能有一个,可以有一个或多个键对应的值为null。但是在HashTable中只要有一个null,就抛出异常。
- 初始容量和扩充容量不同:①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小。
- 底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable则 没有这样的机制。
HashMap的底层原理
数组,链表,红黑树
HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,
HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap 基于 Hash 算法实现的
HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,
HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap 基于 Hash 算法实现的
- 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
- 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
- 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
HashMap拓展
注意
key/value的格式
key只能是引用类型,或者为null,Integer
因为存储数据的时候HashMap结构需要获取key的HashCode值
hashCode方法
返回值为Int
不同的对象可能存在相同的hashCode值
对象的数量是无穷的,int的取值是有范围的,用有限的范围表示无穷,势必有重复的
hashMap如何存储数据的?
是要根据key的hashCode值来判断
hashCode值相同的key,无法存储两个
hashMap无法存储两个相同key,他们的key的hashCode相同equals也相同,只能存一个,后者会把前者覆盖
只能有一个key为null值,可以有多个value为null,前提key不同
引用类型中的hashCode是用来干嘛的
程序给他分配的基本可以表示他自己的数值
用于散列存储数据的时候有据可依
散列存储?
可以提高存储跟寻址的性能
hashCode扩容
达到长度的0.75(负载因子)就扩容
用空间换时间
为什么是0.75多次测试的最优解
数组长度到达64,并且链表长度大于8,就用红黑树
hashMap是如何保证他的综合效率的
jdk1.8前数组+链表, jdk1.8之后加入了红黑树
增大空间,减少冲突
hashCode是int的取值范围
大招 HashMap寻址算法
扩容核心: 新增一倍的空间,将原空间中的一半数据分过去!达到一个新的离散平衡,并且扩容时间成本最低!
如果key为null,则存储到数组下标为0的位置
计key的hashcode为hash
h = hash^ (hash >>> 16)
如果这个key的hashCode值比较小,那么它的高16位跟它自己按位^还是它原来的hashCode值;
如果这个key的hashCode值比较大,那么形成的数更具有代表这个hashCode的能力,
因为高低位都参与了,对于大hashCode值的重新离散效果更好
如果这个key的hashCode值比较大,那么形成的数更具有代表这个hashCode的能力,
因为高低位都参与了,对于大hashCode值的重新离散效果更好
具体k-v要存储到数组对应的下标位置为:index = h&(len-1)
HashSet如何检查重复
当你把对象加入到HashSet中,HashSet会先计算对象的hashcode()的值来判断对象加入的位置,也会与其他加入的对象的hashcode值进行比较,如果没有相符的hashcode,HashSet会假设对象没有出现,但是如果发现有相同的hashcode值的对象,这个时候会调用equals方法来检查hashcode值是否相等,如果真的相同,就不会让其加入成功。
HashSet与HashMap的区别
HashMap HashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用put()向map中添加元素 调用add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 HashSet较HashMap来说比较慢
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用put()向map中添加元素 调用add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 HashSet较HashMap来说比较慢
CurrentHashMap 和 Hashtable 的区别?
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; - 实现线程安全的方式(重要):
① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
HashTable
jdk1.7
jdk 1.8
多线程
线程的创建方式
继承Thread类,重写run方法
实现Runnable接口,重写run方法
实现Callable接口,重写call方法
Executors线程池创建
线程体系
线程状态?
新建状态:new 线程的时候
就绪状态 :调用start()方法时
运行状态:线程获得cpu资源,run()方法执行
阻塞状态
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。
当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态:run()方法执行完成,main()方法结束
Executors线程池的创建
- newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
- newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
- newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
- newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
- newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
- newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
- ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。
sleep() 和 wait() 有什么区别?
sleep()是Thread的静态方法,wait()是Object类方法
sleep()方法时间到期后就醒过来,wait()要调用notify()才能唤醒
sleep()方法不释放锁,wait()方法释放锁
sleep不让出系统资源;wait方法是进入线程池等待,让出其他资源,
其他线程可以抢占CPU
其他线程可以抢占CPU
sleep()可以在任何地方运行,但是wait(),notify(),notifyAll()要写在同步方法,同步代码块里面
wait(),sleep()要捕获异常,但是notify()不需要
本质:sleep是线程运行状态的控制,wait是线程之间的通信
ThreadPoolExecutor()方法参数
构造方法
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){}
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){}
参数说明
int corePoolSize
核心线程池基本大小,核心线程数
1:线程池刚创建时,线程数量为0,当每次执行execute添加新的任务时会在线程池创建一个新的线程,直到
线程数量达到corePoolSize为止。
2:核心线程会一直存活,即使没有任务需要执行,当线程数小于核心线程数时,即使有线程空闲,线程池也
会优先创建新线程处理
3:设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
线程数量达到corePoolSize为止。
2:核心线程会一直存活,即使没有任务需要执行,当线程数小于核心线程数时,即使有线程空闲,线程池也
会优先创建新线程处理
3:设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
int maximumPoolSize
线程池最大线程大小
1:当池中的线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
2:当池中的线程数=maximumPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
2:当池中的线程数=maximumPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
long keepAliveTime
线程空闲后的存活时间
1:当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
2:如果allowCoreThreadTimeout=true,则会直到线程数量=0
2:如果allowCoreThreadTimeout=true,则会直到线程数量=0
TimeUnit unit
线程空闲后的存活单位
可以设置TimeUnit.SECOND
BlockingQueue<Runnable> workQueue
存放任务的阻塞队列
1:当线程池正在运行的线程数量已经达到corePoolSize,那么再通过execute添加新的任务则会被加
workQueue队列中,在队列中排队等待执行,而不会立即执行。
一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
workQueue队列中,在队列中排队等待执行,而不会立即执行。
一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
ThreadFactory threadFactory
创建线程的工厂
线程工厂,主要用来创建线程
RejectedExecutionHandler handler
当阻塞队列和最大线程池都满了之后的饱和策略
1:当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务
2:当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()
和线程池真正shutdown之间提交任务,会拒绝新任务
3:当拒绝处理任务时线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是
AbortPolicy,另外在ThreadPoolExecutor类有几个内部实现类来处理这类情况
2:当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()
和线程池真正shutdown之间提交任务,会拒绝新任务
3:当拒绝处理任务时线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是
AbortPolicy,另外在ThreadPoolExecutor类有几个内部实现类来处理这类情况
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
当加入一个线程时
ThreadLocal
ThreadLocal概念
官方解释:
ThreadLocal
这个类提供线程局部变量,这些变量与其他正常的变量的不同之处在于,每一个访问该变量的线程在其内部都有一个独立的初始化的变量副本;
ThreadLocal实例变量通常采用private static在类中修饰。只要 ThreadLocal 的变量能被访问,并且线程存活,那每个线程都会持有
ThreadLocal变量的副本。当一个线程结束时,它所持有的所有 ThreadLocal 相对的实例副本都可被回收。
简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,
把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal
这个类提供线程局部变量,这些变量与其他正常的变量的不同之处在于,每一个访问该变量的线程在其内部都有一个独立的初始化的变量副本;
ThreadLocal实例变量通常采用private static在类中修饰。只要 ThreadLocal 的变量能被访问,并且线程存活,那每个线程都会持有
ThreadLocal变量的副本。当一个线程结束时,它所持有的所有 ThreadLocal 相对的实例副本都可被回收。
简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,
把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal原理
ThreadLocalMap 中使用 Entry[] 数组来存放对象实例与变量的关系,并且实例对象作为 key,变量作为 value 实现对应关系。
并且这里的 key 采用的是对实例对象的弱引用,(因为我们这里的 key 是对象实例,每个对象实例有自己的生命周期,
这里采用弱引用就可以在不影响对象实例生命周期的情况下对其引用)。
并且这里的 key 采用的是对实例对象的弱引用,(因为我们这里的 key 是对象实例,每个对象实例有自己的生命周期,
这里采用弱引用就可以在不影响对象实例生命周期的情况下对其引用)。
原理图
虚线表示是弱引用。弱引用只要继承WeakReference<T>类即可。所以说,当ThreadLocal对象被GC回收了以后,Entry对象的key就变成null了。这个时候没法访问到 Object Value了。并且最致命的是,Entry持有Object value。所以,value的内存将不会被释放。
因为上述的原因,在ThreadLocal这个类的get()、set()、remove()方法,均有实现回收 key 为 null 的 Entry 的 value所占的内存。所以,为了防止内存泄露(没法访问到的内存),在不会再用ThreadLocal的线程任务末尾,调用一次 上述三个方法的其中一个即可。
因此,可以理解到为什么JDK源码中要把Entry对象,用 弱引用的ThreadLocal对象,设计为key,那是因为要手动编写代码释放ThreadLocalMap中 key为null的Entry对象。
GC什么时候回收弱引用的对象?弱引用对象是存活到下一次垃圾回收发生之前对象。
综上:JVM就会自动回收某些对象将其置为null,从而避免OutOfMemory的错误。弱引用的对象可以被JVM设置为null。我们的代码通过判断key是否为null,从而 手动释放 内存泄露的内存。
因为上述的原因,在ThreadLocal这个类的get()、set()、remove()方法,均有实现回收 key 为 null 的 Entry 的 value所占的内存。所以,为了防止内存泄露(没法访问到的内存),在不会再用ThreadLocal的线程任务末尾,调用一次 上述三个方法的其中一个即可。
因此,可以理解到为什么JDK源码中要把Entry对象,用 弱引用的ThreadLocal对象,设计为key,那是因为要手动编写代码释放ThreadLocalMap中 key为null的Entry对象。
GC什么时候回收弱引用的对象?弱引用对象是存活到下一次垃圾回收发生之前对象。
综上:JVM就会自动回收某些对象将其置为null,从而避免OutOfMemory的错误。弱引用的对象可以被JVM设置为null。我们的代码通过判断key是否为null,从而 手动释放 内存泄露的内存。
为什么要将ThreadLocal设计为弱引用?
答:因为弱引用的对象的生命周期直到下一次垃圾回收之前被回收。弱引用的对象将会被置为null。
我们可以通过判断弱引用对象是否已经为null,来进行相关的操作。在ThreadLocalMap中,如果键ThreadLocal已经被回收,
说明ThreadLocal对象已经为null,所以其对应的值已经无法被访问到。
这个时候,需要及时手动编写代码清理掉这个键值对,防止内存泄露导致的内存溢出
我们可以通过判断弱引用对象是否已经为null,来进行相关的操作。在ThreadLocalMap中,如果键ThreadLocal已经被回收,
说明ThreadLocal对象已经为null,所以其对应的值已经无法被访问到。
这个时候,需要及时手动编写代码清理掉这个键值对,防止内存泄露导致的内存溢出
使用场景
ThreadLocal 的经典使用场景是数据库连接和 session 管理等
解决内存泄漏的解决方案
解决办法:
1,每次使用完ThreadLocal都调用它的remove()方法清除数据,
2,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过
ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
1,每次使用完ThreadLocal都调用它的remove()方法清除数据,
2,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过
ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
synchronized 和 ReentrantLock 区别是什么?
都是可重入锁
“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
说说 synchronized 关键字和 volatile 关键字的区别
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
异常
学成在线异常处理机制
问题
1、上边的代码只要操作不成功仅向用户返回“错误代码:11111,失败信息:操作失败”,无法区别具体的错误信息。
2、service方法在执行过程出现异常在哪捕获?在service中需要都加try/catch,如果在controller也需要添加try/catch,代码冗余严重且不易维护。
解决方案
1、在Service方法中的编码顺序是先校验判断,有问题则抛出具体的异常信息,最后执行具体的业务操作,返回成功信息。
2、在统一异常处理类中去捕获异常,无需controller捕获异常,向用户返回统一规范的响应信息。
解决方案流程
子主题
1,可预知异常:程序员明确知道这里会抛什么异常,throw
2,不可预知异常
我们会用一个map统一收藏
没有收藏的map会统一报一个错
编写自定义异常类型
继承RunTimeException
public class CustomException extends RuntimeException
RunTimeException继承Exception,如果要继承Exception的话还有抛出异常,对代码的侵入性大
编写自定义异常类型的封装
直接CatchException.cast(code)调用
编写统一异常捕获类
@ControllerAdvice
@ExceptionHandler(异常字节码)
@ResponseBody返回json
不可预知异常处理
定义一个immutableMap存储已知的错误
引入谷歌guava依赖
定义一个map,线程安全,构建之后不可变
public static ImmutableMap<Class<? extends Throwable>, Integer> immutableMap;
构建一个builer,该map必须通过构建才能创建
public static ImmutableMap.Builder<Class<? extends Throwable>, Integer> builder = ImmutableMap.builder();
静态方法给builder赋值
static {
builder.put(HttpMessageNotReadableException.class, 200);
builder.put(NullPointerException.class, 201);
}
builder.put(HttpMessageNotReadableException.class, 200);
builder.put(NullPointerException.class, 201);
}
代码
子主题
javaWeb
Mysql
数据库引擎
1,什么是数据库引擎
数据库引擎:用于存储、处理、保护数据的核心服务。当你访问数据库时,不管是手工访问,还是程序访问, 都不是直接读写数据库文件,而是通过数据库引擎去访问数据库文件啊啊
2,数据库引擎有哪些
INNODB
MyISAM
MEMORY
Archive
Archive支持高并发的插入操作,但是本身不是事务安全的。Archive非常适合存储归档数据,如记录日志信息可以使用Archive
3,InnoDB跟MYISAM区别
- InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
- InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
- InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
- Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;
- 锁机制不同: InnoDB 为行级锁,myisam 为表级锁
- mysql5.5.5之后,InnoDB是默认引擎
4,Memory特点
MEMORY是MySQL中一类特殊的存储引擎。它使用存储在内存中的内容来创建表,而且数据全部放在内存中。这些特性与前面的两个很不同。
每个基于MEMORY存储引擎的表实际对应一个磁盘文件。该文件的文件名与表名相同,类型为frm类型。该文件中只存储表的结构。而其数据文件,都是存储在内存中,这样有利于数据的快速处理,提高整个表的效率。值得注意的是,服务器需要有足够的内存来维持MEMORY存储引擎的表的使用。如果不需要了,可以释放内存,甚至删除不需要的表。
MEMORY默认使用哈希索引。速度比使用B型树索引快。当然如果你想用B型树索引,可以在创建索引时指定。
注意,MEMORY用到的很少,因为它是把数据存到内存中,如果内存出现异常就会影响数据。如果重启或者关机,所有数据都会消失。因此,基于MEMORY的表的生命周期很短,一般是一次性的。
每个基于MEMORY存储引擎的表实际对应一个磁盘文件。该文件的文件名与表名相同,类型为frm类型。该文件中只存储表的结构。而其数据文件,都是存储在内存中,这样有利于数据的快速处理,提高整个表的效率。值得注意的是,服务器需要有足够的内存来维持MEMORY存储引擎的表的使用。如果不需要了,可以释放内存,甚至删除不需要的表。
MEMORY默认使用哈希索引。速度比使用B型树索引快。当然如果你想用B型树索引,可以在创建索引时指定。
注意,MEMORY用到的很少,因为它是把数据存到内存中,如果内存出现异常就会影响数据。如果重启或者关机,所有数据都会消失。因此,基于MEMORY的表的生命周期很短,一般是一次性的。
5,谈谈你对Innodb的认识
特点是:
1、具有较好的事务支持:支持4个事务隔离级别,支持多版本读
2、行级锁定:通过索引实现,全表扫描仍然会是表锁,注意间隙锁的影响
3、读写阻塞与事务隔离级别相关
4、具有非常高效的缓存特性:能缓存索引,也能缓存数据
5、整个表和主键以Cluster方式存储,组成一颗平衡树
6、所有Secondary Index都会保存主键信息
适用场景:
1、需要事务支持(具有较好的事务特性)
2、行级锁定对高并发有很好的适应能力,但需要确保查询是通过索引完成
3、数据更新较为频繁的场景
4、数据一致性要求较高
5、硬件设备内存较大,可以利用InnoDB较好的缓存能力来提高内存利用率,尽可能减少磁盘IO
1、具有较好的事务支持:支持4个事务隔离级别,支持多版本读
2、行级锁定:通过索引实现,全表扫描仍然会是表锁,注意间隙锁的影响
3、读写阻塞与事务隔离级别相关
4、具有非常高效的缓存特性:能缓存索引,也能缓存数据
5、整个表和主键以Cluster方式存储,组成一颗平衡树
6、所有Secondary Index都会保存主键信息
适用场景:
1、需要事务支持(具有较好的事务特性)
2、行级锁定对高并发有很好的适应能力,但需要确保查询是通过索引完成
3、数据更新较为频繁的场景
4、数据一致性要求较高
5、硬件设备内存较大,可以利用InnoDB较好的缓存能力来提高内存利用率,尽可能减少磁盘IO
子主题
框架
spring
为什么要使用spring
spring提供ioc容器,容器会帮你管理依赖的对象,而不需要自己管理跟创建,实现了解耦
spring 提供了事务支持,更方便的操作事务
spring提供面向切面编程,可以使程序在运行期间增加
spring能够方便的集成市面上许多优秀的框架
Spring 框架中都用到了哪些设计模式?
工厂模式
BeanFactory工厂,用来创建对象
单例模式
bean默认为单例模式
代理模式
spring AOP 用的jdk动态代理跟cglib代理模式
观察者模式
定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新
模板方法
用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
什么是Spring IOC 容器?
IOC就是控制反转,是指创建对象的控制权的转移,以前创建对象的主动权和时机是由自己把控的,而现在这种权力转移到Spring容器中,并由容器根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。
DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部资源。
(2)最直观的表达就是,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。
(3)Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。
Spring的容器创建对象有三种方式
无参构造
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
工厂静态方法实例化
工厂类中有一个静态方法,返回一个对象
<bean id="userDao" class="com.itheima.factory.StaticFactoryBean" factory-method="createUserDao" />
工厂实例方法实例化
工厂中有一个实例方法,返回一个对象
因为不是静态方法,所以要先把工厂添加到ioc容器,再声明工厂方法
<bean id="factoryBean" class="com.itheima.factory.DynamicFactoryBean"/>
<bean id="userDao" factory-bean="factoryBean" factory-method="createUserDao"/>
<bean id="userDao" factory-bean="factoryBean" factory-method="createUserDao"/>
Spring的AOP理解
aop 是面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),
减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。
减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。
作用:在程序运行期间,不修改源码对已有方法进行增强。可以减少重复代码,提高开发效率以及维护方便!
底层实现:
在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
JDK 代理 : 基于接口的动态代理技术
Invocationhandler
JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
cglib 代理:基于父类的动态代理技术
new MethodInterceptor
如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
重要概念
joinpoint(连接点)
目标对象的所有方法
pointcut(切入点)
连接点中需要被增强的方法
advice(通知/增强)
封装增强的业务
Aspect(切面)
切点加通知
weaving(织入)
将切点与通知结合
Spring切面可以应用5种类型的通知?
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning ):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。
Spring支持的几种bean的作用域
singleton
bean在ioc容器中只有一个实例,初始化容器时创建
prototype
bean在ioc容器中有多个实例,getBean()时初始化
request
每次http请求创建一个实例
session
在一个HTTP Session中,一个bean定义对应一个实例
global-session
在一个全局的HTTP Session中,一个bean定义对应一个实例
spring事务机制
编程式事务
提供编码的形式管理和维护事务。(transtoin)
PlatformTransactionManger API
声明式事务
声明式事务也有两种实现方式,基于 xml 配置文件的方式和注解方式(在类上添加 @Transaction 注解)。
实现原理aop,动态代理
spring事务
spring事务隔离级别
spring 有五大隔离级别,默认值为 ISOLATION_DEFAULT(使用数据库的设置),其他四个隔离级别和数据库的隔离级别一致:
ISOLATION_DEFAULT:用底层数据库的设置隔离级别,数据库设置的是什么我就用什么;
ISOLATION_READ_UNCOMMITTED:读未提交,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读);
ISOLATION_READ_COMMITTED:读已提交,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读),SQL server 的默认级别;
ISOLATION_REPEATABLE_READ:可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读),MySQL 的默认级别;
ISOLATION_SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。
脏读,幻读,不可重复读
脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。
不可重复读 :是指在一个事务内,多次读同一数据。(一个事务中两次读取的数据的内容不一致就叫做不可重复读)
幻读(虚读) :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。
事务的传播行为
① PROPAGATION_REQUIRED(required):如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
② PROPAGATION_SUPPORTS(supports):支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
③ PROPAGATION_MANDATORY(mandatory):支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
springmvc
mybatis
springboot
springcloud
中间件
redis
1,什么是Redis?
Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。
Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
redis的基本数据结构
string字符串
做简单的键值对缓存,短信验证码
set key value
list列表
存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据
lpush name value
Hash散列
结构化的数据,比如一个对象
hmset name key1 value1 key2 value2
我们的项目,购物车,以用户名为大key,商品id为小key,具体商品为value
set集合
交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集
sadd name value
zset有序集合
去重但可以排序,如获取排名前几名的用户
zadd name score value
Redis 有哪些功能?
数据缓存功能
分布式锁的功能
支持数据持久化
支持事务
支持消息队列
Redis 的持久化机制是什么?各自的优缺点?
RDB(redis Database)是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。
通过配置文件中的save参数来定义快照的周期。
通过配置文件中的save参数来定义快照的周期。
AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
- AOF文件比RDB更新频率高,优先使用AOF还原数据。
- AOF比RDB更安全也更大
- RDB性能比AOF好
- 如果两个都配了优先加载AOF
缓存雪崩,缓存穿透,缓存击穿
缓存雪崩
redis许多key在同一时间到期,导致许多请求因为redis查询不到数据,而去数据库查询,造成数据库宕机
解决方案:
1,可以给热门的key设置不同的过期时间,避免同一时间过期
1,可以给热门的key设置不同的过期时间,避免同一时间过期
缓存穿透
恶意查询数据库跟缓存中一定不存在的值,导致数据库宕机
解决方案:
1,该请求在数据库一定查询不到数据,我们给他缓存一个null值,并且设置一个过期时间
2,可以采用bloom过滤器,或者用一个bigmap存放数据(商品id)
1,该请求在数据库一定查询不到数据,我们给他缓存一个null值,并且设置一个过期时间
2,可以采用bloom过滤器,或者用一个bigmap存放数据(商品id)
缓存击穿
redis中有一条热门数据刚好到期,恰巧有这个时候有很多请求访问这个数据,造成数据库宕机
解决方案:
1,设置热点数据永远不过期。,
2,加互斥锁,加锁就能防止一次只能一个请求访问数据库
1,设置热点数据永远不过期。,
2,加互斥锁,加锁就能防止一次只能一个请求访问数据库
使用Redis做过异步队列吗,是如何实现的?
使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。
redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
怎么保证缓存和数据库数据的一致性?
合理设置缓存的过期时间。
新增、更改、删除数据库操作时同步更新 Redis,可以使用事物机制来保证数据的一致性。
先写入数据库,然后把修改的数据通过MQ,或者task任务,或者canal监控数据库发送mq,然后修改redsi,实现数据最终一致性
我的想法:先删除缓存,再更新数据库,再删除缓存
Redis与Memcached的区别
存储方式不同:memcache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小;Redis 有部份存在硬盘上,这样能保证数据的持久性。
数据支持类型:memcache 对数据类型支持相对简单;Redis 有复杂的数据类型。
使用底层模型不同:它们之间底层实现方式,以及与客户端之间通信的应用协议不一样,Redis 自己构建了 vm 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
value 值大小不同:Redis 最大可以达到 512mb;memcache 只有 1mb。
如何使用 Redis 实现分布式锁?
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); 返回OK
- 第一个为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的过期时间。
redisTemplate.opsForValue().setIfAbsent()
rabbitMq
RabbitMQ 的使用场景有哪些?
RabbitMQ是一款开源的,Erlang编写的,基于AMQP协议的消息中间件
应用解耦
异步传输
削峰填谷
RabbitMq的工作模式
简单模式(simple)
一个生产者对应一个消费者,可以不需要交换机
工作模式(work)
一个生产者对应多个消费者,消息只能被消费一次,所以是竞争关系,可以不需要交换机
发布与订阅模式(publish/subscribe)
一个消费者将消息首先发送到交换器(类型为fanout),
交换器绑定多个队列,然后被监听该队列的消费者所接收并消费
交换器绑定多个队列,然后被监听该队列的消费者所接收并消费
路由模式(routing)
生产者将消息发送到direct交换器,在绑定队列和交换器的时候有一个路由key, 生产者发送的消息会指定一个路由key,
那么消息只会发送到相应key相同的队列,接着监听该队列的消费者消费信息.
那么消息只会发送到相应key相同的队列,接着监听该队列的消费者消费信息.
通配符模式(Topices)
*代表一个单词,#代表一个或多个单词
交换机(topic)根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消费
交换机(topic)根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消费
如何保证RabbitMQ消息的可靠传输?
生产者丢失数据
事务机制
发送信息前开启事务,发送异常就回滚事务,发送成功就提交事务,吞吐量下降,不推荐
confirm机制
confirm确认模式
消息从 producer 到 exchange 则会返回一个 confirmCallback 。
return退回模式
消息从 exchange 到 queue 投递失败则会返回一个 returnCallback 。
总结
对于确认模式:
设置ConnectionFactory的publisher-confirms="true" 开启 确认模式。
使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
对于退回模式
设置ConnectionFactory的publisher-returns="true" 开启 退回模式。
使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。
设置ConnectionFactory的publisher-confirms="true" 开启 确认模式。
使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
对于退回模式
设置ConnectionFactory的publisher-returns="true" 开启 退回模式。
使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。
消息队列丢失数据
消息持久化
这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号
将queue的持久化标识durable设置为true,则代表是一个持久的队列
发送消息的时候将deliveryMode=2
消费者丢失数据
设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
公司常用的消息补偿机制,确保信息一定被消费
流程图
一句话:发送方存储消息到DB,消费者消费完也存到DB,在检测两个数据库的信息是否一致
1,消费者存到DB1之后,发送一条消息到Q1队列,然后延迟发送一条信息到Q3队列,一模一样
2,如果消费者消费成功,就会发送一条消费成功的信息给Q2被回调检测服务消费并且存到数据库DB2
3,同时回调检测服务也会消费生产者发送的延迟消息,然后去DB2中查找,如果找到了,就代表被消费了
4,如果没找到就通知生产者重新发送信息
5,如果生产方两条信息都没有发送出去,则还有定时任务定时检测比较两个数据库,将DB1多的重新发送
如果延迟信息被回调检查服务接收之后才接收到了消费成功的信息,这样就会造成消费重复发送,当前这里只保证
消息的100%发送,另外延迟消息可以等待1分钟之后再发送,如果消费者一分钟之后还没消费,可能出错了
消息的100%发送,另外延迟消息可以等待1分钟之后再发送,如果消费者一分钟之后还没消费,可能出错了
消息的幂等性保证?
概念:消费多条相同的信息,得到与消费一条的消息的结果相同
数据库乐观锁的机制
第一次执行version=1,保存到数据库之后就version = 2
如果再来一条version=1的数据,会找不到,修改失败
所以不管有多少条相同的数据,我们都不用担心
elasticSearch
项目
传智健康
畅购商城
12,分布式事务
1,什么是事务
ACID
1,原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么
都不执行。
2,一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态是
指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态不能
被观察到(这层语义也有说应该属于原子性)。转账例子,钱守恒,几个并行的事务最终的执行结果与串行的执行结果一样
3,隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行,如同只有这一个
操作在被数据库所执行一样。
4,持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操
作将不可逆转。
都不执行。
2,一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态是
指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态不能
被观察到(这层语义也有说应该属于原子性)。转账例子,钱守恒,几个并行的事务最终的执行结果与串行的执行结果一样
3,隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行,如同只有这一个
操作在被数据库所执行一样。
4,持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操
作将不可逆转。
数据库事务
要么一起成功,要么一起失败
原子性
2,分布式事务有什么
不同服务,每个服务都有自己的分支事务
分布式事务要求,可以统一管理各个服务的分支事务,一起成功,一起失败
3,CAP了解
一致性(Consistency )
数据节点之间,数据状态保持一致(所有数据服务器数据一样)
可用性(Availability)
系统中,不会因为某个节点出现问题,导致整个系统的不可以
分区容错性(Partition tolerance)
系统设计兼容网络传输的不可靠因素
P一定有,CA只能取其一
4,BASE理论
基本可以
允许短暂超时
软状态
允许存在一个中间状态
最终一致性
不能保证每个节点每时每刻的数据都一样,允许短暂的延迟之后保持一致性
比如Eureka,他每个节点的数据,是采用的最终一致性设计方案
5,分布式事务解决方案
二阶段提交
第一阶段TM(事务管理器)要求所有的RM准备提交对应的事务分支,询问RM(资源管理器)是否有能力保证成功的提交事务分支,
RM根据自己的情况,如果判断自己进行的工作可以被提交,那就对工作内容进行持久化,并给TM回执
OK;否者给TM的回执NO。RM在发送了否定答复并回滚了已经完成的工作后,就可以丢弃这个事务分
支信息了。
第二阶段TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare
成功,那么TM通知所有的RM进行提交;如果有RM prepare回执NO的话,则TM通知所有RM回滚自己
的事务分支。
也就是TM与RM之间是通过两阶段提 交协议进行交互的.
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强
一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
RM根据自己的情况,如果判断自己进行的工作可以被提交,那就对工作内容进行持久化,并给TM回执
OK;否者给TM的回执NO。RM在发送了否定答复并回滚了已经完成的工作后,就可以丢弃这个事务分
支信息了。
第二阶段TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare
成功,那么TM通知所有的RM进行提交;如果有RM prepare回执NO的话,则TM通知所有RM回滚自己
的事务分支。
也就是TM与RM之间是通过两阶段提 交协议进行交互的.
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强
一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
mysql数据库事务
准备阶段
提交/回滚阶段
三阶段提交
询问阶段:在最开始有个询问阶段,问下你能不能执行任务
准备阶段
提交阶段
TCC补偿机制
基于业务操作,实现事务的一致性
try阶段
做具体操作,锁定资源
try阶段主要是对业务系统做检测及资源预留
cancel阶段
回滚操作,释放资源
confirm阶段
释放资源
阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认
Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Seata(fescar)
角色
TC事务协调者
全局事务管理
TM事务管理器
发起决策
决议
RM 控制分支事务
参与者
首先是我们的业务服务,TM会开启一个全局事务,并且会注册到TC进行统一管理
并且开启一个全局锁
全局事务会随着调用链路进行传播
各个参与者,会将自身分支事务注册到TC,然后被全局事务统一管理
每个参与者会将本地数据库事务进行操作
6,基于消息队列解决分布式事务最终一致性
业务服务——生产者
业务数据操作
记录消息任务到数据库
定时任务
每隔几秒扫描任务表
发送信息
消费者
消费信息
查询redis是否存在当前消息记录
如果存在,则代表当前信息正在被处理
不存在则继续
redis用来防止重复消费
查询数据库消费处理日志表
如果存在,直接返回
不存在则继续
业务处理
设置消息数据到redis 注意设置超时时间(前面判断)
做具体业务数据变更
增加日志表(前面判断)
移除redis
发送信息处理成功的消息
生产者
接收消费者消费成功的信息
移除消息任务数据,并且添加到消息历史表
13,微信支付
微信支付快速入门
企业认证
appid
mchId
key
开发 SDK
com.github.wxpay.sdk.WXPay
public class MyConfig extends WXPayConfig
自定义配置类实现微信config,封装上面三个参数
QRCode.js 是一个用于生成二维码的 JavaScript 库
用户支付成功,微信会异步回调我的结果,回调地址是我们请求是时封装的回调url
微信支付二维码
下单接口生成订单
下单之后就生成订单,给前台响应下单成功
同时前端页面会跳转支付页面
用户选择微信支付,请求支付服务
支付服务请求统一下单接口
响应支付二维码
支付回调逻辑处理
方便我们开发测试微信支付成功回调我们的服务
内网穿透工具
echosite
调用成功,微信支付返回回调url
微信返回一个流,我们通过request.getInputStream()获取流再通过工具类转换为String
//输入流转换为xml字符串
String xml = ConvertUtils.convertToString( request.getInputStream() );
System.out.println(xml);
//给微信支付一个成功的响应
response.setContentType("text/xml");
String data = "<xml><return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg></xml>";
response.getWriter().write(data);
String xml = ConvertUtils.convertToString( request.getInputStream() );
System.out.println(xml);
//给微信支付一个成功的响应
response.setContentType("text/xml");
String data = "<xml><return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg></xml>";
response.getWriter().write(data);
回调具体逻辑
如果回调成功
调用微信查询订单接口再次查询(我们不用他们返回的)
如果我们自己查询也是支付成功
那就发送信息到MQ给我们的订单服务修改订单状态
微信订单流水号transationId
我们的订单号
发送信息到MQ给前端发送信息(webSocket)
推送支付通知
webSocket协议
浏览器与服务器之间的全双工通信
tcp长连接
子主题
Web STOMP 插件
服务器与客户端之间rabbitmq通信
14,订单处理
超时未支付订单处理
需求分析
超过限定时间并未支付的订单,我们需要进行超时订单的处理:先调用微信支付api,查询该订单的支
付状态。如果未支付调用关闭订单的api,并修改订单状态为已关闭,并回滚库存数。如果该订单已经
支付,则做补偿操作(修改订单状态和记录)。
付状态。如果未支付调用关闭订单的api,并修改订单状态为已关闭,并回滚库存数。如果该订单已经
支付,则做补偿操作(修改订单状态和记录)。
业务处理
用户确认下单后,我们生成了订单,扣减了库存
下单之后,我们存储一个超时任务
如果用户一直不支付,到了订单超时时间
则订单取消,回滚库存,记录订单日志
延迟处理技术方案
Rabbit死信队列
通过死信实现延迟消费
流程细节
订单服务下单成功发送延迟信息到MQ
订单服务进行延迟消费消息监听
查询我们自己的订单状态
查询微信订单支付结果
关闭订单,调用微信关闭订单的接口
具体代码
查询微信支付查询订单接口
支付成功补偿
如果我们数据库显示未支付,但是查询微信接口是已支付,我们就要修改订单状态
更新订单日志表
如果是未支付
需要关闭订单
然后增加库存跟减少销量(用户积分是要确认收货之后才增加积分)
通过订单id查询所有的orderItem
通过遍历集合,逐个远程调用恢复库存(建议异步处理)
修改订单日志表,已关闭状态
订单批量发货
是我们后台管理系统跟物流系统的对接,生成物流公司名单,发货单号,然后批量提交到后台,今天订单发货状态统一更新
三个for循环
参数校验
判断运单号和物流公司是否为空
状态检验
订单支付状态要为已支付
循环订单更新
确认收货,自动收货
确认收货
当物流公司将货物送到了用户收货地址之后,需要用户点击确认收货,
当用户点击了确认收货之后,会修改订单状态为已完成
当用户点击了确认收货之后,会修改订单状态为已完成
自动收货
如果用户在15天(可以在订单配置表中配置)没有确认收货,系统将自动收货。
如何实现?我们这里采用定时任务springTask来实现.
如何实现?我们这里采用定时任务springTask来实现.
自动收货流程
在任务服务每天凌晨查询超时的订单,发送到mq,随便发送一个,只要提醒订单服务运行方法就行
订单服务方法流程如下
1)从订单配置表中获取订单自动确认期限
2)得到当前日期向前数(订单自动确认期限)天。作为过期时间节点
3)从订单表中获取过期订单(发货时间小于过期时间,且为未确认收货状态)
4)循环批量处理,执行确认收货
2)得到当前日期向前数(订单自动确认期限)天。作为过期时间节点
3)从订单表中获取过期订单(发货时间小于过期时间,且为未确认收货状态)
4)循环批量处理,执行确认收货
冲吧
第一天
延迟队列单机实现
Timer定时器
DelayQueue
两种方案万一程序出错了怎么办?
RabbitMq实现延迟队列
第二天
大致设计流程
子主题
服务启动初始化
@PostContruct
查询数据库任务
清除缓存
cacheService.scan或者keys通配符查询key
当前队列
未来队列
重新添加到缓存
根据task的执行时间判断是否需要添加到那个队列
按照执行进行保存到具体 哪个队列的
按任务类型+优先级进行Key的确定
定时任务
定时把未来队列的任务刷到当前队列来
先按scan或keys查询所有的未来队列,得到一个集合
遍历集合,通过分数按范围查询0-Sysytem.currentTimeMillis,得到要刷到当前消费队列的集合
然后存到redis
获取任务
根据任务类型跟优先级
获取任务数量
当前队列的长度
未来队列的长度
拉取任务
直接从list中pop
延迟任务系统实现
添加任务
添加任务到数据库 Task TaskLog
判断task.getExecuteTime() <= System.currentTimeMillis()
如果当前时间到了执行时间,则存到消费队列 Topic_ list存储
如果当前时间没到执行时间,则存储到未来消费队列,Future_ zset存储
取消任务
mysql移除task任务数据,修改TaskLog为已执行状态
判断执行时间确定要删除的任务位于哪个队列
如果在消费队列 LRemove
如果在未来队列 ZRemove
获取任务
size 用于判断队列中是否存在任务,有则拉取
poll用于获取list(当前队列)中的准备就绪的任务
初始化同步任务mysql到redis
scan
定时任务 把 未来任务刷到 当前任务
管道技术
第三天
第四天
第五天
第六天
第七天
0 条评论
下一页