Java常见问题-汇总
一、面试到底在问些什么东西?
首先你要知道,面试官的提问和你简历上写的内容是紧密联系的,所以你简历上写的技能一定要会。
一般面试包括下面几方面知识类型:
Java基础、多线程、IO与NIO、虚拟机、设计模式
数据结构与算法(要有手写算法的能力)
计算机网络(TCP三次握手和四次挥手)
数据通信(RESTful、RPC、消息队列)
操作系统(Linux的基本命令以及使用)
主流框架(Spring底层原理与源码问的很多)
数据存储(最常见的是MySQL、Redis)
分布式
其他问题:
实际场景题
生活方面的问题
性格/其他方面的问题
二、面试常问的知识点
1)集合相关问题(必问)
说说常见的集合有哪些吧?
Map接口和Collection接口是所有集合框架的父接口:
- Collection接口的子接口包括:Set接口和List接口
- Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
- Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
HashMap、LinkedHashMap、ConcurrentHashMap、ArrayList、LinkedList的底层实现
问这个问题的自己心里没点逼数吗。。。
HashMap和Hashtable(已过时)的区别
- HashMap不是线程安全的,效率高;而HashTable使用了synchronized关键字,是线程安全的,效率低
- HashMap中允许存在null键和null值;而HashTable中不允许,遇到null,直接返回 NullPointerException
- HashMap继承自AbstractMap类,AbstractMap基于Map接口的实现;而Hashtable继承自Dictionary类;
- HashMap中hash数组的默认大小是16,且一定是2的指数;Hashtable中hash数组默认大小是11,扩充方式是old*2+1
HashMap是怎么解决哈希冲突的?
- 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
- 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
- 引入红黑树进一步降低遍历的时间复杂度,使得遍历更
ArrayList、LinkedList、Vector(已过时)的区别
- ArrayList: 内部采用数组存储元素,支持高效随机访问,支持动态调整大小
- LinkedList: 内部采用链表来存储元素,支持快速插入/删除元素,但不支持高效地随机访问
- Vector: 可以看作线程安全版的ArrayList
HashTable(已过时)和ConcurrentHashMap的区别
- ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。
- HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。
- ConcurrentHashMap 锁的方式是稍微细粒度的。
HashMap和ConcurrentHashMap的区别
HashMap不是线程安全的,而ConcurrentHashMap兼顾了线程安全和效率的问题
除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有null,但是ConCurrentHashMap 都不允许。
分析问题:HashTable(已过时)使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;
解决方案:ConcurrentHashMap把数据分段,执行分段锁(分离锁),核心把锁的范围变小,这样出现并发冲突的概率就变小,在保存的时候,计算所存储的数据是属于哪一段,只锁当前这一段
- JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。
- JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry)。锁粒度降低了。
注意:分段锁(分离锁)是JDK1.8之前的一种的方案,JDK1.8之后做了优化。
HashMap和LinkedHashMap、TreeMap的区别
- LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;
- TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)
使用场景:ConcurrentHashMap 线程安全,LinkedHashMap 可以记录插入顺序和访问顺序,TreeMap 可以自定义排序,一般场景基本都可以使用 HashMap
- HashMap:在 Map 中插入、删除和定位元素时;
- LinkedHashMap:在需要输出和输入的顺序相同的情况下;
- TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下。
ConcurrentHashMap是怎么实现线程安全的
JDK 1.8 以前通过分段锁,JDK 1.8 以后通过 CAS + Synchronized
ConcurrentHashMap 在 JDK 1.8 前后的锁有什么区别?
JDK 1.8 以前锁分段,JDK 1.8 以后锁单个节点,锁粒度降低,并发度变高
JDK 1.7 中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现。
采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。
①、Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;
②、HashEntry 用来封装映射表的键-值对;
③、每个桶是由若干个 HashEntry 对象链接起来的链表。
JDK 1.8 中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现。直接用 table 数组存储键值对;
当 HashEntry 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。
HashMap 的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
2)多线程并发相关问题(必问)
创建线程的4种方式
- 继承Thread类创建线程类(不可继承其它类;使用this即可获得当前线程)
- 通过Runnable接口创建线程类(可继承其它类;必须使用Thread.currentThread()方法获取当前线程)
- 通过ExecutorService和Callable创建线程(可继承其它类,有返回值,call方法可抛异常;必须使用Thread.currentThread()方法获取当前线程)
- 基于线程池的execute(),创建临时线程
什么是线程安全
线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
如何实现线程安全
存在线程安全问题必须满足三个条件:
- 有共享变量
- 处在多线程环境下
- 共享变量有修改操作
解决方案:https://www.cnblogs.com/mjtabu/p/12853634.html
- 线程封闭(把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。)
- 无状态的类(没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。无状态就是一次操作,不能保存数据。)
- 让类不可变(1、加 final 关键字 2、根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值。)
- volatile(并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的情景。)
- 加锁和CAS(我们最常使用的保证线程安全的手段,使用 synchronized 关键字,使用显式锁,使用各种原子变量,修改数据时使用 CAS 机制等等。)
- 安全的发布(类中持有的成员变量,如果是基本类型,发布出去,并没有关系,因为发布出去的其实是这个变量的一个副本.)
- ThreadLocal(ThreadLocal 是实现线程封闭的最好方法。)
要考虑线程安全问题,就需要先考虑Java并发的三大基本特性:原子性、可见性以及有序性。
原子性:指在一个操作中就是cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行的顺序按照代码的先后顺序执行,在多线程编程时就得考虑这个问题。
所谓解决线程安全问题无非就是将操作原子化,正如加sychronized,或者加lock什么的,只要将操作原子化就能避免线程安全的问题。但加锁会有性能问题。
所以在多线程情况下,优先考虑能否不用共享,优先使用局部变量代替共享的全局变量。
只能用共享变量的时候优先使用原子类,诸如AtomicInteger尔尔。没有原子类,可以自己创造自己的原子类。
如以上方法都不能奏效,再考虑使用sychronized,lock之类尔尔。别一上来就用sychronized,不仅low,而且显的不够专业。
分布式环境下,怎么保证线程安全
对于分布式系统,保证一台服务器的同步,并不能保证访问共享资源是同步的;
所以可以考虑使用分布式锁的方式来保证分布式中的线程的安全线,这样不同的服务不同的线程通过竞争分布式锁来获取共享资源的操作权限;
例如redis的分布式锁、zookeeper锁,都可以作为分布式线程安全的手段。
对于商城一类系统中,单点登录、购物车、订单这些都有并发。
用AtomicInteger、synchronized、Lock、ThreadLocal等类来保证在代码层面上的线程安全;如果是功能上需要自主多线程处理,那么也会使用线程池ThreadPool来提高并发效率。
对高并发的处理会使用Redis的分布式锁(setnx),将对于服务器的承载力达到一定数量后,之后的请求全部加入队列处理。
多线程概念理解
通常多线程的应用不是为了提高运行效率,而是为了提高资源使用效率。比如你的应用程序需要访问网络,因为网络有延时,如果在界面线程访问,那么在网络访问期间界面将无法响应用户消息,这是就应该使用多线程。
多线程编程的目的,就是"最大限度地利用CPU资源",当某一线程的处理不需要占用CPU而只和I/O,OEMBIOS等资源打交道时,让需要占用CPU资源的其它线程有机会获得CPU资源。每个程序执行时都会产生一个进程,而每一个进程至少要有一个主线程。这个线程其实是进程执行的一条线索,除了主线程外你还可以给进程增加其它的线程,也即增加其它的执行线索,由此在某种程度上可以看成是给一个应用程序增加了多任务功能。
线程切换是有代价的,因此如果采用多进程,那么就需要将线程所隶属的该进程所需要的内存进行切换,这时间代价是很多的。而线程切换代价就很少,线程是可以共享内存的。所以采用多线程在切换上花费的比多进程少得多。但是,线程切换还是需要时间消耗的,所以采用一个拥有两个线程的进程执行所需要的时间比一个线程的进程执行两次所需要的时间要多一些。
即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间。上述结果只是针对单CPU,如果对于多CPU或者CPU采用超线程技术的话,采用多线程技术还是会提高程序的执行速度的。因为单线程只会映射到一个CPU上,而多线程会映射到多个CPU上,超线程技术本质是多线程硬件化,所以也会加快程序的执行速度。
使用线程的好处有以下几点: ·
- 使用线程可以把占据长时间的程序中的任务放到后台去处理,比如批量发短信(发送中-发送完成时再通知结果)
- 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源如内存占用等等。
- 程序的运行速度可能加快,在多CPU情况下,会提高程序执行速度。
- 还有其他很多使用多线程的好处,这里就不一一说明了。一些线程模型的背景
wait方法和sleep方法的区别
最大的不同是线程在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间交互,sleep 通常被用于暂停执行。
synchronized、Lock、ReentrantLock、ReadWriteLock
Synchronized :是JVM实现的一种锁, 用于同步方法和代码块,执行完(或发生异常)后自动释放锁。(多个线程同步执行,效率非常低下)
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁。
如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
Lock:是一个锁的接口,提供获取锁和解锁的方法(lock,trylock,unlock)。如果锁已被其他线程获取,则进行等待。(大量线程同时竞争时,Lock的性能要远远优于synchronized)
如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
Lock和synchronized最大的区别就是当使用synchronized,一个线程抢占到锁资源,其他线程只能干巴巴地等待;而使用Lock,一个线程抢占到锁资源,其他的线程可以不等待或者设置等待时间,实在抢不到可以去做其他的业务逻辑。
ReentrantLock:(重入锁),也是唯一个实现了Lock接口的类,基于AQS实现的,并且提供了更多的方法。
作用和synchronize是一样的,都是实现锁的功能,但是ReentrantLock需要手写代码对锁进行获取和释放(一定要在finally里面释放),要不然就永远死锁了。ReentrantLock也可以用来做线程之间的挂起和通知,synchronize一般是使用object的wait和notify来实现,ReentrantLock使用Condition来实现线程之间的通信。
ReadWriteLock:(读写锁),也是个接口,提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁,他的一个实现类是ReentrantReadWriteLock。(多个读线程并发执行,效率非常高)
其核心就是实现读写分离的锁,在高并发访问下,尤其是读多写少的情况下,性能要远高于重入锁。Synchronized和ReentrantLock,在同一时间内,只能有一个线程进行访问被锁定的代码,那么读写锁则不同,其本质是分成两个锁,即读锁、写锁。在在读锁的时候,多个线程可以并发的进行访问,但是在写锁的时候,只能一个一个的按顺序访问(write是排它锁)。(口诀:读读共享、写写互斥、读写互斥)
volatile关键字的作用和原理
volatile可以看成是synchronized的一种轻量级的实现,但volatile并不能完全代替synchronized,volatile有synchronized可见性的特性,但没有synchronized原子性的特性。
可见性即用volatile关键字修饰的成员变量表明该变量不存在工作线程的副本,线程每次直接都从主内存中读取,每次读取的都是最新的值,这也就保证了变量对其他线程的可见性。
另外,使用volatile还能确保变量不能被重排序,保证了有序性。
当一个变量定义为volatile之后,它将具备两种特性:
- 保证此变量对所有线程的可见性
- 禁止指令重排序优化
synchronized和lock区别以及volatile和synchronized的区别
lock和synchronized区别:
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。
- 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized
volatile与synchronized的区别:
- volatile只能修饰实例变量和类变量;而synchronized可以修饰方法,以及代码块。
- volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;而synchronized可能会造成线程的阻塞。
- 从内存可见性角度,volatile读相当于加锁,volatile写相当于解锁;
- volatile只能保证可见性,无法保证原子性;而synchronized既能够保证可见性,又能保证原子性。
- volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。
- volatile并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的情景。
介绍下CAS(无锁技术),什么是悲观锁和乐观锁
我们在Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。
2. 悲观锁是数据库层面加锁,都会阻塞去等待锁。例如在select ... for update前加个事务就可以防止更新丢失。
乐观锁优点程序实现,不会存在死锁等问题。他的适用场景也相对乐观。阻止不了除了程序之外的数据库操作。
悲观锁是数据库实现,他阻止数据库写入操作。
再来说更新数据丢失,所有的读锁都是为了保持数据一致性。乐观锁如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户重新操作。悲观锁则会等待前一个更新完成。这也是区别。具体业务具体分析。
什么是ThreadLocal
ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
最典型的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。
创建线程池的5种方式
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。
一、通过Executors类提供的4种方法:
- newCachedThreadPool(创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。)
- newFixedThreadPool(创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。)
- newScheduledThreadPool(创建一个定长线程池,支持定时及周期性任务执行。)
- newSingleThreadExecutor(创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。)
二、通过ThreadPoolExecutor类自定义:
ThreadPoolExecutor类提供了4种构造方法,可根据需要来自定义一个线程池。
如何使用线程池:
线程池使用有很多种方式,不过按照《Java 开发手册》描述,尽量还是要使用 ThreadPoolExecutor
进行创建。
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让我们更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM(内存溢出)。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM(内存溢出)。
池化思想在计算机的应用也比较广泛,比如以下这些:
- 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
- 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
- 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。
线程池的优势主要体现在以下 4 点:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:可以控制线程数,有效的提升服务器的使用资源,避免由于资源不足而发生宕机等问题。
- 提供更多更强大的功能:比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
ThreadPoolExecutor的内部工作原理及相关参数
主要参数为:核心线程数、阻塞队列、最大线程数、拒绝策略。
3)分布式并发问题
消息与消息队列
消息(Message)是指应用于应用之间传送的数据,消息的类型包括文本字符串、JSON、XML、内嵌对象等等...
所谓 消息中间件 / 消息队列(Message Queue Middleware,简称MQ)是利用高效可靠的消息传递机制进行数据交流,同时可以基于数据通信来进行分布式系统的继承,消息中间件一般有两种传递模式:点对点(Point-to-Point)模式和发布/订阅(Pub/Sub)模式,点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息,队列的存在使得消息的异步传输成为了可能,发布订阅模式定义了如何向一个内容节点发布和订阅内容,这个内容节点叫topic,这种模式可以满足消费者发布一个消息,多个消费者同时消费同一信息的需求。
一般来说,消息队列是一种异步的服务间通信方式,是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。使用较多的消息队列有ActiveMQ、RocketMQ、RabbitMQ、Kafka等。
RabbitMQ是一个开元基于 erlang 语言开发具有高可用高并发的优点,适合集群消息代理和队列服务器,它是基于AMQP协议来实现的,AMQP的和主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全,RabbitMQ支持多种语言,有消息确认机制和持久化机制,保证数据不丢失的前提做到可靠性、可用性。
什么是AMQP协议?
AMQP的全称:Advanced Message Queuing Protocol(高级消息队列协议),它是消息队列的一个规范,其中定义个很多核心的概念,AMQP与JMS(Java Message Service)Java平台的专业技术规范类似,同样提供了很多面向中间件的API,用于两个应用程序之间,或者分布式系统之间的发送消息,进行异步通信。
AMQP的协议模型
解释:Producer(生产者)将信息投递到Server端的RabbitMQ的Exchange中(过程:message->server->virtual host->RabbitMQ->Exchange),Consumer(消费者)只需要订阅消息队列Message Queue,每当有消息投递到队列queue中时,都会通知消费者进行消费。
生产者只需要将消息投递到Exchange交换机中,不需要关注消息被投递到哪个队列。
消费者只需要监听队列来消费消息,不需要关注消息来自于哪个Exchange。
Exchange和Message Queue存在着绑定的关系,一个Exchage可以绑定多个消息队列。
多线程和消息队列的区别?
- 多线程是防止系统的阻塞(优先响应用户,后台任务执行)
- 消息队列是提高系统处理业务的效率(异步处理加快程序执行速度)
消息队列和多线程的选择
- 可靠性要求高时选择消息队列:消息队列和多线程两者并不冲突,多线程可以作为队列的生产者和消费者。使用外部的消息队列时,第一是可以提高应用的稳定性,当程序fail后,已经写入外部消息队列的数据依旧是保存的,如果使用两步commit的队列的话,可以更加提高这个项目。
- 不着急知道结果,尽量使用消息队列,保证服务器的压力减小,因为多线程对cpu的消耗大一点:用线程的话,会占用主服务器资源, 消息队列的话, 可以放到其他机器上运行, 让主服务器尽量多的服务其他请求。我个人认为, 如果用户不急着知道结果的操作, 用消息队列, 否则再考虑用不用线程。
- 需要解耦的时候用消息队列:解耦更充分,架构更合理。多线程是在编程语言层面解决问题,消息队列是在架构层面解决问题。我认为架构层面解决问题是“觉悟比较高的方式“,理想情况下应该限制语言层面滥用多线程,能不用就不用。
- 如果容易出现线程安全问题的业务或者批量操作时,也尽量使用消息队列:批量发送邮件时,数据量庞大,如果使用多线程对系统不安全。
消息队列和线程池的比较
- 两者内部都使用了队列,如阻塞队列、优先级队列;
- 使用线程池时应用服务器既充当生产者又充当消费者,也是消息队列中间件的实现者,使用消息队列时中间件、生产者、消费者可以部署在不同的应用机器上(当然也可以部署在一台服务器上但很少有人这么用);
- 出于第2点线程池更适合非分布式的系统,但在分布式架构下消息队列明显是更突出优势;
- 使用消息队列会带来额外的网络开销;
- 消息队列的耦合性更低,可扩展性更好,适用于弱一致性的场景,如对log日志的解耦;
- 消息队列自动实现消息的持久化,中间已经实现了大量功能,如消息转发、消息拒绝、消息重试,以及对消息的一些监控,例如消息的消费状态、消息的消费速率等,使用线程池如果需要很多功能还要自己去实现,例如需要执行状态需要打印队列数量、计算消息消费速度;
- 在不同系统间的服务调用(调用协议也可能不一致)线程池很难实现或开销很大,这时候消息队列可以屏蔽不同机器或不同协议的问题;
- 使用消息队列会提升系统的复杂度,网络抖动怎么办?最大队列长度怎么设置?超时时间又设置多少?Qos又设置为多少?消费者多少个比较合适?Channel cache size又该设置为多少?业务线可能都是用同一个Mq,你占资源太多,或者设计不当可能会导致整个Mq故障。
4)JVM相关问题
介绍下垃圾回收机制(GC是在什么时候,对什么东西,做了什么事情?)。
在系统运行过程中,会产生一些无用的对象,这些对象占据着一定的内存,如果不对这些对象清理回收无用对象的内存,可能会导致内存的耗尽,所以垃圾回收机制回收的是内存。同时 GC 回收的是堆区和方法区的内存。
什么时候?
1.系统空闲的时候。
2.系统自身决定,不可预测的时间/调用System.gc()的时候。
对什么东西?
1.不使用的对象。
2.超出作用域的对象/引用计数为空的对象。
3.从gc root开始搜索,搜索不到的对象。
4.从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象。
做什么事情?
1.删除不使用的对象,腾出内存空间。
2.补充一些诸如停止其他线程执行、运行finalize等的说明。
垃圾收集有哪些算法,各自的特点。
常见的垃圾回收算法有:标记-清除算法、复制算法、标记-整理算法、分代收集算法
标记-清除算法
标记—清除算法包括两个阶段:“标记”和“清除”。 标记阶段:确定所有要回收的对象,并做标记。 清除阶段:将标记阶段确定不可用的对象清除。
缺点:
标记和清除的效率都不高。
会产生大量的碎片,而导致频繁的回收。
复制算法
内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候, 把存活的对象复制到另一块上,然后把这块内存整个清理掉。
缺点:
需要浪费额外的内存作为复制区。
当存活率较高时,复制算法效率会下降。
标记-整理算法
标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。
缺点: 算法复杂度大,执行步骤较多
分代收集算法
目前大部分 JVM 的垃圾收集器采用的算法。根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为新生代和老年代,永久代。
Young:存放新创建的对象,对象生命周期非常短,几乎用完可以立即回收,也叫 Eden 区。
Tenured: young 区多次回收后存活下来的对象将被移到 tenured 区,也叫 old 区。
Perm:永久带,主要存加载的类信息,生命周期长,几乎不会被回收。
缺点: 算法复杂度大,执行步骤较多。
类加载的过程。
当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三步来实现对这个类进行初始化。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:
加载
、验证
、准备
、初始化
和卸载
这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
双亲委派模型。
双亲委派模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。
一言概之,双亲委派模型,其实就是一种类加载器的层次关系。
- 安全,避免核心类库被修改;
- 避免重复加载;
- 保证类的唯一性。
有哪些类加载器。
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分
- 所有其它类的加载器,使用 Java 实现,独立于虚拟机,并且全部继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
- 启动类加载器(Bootstrap ClassLoader):启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 扩展类加载器(Extension ClassLoader):开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
能不能自己写一个类叫java.lang.String。
可以,但在应用的时候,需要用自己的类加载器去加载,否则,系统的类加载器永远只是去加载jre.jar包中的那个java.lang.String。由于在tomcat的web应用程序中,都是由webapp自己的类加载器先自己加载WEB-INF/classess目录中的类,然后才委托上级的类加载器加载,如果我们在tomcat的web应用程序中写一个java.lang.String,这时候Servlet程序加载的就是我们自己写的java.lang.String,但是这么干就会出很多潜在的问题,原来所有用了java.lang.String类的都将出现问题。
总结:不能自己写以"java."开头的类,要么不能加载进内存,要么即使你用自定义的类加载器去强行加载,也会收到一个SecurityException。
5)设计模式相关问题(必问)
设计模式比较常见的就是让你手写一个单例模式(注意单例模式的几种不同的实现方法)或者让你说一下某个常见的设计模式在你的项目中是如何使用的。
另外面试官还有可能问你抽象工厂和工厂方法模式的区别、工厂模式的思想这样的问题。
建议把代理模式、观察者模式、(抽象)工厂模式好好看一下,这三个设计模式很有用。
6)数据库相关问题,针对MySQL(必问)
给题目让你手写SQL。
MySQL索引的数据结构。
MySQL 建索引可使用的数据结构有B+树和Hash两种:
但是Hash用得很少, 优点是可以快速定位到某一行,缺点是不能解决范围查询问题。对于如果不需要使用范围查询、只需要精准查询的场景,可以使用Hash索引方法,比如查电话号码。
为什么使用B+树?(B+树是B树的变种,索引做了冗余,存了多份,但是没关系,索引只占很小空间,比如下图中的15节点)
B+树的特点:
- 非叶子节点不存储data,只存储key,可以增大度(相比B树,B+树的深度更浅)
- 叶子节点不存储指针
- 顺序访问指针,提高区间访问的性能(实际上是双向指针)
- 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引;
- 避免使用 NULL 字段,很难查询优化且占用额外索引空间,可以设置默认值0或'';
- 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描;
- 应尽量避免在 where 子句中使用 or 来连接条件,可以使用union all查询,否则同上;
- in 和 not in 也要慎用,对于连续的数值,能用 between 就不要用 in 了,很多时候用 exists 代替 in 是一个好的选择:where exists(select 1 from b where num=a.num)
- 并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引
- 索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,一个表的索引数最好不要超过6个
- 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。
- 尽可能的使用 varchar 代替 char ,因为首先变长字段存储空间小,可以节省存储空间
- 任何地方都不要使用 select * from t ,用具体的字段列表代替*
- 避免频繁创建和删除临时表,以减少系统表资源的消耗,如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除
SQL关键字的执行顺序。
from->on->join->where->group by(开始使用select中的别名,后面的语句中都可以使用别名)->sum、count、max、avg->having->select->distinct->order by->limit
- from:需要从哪个数据表检索数据
- join:对需要关联查询的表进行关联
- on:关联条件
- where:过滤表中数据的条件
- group by:如何将上面过滤出的数据分组
- avg:求平均值
- having:对上面已经分组的数据进行过滤的条件
- select:查看结果集中的哪个列或列的计算结果
- distinct:对结果集重复值去重
- order by:按照什么样的顺序来查看返回的数据
- limit:截取出目标页数据
MySQL索引包括普通索引、唯一索引、全文索引、主键索引、聚集索引
- 普通索引:它的结构主要以B+树和哈希索引为主,主要是对数据表中的数据进行精确查找。
- 唯一索引:在创建唯一索引时要不能给具有相同的索引值。
- 全文索引:它的作用是搜索数据表中的字段是不是包含我们搜索的关键字,就像搜索引擎中的模糊查询。
- 主键索引:在我们给一个字段设置主键的时候,它就会自动创建主键索引,用来确保每一个值都是唯一的。
- 聚集索引:我们在表中添加数据的顺序,与我们创建的索引键值相同,而且一个表中只能有一个聚集索引。
使用索引的优点
提高数据的搜索速度加快表与表之间的连接速度在信息检索过程中,若使用分组及排序子句进行时,通过建立索引能有效地减少检索过程中所需的分组及排序时间,提高检索效率。
使用索引的缺点
在我们建立数据库的时候,需要花费的时间去建立和维护索引,而且随着数据量的增加,需要维护它的时间也会增加。在创建索引的时候会占用存储空间。在我们需要修改表中的数据时,索引还需要进行动态的维护,所以对数据库的维护带来了一定的麻烦。
什么时候该(不该)建索引。
应该使用索引的情况
- 较频繁地作为查询条件的字段
- 经常用连接(join)的字段
- 经常需要根据范围进行搜索的字段
- 需要排序的字段
不应该使用索引的情况
- 唯一性很小的情况(select count(discount(column))/count(column) )
- 表数据需要频繁修改
- 字段不在where语句出现时不要添加索引
- 数据量少的表不要使用索引
首先需要注意:MYSQL 5.6.3以前只能EXPLAIN SELECT
,MYSQL5.6.3以后就可以EXPLAIN SELECT,UPDATE,DELETE
mysql> explain select * from staff;
7)框架相关问题
mybatis:入门简单,程序容易上手开发,节省开发成本 。mybatis需要程序员自己编写sql语句,是一个不完全 的ORM(对象关系映射)框架,对sql修改和优化非常容易实现 。
MyBatis的优势是可以进行更为细致的SQL优化,可以减少查询字段,并且容易掌握。
mybatis适合开发需求变更频繁的系统,比如:互联网项目。
hibernate:入门门槛高,如果用hibernate写出高性能的程序不容易实现。hibernate不用写sql语句,是一个 ORM框架。
Hibernate的优势是DAO层开发比MyBatis简单,Mybatis需要维护SQL和结果映射。
Hibernate的数据库移植性很好,MyBatis的数据库移植性不好,不同的数据库需要写不同SQL。
hibernate适合需求固定,对象数据模型稳定,中小型项目,比如:企业OA系统
Hibernate有更好的二级缓存机制,可以使用第三方缓存。MyBatis本身提供的缓存机制不佳。
Mybatis的一级缓存:
sqlSession级别的缓存,操作数据库时需要构造sqlSession对象,对象中有一个数据结构(Hashmap)用于存储缓存数据。
不同的sqlSession对象互不影响。Mybatis默认开启一级缓存
Mybatis的二级缓存:
Mapper级别的缓存,多个sqlSession共用一个Mapper,多个sqlSession操作数据库会将数据存储在二级缓存,并且可以共用二级缓存,
作用域是同一个mapper的namespace的。Mybatis的二级缓存默认是不开启的,需要在配置文件中开启。
hibernate的缓存机制
一级缓存:session级别的缓存,也称为线程级别的缓存,只在session的范围内有效
二级缓存:sessionFactory级别的缓存,也称为进程级别的缓存,在所有的session中都有效
一般需要配置第三方的缓存支持,比如EhCache
查询缓存:依赖于二级缓存,在HQL的查询语句中生效
Spring MVC和Struts2的区别。
共同点:
1、它们都是表现层框架,都是基于MVC模型编写的
2、它们的底层都离不开原始ServletAPI
3、它们处理请求的机制都是一个核心控制器
区别:
1、springMVC入口是servlet,而Sturts2 是Filter;
2、springMVC是基于方法设计的,而Sturts2是基于类,Struts2每次执行都会创建一个动作类。所以springMVC会稍微比Struts2快些
3、springMVC使用更加简洁,同时还支持JSP303,处理ajax的请求更加方便(JSR303是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。)
4、Struts2的OGNL表达式使页面的开发效率相比springMVC更高些,但执行效率并没有比JSTL提升,尤其是Struts2的表单标签,远没有html执行效率高。
- 工厂设计模式 : Spring使用工厂模式通过
BeanFactory
、ApplicationContext
创建 bean 对象。 - 单例设计模式 : Spring 中的 Bean 默认都是单例的。在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
- 代理设计模式 : Spring AOP 功能的实现。AOP(面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
- 模板方法模式 : Spring 中
jdbcTemplate
、hibernateTemplate
等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 - 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
ApplicationEvent充当事件角色,
ApplicationListener
充当了事件监听者角色,ApplicationEventPublisher充当事件的发布者角色。 - 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配
Controller
。 - 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 工厂设计模式 : Spring使用工厂模式通过
Spring中AOP主要用来做什么。
AOP(面向切面编程)是OOP(面向对象编程)的延续,它提出了横向抽取机制,将横切逻辑代码和业务逻辑代码拆分出来。
但如果只是普通的提取出一个公用方法,代码是拆分了,但耦合还是存在,因为每次还要调用这个公共方法,于是要解决的问题就变成了如何将横切逻辑代码悄无声息的加入到业务逻辑中。
AOP本质:在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往是权限校验代码、日志代码、事务控制代码、性能监控代码。
三种常规注入方式
属性注入
通过属性注入的方式非常常用,这个应该是大家比较熟悉的一种方式:
setter 方法注入
构造器注入
当两个类属于强关联时,我们也可以通过构造器的方式来实现注入:
假如我们想要注入一个接口,而当前接口又有多个实现类,那么这时候就会报错,因为 Spring
无法知道到底应该注入哪一个实现类。比如我们上面的三个类全部实现同一个接口 IWolf
,那么这时候直接使用常规的,不带任何注解元数据的注入方式来注入接口 IWolf
。
接口注入
解决思路主要有以下 5
种:
1、通过配置文件和 @ConditionalOnProperty 注解实现
通过 @ConditionalOnProperty
注解可以结合配置文件来实现唯一注入。下面示例就是说如果配置文件中配置了 lonely.wolf=test1
,那么就会将 Wolf1Bean
初始化到容器,此时因为其他实现类不满足条件,所以不会被初始化到 IOC
容器,所以就可以正常注入接口:
当然,这种配置方式,编译器可能还是会提示有多个 Bean
,但是只要我们确保每个实现类的条件不一致,就可以正常使用。
2、通过其他 @Condition 条件注解
除了上面的配置文件条件,还可以通过其他类似的条件注解,如:
- @ConditionalOnBean:当存在某一个
Bean
时,初始化此类到容器。 - @ConditionalOnClass:当存在某一个类时,初始化此类的容器。
- @ConditionalOnMissingBean:当不存在某一个
Bean
时,初始化此类到容器。 - @ConditionalOnMissingClass:当不存在某一个类时,初始化此类到容器。
- ...
- @ConditionalOnBean:当存在某一个
类似这种实现方式也可以非常灵活的实现动态化配置。
不过上面介绍的这些方法似乎每次都只能固定注入一个实现类,那么如果我们就是想多个类同时注入,不同的场景可以动态切换而又不需要重启或者修改配置文件,又该如何实现呢?
3、通过 @Resource 注解动态获取
如果不想手动获取,我们也可以通过 @Resource
注解的形式动态指定 BeanName
来获取:
如上所示则只会注入 BeanName
为 wolf1Bean
的实现类。
4、通过集合注入
除了指定 Bean
的方式注入,我们也可以通过集合的方式一次性注入接口的所有实现类:
上面的两种形式都会将 IWolf
中所有的实现类注入集合中。如果使用的是 List
集合,那么我们可以取出来再通过 instanceof
关键字来判定类型;而通过 Map
集合注入的话,Spring
会将 Bean
的名称(默认类名首字母小写)作为 key
来存储,这样我们就可以在需要的时候动态获取自己想要的实现类。
5、@Primary 注解实现默认注入
除了上面的几种方式,我们还可以在其中某一个实现类上加上 @Primary
注解来表示当有多个 Bean
满足条件时,优先注入当前带有 @Primary
注解的 Bean
:
通过这种方式,Spring
就会默认注入 wolf1Bean
,而同时我们仍然可以通过上下文手动获取其他实现类,因为其他实现类也存在容器中。
手动获取 Bean 的几种方式
在 Spring
项目中,手动获取 Bean
需要通过 ApplicationContext
对象,这时候可以通过以下 5
种方式进行获取:
- 最简单的一种方法就是通过直接注入的方式获取
ApplicationContext
对象,然后就可以通过ApplicationContext
对象获取Bean
- 通过 ApplicationContextAware 接口获取
- 通过 ApplicationObjectSupport 和 WebApplicationObjectSupport 获取
- 通过 HttpServletRequest 获取
- 其他方式获取
什么是IOC,什么是依赖注入。
控制反转IOC是一种设计思想,其实就是统一的管理bean的注册,实例化,销毁等整个生命周期,且都是单例不用创建多个实例。以前是在需要用到的类就要new一个实例。
依赖注入可以作为控制反转的一种实现方式,将实例变量传入到一个对象中去。
Spring是单例还是多例,怎么修改。
String的controller默认是单例的,不要使用非静态的成员变量,否则会发生数据逻辑混乱。正因为单例所以不是线程安全的。会导致属性重复使用。
解决方案
- 不要在controller中定义成员变量。
- 万一必须要定义一个非静态成员变量时候,则通过注解@Scope(“prototype”),将其设置为多例模式。(prototype:原型模式,每次通过getBean获取该bean就会新产生一个实例,创建后spring将不再对其管理)
- 在Controller中使用ThreadLocal变量
Spring框架的隔离级别
- ISOLATION_DEFAULT 这是一个默认的隔离级别,使用数据库默认的事务隔离级别
- ISOLATION_READ_UNCOMMITTED(未提交读):允许读取改变了的还未提交的数据,可能导致脏读、不可重复读和幻读
- ISOLATION_READ_COMMITTED(已提交读):允许并发事务提交之后读取,可以避免脏读,可能导致重复读和幻读
- ISOLATION_REPEATABLE_READ(可重复读):对相同字段的多次读取结果一致,可导致幻读
- ISOLATION_SERIALIZABLE(串行化的):完全服从ACID的原则,确保不发生脏读、不可重复读和幻读
Spring的事务传播级别(事务的传播级别是一个事务调用另一个事务的规范,在spring中就是service层对象相互调用的规范)
- PROPAGATION_REQUIRED 如果没有被事务包裹,创建事务; 如果被事务包裹,融入外部事务 (适合增删改)
- PROPAGATION_SUPPORTS 如果被事务包裹,不创建事务 ;如果被事务包裹 ,融入外部事务 (适合查询)
- PROPAGATION_REQUIRES_NEW 如果被包裹 ,挂起外部事务,创建自己的事务 ; 如果没有事务包裹,开启事务
- PROPAGATION_NO_SUPPORTED 外面有没有事务,都不需要事务 什么都不要
- PROPAGATION_NEVER 如果没有被事务包裹,不开启新事务 ;如果被事务包裹,抛出异常
- PROPAGATION_MANDATORY 如果没有被事务包裹 ,就抛出异常 ;如果被事务包裹 就融合外部事务
- PROPAGATION_NESTED 如果被事务包裹,就融入事务 ;如果没有,创建新事务
Mybatis的mapper文件中#和$的区别。
- ${}是字符串替换,会将${}替换成变量的值
- #{}是预编译处理,会把sql当中的#{}替换成为?号,能够有效的防止SQL注入
Mybatis的mapper文件中resultType和resultMap的区别。
- 使用resultType时,对于select语句查询出的字段在相应的pojo中必须有和它相同的字段对应,而resultType中的内容就是pojo在本项目中的位置。
- 使用resultMap做select语句返回结果类型处理时,通常需要在mapper.xml中定义resultMap进行pojo和相应表字段的对应。
当我们遇到在实体类中的字段名和数据库中的字段名不一致时,resultMap就可以帮助我们将数据库字段名和本类字段名相应射。
注意:resultType跟resultMap不能同时存在。
8)开发中常见问题
如果核心流程处理到一半,服务器崩溃了,会怎么处理
这里同时存在三个问题:
1.问题排查以及快速恢复
2.异常数据修复
3.服务高可用,规避服务宕机
先抢通业务
当发现服务器宕机后,最关键的是抢通业务,而不是抢修服务器。
因此,需要做应急方案。最好准备2个网站服务器,他们存放的内容相同,而ip不同,并且机房的地理位置不同。这样第一时间发现宕机问题后,可以迅速的通过修域名记录,指向目前正常的网站空间。而且2个主机,同时宕机的可能性就大大降低了。
服务器崩溃问题定位
1.内存溢出,磁盘资源耗尽
2.线程死锁,进程过多或者不断创建,耗尽资源导致
3.数据库慢查询,连接数过多,临时表不够用,程序死锁
4.主备数据不一致
5.应用程序异常
6.流量负载过大
7.DOSS攻击
8.散热问题
异常数据修复
1.写数据做事务控制,保障数据安全。
2.磁盘备份,重启服务时恢复数据。
3.记录关键日志。
服务高可用
1.服务多实例集群部署,负载均衡策略访问,做好服务降级、服务限流。
2.数据库读写分离、分库分表方案。
3.做好服务性能测试、压力测试。(如何规避服务器宕机风险:https://wetest.qq.com/lab/view/310.html?from=content_SegmentFault)
项目中遇到过哪些挑战或问题,怎么解决的
接触没碰过的技术时会让人很懵吧,其实只要先了解其工作原理,再看下相关实际应用的代码都不是什么大问题。
面对新的编程语言,无非就是弄清楚数据存储和传输方式,再熟悉下相关语法就差不多了。
实际项目中最常遇到的是在处理一些量大的业务,耗时长效率慢。拆分业务步骤或使用多线程。
可能解决一个问题的方式都很多种,但在实际项目中,我们都要考虑在源代码的基础上解决问题,不能改动太大而影响到其它业务。比例:一个问题用某个技术可以简单处理,但需要更换当前jdk版本,就需要慎重考虑了。
项目的稳定性和可用性怎么保障
发现问题:通过排查,系统的主要问题,从大方面说就是:外部的问题和自身的问题,当然症结在于架构的问题。
值的的提的是:如果业务量没有上来,这些问题本不是问题。
分析问题:首先我们要对目前所面临的问题进行分析。
事务中包含外部调用
外部调用包括对外部系统的调用和基础组件的调用。它具有返回时间不确定性的特征,必然会造成大事务。
大的数据库事务会造成其他对数据库连接的请求获取不到,那么和这个数据库相关的所有服务都很可能处于等待状态,造成连接池被打满,多个服务直接宕掉。如果这个没做好,危险指数五颗星。
排查各个系统的代码,检查在事务中是否存在 RPC 调用、HTTP 调用、消息队列操作、缓存、循环查询等耗时的操作,这个操作应该移到事务之外,理想的情况是事务内只处理数据库操作。
对大事务添加监控报警。大事务发生时,会收到邮件和短信提醒,一般的报警标准是 1s。
建议不要用 XML 配置事务,而采用注解的方式。原因是 XML 配置事务,第一可读性不强,第二切面通常配置的比较泛滥,容易造成事务过大,第三对于嵌套情况的规则不好处理。
超时时间和重试次数不合理
对外部系统和缓存、消息队列等基础组件的依赖,如果超时时间设置过长、重试过多,系统长时间不返回,可能会导致连接池被打满,系统死掉;如果超时时间设置过短,499 错误会增多,系统的可用性会降低。
如果超时时间设置得短,重试次数设置得多,会增加系统的整体耗时;如果超时时间设置得短,重试次数设置得也少,那么这次请求的返回结果会不准确。
首先要调研被依赖服务自己调用下游的超时时间是多少。调用方的超时时间要大于被依赖方调用下游的时间。
统计这个接口 99% 的超时时间是多少,设置的超时时间在这个基础上加 50%。
重试次数如果系统服务重要性高,则按照默认,一般是重试三次。否则,可以不重试。
外部依赖的地方没有熔断
在依赖的服务不可用时,服务调用方应该通过一些技术手段,向上提供有损服务,保证业务柔性可用。
而系统没有熔断,如果由于代码逻辑问题上线引起故障、网络问题、调用超时、业务促销调用量激增、服务容量不足等原因,服务调用链路上有一个下游服务出现故障,就可能导致接入层其他的业务不可用。
自动熔断:可以使用 Netflix 的 Hystrix 或者美团点评自己研发的 Rhino 来做快速失败。
手动熔断:确认下游支付通道抖动或不可用,可以手动关闭通道。
对于依赖我们的上游没有限流
在开放式的网络环境下,对外系统往往会收到很多有意无意的恶意攻击,如 DDoS 攻击、用户失败重刷。
虽然我们的队友各个是精英,但还是要做好保障,不被上游的疏忽影响,毕竟,谁也无法保证其他同学哪天会写一个如果下游返回不符合预期就无限次重试的代码。
这些内部和外部的巨量调用,如果不加以保护,往往会扩散到后台服务,最终可能引起后台基础服务宕机。
通过对服务端的业务性能压测,可以分析出一个相对合理的最大 QPS。
可以使用 Netflix 的 Hystrix 或者美团点评自己研发的 Rhino 来限流。
慢查询问题
慢查询会降低应用的响应性能和并发性能。在业务量增加的情况下造成数据库所在的服务器 CPU 利用率急剧攀升,严重的会导致数据库不响应,只能重启解决。
将查询分成实时查询、近实时查询和离线查询。实时查询可穿透数据库,其他的不走数据库,可以用 Elasticsearch 来实现一个查询中心,处理近实时查询和离线查询。
读写分离。写走主库,读走从库。
索引优化。索引过多会影响数据库写性能。索引不够查询会慢。 像核心交易这种数据库读写 TPS 差不多的,一般建议索引不超过 4 个。如果这还不能解决问题,那很可能需要调整表结构设计了。
对慢查询对应监控报警。我们这边设置的慢查询报警阈值是 100ms。
依赖不合理
每多一个依赖方,风险就会累加。特别是强依赖,它本身意味着一荣俱荣、一损俱损。
1、能去依赖就去依赖
2、尽量同步强依赖改成异步弱依赖:
划清业务边界,只做该做的事情。
如果依赖一个系统提供的数据,上游可以作为参数传入或者下游可以作为返回值返回,则可以下线专门去取数据的逻辑,尽量让上下游给我们数据。
我们写入基础组件,数据提供给其他端,如果其他端有兜底策略,则我们可以异步写入,不用保证数据 100% 不丢失。
废弃逻辑和临时代码
过期的代码会对正常逻辑有干扰,让代码不清晰。特别是对新加入的同事,他们对明白干什么用的代码可以处理。但是已经废弃的和临时的代码,因为不知道干什么用的,所以改起来更忐忑。
如果知道是废弃的,其他功能改了这一块没改,也有可能因为这块不兼容,引发问题。
代码类别,不持续维护的代码从长期成本来说是很高的:
精简代码逻辑
梳理每个接口的调用情况,对于没有调用量的接口,确认不再使用后及时下线。
code review 保证每段逻辑都明白其含义,弄清楚是否是历史逻辑或者临时逻辑。
没有有效的资源隔离
容易造成级联多米诺骨牌效应。
服务器物理隔离原则:
内外有别:内部系统与对外开放平台区分对待。
内部隔离:从上游到下游按通道从物理服务器上进行隔离,低流量服务合并。
外部隔离:按渠道隔离,渠道之间互不影响。
线程池资源隔离:
Hystrix 通过命令模式,将每个类型的业务请求封装成对应的命令请求。
每个命令请求对应一个线程池,创建好的线程池是被放入到 Concurrent Hash Map 中。
注意:尽管线程池提供了线程隔离,客户端底层代码也必须要有超时设置,不能无限制的阻塞以致于线程池一直饱和。
信号量资源隔离:
开发者可以使用 Hystrix 限制系统对某一个依赖的最高并发数,这个基本上就是一个限流策略。
每次调用依赖时都会检查一下是否到达信号量的限制值,如达到,则拒绝。
1 - 输入校验
SQL 注入防范, XSS防范, 代码注入/命令执行防范,日志伪造防范,XML 外部实体攻击, XML 注入防范,URL 重定向防范
2 - 异常处理
敏感信息泄露防范,保持对象一致性
3 - I/O 操作
资源释放,清除临时文件,避免将 bufer 暴露给不可信代码,任意文件下载/路径遍历防范,非法文件上传防范
4 - 序列化/反序列化操作
敏感数据禁止序列化,正确使用安全管理器
5 - 运行环境
不要禁用字节码验证,不要远程调试/监控生产环境的应用,生产应用只能有一个入口
6 - 业务逻辑
安全设计 API,身份验证,访问控制,会话管理
如何设计一个安全对外的接口?
个人觉得安全措施大体来看主要在两个方面,一方面就是如何保证数据在传输过程中的安全性,另一个方面是数据已经到达服务器端,服务器端如何识别数据,如何不被攻击;
下面具体看看都有哪些安全措施:
- 数据合法性校验:只有在数据是合法的情况下才会进行数据处理;每个系统都有自己的验证规则,当然也可能有一些常规性的规则,比如身份证长度和组成,电话号码长度和组成等等;
- 数据加密:常见的做法对关键字段加密比如用户密码直接通过md5加密;现在主流的做法是使用https协议,在http和tcp之间添加一层加密层(SSL层),这一层负责数据的加密和解密。
- 数据加签:数据加签就是由发送者产生一段无法伪造的一段数字串,来保证数据在传输过程中不被篡改;
- Appid机制:在调用的接口中需要提供appid+密钥,服务器端会进行相关的验证;
- 限流机制:本来就是真实的用户,并且开通了appid,但是出现频繁调用接口的情况;这种情况需要给相关appid限流处理,常用的限流算法有令牌桶和漏桶算法;
- 黑名单机制:如果此appid进行过很多非法操作,或者说专门有一个中黑系统,经过分析之后直接将此appid列入黑名单,所有请求直接返回错误码;
- 时间戳机制:在每次请求中加入当前的时间,服务器端会拿到当前时间和消息中的时间相减,看看是否在一个固定的时间范围内比如5分钟内;这样恶意请求的数据包是无法更改里面时间的,所以5分钟后就视为非法请求了;
项目的技术选型,为什么选这些
从以下方面去讲:
- Java开发框架:Spring MVC+MyBatis
- 缓存:Redis
- 前后端分离:单页应用/模板引擎
- 前后端接口文档自动生成:Swagger
- 业务端逻辑校验框架:Functional Validator/Fluent Validator/Hibernate Validator?
Git的分支你们是怎么管理的?
- develop:开发环境的稳定分支,公共开发环境基于该分支构建。
- release:测试环境的稳定分支,测试环境基于该分支构建。
- master:生产环境的稳定分支,生产环境基于该分支构建。仅用来发布新版本,除了从pre-release或生产环境Bug修复分支进行merge,不接受任何其它修改
- 从develop分支切出一个新分支,根据是功能还是bug,命名为feature-* 或 fixbug-*。
- 开发者完成开发,提交分支到远程仓库。
接口保证幂等性是基本的要求,那么幂等性你们是怎么做的?
幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。
要做到幂等性,从接口设计上来说不设计任何非幂等的操作即可。
例如很常见的支付下单等场景,由于分布式环境中网络的复杂性,用户误操作,网络抖动,消息重复,服务超时导致业务自动重试等等各种情况都可能会使线上数据产生了不一致,造成生产事故。
- 业务字段加唯一约束(简单)
- 令牌+唯一约束(简单推荐)客户端每次在调用接口的时候,需要在请求头中,传递令牌参数(令牌可以存储到redis中),每次令牌只能用一次。一旦使用之后,就会被删除,这样可以有效防止重复提交。
- mysql的insert ignore或者on duplicate key update(简单)
- 共享锁+普通索引(简单)
- 利用MQ或者Redis扩展(排队)
- 同步锁(单线程,集群可能会失效)
- 分布式锁如redis(实现复杂)
- 其他方案如多版本控制MVCC 乐观锁 悲观锁 状态机等。。。
对客户端请求排队或者单线程都可以处理幂等问题,需要根据具体业务选择合适的方案,但必须前后端一起做,前端做了可以提升用户体验,后端则可以保证数据安全。
你们有用@Transactional来控制事务是吧,那么能不能说出一些事务不生效的场景?
- 数据库引擎不支持事务,以MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。
- 数据源没有配置事务管理器
- 没有被 Spring 管理,就是没有
@Service
注解 - 自身调用问题,目标方法上面没有加
@Transactional
注解,实际调用了其它方法 - 方法不是 public 的,就是
@Transactional
只能用于 public 的方法上,否则事务不会失效 - 不支持事务@Transactional(propagation = Propagation.NOT_SUPPORTED) 这样主动不支持以事务方式运行了,那事务生效也是白搭!
- 异常被吃了,没有抛出异常(catch无返回),无法回滚
- 异常类型错误,默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:@Transactional(rollbackFor = Exception.class)
其中发生最多就是自身调用问题、异常被吃、异常抛出类型错误这三个了。
9)其他问题
介绍下栈和队列。
- 栈的特点是先进后出,栈只能查看栈顶的一个元素(网页的Back返回功能,Back需要记住前面网页的顺序保证正确的回退)
- 队列的特点是先进先出,只能查看队头的一个元素(常见的阻塞现象,先到先处理)
- 优先级队列遵循的不是先进先出、而是谁的优先级最高,谁先出列
- 栈和队列,可以用数组实现,也可以用其他数据结构实现
- 栈和队列是为了完成某些工作,手动构造的数据结构
IO和NIO的区别。
Java NIO:一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如P2P网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。一个线程多个连接的设计方案如下图所示:
Java IO: 一个典型的IO服务器设计- 一个连接通过一个线程处理.
如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。下图说明了一个典型的IO服务器设计:
接口和抽象类的区别。
- 接口和抽象类都不能实例化
- 接口是用来实现多继承,而抽象类只能单继承
- 接口只能定义方法不能实现,而抽象类中可以有普通方法
- 一个类要实现接口的所有方法,而抽象类不需要
- 抽象类是一种模板设计,接口是一种行为的规范
int和Integer的自动拆箱/装箱相关问题。
- 自动装箱:编译器调用valueOf方法将基本数据类型int转换为引用数据类型Integer
- 自动拆箱:编译器调用类似intValue(),doubleValue()等方法将引用数据类型Integer转化为基本数据类型int
- 可以看出对于Integer与int使用==比较大小的话,优先Integer拆箱。
- 如果使用equals比较大小的话,则int装箱。(Integer对于-128到127之间的数字在缓存中拿,不是创建新对象。)
提示:对于Integer与int之间大小比较优先使用equals比较,否则容易出现空指针
装箱和拆箱/拷贝操作会从速度和内存两个方面损伤应用程序的性能。因此我们应该清楚编译器会在何时自动产生执行这些操作的指令,并使我们编写的代码尽可能减少导致这种情况发生的机会。
==和equals的区别。
- == 是判断两个变量或实例是不是指向同一个内存空间,是指对内存地址进行比较
- equals用来比较两个字符串的内容是否相等,代码中经常有人犯这种错误写法:null.equals("小红")
常量池相关问题。
常量池大体可以分为以下两类:
- 静态常量池 存在于class文件中 (可使用javap -verbose进行使用)
- 运行时常量池 class文件被加载进内存之后,常量池保存在了方法区,就称为运行时常量池
变量a的“abc”作为字面量开始存储在class文件中,在运行时转存至方法区。变量b是一个对象,对象存储在堆中。
栈:由系统自动分配,速度较快。但程序员是无法控制的。(使用的是一级缓存),栈就像是一个桐,是一种先进后出的数据结构。
堆:是由
new
分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。(存放在二级缓存中),堆可以被看成是一棵树的数组对象。顺序随意。
什么是JDK?什么是JRE?什么是JVM?三者之间的联系与区别
JDK是Java开发工具包,JRE是Java运行时环境,Java虚拟机(JVM)是运行Java字节码(代码)的虚拟机。
这三者的关系是:一层层的嵌套关系。JDK>JRE>JVM
JDK是 JAVA 程序开发时用的开发工具包,其内部也有运行环境 JRE 。
JRE 是 JAVA 程序运行时需要的运行环境,如果不搞开发可以不用装JDK,只要安装 JRE 就能运行已经存在的 JAVA 程序。
JDK、JRE 内部都包含 JAVA 虚拟机 JVM,JAVA 虚拟机内部包含许多应用程序的类的解释器和类加载器等等。
Java和C++的区别
尽管Java与C++拥有类似的语法,但其执行与处理机制则完全不同。
- 解释对编译:Java是一种解释性语言,意味着其在执行时会被“翻译”为二进制形式,必须用JVM去解释它。而C++则是编译语言,意味着程序只能在特定操作系统上编译并在特定系统上运行。
- 内存安全:Java是一种内存安全型语言,意味着大家可以为给定数组分配任意参数,即使超出范围也只会 返回错误提示。C++更为灵活,但代价是一旦分配的参数超出资源范围,则会引起错误甚至严重崩溃。
- 性能:Java代码由于需要在运行前进行解释因此性能表现更差些。C++会被编译为二进制形 式,因此其能够立即运行且速度更快。如果你写一个c++的程序和做同样事情的java程序,可能你感觉两 者速度差不多。但如果这两个程序都足够大、而且c++的代码经过过优化,两者的速度差就会变得很显著 甚至很惊人,C++会比java快很多。
- 指针:指针是一种C++结构,允许您直接在内存空间中进行值管理。Java不支持指针,因此您可能使用值 引用的方式进行值传递。
- 重载:重载是指对某种方法或者运算符的功能进行“重新定义”。Java允许方法重载,而C++则允许进行运算符重载。
重载和重写的区别。
重写(Override)是父类与子类之间多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Override)。则父类中的定义如同被“屏蔽”了。
重载(Overload)是一个类中多态性的一种表现。如果在一个类中定义了多个同名的方法,它们参数个数或类型不同,则称为方法的重载(Overload)。
区别:重载实现于一个类中;重写实现于子类中。
String和StringBuilder、StringBuffer的区别。
就看一张上述a对象的内存存储空间图
String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。
在看一下b对象的内存空间图:
StringBuffer对象是一个字符序列可变的字符串,它没有重新生成一个对象,而且在原来的对象中可以连接新的字符串。(线程安全的)
StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。(所以性能略高。)
静态变量、实例变量、局部变量线程安全吗,为什么。
静态变量:静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。
实例变量:单例模式(只有一个对象实例存在)线程非安全。在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;
局部变量:线程安全。每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
- 不管有没有异常,finally块中代码都会执行;
- 当try.catch中有return时,finally仍然会执行;
- finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
- 在执行时,是return语句先把返回值写入内存中,然后停下来等待finally语句块执行完,return再执行后面的一段。
- 至于返回值到底变不变,当finally调用任何可变的API,会修改返回值;当finally调用任何的不可变的API,对返回值没有影响。
结论:任何try或者catch在return语句之前,如果有finally存在的话,都会先执行finally语句,如果finally中有return语句,那么程序就此结束了。
B和B+树:主要用在文件系统以及数据库中做索引等
AVL树:严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1)。应用相对其他数据结构比较少,windows对进程地址空间的管理用到了AVL
红黑树:弱平衡二叉树,并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。广泛应用在C++STL中,比如map和set,Java的TreeMap
1.1、二叉查找树
这种查找方式,在数据量不多的情况下,是没有任何问题的,但是,如果数据量多起来,每次去比较所有的数据,查询速度就会越来越低的。
特点:左子树的键值小于根节点的键值,右子树的节点大于根节点的键值。(左图)
缺点:如果存放的值一直大于根节点的值,就会造成树的不平衡,导致查询效率低下。(右图)
1.2、平衡二叉树(基于二分法的策略提高数据查找速度的二叉树的数据结构。)
特点:在符合二叉查找树的条件下,还满足任何节点的两个子树节点的高度差小于等于1.(右图数字8和5相差有2个高度所以非AVL树)
如果比较二叉树和平衡二叉树,在查询效率方面,二叉树查询会偶然性的时快时慢,而平衡二叉树查询效率则相对均匀,且总树结构层级高度远小于二叉树层级高度。这就是平衡二叉树的优点。
1.3、红黑树
1、为什么有了平衡树还需要红黑树?
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严格了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。
2、红黑树的特性
显然,如果在插入、删除很频繁的场景中,平衡树需要频繁调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树,红黑树具有如下特点:
- 每个节点或者是黑色,或者是红色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点]
- 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。[这里指到叶子节点的路径]
包含n个内部节点的红黑树的高度是 O(log(n))。如图:
3、红黑树的使用场景
java中使用到红黑树的有TreeSet和JDK1.8的HashMap。红黑树的插入和删除都要满足以上5个特性,操作非常复杂,为什么要使用红黑树?
原因:
红黑树是一种平衡树,复杂的定义和规则都是为了保证树的平衡性。如果树不能保证平衡性就很显然变成一个链表了。
保证平衡性的最大的目的就是降低树的高度,因为树的查找性能取决于树的高度。所以树的高度越低搜索的效率越高!
2.1、B树
B树又名平衡多路查找树(查找路径不只两个),不同于常见的二叉树,它是一种多叉树,我们常见的使用场景一般是在数据库索引技术里,大量使用者B树和B+树的数据结构。
特征如下:
1)树中的每个节点最多有m个孩子;且M>2,除根节点和叶子结点外。
2)所有叶子节点都在同一层,叶子节点不包含任何关键字信息。
3)每个非终端节点中包含有n个关键字信息 。
搜索B树时,很明显,访问节点(即读取磁盘)的次数与树的高度呈正比,而B树与红黑树和普通的二叉查找树相比,虽然高度都是对数数量级,但是显然B树中log函数的底可以比2更大,因此,和二叉树相比,极大地减少了磁盘读取的次数。
2.2、B+树
B+树是B树的一种升级版本,B+树查找的效率要比B树更高、更稳定。
B+树是应文件系统所需而产生的一种B树的变形树(文件的目录一级一级索引,只有最底层的叶子节点(文件)保存数据.),非叶子节点只保存索引,不保存实际的数据,数据都保存在叶子节点中。
特征如下:
B+树和B树类似,但多了几条规则
- 非叶子结点的子树指针个数与关键字(节点中的元素个数)个数相同
- 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间)
- 所有叶子结点有一个链指针
- 所有关键字都在叶子结点出现
- 只有叶子节点有Data域
B+树相对于B树的最主要的优点:
1. B+树只有叶子节点存放数据,而其他节点只存放索引,而B树每个节点都有Data域。所以相同大小的节点B+树包含的索引比B树的索引更多(因为B树每个节点还有Data域)
2. B+树的叶子节点是通过链表连接的,所以找到下限后能很快进行区间查询,比B树中序遍历快
2.3、B*树
B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针;B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2)。
特征如下:
在B+树的基础上因其初始化的容量变大,使得节点空间使用率更高,而又存有兄弟节点的指针,可以向兄弟节点转移关键字的特性使得B*树额分解次数变得更少;
总结
1、相同思想和策略
从平衡二叉树、B树、B+树、B*树总体来看它们的贯彻的思想是相同的,都是采用二分法和数据平衡策略来提升查找数据的速度。
2、不同方式的磁盘空间利用
不同点是它们一个一个在演变的过程中通过IO从磁盘读取数据的原理进行一步步的演变,每一次演变都是为了让节点的空间更合理的运用起来,从而使树的层级减少达到快速查找数据的目的。
说起分布式的概念,首当其冲就是CAP理论,即满足一致性、可用性和分区容错性。但是CAP理论告诉我们,任何系统只能满足其中两个,所以都要求去做取舍。那么人们常说的一般都是,需要牺牲一致性来保证系统的高可用性,只要保证系统的最终一致性,并且允许的时间差值能够被接受就行。
例如对于订单系统来说,用户端的一致性需要保证强一致性,但是对于后台或者商家来说的话,这个订单的状态只要保证最终一致性就行,时间差值在可接受范围内就OK。
1、单机的情况下:
单机情况要解决共享资源的访问很容易,Java的API提供了很丰富的解决方案,常见的诸如synchronize,lock,volatile,c.u.t包等等,很多,但是这是在单机情况下,因为只有一个JVM在运行我们的代码。
2、多机的情况下:
这个时候就会出现一套代码出现在多个JVM中,请求落在哪一个上面是随机的。这个时候上面提到的基于Java的API提供的一些解决机制就没法满足要求,它只能解决当前机器中能保证顺序访问共享资源,但是不能保证其他机器。
那么对于多机的情况怎么去解决这个问题呢,其实很简单,只要保证互斥就行了,原理和单机是一样的,找到一个互斥点就行。那么这个互斥点就必须在大家共有的一个环境中。
那么我所了解到现在分布式锁有三种实现方案:
1.基于数据库。
2.基于缓存环境,redis,memcache等。
3.基于zookeeper。
方案的实现注意点
1.首先保证在分布式的环境中,同一个方法只能被同一个服务器上的一个线程执行。
2.锁要可重入,严重一点的场景不能获取锁之后如果需要再次获取时发现不能获取了,造成死锁。
3.锁要可阻塞。这一般只要保证有个超时时间就行。
4.高可用的加锁和释放锁功能。
5.加锁和释放锁的性能要好。
数据库分布式锁实现-缺点:
1.db操作性能较差,并且有锁表的风险
2.非阻塞操作失败后,需要轮询,占用cpu资源;
3.长时间不commit或者长时间轮询,可能会占用较多连接资源
Redis(缓存)分布式锁实现-缺点:
1.锁删除失败 过期时间不好控制
2.非阻塞,操作失败后,需要轮询,占用cpu资源;
ZK分布式锁实现-缺点:
因为需要频繁的创建和删除节点,性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在Leader上执行,然后同步到follower。
总之:ZooKeeper有较好的性能和可靠性。
从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库
分布式session存储解决方案。
Session工作原理图:
方案一:客户端存储,将Session数据放在Cookie里,访问Web服务器的时候,再由Web服务器生成对应的Session数据。
缺点:
- 数据存储在客户端,存在安全隐患
- cookie存储大小、类型存在限制
- 数据存储在cookie中,如果一次请求cookie过大,会给网络增加更大的开销
方案二:使用nginx进行session绑定
可以基于nginx的ip-hash策略对客户端和服务器进行绑定,同一个客户端就只能访问该服务器,无论客户端发送多少次请求都被同一个服务器处理。
方案三:服务器session复制
session复制是小型企业应用使用较多的一种服务器集群session管理机制,在真正的开发使用的并不是很多,通过对web服务器(例如Tomcat)进行搭建集群。
方案四:基于redis存储session方案,Session统一缓存。
这是企业中使用的最多的一种方式,spring为我们封装好了spring-session,直接引入依赖即可。
一般会将web容器所在的服务器和redis所在的服务器放在同一个机房,减少网络开销,走内网进行连接。
原文网址1:https://blog.csdn.net/blackspoon123/article/details/112002384
原文网址2:https://blog.csdn.net/qq_35620501/article/details/95047642
常用的linux命令。
查看具体的java jar进程
ps -ef | grep ***.jar
ps -ef | grep java (先查java进程ID)
kill -9 PID(生产环境谨慎使用)
#后台运行程序
nohup java -jar test.jar &
#后台运行程序 标准日志输出->黑洞 错误日志重定向到黑洞
nohup java -jar yourProject.jar >/dev/null 2>&1 &
#后台运行程序 标准日志输出->黑洞 错误日志输出至log
nohup java -jar yourProject.jar >/dev/null 2>log &
tail -100f test.log 实时监控100行日志
tail -n 10 test.log 查询日志尾部最后10行的日志;
tail -n +10 test.log 查询10行之后的所有日志;
head:
跟tail是相反的,tail是看后多少行日志;例子如下:
head -n 10 test.log 查询日志文件中的头10行日志;
head -n -10 test.log 查询日志文件除了最后10行的其他所有日志;
cat:
tac是倒序查看,是cat单词反写;例子如下:
cat -n test.log |grep "debug" 查询关键字的日志
三、其他经验分享
1)不要一开始就去面试自己最想去的公司,把面试当作一次技术的交流,面试的越多,经验越多,等面出了心得再去投理想的公司。
2)不熟悉的技术不要主动提。
3)如果没有明白面试官的问题,不要不懂装懂,可以礼貌地让对方重复一遍,也让自己多一点时间思考。
4)在面试的日子里,要保持每天学习,无论是学习新东西还是复习旧东西。
5)如果超过3-5天还没有得到结果,自己又很想去这家公司,可以主动联系HR询问面试结果,就算面试没有通过,也可以问问面试失败的原因,总结经验。
四、自我认知能力,认清自身缺陷,突出个人优点:
由于之前工作经历可能没有实际运用到一些大厂技术点,比如分布式应用和消息中间件,就只知道有这些东西,也知道用来做什么,就是没有在实际工作应用过。
自我学习能力强,可快速get新的编程语言和技术难点。善于自我学习和总结,熟练开发web项目,对于手机端H5和微信小程序开发也接触过,都有能力运用起来。
Java常见问题-汇总的更多相关文章
- Java常见问题汇总
1.String,StringBuffer,StringBulider的区别及应用场景 2.Servlet生命周期 3.向上转型与向下转型 4.Java的多态性 5.重写和重载的区别 6.深拷贝和浅拷 ...
- CentOS安装Oracle数据库详细介绍及常见问题汇总
一.安装前准备 1.软件硬件要求 操作系统:CentOS 6.4(32bit)Oracle数据库版本:Oracle 10g(10201_database_linux32.zip)最小内存:1G(检查命 ...
- mysql进阶(十六)常见问题汇总
mysql进阶(十六)常见问题汇总 MySQL视图学习: http://www.itokit.com/2011/0908/67848.html 执行删除操作时,出现如下错误提示: 出现以上问题的原因是 ...
- 转---CentOS安装Oracle数据库详细介绍及常见问题汇总
一.安装前准备 1.软件硬件要求 操作系统:CentOS 6.4(32bit)Oracle数据库版本:Oracle 10g(10201_database_linux32.zip)最小内存:1G(检查命 ...
- Hive常见问题汇总
参考资料: Hive常见问题汇总 啟動hive出錯,提示沒有權限 2015年04月02日 09:58:49 阅读数:31769 这里小编汇集,使用Hive时遇到的常见问题. 1,执行#hive命令进入 ...
- gpload导入常见问题汇总
gpload导入常见问题汇总 java写文件后使用gpload命令导入greenplum: 问题一: 报错信息:invalid byte sequence for encoding "UTF ...
- SVN集中式版本控制器的安装、使用与常见问题汇总
SVN是Subversion的简称,是一个开放源代码的版本控制系统,它采用了分支管理系统,集中式版本控制器 官方网站:https://www.visualsvn.com/ 下载右边的服务器端,左边的客 ...
- H5项目常见问题汇总及解决方案
H5项目常见问题汇总及解决方案 H5 2015-12-06 10:15:33 发布 您的评价: 4.5 收藏 4收藏 H5项目常见问题及注意事项 Meta基础知识: H5页 ...
- Installshield脚本拷贝文件常见问题汇总
原文:Installshield脚本拷贝文件常见问题汇总 很多朋友经常来问:为什么我用CopyFile/XCopyFile函数拷贝文件无效?引起这种情况的原因有很多,今天略微总结了一下,欢迎各位朋友跟 ...
- (转)JAVA排序汇总
JAVA排序汇总 package com.softeem.jbs.lesson4; import java.util.Random; /** * 排序测试类 * * 排序算法的分类如下: * 1.插入 ...
随机推荐
- SRAM、DRAM、Flash、DDR有什么区别
SRAM SRAM的全称是Static Rnadom Access Memory,翻译过来即静态随机存储器.这里的静态是指这种存储器只需要保持通电,里面的数据就可以永远保持.但是当断点之后,里面的数据 ...
- PCI-E与SATA SSD
为什么要采用PCI-E通道 目前在固态硬盘SSD中,有一部分采用了SATA3.0接口,而一些高端的固态硬盘则采用了PCI-E接口.那么为什么高端固态硬盘要采用PCI-E接口呢?为了弄清楚这个问题,先看 ...
- 用Python画一个冰墩墩!
北京2022年冬奥会的召开,吉祥物冰墩墩着实火了,真的是一墩难求,为了实现冰墩墩的自由,经过资料搜集及参考冰墩墩网上的开源代码(https://github.com/HelloCoder-HaC/bi ...
- 基于 three.js 加载器分别加载模型
点击查看代码 /** * 参数:模型文件路径,成功回调函数 * * 基于 three.js 加载器分别加载模型 * * 全部加载后通过回调函数传出打印 */ import { FBXLoader } ...
- 使用 Splashtop 启用员工远程访问
使员工进行远程工作似乎是一项耗时.不安全且昂贵的任务.但是,借助 Splashtop,您可以快速.轻松.安全地使您的员工从任何位置以最高 价值远程访问其工作站. 如何使用 Splashtop 启用 ...
- .NET 代理模式(二) 动态代理-DispatchProxy
前言 我们都知道,在.NET中实现动态代理AOP有多种方案,也有很多框架支持,但大多框架的实现原理都是通过Emit配合Activator一起使用,从IL级别上实现动态代理. 其实在.NET中有一个更为 ...
- Python:当函数做为参数时的技巧
我们之前在<Python技法3: 匿名函数.回调函数.高阶函数>中提到,可以通过lambda表达式来为函数设置默认参数,从而修改函数的参数个数: import math def dista ...
- kubernets之带有limit的资源
一 pod中容器的limits属性的作用 1.1 创建一个带有资源limits的pod apiVersion: v1 kind: Pod metadata: name: limited-pod s ...
- sass变量的详细使用
sass变量同javascript变量,可以用来存储一些信息,并且可以重复使用. 先来对比一下css中的变量 同css变量对比 CSS 变量是由 CSS 作者定义的,它包含的值可以在整个文档或指定的范 ...
- Gitea 代码仓库平台
引言 Gitea 是一个自己托管的 Git 服务程序.他和 GitHub,Bitbucket or Gitlab 等比较类似.它是从 Gogs 发展而来,不过它已经 Fork 并且命名为 Gitea. ...