最近,在看一些java高级面试题,我发现我在认真研究一个面试题的时候,我自己的收获是很大的,我们在看看面试题的时候,不仅仅要看这个问题本身,还要看这个问题的衍生问题,一个问题有些时候可能是一个问题群(如果只关注问题本身,可以跳过补充部分)。
这个是我一个多星期的奋战结果,把它记录下来,如有不当,希望大家不吝赐教。
java 线程池的实现原理,threadpoolexecutor关键参数解释
原理见下图 (或者:https://blog.csdn.net/u013332124/article/details/79587436) 实际开发中,直接调用ThreadPoolExecutor的情况比较少,更多的是使用Executors实现(Executors内部很多地方都是调用的ThreadPoolExecutor) Executors 是一个工厂类,用于创建线程池,常用的有5个api: newCachedThreadPool():创建无界限线程池 newFixedThreadPool(int) :创建的是有界线线程池(线程个数可以指定最大数量,如果超过最大数量,则后加入的线程需要等待) newSingleThreadExecutor() :创建单一线程池,实现以队列的方式来执行任务 newScheduledThreadPool(...):创建即将执行的任务线程池,每隔多久执行一次 newSingleThreadScheduledExecutor():只有一个线程,用来调度任务在指定时间内执行 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler) corePoolSize:线程池核心线程数量 maximumPoolSize:线程池最大线程数量( 当workQueue满了,不能添加任务的时候,这个参数才会生效。) keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间 unit:存活时间的单位 workQueue:存放任务的队列 handler:超出线程范围和队列容量的任务的处理程序
hashmap的原理,容量为什么是2的幂次
HashMap使用的是哈希表(也称散列表,是根据关键码值(Key value)而直接进行访问的数据结构),哈希表有多种不同的实现方法,常用的实现方式是“数组+链表”实现,即“链表的数组”。 首先,HashMap中存储的对象为数组Entry[](每个Entry就是一个<key,value>),存和取时根据 key 的 hashCode 相关的算法得到数组的下标,put 和 get 都根据算法的下标取得元素在数组中的位置。 万一两个key分别有相同的下标,那么怎么处理呢? 使用链表,把下标相同的 Entry 放到一个链表中,存储时,存到第一个位置,并把next指向之前的第一个元素(如果是已存在的key,则需要遍历链表),取值时,遍历链表,通过equals方法获取Entry。 // 存储时: int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值 int index = hash & (Entry[].length-1); // 二进制的按位与运算 Entry[index] = value; // 取值时: int hash = key.hashCode(); int index = hash & (Entry[].length-1); // 二进制的按位与运算 return Entry[index]; // 此处需要遍历 Entry[index] 的链表取值,此处为简写 关于原理细节,可参考文章 https://www.cnblogs.com/holyshengjie/p/6500463.html 前半部分 关于源码实现,可参考文章 https://www.cnblogs.com/chengxiao/p/6059914.html 【关于为什么覆盖 equals方法 后需要同时覆盖 hashCode方法】 在此文末尾 通过以上理解,如果 hashCode方法 写得不好,比如所有 key 对象 都返回 同一个 int 值,那么效率也不会高,因为就相当于一个链表了。 至于容量为什么是2的幂次? 因为HashMap的机制是,当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,因为获取数组下标的方法是【hashCode和数组容量的按位与运算】结果,故只有数组大小为2^n-1(即二进制所有位都是1,如:3,7,15,31...)才能保按位与运算得到的 index 均匀分布。
补充:二进制的按位与运算举例:23 & 15 = 7
0 1 0 1 1 1
0 0 1 1 1 1
-------------
0 0 0 1 1 1
为什么要同时重写hashcode和equals
原因在上一个问题中已经描述过了,这里再说一下,注意看main方法中的注释:
举个小例子来看看,如果重写了equals而不重写hashcode会发生什么样的问题
public class MyTest { private static class Person{ int idCard; String name; public Person(int idCard, String name) { this.idCard = idCard; this.name = name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()){ return false; } Person person = (Person) o; //两个对象是否等值,通过idCard来确定 return this.idCard == person.idCard; } } public static void main(String []args){ HashMap<Person,String> map = new HashMap<Person, String>(); Person person = new Person(1234,"乔峰"); //put到HashMap中去 map.put(person,"天龙八部"); //get取出,从逻辑上讲应该能输出“天龙八部” System.out.println("结果:"+map.get(new Person(1234,"萧峰"))); // 结果:null } }
要理解为什么要同时重写hashCode和equals,需要首先理解 HashMap 的原理。
这里的例子中,由于没有重写hashCode方法,虽然两个对象的 equals 方法时一致的,但是两个对象的hashCode 返回的 int 值不一致,导致put操作和get操作时,最终存和取的数组下标不同,取出来的就是null 了。
ConcurrentHashMap如何实现线程安全?
这个问题可以参考文章:https://blog.csdn.net/V_Axis/article/details/78616700 以下是个人总结:
先说一下HashMap为什么线程不安全(hash碰撞与扩容导致,2点):
1、Entry内部的变量分别是 key、value、next,如果多个线程,在某一时刻同时操作HashMap并执行put操作,而有两个key的hash值相同(这两个key分别是a1 和 a2),这个时候需要解决碰撞冲突,不论是从链表头部插入还是从尾部初入,这个时候两个线程如果恰好都取到了对应位置的头结点e1,而最终的结果可想而知,a1、a2两个数据中势必会有一个会丢失;
2、在扩容时,当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。
解决HashMap的线程不安全,可以使用HashTable或者Collections.synchronizedMap,但是这两位选手都有一个共同的问题:性能。因为不管是读还是写操作,他们都会给整个集合上锁(每个方法都是 synchronized 的),导致同一时间的其他操作被阻塞。
ConcurrentHashMap 是 HashMap 与 HashTable 的折中,ConcurrentHashMap 在进行操作时,会给集合上锁,但是只锁了部分(segment)。
ConcurrentHashMap 集合 中有多个Segment,Segment其实就是一个HashMap,Segment也包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。
Segment对象在 ConcurrentHashMap 集合中有2的n次方个,共同保存在一个名为segments的数组当中(类比HashMap来理解Segment就好)。换言之,ConcurrentHashMap是一个双层哈希表。
每个segment的读写是高度自治的,segment之间互不影响。这称之为“锁分段技术”。
补充:
每一个segment各自持有锁,那么在调用size()方法的时候(size()在实际开发大量使用),怎么保持一致性呢?
1.遍历所有的Segment。
2.把Segment的修改次数累加起来。
3.把Segment的元素数量累加起来。
4.判断所有Segment的总修改次数(重复获取一次2的统计)是否大于上一次的总修改次数(第2步统计的结果)。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
7.释放锁,统计结束。
这个逻辑有些类似于乐观锁(参考:https://blog.csdn.net/V_Axis/article/details/78532806)
介绍Java多线程的5大状态,以及状态图流转过程
1. 新建(NEW):新创建了一个线程对象。
2. 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
详见:https://blog.csdn.net/maijia0754/article/details/79004412
介绍下synchronized、Volatile、CAS、AQS,以及各自的使用场景
synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的互斥锁(Mutex Lock)来实现的。synchronized属于重量级的,多线程时只能一个线程访问相关代码块,安全性高,但有时会效率低下。 volatile 通常用来修饰变量,能保证变量的内存可见性(即变量的读取和修改是直接操作原变量<每次读取前必须先从主内存刷新最新的值,每次写入后必须立即同步回主内存当中>,而不是copy后的变量),同时能防止指令被重排序(JVM运行时,为了效率考虑会对某些指令的执行顺序进行调整),但无法保证原子性(如语句:count++; 在多线程高并发时,就会出现数值不准,此时可以使用synchronized 或者 ReentrantLock 加锁)。 CAS(compare and swap 的简写,即比较并交换)是乐观锁的一个实现,它需要三个参数,分别是内存位置 V,旧的预期值 A 和新的值 B。操作时,先从内存位置读取到值,然后和预期值A比较。如果相等,则将此内存位置的值改为新值 B,返回 true。如果不相等,说明和其他线程冲突了,则不做任何改变,返回 false。乐观锁中,一般比较操作比的是版本号,CAS中也可以改旧值比较为版本号比较(安全性更高)。 AQS(AbstractQueuedSynchronizer)中有两个重要的成员: 1、成员变量 state。用于表示锁现在的状态,用 volatile 修饰,保证内存一致性。同时所用对 state 的操作都是使用 CAS 进行的。state 为0表示没有任何线程持有这个锁,线程持有该锁后将 state 加1,释放时减1。多次持有释放则多次加减。 2、还有一个双向链表,链表除了头结点外,每一个节点都记录了线程的信息,代表一个等待线程。这是一个 FIFO 的链表。 AQS 请求锁时有三种可能: 1、如果没有线程持有锁,则请求成功,当前线程直接获取到锁。 2、如果当前线程已经持有锁,则使用 CAS 将 state 值加1,表示自己再次申请了锁,释放锁时减1。这就是可重入性的实现。 3、如果由其他线程持有锁,那么将自己添加进等待队列。
其中,ReentrantLock 就是 AQS原理实现的,而synchronized 则是jvm层面实现的。
补充:
ReentrantLock 使用代码实现了和 synchronized 一样的语义,包括可重入,保证内存可见性和解决竞态条件问题等。相比 synchronized,它还有如下好处:
支持以非阻塞方式获取锁
可以响应中断
可以限时
支持了公平锁和非公平锁(公平锁是指各个线程在加锁前先检查有无排队的线程,按排队顺序去获得锁。 非公平锁是指线程加锁前不考虑排队问题,直接尝试获取锁,获取不到再去队尾排队。)
基本用法如下:
public class Counter { private final Lock lock = new ReentrantLock(); // 如果 new ReentrantLock(true); 则表示公平锁 private volatile int count; public void incr() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { return count; } }
B+树和红黑树时间复杂度
B+数时间复杂度是 O(lgn)
红黑树时间复杂度是 O(lgn)
补充:
B+树是为磁盘或其他直接存取辅助设备而设计的一种平衡查找树,所有记录节点都是按键值的大小顺序存放在同一层的叶节点中,各叶节点指针进行连接。
(B+树不详细说了,详细网上资料很多)
红黑树(RBT)的定义:它或者是一颗空树,或者是具有一下性质的二叉查找树:
1.节点非红即黑。
2.根节点是黑色。
3.所有NULL结点称为叶子节点,且认为颜色为黑。
4.所有红节点的子节点都为黑色。
5.从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点。
红黑树插入:
插入点不能为黑节点,应插入红节点。因为你插入黑节点将破坏性质5,所以每次插入的点都是红结点,但是若他的父节点也为红,那岂不是破坏了性质4?对啊,所以要做一些“旋转”和一些节点的变色。
红黑树参考自:http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html
平衡二叉树(AVL)适合用于插入删除次数比较少,但查找多的情况;红黑树的旋转保持平衡(插入和删除时会旋转保持平衡)次数较少,用于搜索时,插入删除次数多的情况下我们就用红黑树来取代AVL。
如果频繁老年代回收怎么分析解决
老年代频繁回收,一般是Full GC,Full GC 消耗很大,因为在所有用户线程停止的情况下完成回收,而造成频繁 Full GC 的原因可能是,程序存在问题,或者环境存在问题。
对jvm的GC进行必要的监控,操作如下:
1、使用jps命令(或者ps -eaf|grep java)获取到当前的java进程(取得进程id,假如pid为 1234)
2、使用jstat查看GC情况(如:jstat -gc 1234 1000,后面的1000表示每个1000毫米打印一次监控),jstat命令可以参考:https://www.cnblogs.com/yjd_hycf_space/p/7755633.html (此文使用的是jdk8,但是本人亲测jstat在jdk7也是这样的)
jstat -class pid:显示加载class的数量,及所占空间等信息。
jstat -compiler pid:显示VM实时编译的数量等信息。
jstat -gc pid:可以显示gc的信息,查看gc的次数,及时间。其中最后五项,分别是young gc的次数,young gc的时间,full gc的次数,full gc的时间,gc的总时间。
jstat -gccapacity:可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小,如:PGCMN显示的是最小perm的内存使用量,PGCMX显示的是perm的内存最大使用量,PGC是当前新生成的perm内存占用量,PC是但前perm内存占用量。其他的可以根据这个类推, OC是old内纯的占用量。
jstat -gcnew pid:new对象的信息。
jstat -gcnewcapacity pid:new对象的信息及其占用量。
jstat -gcold pid:old对象的信息。
jstat -gcoldcapacity pid:old对象的信息及其占用量。
jstat -gcpermcapacity pid: perm对象的信息及其占用量。
jstat -util pid:统计gc信息统计。
jstat -printcompilation pid:当前VM执行的信息。
除了以上一个参数外,还可以同时加上 两个数字,如:jstat -printcompilation 3024 250 6是每250毫秒打印一次,一共打印6次,还可以加上-h3每三行显示一下标题
3、使用jmap(jmap 是一个可以输出所有内存中对象的工具)导出对象文件。如对于java进程(pid=1234),可以这样:jmap -histo 1234 > a.log 将对象导出到文件,然后通过查看对象内存占用大小,返回去代码里面找问题。
(也可以使用命令,导出对象二进制内容(这样导出内容比jmap -histo多得多,更耗时,对jvm的消耗也更大):jmap -dump:format=b,file=a.log 1234)
(以上操作本人有亲自实验,jdk版本是7,第2步其实可以跳过)
JVM内存模型,新生代和老年的回收机制
新生代的GC:
新生代通常存活时间较短,因此基于复制算法来进行回收,所谓复制算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中。
在Eden和其中一个Survivor,复制到另一个之间Survivor空间中,然后清理掉原来就是在Eden和其中一个Survivor中的对象。
新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。
当连续分配对象时,对象会逐渐从eden到 survivor,最后到老年代,然后清空继续装载,当老年代也满了后,就会报outofmemory的异常。
老年代的GC:
老年代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收。
扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。
补充: Java 中的堆也是 GC 收集垃圾的主要区域: GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。 Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。 Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。 Minor GC思路: 当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳,并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。 对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。 Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。 标记-清除算法收集垃圾的时候会产生许多的内存碎片(不连续内存空间)。 标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。 清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。 标记-清除算法 容易产生内存碎片,故老年代中会使用标记整理算法(首先标记,但后续不是直接清除,而是将存活的对象都向一端移动,最后直接清理掉边界以外的内存。) GC Roots有哪些: Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,. Thread - 活着的线程 Stack Local - Java方法的local变量或参数(个人理解,就是ThreadLocal 相关的类 ) JNI Local - JNI方法的local变量或参数(JNI 是 Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)) JNI Global - 全局JNI引用 Monitor Used - 用于同步的监控对象 Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。
mysql limit分页如何保证可靠性
limit 查询,当表中数据量很大时,在十万条后面 limit 速度会变慢,如: select XXX from tableA limit 1000000,10; 上面的语句是取1000000后面的10条记录,但是这样会导致mysql将1000000之前的所有数据全部扫描一次,大量浪费了时间。 解决思路1: 直接使用主键查询,如: -- 此查询耗时:0.00 秒 select * from couponmodel_user where id > 3000000 and id < 3000000+10; 这种方式会造成某次查询出来的条数不足10条,有不足,但是优点就在特别快。 解决思路2: 直接贴我实际操作的代码吧 -- couponmodel_user 表中有一千万条数据,此查询耗时:3.02 秒 select * from couponmodel_user order by id limit 3000000,10; -- 只查id,此查询耗时:0.85 秒 select id from couponmodel_user order by id limit 3000000,10; -- 通过以上的启发,修改sql,此查询耗时:0.84 秒 select a.* from couponmodel_user a,(select id from couponmodel_user order by id limit 3000000,10) b where a.id=b.id order by a.id; 简单讲,就是使用主键的复合索引,关联查询 思路2解决了思路1的不足,速度也显著提升,但是问题就是速度还不如思路1快。
java nio,bio,aio,操作系统底层nio实现原理
BIO:
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
BIO方式适用于连接数目比较小且固定的架构,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO:
同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO(NIO.2):
异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
可以参考文章:https://blog.csdn.net/ty497122758/article/details/78979302 或者 https://www.cnblogs.com/diegodu/p/6823855.html
Spring事务传播机制
PROPAGATION_REQUIRED:有事务就用已有的,没有就重新开启一个
PROPAGATION_SUPPORTS:有事务就用已有的,没有也不会重新开启
PROPAGATION_MANDATORY:必须要有事务,没事务抛异常
PROPAGATION_REQUIRES_NEW:开启新事务,若当前已有事务,挂起当前事务
PROPAGATION_NOT_SUPPORTED:不需要事务,若当前已有事务,挂起当前事务
PROPAGATION_NEVER:不需要事务,若当前已有事务,抛出异常
PROPAGATION_NESTED:嵌套事务,如果外部事务回滚,则嵌套事务也会回滚!!!外部事务提交的时候,嵌套它才会被提交。嵌套事务回滚不会影响外部事务。
线程死锁排查
使用 jps + jstack,排查步骤:
1、使用命令列出当前运行的java进程的pid: jps -l
2、使用命令查看(假如第1步查到的java进程pid为 1234):jstack -l 1234
注意第2步中是否有提示 “ Found x deadlock ” 如有,则表示有死锁存在,其中 x 表示死锁的个数
以上两个命令的 -l 选项都可以省略,以上操作本人亲自实验可用。
参考:https://blog.csdn.net/mynamepg/article/details/80758540
补充:
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
MySQL引擎及区别,项目用的哪个,为什么
ISAM:
ISAM执行读取操作的速度很快,且不占用大量的内存和存储资源。
ISAM的两个主要不足之处在于,不支持事务处理,也不能够容错(如果你的硬盘崩溃了,那么数据文件就无法恢复了,需实时备份数据)。
MyISAM:
MyISAM是MySQL的ISAM扩展格式和缺省的数据库引擎。提供了索引和字段管理的大量功能,使用一种表格锁定的机制,来优化多个并发的读写操作。
MyISAM的不足仍然是不支持事务管理。
HEAP:
HEAP允许只驻留在内存里的临时表格。驻留在内存里让HEAP要比ISAM和MYISAM都快。
不足:管理的数据是不稳定的,而且如果在关机之前没有进行保存,那么所有的数据都会丢失。在数据行被删除的时候,HEAP也不会浪费大量的空间。
HEAP表格在你需要使用SELECT表达式来选择和操控数据的时候非常有用。要记住,在用完表格之后就删除表格。
InnoDB:
InnoDB数据库引擎尽管要比ISAM和 MyISAM引擎慢很多,但是InnoDB包括了对事务处理和外来键的支持,这两点都是前两个引擎所没有的。如前所述,如果你的设计需要这些特性中的一者 或者两者,那你就要被迫使用后两个引擎中的一个了。
InnoDB和MyISAM是许多人在使用MySQL时最常用的两个表类型,这两个表类型各有优劣:
MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持。MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快,但是不提供事务支持,而InnoDB提供事务支持已经外部键等高级数据库功能。
RPC为什么用http做通信?
RPC(Remote Produce Call)是一种概念,HTTP是一种协议,RPC可以通过HTTP来实现。RPC也可以使用socket来实现,但是问题就是socket是阻塞式的,并不好,大多RPC框架可能会使用netty来实现,而netty支持多种通讯协议,HTTP、WebSocket等协议,也可以使用自定义的协议进行通讯。
之所以使用HTTP协议,个人认为有可能是HTTP协议的特点决定的,还有就是HTTP比较成熟。
补充:
HTTP协议的主要特点概括如下:
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
RPC两端如何进行负载均衡?
个人理解,常见的算法是轮询(将请求轮流的分配到后端服务器上,均衡的对待后端的每一台服务器,而不关心服务器的实际连接数和当前的系统负载),扩展一下,是加权轮询法(每个主机轮询的多少)。
比如dubbo,可以使用zookeeper作为注册中心,将服务注册到zookeeper,进行负载均衡。
mycat分库分表、读写分离的实现
MyCat是mysql中间件,其核心就是分表分库,具体如何实现分表分库,由配置文件的配置决定:
配置文件,conf目录下主要以下三个需要熟悉。
server.xml Mycat的配置文件,设置账号、参数等
schema.xml Mycat对应的物理数据库和数据库表的配置
rule.xml Mycat分片(分库分表)规则
分库分表配置:在 rule.xml 中进行配置
读写分离配置:schema.xml 中的 dataHost 的 balance 属性为1、2 或者 3,因为 为 0 表示不支持读写分离。参考:https://www.cnblogs.com/ivictor/p/5131480.html
补充:
MyCat是一个开源的分布式数据库系统,MyCat是mysql中间件,是一个实现了MySQL协议的服务器,前端用户可以把它看作是一个数据库代理,其核心功能是分表分库,即将一个大表水平分割为N个小表,存储在后端MySQL服务器里或者其他数据库里。
分布式数据如何保证数据一致性
在分布式系统来说,如果不想牺牲一致性,CAP 理论告诉我们只能放弃可用性,这显然不能接受。 强一致: 当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可用性。 弱一致性: 系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。 最终一致性: 弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。 互联网系统大多将强一致性需求转换成最终一致性的需求,既保证一致性,又保证可用性。 保证最终一致性的 3 种解决方案: 1、经典方案 - eBay 模式 核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。 一个最常见的场景,如果产生了一笔交易,需要在交易表增加记录,同时还要修改用户表的金额。这两个表属于不同的远程服务,所以就涉及到分布式事务一致性的问题。 以上场景解决方案:将主要修改操作以及更新用户表的消息放在一个本地事务来完成。同时为了避免重复消费用户表消息带来的问题,增加一个更新记录表 updates_applied 来记录已经处理过的消息。 2、 随着业务规模不断地扩大,电商网站一般都要面临拆分之路。 将原来一个单体应用拆分成多个不同职责的子系统。比如以前可能将面向用户、客户和运营的功能都放在一个系统里,现在拆分为订单中心、代理商管理、运营系统、报价中心、库存管理等多个子系统。 还是和方案1一样,优先使用异步消息。 拆分多了之后,涉及到一个问题:分布式事物。 处理分布式事物,将分布式事务转换为多个本地事务,然后依靠重试等方式达到最终一致性。如果其中的一个事物确实是执行不成功,则考虑回滚整个分布式事物。 比如 A 同步调用 B 和 C,比如 B 是扣库存服务,在第一次调用的时候因为某种原因失败了,但是重试的时候库存已经变为 0,无法重试成功,这个时候只有回滚 A 和 C 了。 3、 如果分布式事物交易订单中是10个步骤,最后一个步骤操作失败了,其他步骤都要回滚,步骤过多,可以这样: 首先创建一个不可见订单,其中某一步失败后,发送一个MQ废单消息(如果消息发送失败,本地继续异步重试),其他步骤收到消息就可以进行回滚。 还可以使用支付宝的 分布式事务服务框架 DTS(由支付宝在 2PC <2 Phase Commit, 两阶段提交> 的基础上改进而来) 参考:https://www.cnblogs.com/wangdaijun/p/7272677.html
补充:
CAP原则,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
高并发请求处理,流量削峰措施有哪些
削峰从本质上来说就是更多地延缓用户请求,以及层层过滤用户的访问需求,遵从“最后落地到数据库的请求数要尽量少”的原则。
1、消息队列解决削峰:使用较多的消息队列有 ActiveMQ、RabbitMQ、 ZeroMQ、Kafka、MetaMQ、RocketMQ 等。
2、流量削峰漏斗:层层削峰
通过在不同的层次尽可能地过滤掉无效请求。
通过CDN过滤掉大量的图片,静态资源的请求;
再通过类似Redis这样的分布式缓存,过滤请求等就是典型的在上游拦截读请求;
对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
对写请求做限流保护,将超出系统承载能力的请求过滤掉(如可以使用 nginx 进行限流)
补充:
CDN 的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。
CDN的优势很明显:(1)CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;(2)大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源站的负载。
浏览器本地缓存失效后,浏览器会向CDN边缘节点发起请求。类似浏览器缓存,CDN边缘节点也存在着一套缓存机制。
可参考:https://www.cnblogs.com/tinywan/p/6067126.html
Redis持久化RDB和AOF 的区别
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
RDB是二进制存储的,恢复和启动更快,AOF 启动时需要重新加载执行一次所有操作日志 速度相对要慢 但优点是安全性更高。
补充:
RDB优势:
1、一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的(比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复)。
2、对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
3、性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
4、相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
RDB劣势:
1、如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
2、由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
AOF优势:
1、该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。
2、由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
3、如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
4、AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
AOF劣势:
1、对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2、根据同步策略的不同,AOF在运行效率上往往会慢于RDB。
MQ底层实现原理
就ActiveMQ来说:
消息通信机制:
点对点模式(p2p),每个消息只有1个消费者,它的目的地称为queue队列;
发布/订阅模式(pub/sub),每个消息可以有多个消费者,而且订阅一个主题的消费者,只能消费自它订阅之后发布的消息。
底层使用的是RPC调用。
详细介绍下分布式 一致性Hash算法
典型的应用场景是: 有N台服务器提供缓存服务,需要对服务器进行负载均衡,将请求平均分发到每台服务器上,每台机器负责1/N的服务。
Memcached client也选择这种算法,解决将key-value均匀分配到众多Memcached server上的问题。
1、一致性哈希将整个哈希值空间组织成一个虚拟的圆环;
2、将各个服务器使用H进行一个哈希(可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置);
3、将数据key使用相同的函数H计算出哈希值h,通根据h确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。
此算法的容错性较高,某个服务器挂了之后,会自动定位到下一个主机。
详细参考:https://www.cnblogs.com/moonandstar08/p/5405991.html
nginx负载均衡的算法
1、轮询(默认):
每个请求按时间顺序逐一分配到不同的后端服务,如果后端某台服务器死机,自动剔除故障系统,使用户访问不受影响。
2、weight(轮询权值):
weight的值越大分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。或者仅仅为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。
如:
upstream server_myServer_pool {
server 192.168.0.14 weight=10;
server 192.168.0.15 weight=10;
}
这里配置了的 server_myServer_pool 可在 location 中使用,如: location / { proxy_pass http://server_myServer_pool }
3、ip_hash:
每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题。
如:
upstream server_myServer_pool {
ip_hash;
server 192.168.0.14:88;
server 192.168.0.15:80;
}
4、fair:
比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,也就是根据后端服务器的响应时间 来分配请求,响应时间短的优先分配。Nginx本身不支持fair,如果需要这种调度算法,则必须安装upstream_fair模块。
如:
upstream server_myServer_pool {
fair;
server 192.168.0.14:88;
server 192.168.0.15:80;
}
5、url_hash:
按访问的URL的哈希结果来分配请求,使每个URL定向到一台后端服务器,可以进一步提高后端缓存服务器的效率。Nginx本身不支持url_hash,如果需要这种调度算法,则必须安装Nginx的hash软件包。
如:
upstream resinserver{
server 10.0.0.10:7777;
server 10.0.0.11:8888;
hash $request_uri;
hash_method crc32;
}
Nginx 的 upstream 目前支持 哪4 种方式的分配
weight(轮询权值)、ip_hash(访问IP的哈希结果分配)、fair(根据页面大小和加载时间长短智能分配,第三方)、url_hash(按访问的URL的哈希结果分配,第三方)
详细见上一个问题
Dubbo默认使用什么注册中心,还有别的选择吗?
Dubbo默认使用的注册中心是Zookeeper,其他的选择包括 Multicast、Zookeeper、Redis、Simple 等。
mongoDB、redis和memcached的应用场景,各自优势
mongoDB(基于分布式文件存储的数据库):
文档型的非关系型数据库,使用bson结构。其优势在于查询功能比较强大,能存储海量数据,缺点是比较消耗内存。
一般可以用来存放评论等半结构化数据,支持二级索引。适合存储json类型数据,不经常变化。
redis(内存数据库):
是内存型数据库,数据保存在内存中,通过tcp直接存取,优势是读写性能高。
redis是内存型KV数据库,不支持二级索引,支持list,set等多种数据格式。适合存储全局变量,适合读多写少的业务场景。很适合做缓存。
memcached(内存Cache):
Memcached基于一个存储键/值对的hashmap,是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。
性能:都比较高,性能都不是瓶颈,redis 和 memcache 差不多,要大于 mongodb。
操作的便利性:memcache 数据结构单一(key-value);redis 丰富一些,还提供 list、set、hash 等数据结构的存储;mongodb 支持丰富的数据表达,索引,最类似关系型数据库。
谈谈你性能优化的实践案例,优化思路?
个人感觉这个问题比较开放,可以举例上面的问题来回答:
如:
mysql limit分页如何保证可靠性
高并发请求处理,流量削峰措施有哪些
或者可以举例分布式架构方面内容:数据库分表分库、读写分离,使用缓存(分布式缓存),等等
两千万用户并发抢购,你怎么来设计?
此问题和 《高并发请求处理,流量削峰措施有哪些》这个问题类似,可以参考这个问题来回答。
这些就是这一期总结的问题,有些问题总结的可能不到位,也希望还能有下一期,大家共同进步。