Java基础
-
==和equals的区别
==比较的是内存地址,equals比较的是值
-
java基本类型

byte和boolean是八位,short和char是16位,int和float是32位,double和long是64位
-
final、finally和finalize的区别
final是定义变量不允许修改的关键字,finally是try、catch的最终执行,finnlize是垃圾回收的通知
-
try return和finally return
1,如果try块中有return语句,那么finally会执行吗?
答案:会(实践在后面)
2,如果finally块中改变了try块中的return语句要返回的值,那么返回值会改变吗?
答案:不会(实践在后面)
3,如果finally块中有return语句,那么函数会通过try块中的return返回还是通过try块中的return返回?
答案:通过finally块中的return返回。
压栈的操作:try的return先压栈、finally在压栈
return i;语句会新开辟一个空间,将 i 的值赋给新开辟的新开辟的空间。但是 i 是引用类型,如果对 i 指向的对象进行操作,那么也会影响到返回值,因为他们指向同一块内存空间
-
Math.round(-1.5)的结果
Math.round():执行结果为-1.5+0.5取整 =-1
-
ArrayList和LinkedList的区别
ArrayList:顺序存储、查找快、大小固定插入慢
LinkedList:双向链表、查找慢、更改指针插入快
-
HashMap1.7和HashMap1.8的区别
比较 | HashMap1.7 | HashMap1.8 |
数据结构 | 数组+链表 | 数组+链表+红黑树 |
节点 | Entry | Node TreeNode |
hash算法 | 比较复杂 | 异或hash右移16位 |
对Null的处理 | putForNull方法 | hash为0的节点 |
初始化 | 空数组 put初始化 | 没有赋值、懒加载、put初始化 |
扩容 | 插入前扩容 | 插入后扩容:先扩容再插入时频繁转变的概率要比先插入后扩容频繁转变的概率高 |
节点插入 | 头插法,容易出现逆序且环形链表死循环问题 | 尾插法能够避免出现逆序且链表死循环的问题。 |
-
hash的put过程
-
首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
-
如果数组是空的,则调用 resize 进行初始化;
-
如果没有哈希冲突直接放在对应的数组下标里;
-
如果冲突了,且 key 已经存在,就覆盖掉 value;
-
如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
-
如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。
因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3
-
hashmap为啥线程不安全

-
hashmap环形链表的场景

总结一下产生这个问题的原因:
插入的时候和平时我们追加到尾部的思路是不一致的,是链表的头结点开始循环插入,导致插入的顺序和原来链表的顺序相反的。
table 是共享的,table 里面的元素也是共享的,while 循环都直接修改 table 里面的元素的 next 指向,导致指向混乱。
1.8的解决:JDK8 是等链表整个 while 循环结束后,才给数组赋值,此时使用局部变量 loHead 和 hiHead来保存链表的值,因为是局部变量,所以多线程的情况下,肯定是没有问题的。
为什么有 loHead 和 hiHead两个新老值来保存链表呢,主要是因为扩容后,链表中的元素的索引位置是可能发生变化的
-
currentHashMap为啥不支持key和value为null
ConcurrentHashMap 是用于多线程的 ,如果map.get(key)得到了 null ,无法判断,是映射的value是 null ,还是对应的key被别的线程修改为null ,这就有了二义性
-
currentHashMap迭代器弱一致性
主要是为了提升性能,如果强一致性就要到处使用锁,这跟HashTable就没啥区别了
-
currentHashMap在jdk1.7和1.8的区别
jdk1.7:分段锁基于reentrantlock实现
初始化有三个参数:initialCapacity:初始容量大小 ,默认16。loadFactor, 扩容因子,默认0.75,当一个Segment存储的元素数量大于initialCapacity* loadFactor时,该Segment会进行一次扩容。
concurrencyLevel 并发度,默认16。并发度可以理解为程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数,即ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。如果并发度设置的过小,会带来严重的锁竞争问题;
如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。
jdk1.8:synchronized锁+CAS
用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,是优化过且线程安全的HashMap。
JDK1.8取消了segment数组,直接用table保存数据,锁的粒度更小,减少并发冲突的概率;
采用了链表+红黑树的形式,纯链表的形式时间复杂度为O(n),红黑树则为O(logn),性能提升很大;
降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,synchronized替换reentrantLock包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
-
treemap
TreeMap底层是树形(红黑树)结构,和HashMap最大的区别是丢进去的东西自动排序。要注意的是默认的是对Key排序,也可以重写Comparator对Value排序
-
快速失败和安全失败
快速失败(java.util包):当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
安全失败(java.util.current包):采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常
JVM
-
JVM的组成
虚拟机栈:生命周期和使用线程一样,每个方法执行的同时创建一个栈帧,用于存放局部变量、操作数栈、动态链接和方法出口等。局部变量表存放数据基本类型、对象引用和returnAddress类型
本地方法栈:本地Native方法服务
程序计数器:执行字节码的行号指示器,唯一没有内存溢出的区域
方法区:存放虚拟机加载的类信息,常量,静态变量等,运行时常量池主要是编译期的各种字面量和符号引用
堆:所有线程共享的区域,内存管理的核心区域 -Xmx和-Xms控制大小
-
对象的创建过程
new关键字-->虚拟机
-
先去方法区找到类信息和符号引用 无则执行类加载
-
分配内存 分为指针碰撞和空闲列表两种 前者移动内存指针 后者分配内存range压缩整理
-
设置对象头:包含元数据信息、hash码、gc分代信息、锁标志位
-
init 设置对象字段属性
-
对象的访问方式
句柄:reference指向地址,移动对象不需要移动存储地址
直接指针:访问速度快,节省成本
-
类加载的机制

双亲委派机制,保证上下文中加载的类一致
自定义加载器的使用场景
解决依赖冲突
热部署
加密保护
自定义加载器简单实现
继承ClassLoader
重写findClass()方法
-
类加载机制在tomcat中的应用
tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,不同的应用程序可能会依赖同一个第三方类库的不同版本,为了防止一些基础类被web里面的类覆盖,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器
先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回,否则 继续下一步。
让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。
JDBC,JNDI,Thread.currentThread().setContextClassLoader()等很多地方都一样是违反了双亲委托

-
内存泄漏和内存溢出
内存泄漏:对象长时间没有被回收,gcroot引用链 dump堆日志信息或可视化工具VisualVM
内存溢出:正常的使用对象超出内存大小 通过-Xmx修改堆大小
dump日志指令:jmap -dump:live,format=b,file=myjmapfile.hprof 31324
-
垃圾回收的判断和什么时候回收
引用计数法:引用数 难以解决循环依赖的问题
可达性分析:gcroot
gcroot的对象:虚拟机栈的引用、方法区静态变量的引用、方法区常量的引用和本地方法区的引用
强引用:不回收
弱引用:内存溢出之前回收
软引用:下次gc的时候回收
虚饮用:直接回收、给系统通知
回收方法区废弃的常量和无用的类:类的所有实例被回收、类的加载器被回收、没有反射引用
-
垃圾回收算法和垃圾回收器
回收算法:标记-清除、标记-复制、标记-整理、分代回收
CMS:
初始标记:阻塞
并发标记:
重新标记:阻塞
并发清除:
容易产生浮动垃圾、对cpu要求高 (cpu+3)/4
G1:
初始标记:阻塞
并发标记:
最终标记:阻塞
筛选回收:阻塞 可预测的停顿
分代回收、remberset 记录内存分配信息 选择回收的最大收益、可预测的停顿
ZGC: JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS
-
内存分配策略
新生代 eden--一次minigc-> survivor--15次-->老年代
相同年龄的对象大于survivor区的一半直接到老年代
大对象直接分配到老年代
-
空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
-
young gc、old gc和full gc
-
部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
-
新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
-
老年代收集(Major GC/Old GC):只是老年代的垃圾收集
-
目前,只有 CMS GC 会有单独收集老年代的行为
-
很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
-
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
-
目前只有 G1 GC 会有这种行为
-
整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾
-
触发full gc的几种场景
-
调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
-
老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
-
空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
-
JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
-
虚拟机的类加载机制
加载-->验证-->准备-->解析-->初始化-->使用-->回收
-
TLAB内存分配策略
从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内
-
堆区是线程共享的,任何线程都可以访问到堆区中的共享数据
-
由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
-
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%,我们可以通过 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。
TLAB 是线程私有的,线程初始化的时候,会创建并初始化 TLAB。同时,在 GC 扫描对象发生之后,线程第一次尝试分配对象的时候,也会创建并初始化 TLAB。 TLAB 生命周期停止(TLAB 声明周期停止不代表内存被回收,只是代表这个 TLAB 不再被这个线程私有管理)在:
当前 TLAB 不够分配,并且剩余空间小于最大浪费空间限制,那么这个 TLAB 会被退回 Eden,重新申请一个新的
发生 GC 的时候,TLAB 被回收。
TLAB 要解决的问题很明显,尽量避免从堆上直接分配内存从而避免频繁的锁争
一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。
-
java对象一定在堆上创建吗
先尝试着在栈里面去创建对象,栈指针直接往下偏移,它不用像堆内存那样需要垃圾回收,所以效率高
要满足逃逸分析和标量替换两个条件,才能在栈里面创建对象
逃逸分析:指向这个对象的指针有没有逃出对象所在栈帧的范围,如果没有逃出,那么就满足逃逸分析法的条件,或者可以说:我这个方法里面new的对象,别的方法里面有没有用到?在我这个方法外面的地方有没有人用到
标量替换:但即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了,当通过逃逸分析一个对象只会作用于方法内部,虚拟机可以通过使用标量替换来进行优化
-
jdk1.8默认的垃圾回收器
JDK 1.7和1.8 默认新生代采用的是ParallelGC ,老年代默认就是Parallel old ,
JDK 1.9默认采用的就是 G1 垃圾回收器了
ParallelGC:Server下默认的GC方式,当在Eden区申请内存空间时,如果Eden区不够,那么看当前申请的空间是否大于等于Eden的一半,如果大于则这次申请的空间直接在Old中分配,小于则触发Minor GC。在触发GC之前首先会检查每次晋升到Old区的平均大小是否大于Old区的剩余空间,如果大于则再出发Full GC。在这次触发GC后仍然会按照这个规则重新检查一次
ParallelOldGC:前者Full GC进行的动作为清空整个Heap堆中的垃圾对象,清楚Perm区中已经被卸载的类信息,并进行压缩。而后者是清楚Heap堆中部分垃圾对象,并进行部分的空间压缩
-
eden和survivor区的比例
8:1:1
Java多线程
-
线程工作内存模型
主内存<-->save、load<-->工作内存<-->java线程
JMM 抽象出主存储器(Main Memory)和工作存储器(Working Memory)两种。
·主存储器是实例对象所在的区域,所有的实例都存在于主存储器内。比如,实例所拥有的字段即位于主存储器内,主存储器是所有的线程所共享的。
·工作存储器是线程所拥有的作业区,每个线程都有其专用的工作存储器。工作存储器存有主存储器中必要部分的拷贝,称之为工作拷贝(Working Copy)。
因为大部分的cpu大部分指令都是无法操作主存的,必须要读取到寄存器后才能操作
所以,线程无法直接对主内存进行操作,此外,线程A想要和线程B通信,只能通过主存进行。
-
voiltile、synchronized和reentrantlock的区别
voiltile:只能修饰类变量和实例变量,修改对其他线程可见,禁止指令重排,在代码中插入内存屏障,轻量级同步机制不加锁,保证可见性和有序性,不保证原子性
synchronized:修饰方法和代码块,隐式加锁,非公平锁,悲观锁 monitorenter、monitorexit
reentrantlocak:显式加锁需要在finally释放锁,可以实现公平锁,乐观锁,使用灵活
loadload、storestore、loadsotre、storeload
happens-before原则
-
CAS机制
包括三种操作数,内存位置(V),预期原值(A)和新值(B)。
当V的值和A相匹配时,才会将V的值设为B,如果不匹配就不做任何操作。
存在的问题:
ABA问题:线程1把A取出来,线程2把A改成B又改成A,线程1认为A还是A。
解决方法:版本号
循环开销大:cpu开销大,可以使用synchronized
只能保证一个共享变量的原子性操作
-
synchronized的优化
无锁-->偏向锁-->轻量级锁-->重量级锁
0 01 1 01 0 00 0 10 0 11gc
一个线程 多个线程 竞争失败
无竞争 有竞争 无自旋失败
-
线程的几种状态
新建、就绪、运行、阻塞、等待、超时等待、终止

-
yield、wait和sleep的区别
yield:给相同或更高优先级线程机会,进入就绪状态、不释放锁
wait:条件等待、释放锁
sleep:给其他线程机会不考虑优先级,进入阻塞状态,不释放锁
-
死锁的条件
互斥、请求和保持、不可剥夺、获取多个资源
-
thread的join方法
A线程在B线程之后执行
-
threadLocal原理及threadLocal内存泄漏

每个thread线程内部有一个map(threadlocalmap),map中存储threadlocal对象(key)和线程变量副本,
对于不同线程,每次获取副本时只能获取当前线程的副本,形成线程间的数据隔离,互不干扰
threadlocal内存泄漏:一种是因为ThreadLocal中包含了ThreadLocalMap,如果Thread没有结束,则ThreadLocalMap一直不会释放,假如ThreadLocalMap中设置了很多值,而且没有手动设置remove(),则可能会造成内存泄露。
另一种是ThreadLocal作为一个弱引用的key然后造成每次GC的时候会回收掉ThreadLocal,导致无法访问value,然后造成了内存泄漏。每次操作set、get、remove操作时, 都会直接或间接调用一个 expungeStaleEntry() 方法,这个方法会将key为null的 Entry 删除,从而避免内存泄漏
-
java的原子操作类
原子更新基本类型AtomicInteger、AtomicLong、AtomicBoolean
原子更新数组AtomicIntegerArray、AtomicLongArray、AtomicRefrenceArray
原子更新引用类型 AtomicRefrence、AtomicRefrenceFieldUpdater、AtomicMarkableRefrence
原子更新字段类型AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStamepedRefrence
-
java并发工具类
CountDownLatch:一个线程等待其他线程完成 后当前线程执行通过 会阻塞主线程 只能使用一次
CyclicBarrier:一组线程相互等待知道屏障点 不会阻塞主线程 可循环使用 类似于阀门
Semaphore:控制并发线程数,协调各个线程
Exchanger:各线程数据交换,提供一个同步点交换两个线程的数据,可以用于遗传算法校对工作
-
java线程的创建方式
继承Thread类:extend
实现Runnable接口:implement 推荐使用
通过Callable和FutureTask接口:实现Callable接口,通过FutureTask可以异步获取执行结果
-
java线程池的核心参数及作用
java线程池使用的目的:降低资源消耗、提高响应速度、提高系统的可管理性
线程池的核心参数:
corePoolSize:核心线程数
maxnumPoolSize:最大线程数
threadFactory:线程工厂
rejectedExceptionHandler:饱和策略 抛出异常(默认)、使用当前线程、丢弃最近的、不处理直接丢弃
runnableQueue:任务队列 数组有界阻塞队列、链表有界阻塞队列、优先级无界阻塞队列
两种执行方法
execute():无返回值
submit():有返回值 FutureTask接收
线程配置数量: cpu密集型 cpu数+1 IO密集型:2*cpu数
提供的三种线程池:SigleThreadPool(单一线程池)、FixedThreadPool(定长有界线程池)、CacheThreadPool(缓存无界线程池)
线程池的分配过程:
核心线程数是否满了 未满创建 满了下一步
任务队列是否满了 未满创建 满了下一步
尝试其他线程创建 最大线程数是否满了 满了饱和策略
无界队列存在比较大的潜在风险,如果在并发量较大的情况下,线程池中可以几乎无限制的添加任务,容易导致内存溢出的问题!
-
java线程池的使用场景
商品和订单信息的导入导出、tomcat线程池
-
AQS为啥是双向队列,头节点放什么
AQS是依赖于内部的同步队列,FIFO队列
用于获取锁处理的中断信息,用于唤醒前一个线程
头节点放同步器
线程访问资源,如果资源足够,则把线程封装成一个Node,设置为活跃线程进入CLH队列,并扣去资源
资源不足,则变成等待线程Node,也进入CLH队列
CLH是一个双向链式队列, head节点是实际占用锁的线程,后面的节点则都是等待线程所对应对应的节点

-
哪些类基于AQS实现
ReentrantLock、CountDownLatch、CyclicBarrier
Redis缓存相关
-
五种数据类型及其底层结构
string:底层数据结构是动态字符串,最大512m,可用于基础缓存
list:双向链表,底层结构是快速列表和压缩列表(quicklist+ziplist),可用于消息队列
hash:string类型的字段和属性映射表适合用于存储对象,redis是渐进式rehash扩容,底层数据是压缩列表和字典(ziplist+dict),可用于分布式锁
set:无序集合,底层是整数集合和字典(intset+dict),可用于打标签、点赞以及数据去重
zset:有序集合每个元素都会关联一个double类型的权重(score),底层结构是跳表和压缩列表(skiplist+ziplist)可用于权重排行
-
持久化方式的对比

AOF:日志追加,丢失的数据少、日志文件清晰 即每秒同步,每修改同步,不同步
对于相同数量的数据集而言,AOF文件通常要大于RDB文件。AOF在运行效率上往往会慢于RDB

RDB:文件唯一,执行效率高 全量备份,丢失的数据多 由于rdb是通过fork的子进程来协助完成数据持久化的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至一秒 丢失的数据多
redis4.0版本提出混合使用
-
过期策略和内存淘汰机制
过期策略:定时过期(消耗cpu)、惰性删除、定期淘汰(把key放入字典定期随机扫描)
采用惰性删除+定期淘汰策略
内存淘汰机制:不淘汰、随机淘汰、最少使用(LFU)、最远使用(LRU)
-
内存使用优化
缩短对象长度
共享对象池
字符串优化
控制key数量
-
集群模式及选举机制
集群模式:主从复制、哨兵模式、Cluster
主从复制

-
优点: 主从结构具有读写分离,提高效率、数据备份,提供多个副本等优点。
-
不足: 最大的不足就是主从模式不具备自动容错和恢复功能,主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预。
普通的主从模式,当主数据库崩溃时,需要手动切换从数据库成为主数据库:
哨兵模式

1.优点
-
哨兵模式是基于主从模式的,解决可主从模式中master故障不可以自动切换故障的问题。
2.不足-问题
-
是一种中心化的集群实现方案:始终只有一个Redis主机来接收和处理写请求,写操作受单机瓶颈影响。
-
集群里所有节点保存的都是全量数据,浪费内存空间,没有真正实现分布式存储。数据量过大时,主从同步严重影响master的性能。
-
Redis主机宕机后,哨兵模式正在投票选举的情况之外,因为投票选举结束之前,谁也不知道主机和从机是谁,此时Redis也会开启保护机制,禁止写操作,直到选举出了新的Redis主机。
全量复制和增量复制
选举机制:大于num/2+1数量,raft算法
Cluster

Redis Cluster集群具有如下几个特点:
-
集群完全去中心化,采用多主多从;所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
-
客户端与 Redis 节点直连,不需要中间代理层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
-
每一个分区都是由一个Redis主机和多个从机组成,分片和分片之间是相互平行的。
-
每一个master节点负责维护一部分槽,以及槽所映射的键值数据;集群中每个节点都有全量的槽信息,通过槽每个node都知道具体数据存储到哪个node上。
redis cluster主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用redis cluster,数据量不是很大时,使用sentinel就够了。redis cluster的性能和高可用性均优于哨兵模式。
Redis Cluster采用虚拟哈希槽分区而非一致性hash算法,预先分配一些卡槽,所有的键根据哈希函数映射到这些槽内,每一个分区内的master节点负责维护一部分槽以及槽所映射的键值数据。
-
redission分布式锁的实现原理
分布式锁的数据结构是hash
key:名字 字段:uuid+threadid 值:重入的次数
trylockinnerAsync:
key不存在 加锁字段,设置过期时间
key存在 字段存在 可重入数+1
key存在 字段不存在 抢锁失败
看门狗机制:key过期时间到了,线程没有执行完成,自动延长过期时间,让守护线程在一段时间后,重新去设置这个锁的LockTime
红锁机制:有半数以上节点操作完成才算加锁完成
-
redis缓存存在的问题及解决
缓存穿透:不存在的值一直请求,设置null和布隆过滤器
缓存击穿:缓存过期了大量请求进来,设置互斥锁,热点数据不过期
缓存雪崩:数据同一个时间过期,设置随机过期时间
布隆过滤器(Bloom Filter)是一个高空间利用率的概率性数据结构,由二进制向量(即位数组)和一系列随机映射函数(即哈希函数)两部分组成。
布隆过滤器使用exists()来判断某个元素是否存在于自身结构中。当布隆过滤器判定某个值存在时,其实这个值只是有可能存在(两个元素随机hash到同一个位置);当它说某个值不存在时,那这个值肯定不存在,这个误判概率大约在 1% 左右。
-
redis和memcached的区别
redis存储数据、memcached还可以存储图片或者视频
redis有数据备份,支持数据恢复,memcached不支持
redis可以作为消息队列和过滤器,memcached只适合存放临时性数据
-
redis队列的订阅和发布

Redis 的发布订阅机制包括三个部分:发布者,订阅者和 Channel。
发布订阅模式是无法持久化的,如果出现网络断开、Redis 宕机等,消息就会被丢弃
发布者和订阅者都是 Redis 客户端,Channel 则为 Redis 服务器端,发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。
Redis 的这种发布订阅机制与基于主题的发布订阅类似,Channel 相当于主题
客户端可以订阅频道

当给这个频道发布消息后,消息就会发送给订阅的客户端

聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式。
-
redis数据库个数
16
-
redis的skipList的原理

相关数据结构原理
Redis的五种数据结构的底层实现原理_redis数据结构的底层实现_张维鹏的博客-优快云博客

sds:动态字符串
① 可动态扩展内存。sds表示的字符串其内容可以修改,也可以追加。
② 采用预分配冗余空间的方式来减少内存的频繁分配,从而优化字符串的增长操作
③ 二进制安全(Binary Safe)。sds能存储任意二进制数据,而不仅仅是可打印字符。
④ 惰性空间回收,优化字符串的缩短操作。

zipList:是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作
ziplist的内存结构如下:
<zlbytes><zltail><zllen><entry>...<entry><zlend>
quickList:是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和ziplist的优点,是一个空间和时间的折中:
双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。
Redis提供了一个配置参数list-max-ziplist-size
intset:整数集合

skipList:多级索引+链表
跳表是一种可以进行二分查找的有序链表,采用空间换时间的设计思路,跳表在原有的有序链表上面增加了多级索引(例如每两个节点就提取一个节点到上一级),通过索引来实现快速查找。跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都为O(logn),空间复杂度为 O(n)。跳表非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
① 跳表的删除操作:除了要删除原始链表中的节点,还要删除索引中的节点。
② 插入元素后,索引的动态更新。我们在添加元素的时候,通过一个随机函数,同时选择将这个数据插入到部分索引层。比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第K级这K级的索引中

是允许出现重复的 scores
这里比较不仅是通过key(redis 中的 key 就是 score),还有实际存储的数据对比
支持后向指针,因此,是一个双端链表
即当列表元素个数大于128或者列表元素size大于64时,zset 会使用 skiplist 结构;反之会使用 ziplist 结构。
种检索效率与二分检索一致,因此时间复杂度 O(logN)
相信你也发现了,这种链表的检索效率达到了二分检索的速度,其本质是典型的空间换时间思想。

-
跳表和平衡二叉树
在server端,对并发和性能有要求的情况下,如何选择合适的数据结构(这里是跳跃表和红黑树)。
如果要更新数据,跳跃表需要更新的部分就比较少,锁的东西也就比较少,所以不同线程争锁的代价就相对少了。
红黑树在插入和删除的时候可能需要做一些rebalance的操作,这样的操作可能会涉及到整个树的其他部分
红黑树:
-
红黑树优点:不像是AVL一样追求极致平衡,减少了旋转次数,如果搜索,插入删除次数几乎差不多,红黑树还是比较适合的。
-
红黑树缺点:数据量大,树高度会很高导致查找速度慢”
跳表:
-
跳表的优点:因为是从上层主键比较,所以能像是有序数组的二分查找,提高查询效率
-
跳表的缺点:需要额外维护多个链表,暂用额外链表的空间
平衡二叉树:查询topn和中位数
-
redis5.0新增的stream
-
支持持久化。streams能持久化存储数据,不同于pub/sub机制和list 消息被消费后就会被删除,streams消费过的数据会被持久化的保存在历史中。
-
支持消息的多播、分组消费。 这一点跟 pub/sub有些类似。
-
支持消息的有序性,可以通过消息的ID来控制消息的排序。
-
支持消费者组。streams 允许同一消费组内的消费者竞争消息,并提供了一系列机制允许消费者查看自己的历史消费消息。并允许监控streams的消费者组信息,消费者组内消费者信息,也可以监控streams内消息的状态。

-
消费者组:Consumer Group,即使用XGROUP CREATE命令创建的,一个消费者组中可以存在多个消费者,这些消费者之间是竞争关系。
-
同一条消息,只能被这个消费者组中的某个消费者获取。
-
多个消费者之间是相互独立的,互不干扰。
-
消费者: Consumer 消费消息。
-
last_delivered_id: 这个id保证了在同一个消费者组中,一个消息只能被一个消费者获取。每当消费者组的某个消费者读取到了这个消息后,这个last_delivered_id的值会往后移动一位,保证消费者不会读取到重复的消息。
-
pending_ids:记录了消费者读取到的消息id列表,但是这些消息可能还没有处理,如果认为某个消息处理,需要调用ack命令。这样就确保了某个消息一定会被执行一次。
-
消息内容:是一个键值对的格式。
-
Stream 中 消息的 ID: 默认情况下,ID使用 * ,redis可以自动生成一个,格式为 时间戳-序列号,也可以自己指定,一般使用默认生成的即可,且后生成的id号要比之前生成的大。
若发现当前时间戳退后(小于latest_generated_id所记录的),则采用时间戳不变而序号递增的方案来作为新消息ID
-
redis高效和线程安全原理
高效:
(1) 绝大部分请求是纯粹的内存操作(非常快速)
(2) 采用单线程,避免了不必要的上下文切换和竞争条件
(3) 非阻塞IO - IO多路复用。IO多路复用中有三种方式:select,poll,epoll。需要注意的是,select,poll是线程不安全的,epoll是线程安全的
redis6.0改成多线程:
从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的消耗,Redis支持多线程主要就是两个原因:
1.可以充分利用服务器的CPU资源;
2.多线程任务可以分摊Redis同步IO读写负荷;
线程安全:
Redis中本身就是单线程的能够保证线程安全问题,操作指令原子性。
Redis6.0里面多线程默认是关闭的,需要在redis.conf文件里面修改io-threads-do-reads配置才能开启。
之所以指令执行不使用多线程,我认为有两个方面的原因。
内存的IO操作,本身不存在性能瓶颈,Redis在数据结构上已经做了非常多的优化。
如果指令的执行使用多线程,那Redis为了解决线程安全问题,需要对数据操作增加锁的同步,不仅仅增加了复杂度,还会影响性能,代价太大不合算。
-
redis集群配置
-
redis集群中至少应该有三个节点。要保证集群的高可用,所以每个节点需要一个备用机,所以搭建redis集群至少需要6台服务器。
-
redis渐进式rehash
为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0]里面的所有键值对全部 rehash 到 ht[1], 而是分多次、渐进式地将 ht[0]里面的键值对慢慢地 rehash 到 ht[1]。以下是哈希表渐进式rehash的详细步骤:
-
为ht[1]分配空间,让dict字典同时持有 ht[0] 和 ht[1] 两个哈希表。
-
在字典中维持一个索引计数器变量rehashidx,初始时值为-1,代表没有rehash操作,当rehash工作正式开始,会将它的值设置为0。
-
在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在 rehashidx索引(table[rehashidx]桶上的链表)上的所有键值对rehash到ht[1]上,当rehash工作完成之后,将rehashidx属性的值+1,表示下一次要迁移链表所在桶的位置。
-
随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有桶对应的键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。
-
redis单机并发量
Redis单点吞吐量单点TPS达到8万/秒,QPS达到10万/秒
QPS:应用系统每秒钟最大能接受的用户访问量每秒钟处理完请求的次数,注意这里是处理完,具体是指发出请求到服务器处理完成功返回结果
TPS:即服务器每秒处理的事务数,包括一条消息入和一条消息出,加上一次用户数据库访问
-
redis大key的删除处理
key对应的value值很大,如果String类型值大于10KB,Hash,Set,Zset,List类型的元素的个数大于5000个都可以称之为大key
存在的问题
-
客户端超时等待:由于Redis执行命令是单线程处理,然后在操作大key时会比较耗时,那么就会阻塞Redis,从客户端这一视角来看就是很久很久都没有响应
-
引发网络阻塞:每次获取大key产生的流量较大,如果一个key的大小是1MB,每秒访问量为1000,那么每秒会产生1000MB的流量这对于普通千兆网卡是灾难的
-
阻塞工作线程:如果使用del删除大key,会阻塞工作线程这样就没有办法处理后续的命令
-
内存分布不均匀:集群模型在slot分片均匀的情况下,会出现数据和查询倾斜情况,部分有大key的Redis节点占用内存多,QPS比较大
定位大key:redi-cli --bigkeys,使用SCAN命令查找大key
解决方案:
-
分批次删除
使用SCAN扫描key,比如删除Hash,先取100字段删除删除再取
-
异步删除
在Redis4.0版本开始,可以采用异步删除法用unlink命令代替del删除
这样Redis会将这个key放入到一个异步线程中进行删除,这样不会阻塞主线程
-
被动删除
开启 lazy free 机制删除
避免大key的出现
-
对大key进行拆分
将一个Big Key拆分为多个key-value这样的小Key,并确保每个key的成员数量或者大小在合理范围内,然后再进行存储,通过get不同的key或者使用mget批量获取。
-
对大key进行清理
对Redis中的大Key进行清理,从Redis中删除此类数据。Redis自4.0起提供了UNLINK命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的Key,通过UNLINK,你可以安全的删除大Key甚至特大Key
-
监控Redis内存、网络带宽、超时等指标
通过监控系统并设置合理的Redis内存报警阈值来提醒我们此时可能有大Key正在产生,如:Redis内存使用率超过70%,Redis内存1小时内增长率超过20%等。
-
压缩value
使用序列化、压缩算法将key的大小控制在合理范围内,但是需要注意序列化、反序列化都会带来一定的消耗。如果压缩后,value还是很大,那么可以进一步对key进行拆分。
Mysql数据库相关
-
事务特性
原子性、隔离性、一致性、持久性
-
事务的隔离级别
读未提及:造成脏读(数据没了)、不可重复读(一个事物中两次查询不一致)、幻读(两次查询的数据条数不一致)
读已提交:不可重复读(一个事物中两次查询不一致)、幻读(两次查询数据条数不一致)
可重复读:幻读(两次查询的数据不一致)
串行化:效率很慢
-
可重复读的实现方式
基于undolog版本链和mvcc的readview实现
readview的组成部分:
m_ids:当前以哪些事务在执行,且还没有提交
min_trx_id:指m_ids中最小的值
max_trx_id:指下一个要生成事务的id,比现在所有的事务id都大
creator_trx_id:每开启一个事物都会生成一个readview,这个就是生成readview的事务id
事务A读第一次是 值X 生成readview undolog生产一条日志,并让事务A的roll_pointer指向上一个undolog日志
事务B修改值X为值B
事务A第二次读,根据readview的可见性规则发现存在事务B的事务id大于自己,接着通过roll_pointer往下找,找到事务B操作之前的undolog日志,获取到事务B修改之前的数据
-
事务提交的流程

undolog:执行之前的数据、redolog:执行之后的数据样子、binlog:整个表的操作记录
mysql查询执行之前的数据,存储带缓存池写入undolog,更新在bufferpool中,将数据添加到redologbuffer中
提交事务:
将redologbuffer数据写入redolog中
更新数据记录,缓存操作并异步刷盘
将本次操作写入binlog
redolog在添加commit标记
redo log和binlog的区别
-
redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用
-
redo log是物理日志,记录的是“在XXX数据页上做了XXX修改”;binlog是逻辑日志,记录的是原始逻辑,其记录是对应的SQL语句
-
redo log是循环写的,空间一定会用完,需要write pos和check point搭配;binlog是追加写,写到一定大小会切换到下一个,并不会覆盖以前的日志
-
innodb和myisam的区别
innodb:b+tree,支持事务、外键、索引,并通过mvcc来支持高并发
myisam:支持全文检索、压缩,不支持事务和、行级锁和外键,索引和数据分开存储
-
数据页:页是MySQL中磁盘与内存交互的基本单位,默认是16KB,相当于4个磁盘块。意味着MySQL每读取一次数据就是16KB,这个值由innodb_page_size控制,可以更改。
-
预读:当发生一次磁盘IO时,不光是读取当前磁盘地址的数据,还会把相邻的数据也读取到内存缓冲区中。
-
mysql索引的原理
主键索引:一个b+tree,叶子节点存储数据
辅助索引:叶子节点存储主键值
-
索引覆盖和回表
索引覆盖:查询的数据全部在索引上
回表:查询的数据不全在索引上,需要回主键树在查询一次
-
sql优化流程
建表优化:字段非空,少用长字段类型,text类型优先放在子表里面
数据量优化:读写分离(数据冗余)、分库分表
查询优化:int和notin用exists替代,三张表以上不要联表查询,避免查询数据条数过多,like右%
-
索引规范场景
建立索引:标识度高、非空字段、优先使用联合索引、查询频率高的字段
使用索引:最左匹配原则、小表join大表、关联字段加索引且设置同样的类型和长度、索引列不要有运算符!=、索引列不要使用函数、like右%
索引列使用is null 或者 is not null不一定失效、使用>或者<不一定失效、使用in不一定失效
索引列使用or时要两边都有索引才生效
IN通常是走索引的,当IN后面的数据在数据表中超过30%的匹配时是全表扫描,不走索引,因此IN走不走索引和后面的数据量有关系
-
索引怎么支持范围查询
定位到id=的数字 直接捞出来后面的 直接走指针 不回表

如上索引图需要找到 id >= 20 and < 49的数据
1、加载根数据页到内存
2、在内存中做二分,找到对应的子页
3、在子页做二分,找到对应的子页
4、现在到了叶子节点页,在页中做二分,找到第一条满足的数据,这里是 id = 20
5、一直通过叶子节点的链表指针,找到第一条不满足的为止,这里是 id = 49
6、结束查找,返回数据
-
select count(*)、select count(1)和select count(字段)的区别
没有主键select *比select 1慢
有主键select 主键最快,有二级索引 通过占用空间最小的二级索引字段进行统计
select *和select 1都包含对null字段的统计, select 字段不包含null字段的统计
-
mvcc和主从复制
mvcc:多版本控制,通过对每个事务生成事务id和ReadView,配合undolog日志,根据readview的可见性规则保证查询数据的准确性,解决了不可重复读的问题
主从复制:binlog线程,io线程、sql线程
-
分库分表的方式和实现
阿里云drds:sql解析,通过drds路由,执行逻辑并归并查询结果
shardingsphere: sql解析生产语法树生成分片需要的上下文,路由解析,执行逻辑,归并结果
分库分表的id:
步长:控制简单,请求散列不均匀,容易集中到某一台服务器
hash一致性:扩容性rehash,散列均匀
可以通过异构复制,用空间换时间,冗余多写
分布式事务问题,可以通过事务拆分,或者异步事务及消息补偿来保证数据一致性
-
mybatis相关
mybatis:一个半ORM框架,封装了jdbc的加载驱动、创建connection连接、创建statement、创建SqlSessionFactory、创建SqlSession等一系列过程。通过sql映射xml文件或者注解的方式,支持动态sql查询。
#{}和${}的区别:${}是单纯的变量替换,容易造成sql注入。#{}是设置值为一个变量文本,加上双引号,没有sql注入问题
mybatis执行sql的过程:
-
读取配置文件
-
加载映射文件
-
创建sqlSessionFactory和sqlSession
-
执行器Executor操作数据库封装返回结果
mybatis缓存:
一级缓存:sqlSession级别的缓存,一次sqlSession缓存是一次与数据库的对话,当前会话再次发出同样的sql查询时,会直接走一级缓存,缓存是hashmap结构,key是mapperId+offset+limit+sql+所有入参,value是查询结果,如果出现DmlSql(insert、update、delete)时,一级缓存会被清除,一级缓存最多缓存1024个sql的查询结果
二级缓存:mapper(namespace)对象级别的缓存,可以跨sqlSession使用,缓存是hashmap结构,key是mapperId+offset+limit+sql+所有入参,value是查询结果
如果二级缓存开启:查询会先走二级缓存,再走一级缓存,在查数据库,写入会先写一级缓存,再写二级缓存,再返还用户
缓存更新:如果当前作用域的mapper出现了写操作(insert、update、delete)时,默认情况是该作用域下所有的缓存都会被清除。如果开启了二级缓存,也可以根据配置选择哪些缓存刷新
mybatis延迟加载:可以先查询主表,按需做关联查询,返回结果集
延迟加载的实现:CGLIB为目标对象建立代理对象,当调用目标对象的方法时进入拦截器方法
-
mvcc支持哪几种隔离级别
读已提交和可重复读
-
主键索引和普通索引哪个快

主键索引和普通索引的区别是:主键索引叶子节点存放的是数据,普通索引叶子结点存放的是主键id,主键索引是聚簇索引,普通索引是非聚簇索引
主键索引快,主键唯一且有序
-
为啥主键选择整型和自增
之所以不使用字符串类型的 UUID 作为主键,主要有以下几个方面的考虑
整形的 int 存储是 4 个字节,bigInt 是 8 个字节,假设我们采用的是 UTF-8 字符集, UUID 它所占的字节数是 2 + 3 * 32 = 110 字节(前面 2 个字节用来存储字符串长度,后面每个字符 3 个字节).相较而言,一个整数只有 4 个字节,相差 25 倍.数据库采用 B+ 树索引,其中主键索引的叶子节点指向数据行,而二级索引的叶子节点存储着主键,之后再通过主键索引回表查数据.也就是说,有多少个二级索引,主键就需要被存储几次,因此,索引的空间需求就极速扩张了,还有就是使用整形的主键可以在一个 page 中存储更多的主键,跨 page 遍历的次数就会更少
使用非自增主键坏处
插入数据时:由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL不得不为了将新记录插到合适位置而移动数据,增加系统开销。同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。
-
分库分表数据库异构索引
精卫是一个基于Mysql的实时数据复制框架,也可以认为是一个Mysql的数据触发器+分发管道。
精卫通过抽取器(Extractor)获取到订单数据创建在Mysql数据库中产生的binlog日志,并转换为event对象,然后通过过滤器Filter(比如字段过滤、转换等)或基于接口自定义开发的过滤对event对象中的数据进行处理,最终对分发器Applier将结果转换为发给DRDS的sql语句

-
分库分表hash的扩容

动态扩容:rehash、部分数据rehash、
切换的时间节点:数据双写,部分流量打入
-
分表后怎么保证id唯一
雪花算法+业务适配、id取模
分库分表组件-ShardingShpere
-
数据库的ACID
-
A原子性(atomicity) 由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
-
C一致性(consistency) 一般由代码层面来保证
-
I隔离性(isolation) 由MVCC来保证
-
D持久性(durability) 由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复
-
mysql的锁
MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类
全局锁:全局锁的典型使用场景是,做全库逻辑备份 Flush tables with read lock (FTWRL)
表锁:表锁的语法是 lock tables … read/write 元数据锁:MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是防止DDL和DML并发的冲突,保证读写的正确性
行锁:锁定粒度最小,发生锁冲突的概率最低,并发度最高。会发生在InnoDB 存储引擎
InnoDB 实现了标准的行级锁,也就是共享锁(Shared Lock)和排他锁(Exclusive Lock)。
共享锁(S):多个事务可以一起读,共享锁之间不互斥,共享锁会阻塞排它锁,可以通过 lock in share mode 语句显示使用共享锁。
排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享锁和排他锁。对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会自动给涉及的数据集加排他锁,或者使用 select * for update 显示使用排他锁。
只有在自动提交被禁用时,FOR UPDATE才可以锁定行,若开启自动提交,则匹配的行不会被锁定。
拿不到锁的事务会一直阻塞到拿到锁或者锁超时为止,可以通过set [global|session] innodb_lock_wait_timeout = 10;来设置锁超时时间。
InnoDB 行锁是通过对索引数据页上的记录(record)加锁实现的,所以InnoDB只有在通过索引条件检索数据时使用行级锁,否则使用表锁。
主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。
Record Lock 锁: 单个行记录的锁(锁数据,不锁 Gap)。
Gap Lock 锁: 间隙锁,锁定一个范围,不包括记录本身(不锁数据,仅仅锁数据前面的Gap)。
Next-key Lock 锁: 同时锁住数据,并且锁住数据前面的 Gap(为了解决幻读问题)。
间隙锁引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”。当互相持有想插入记录的间隙锁时,会发生死锁。
如何避免死锁的产生呢?这里给出一些建议:
-
加锁顺序一致(把最可能造成锁冲突、最可能影响并发度的锁尽量往后放);
-
尽量基于 primary 或 unique key 更新数据;
-
单次操作数据量不宜过多,涉及表尽量少;
-
减少表上索引,减少锁定资源。
-
跨区域的数据同步
主从配置:基于binlog的数据同步
id冲突问题解决:hash步长id,雪花算法
-
数据量大分页查询的优化
mysql中的limit offset,count的原理是先取出offset+count条记录,然后抛弃前面offset条,然后读后面的count条,主要是offset的问题。所以会导致偏移量越大,性能越差
SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset
第一个参数指定第一个返回记录行的偏移量,注意从0开始
第二个参数指定返回记录行的最大数目
如果只给定一个参数:它表示返回最大的记录行数目
第二个参数为 -1 表示检索从某一个偏移量到记录集的结束所有的记录行
初始记录行的偏移量是 0(而不是 1)
优化方案:
使用子查询优化
使用id限定优化
使用临时表优化
条件过滤优化:时间范围、状态等
-
sql关键字执行顺序
FROM → ON → JOIN → WHERE → GROUP BY → HAVING → SELECT →DISTINCT → ORDER BY→ LIMIT
Spring相关
-
spring启动过程
主要分为三个部分
-
构建springBootApplication实例 设置初始化相关参数(环境变量、资源、监听器等)
-
执行SpringBootApplication.run()方法
-
设置启动计时器,初始化监听器
-
创建环境对象,设置环境信息、系统属性、输入参数和profile信息
-
获取beanFactory,解析配置文件
-
获取类加载器,注册非懒加载的指定的bean
-
注册beanFactory的后置处理器并执行
-
注册bean的后置处理器 aop
-
初始化国际化相关配置,完成beanFactory的初始化
-
监听器发布初始化完成事件,停止计时器,完成上下文的刷新
-
springmvc的分发过程
请求进入dispatchServlet转发到映射器返回处理器映射
转发到适配器 处理请求返回modeAndView
转发到视图解析器 解析后返回view
转发到view 渲染后返回响应结果
-
bean的生命周期

-
解析bean定义,确定构造方法
-
实例化,设置属性值
-
检查是否Aware相关接口,如果存在则填充相关的资源(业务按需要实现特定的Aware接口,spring容器会主动找到该bean,然后调用特定的方法,将特定的参数传递给bean)
-
执行bean的前置处理器
-
init- method方法
-
执行bean的后置处理器
-
放入缓存池中
-
容器销毁,调用destory-method方法回收
-
spring几种bean的作用域
bean的作用域:
单例:无状态的bean,默认的
原型:有状态的bean,每次都会获取新装载的bean
请求:基于http请求的,限于springmvc使用
会话:基于httpSession,限于springmvc使用
全局:一个上下文中定义一个,记录上下文信息的,限于springmvc使用
-
spring循环依赖的解决方式
提前曝光和三级缓存
A初始化 提前曝光在三级缓存中 然后getB
B初始化一级级缓存去查找A 在三级缓存中找到 B完成初始化 然后A也完成初始化
三级缓存:
一级是成熟的bean
二级是刚实例化未填充属性的bean
三级是代理的bean

三级缓存无法解决构造方法的循环依赖,可以用懒加载处理。spring是构造出了b的代理对象,在真正用到b对象时,b的代理对象才会去单例池中寻找b对象,去实现方法
-
spring为啥用三级缓存
/**
*一级缓存:单例池
*存放已经初始化的bean——成品
*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
/**
*三级缓存:单例工厂的高速缓存
*存放生成bean的工厂
*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
/**
*二级缓存:早期单例对象的高速缓存
*存放已经实例化但未初始化(未填充属性)的的bean——半成品
*/
private final Map<String, Object> earlySingletonObjects = new HashMap(16);
只用一级缓存:一级缓存是可以解决一些简单的循环依赖问题的,但是在Spring中,会变得特别麻烦,要做太多的处理,容易出现 npe的问题
只用一二级缓存:考虑到spring aop的特性,不能一开始就创建所有的类,这样会消耗性能和内容,所以在增强的时候,进行创建,需要用到工厂的缓存只用了二级缓存,也出现了早期被暴露的 bean 和最终的 bean 不一致的问题所参数的异常。二三级缓存联合使用搭配保证多个关联对象对当前bean的引用为同一个
-
Aop的创建过程
注册bean的后置处理器时实现
-
创建aop代理类
-
创建业务逻辑和切面组件
-
切面方法包装成增强器advisor,给业务组件创建一个代理对象
-
得到目标方法的拦截器链,利用连接器的链式机制,依次进入拦截器执行
-
前置通知-目标方法-后置通知-返回通知/异常通知
-
Aop的常用注解
@Aspect: 该注解是把此类声明为一个切面类。
@Before: 该注解是声明此方法为前置通知 (目标方法执行之前就会先执行被此注解标注的方法)
@After: 该注解是声明此方法为后置通知 (目标方法执行完之后就会执行被此注解标注的方法)
@AfterReturning: 该注解是声明此方法为返回通知 (目标方法正常执行返回后就会执行被此注解标注的方法)
@AfterThrowing: 该注解是声明此方法为异常通知 (目标方法在执行出现异常时就会执行被此注解标注的方法)
@Around: 该注解是环绕通知是动态的,可以在前后都设置执行 ProceedingJoinPoint point.proceed();
@PointCut: 该注解是声明一个公用的切入点表达式(通知行为的注解的都可以直接拿来复用)
-
Aop通知执行的顺序
从Spring5.2.7开始,在相同@Aspect类中,通知方法将根据其类型按照从高到低的优先级进行执行:@Around,@Before ,@After,@AfterReturning,@AfterThrowing。
-
around before...
-
before...
-
add...
-
afterReturning...
-
after...
-
around after...
在Spring5.2.7之前,Spring AOP 遵循与 AspectJ 相同的优先级规则来确定通知执行的顺序。
-
around before...
-
before...
-
add...
-
around after...
-
after...
-
afterReturning...
-
spring事务的实现方式及传播机制
事务的实现方式:声明式事务(@Transcation),编程式事务
如果方法加上@Transcation,代理逻辑会把事务的自动提交设置成false,没有出现异常则提交事务,出现了异常就回滚
隔离级别:读未提交、读已提交、可重复读、串行化
传播机制:
-
有就加入没有就创建
-
有就加入没有就非事务
-
有就加入,没有就异常
-
每次以新事务
-
非事务执行,挂起当前事务
-
不使用事务,有就异常
-
有事务就嵌套,没有就创建
-
spring事务失效的几种方式
-
发生自调用,调用本类的方法 :引入自身bean 避免方式springaop增强自调用
-
方法不是public
-
数据库不支持事务
-
异常被吃掉
-
spring使用了哪些设计模式
单例模式:单例bean
工厂模式:bean工厂
适配器模式:controller适配
装饰器模式:增加功能呢
代理模式:springaop
观察者模式:监听器的实现
策略模式:根据策略执行对应方法
模版模式:父类定义骨架,子类实现
-
动态代理和cglib区别
动态代理:实现JDk中InvocationHandler接口的invoke方法,然后通过Proxy的newProxyInstance生成一个代理对象。基于反射的,效率低,执行的时候才知道逻辑
cglib:找到A中所有非final的public方法定义,将定义转化成字节码,将组成的字节码转化成相应的代理对象,实现MethodInterceptor用来处理对代理类上所有方法的请求。原理是对目标类通过字节码增强继承生成一个子类,需要第三方提供
-
spring的自动装配和springboot的自动装配
spring的自动装配:byName,byType @Qualifier+@Autowire=byName
-
组件扫描(component scanning):spring会自动发现应用上下文中所创建的bean;
-
自动装配(autowiring):spring自动满足bean之间的依赖,也就是我们说的IoC/DI;
springboot的自动装配:
从spring.factories文件中获取到对应的需要进行自动装配的类,并生成相应的Bean对象,然后将它们交给spring容器来帮我们进行管理
-
starter如何被启动加载,starter和spi的区别
通过springboot的自动装配

-
springBootApplication.run()启动时,@EnableAutoConfiguration开启了自动装配,那么spring启动的时候,就会扫描该包下及其子包下被@Controller、@Service和@Component标注的类,并将这些类注入到上下文容器中。
-
@Import的SpringFactoriesLoader.loadFactoryNames,其中loadSpringFactorie会从META-INF/spring.factories文件中读取配置,将其封装为Properties对象,将每个key作为返回的map的key,将key对应的配置集合作为该map的value。
SpringBoot利用SpringFactoriesLoader将spring.factories内容映射为Properties,利用反射实例化Bean并注入进容器,来实现组件的动态插拔,实现解耦。jdk借助于ServiceLoader读取指定路径。
在是否实例化实现类的层面上,SpringBoot会依据Conditional注解来判断是否进行实例化并注入进容器中,而jdk会在next方法内部懒加载实现类。
-
jdk的spi、dubbo的spi和spring的spi
spi:是Service Provider Interface。就是服务提供者接口,是一种寻找服务实现的机制。
jdk的spi:ServiceLoader,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好
无法确认具体加载哪一个实现,也无法加载某个指定的实现,仅靠ClassPath的顺序是一个非常不严谨的方式
dubbo的spi:通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下
①配置文件采用键值对配置的方式,使用起来更加灵活和简单 ② 增强了原本SPI的功能,使得SPI具备ioc和aop的功能
@SPI("dubbo") 通过SPI机制提供实现类,实现类是通过将dubbo作为默认key去配置文件里找到的,配置文件名称为接口全限定名,通过dubbo作为key可以找到默认的实现类org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
Dubbo SPI 和 JDK SPI 最大的区别就在于支持“别名”
“加载优先级”,优先加载内置(internal)的,然后加载外部的(external),按优先级顺序加载
spring的spi:Spring 的 SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单
JDK SPI | DUBBO SPI | Spring SPI | |
文件方式 | 每个扩展点单独一个文件 | 每个扩展点单独一个文件 | 所有的扩展点在一个文件 |
获取某个固定的实现 | 不支持,只能按顺序获取所有实现 | 有“别名”的概念,可以通过名称获取扩展点的某个固定实现,配合Dubbo SPI的注解很方便 | 不支持,只能按顺序获取所有实现。但由于Spring Boot ClassLoader会优先加载用户代码中的文件,所以可以保证用户自定义的spring.factoires文件在第一个,通过获取第一个factory的方式就可以固定获取自定义的扩展 |
其他 | 无 | 支持Dubbo内部的依赖注入,通过目录来区分Dubbo 内置SPI和外部SPI,优先加载内部,保证内部的优先级最高 | 不仅仅局限于接口/抽象类,可以是任何类,接口和注解 |
-
spring boot的starter机制和如何自定义starter
只需要在maven中引入starter依赖,SpringBoot就能自动扫描到要加载的信息并启动相应的默认配置
根据spring的spi和自动装配机制实现
自定义starter:
-
实现功能
-
添加Properties
-
添加@EnableConfigurationProperties(xxx.properties)
-
添加spring.factory 在META-INF下创建spring.factory文件
-
maven install
-
spring加载自定义配置文件
@Component
@EnableConfigurationProperites
@Value(文件地址)
-
spring的异常处理机制
spring提供了一种使用ControllerAdvice处理异常的非常有用的方法。通过实现一个ControlerAdvice类,来处理控制类抛出的所有异常。
-
过滤器和拦截器的区别
过滤器-拦截器
-
过滤器依赖于servlet,基于函数回调 拦截器基于反射机制
-
过滤器不能调用IOC里的bean,拦截器可以
-
过滤器,拦截器拦截的是URL。AOP拦截的是类的元数据(包、类、方法名、参数等)
-
tomcat
Tomcat本身就是一个servlet容器,所以Catalina就是tomcat的核心。其它的模块都是为Catalina容器进行提供服务的

两部分核心:
-
处理socket连接,负责处理网络请求 - Connector
-
加载和管理servlet,以及处理request请求 - Container

Container包含了Engine、Host、Context、Wapper,它们不是平行关系,而是父子关系。
Engine:表示整个Catalina的Servlet的引擎,一个Service只能包含一个Engine
Host:代表一个虚拟主机或者一个站点,可以个给Tomcat配置多个虚拟主机
Context:代表一个web应用,一个应用可以有多个Context
Wapper: 代表一个Servlet
-
spring的热部署
-
Spring Loaded
-
Spring-boot-devtools
-
spring security

-
基于Filter技术实现?
首先SpringSecurity是基于Filter技术实现的。Spring通过DelegatingFilterProxy建立Web容器和Spring ApplicationContext的联系,而SpringSecurity使用FilterChainProxy 注册SecurityFilterChain。
微服务相关
-
springcloud组件
feign
Feign是一个HTTP请求的轻量级客户端框架。通过 接口+注解 的方式发起HTTP请求的调用,而不是像Java中通过封装HTTP请求报文的方式直接调用。

Feign执行流程:
在主程序上添加@EnableFeignClients注解开启对已经加@FeignClient注解的接口进行扫描加载
调用接口中的方法时,基于JDK动态代理的方式,通过InvokeHandler调用处理器分发远程调用,生成具体的RequestTemplate
RequestTemplate生成Request请求,结合Ribbon实现服务调用负载均衡策略
Feign最核心的就是动态代理,同时整合了Ribbon和Hystrix,具备负载均衡、隔离、熔断和降级功能
gateway
网关、路由信息、权限校验、限流熔断、请求重试、监控统计

当 请求方 发送一个请求到达 gateway 时,gateway 根据 配置的路由规则,找到 对应的服务名称。
当某个服务存在多个实例时,gateway 会根据 负载均衡算法(比如:轮询)从中挑选出一个实例,然后将请求 转发 过去。
服务实例返回的的响应结果会再经过 gateway 转发给请求方。

gateway 通过 过滤器链 进行具体逻辑处理,并且开发者也可以实现自己的过滤器,然后插入到过滤器链中的某个位置, 从而对请求和响应进行加工。
Spring Webflux 的底层是基于 Netty 和 Reactor, 可以有效的提升网关的性能。
eureka

-
客户端发起服务注册 客户端在启动时首先找到服务端以及自身的信息(分区、服务名称、IP、端口等),调用服务端提供的服务注册接口,将自身的信息发送过去。
-
服务端保存注册信息到注册表 Eureka的注册表是一个双字典结构的数据,服务发现的目的是标识服务和服务状态的管理,所以注册表中有服务标识、服务基本信息、服务状态信息等
-
客户端定时发生心跳检测 默认每30秒发送一次心跳
-
服务端服务剔除及自我保护 90秒内Eureka Server未收到续约,则进行服务剔除 。服务端判断在15分钟内,有超过85%的客户端都没有进行服务续约,则进入自我保护;
进入自我保护机制,服务端不在剔除没有续约的客户端;
进入自我保护机制,服务端只接收新客户端的注册和服务查询;
-
客户端发起服务下线 优雅退出时会发送cancel命令 服务端收到cancel命令时会删除该节点
-
客户端或者服务端注册信息到本地内存 定时向服务端发送请求,获取注册表信息,保存到本地内存
eureka的集群模式:主从模式和对等模式
主从模式:主副本面临所有的写操作压力,可能会成为瓶颈,但是可以保证一致性
对等模式:存在数据同步和数据冲突
eureka支持region(亚洲分区、华北分区等)做分,zone做具体分区下的机房(北京分区下有两个机房):可以实现不同区域不同机房的服务进行就近调用,降低延迟
ribbon
负载均衡算法

hystrix
防止雪崩效应,必须有一个强大的容错机制。该容错机制需实现以下两点:
包裹请求:使用 HystrixCommand 包裹对依赖的调用逻辑,每个命令在独立线程中执行
断路器(跳闸)机制:当某服务的错误率超过一定阈值时,hystrix 可以自动或者手动跳闸,停止请求该服务一段时间
资源隔离:hystrix 为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定
回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑
自我修复:断路器打开一段时间后,会自动进入半开状态

当请求达到阈值时候:默认 10 秒内 20 次请求,当请求失败率达到 50% 时,此时断路器将会开启,所有的请求都不会执行
断路器开启 5 秒(默认)时,这时断路器是半开状态, 会允许其中一个请求执行
如果执行成功,则断路器会关闭;如果失败,则继续开启。循环重复这两个流程
Hystrix提供两个命令,分别是HystrixCommand、HystrixObservableCommand,通常是在Spring中通过注解和AOP来实现对象的构造
熔断:触发熔断直接降级返回信息,在设定时间后尝试恢复。默认触发熔断器的条件为:最近一个时间窗口(默认为10秒),总请求次数大于等于circuitBreakerRequestVolume Threshold(默认为20次)并且异常请求比例大于circuitBreakerError ThresholdPercentage(默认为50%),此时熔断器触发
服务熔断的核心是断路器(跳闸),没有断路器(配置)的熔断那就不是熔断了
服务熔断也会触发服务降级回退方法的
服务熔断的配置:回退,兜底方法 + 断路器配置,二者缺一不可
服务降级的配置:回退,兜底方法
隔离:请求达到设置的大小后走降级逻辑。分为两种:线程池隔离和信号量隔离。线程池隔离指的是每个服务对应一个线程池,线程池满了就会降级;信号量隔离是基于tomcat线程池来控制的,当线程达到某个百分比,走降级流程
降级:作为Hystrix中兜底的策略,当出现业务异常、线程池已满、执行超时等情况,调用fallback方法返回一个错误提示
增加注解 @HystrixCommand并通过fallbackMethod参数指定断路后执行的方法
定义断路处理方法,返回服务/操作断路后的提示
断路器可以防止一个应用程序多次试图执行一个操作,即很可能失败,允许它继续而不等待故障恢复或者浪费 CPU 周期,而它确定该故障是持久的。断路器模式也使应用程序能够检测故障是否已经解决。如果问题似乎已经得到纠正,应用程序可以尝试调用操作

-
springcloud和dubbo的区别

springcloud是微服务架构下的一站式解决方案,dubbo是soa时代的产物,用于服务调用和服务治理
springcloud是依托于spring平台的,生态广泛,dubbo是RPC调用的,生态匮乏
springcloud是rest风格依靠于http,带宽大, dubbo是rpc请求,带宽小
-
springcloud和dubbo调用流程

springcloud服务注册
-
各个微服务启动时,将自己的网络地址、路由等信息注册到服务发现组件中,服务发现组件会存储这些信息
-
服务消费者可从服务发现组件查询服务提供者的网络地址,并使用该地址调用服务提供者的接口
gateway:设置路由相关信息
A想要调用服务B时,使用feign,将请求发送到gateway,gateway根据请求url去nacos服务列表中找到服务B的ip转发请求
springcloud服务调用
-
通过解析路由和接口信息找到对应的服务
-
在注册中心负载均衡到对应的服务器
-
通过熔断、隔离和降级机制后通过代理方式执行对应服务器接口

dubbo服务注册
-
首先服务的提供者启动服务时,将自己的具备的服务注册到注册中心,其中包括当前提供者的ip地址和端口号等信息,Dubbo会同时注册该项目提供的远程调用的方法
-
消费者(使用者)启动项目,也注册到注册中心,同时从注册中心中获得当前项目具备的所有服务列表
-
当注册中心中有新的服务出现时,会通知已经订阅发现的消费者,消费者会更新所有服务列表
-
RPC调用,消费者需要调用远程方法时,根据注册中心服务列表的信息,只需服务名称,不需要ip地址和端口号等信息,就可以利用Dubbo调用远程方法了
dubbo服务调用
-
先客户端调用接口的某个方法,实际调用的是代理类
-
代理类会通过 cluster(默认的 cluster 是 FailoverCluster,会进行容错重试处理)从 directory 中获取一堆 invokers(如果有一堆的话),然后进行 router 的过滤(其中看配置也会添加 mockInvoker 用于服务降级),然后再通过 SPI 得到 loadBalance 进行一波负载均衡
-
根据具体的协议构造请求头,然后将参数根据具体的序列化协议序列化之后构造塞入请求体中,再通过 NettyClient 发起远程调用
支持的协议:dubbo、http、hessian、rmi、webservice
-
服务端 NettyServer 收到请求之后,根据协议得到信息并且反序列化成对象,再按照派发策略派发消息,默认是 All,扔给业务线程池
-
all所有消息都派发到线程池,包括请求,响应,连接事件,断开事件等
-
direct所有消息都不派发到线程池,全部在 IO 线程上直接执行
-
message只有请求和响应消息派发到线程池,其它消息均在 IO 线程上执行
-
execution只有请求消息派发到线程池,不含响应。其它消息均在 IO 线程上执行
-
connection在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池
-
业务线程会根据消息类型判断然后得到 serviceKey 从之前服务暴露生成的 exporterMap 中得到对应的 Invoker ,然后调用真实的实现类。
最终将结果返回,因为请求和响应都有一个统一的 ID, 客户端根据响应的 ID 找到存储起来的 Future, 然后塞入响应再唤醒等待 future 的线程,完成一次远程调用全过程
-
dubbo常见面试题
安全机制:通过token鉴权作内部处理
默认是同步等待结果阻塞的,支持异步调用,Dubbo 是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个 Future 对象
-
dubbo的超时机制
Dubbo有三个级别的超时设置分别为:
①针对方法设置超时时间
②在服务方设置超时时间
③在调用方设置超时时间
超时是针对消费端仍是服务端?
-
若是是争对消费端,那么当消费端发起一次请求后,若是在规定时间内未获得服务端的响应则直接返回超时异常,但服务端的代码依然在执行。
-
若是是争取服务端,那么当消费端发起一次请求后,一直等待服务端的响应,服务端在方法执行到指定时间后若是未执行完,此时返回一个超时异常给到消费端。
dubbo的超时是争对客户端的,因为是一种NIO模式,消费端发起请求后获得一个ResponseFuture,而后消费端一直轮询这个ResponseFuture直至超时
超时的实现原理是什么?
以前有简单提到过, dubbo默认采用了netty作为网络组件,它属于一种NIO的模式。消费端发起远程请求后,线程不会阻塞等待服务端的返回,而是立刻获得一个ResponseFuture,消费端经过不断的轮询机制判断结果是否有返回。由于是经过轮询,轮询有个须要特别注要的就是避免死循环,因此为了解决这个问题就引入了超时机制,只在必定时间范围内作轮询,若是超时时间就返回超时异常。
-
eureka、nacos和zookeeper的区别


zookeeper客户端到server是tcp长连接请求
eureka是定时发送和服务进行联系的
nacos是netty和服务进行连接,属于长链接
zookeeper是cp
eureka是ap
naocs分cp和ap两种,根据配置ephemeral=true即为临时节点,那么Naocs集群对这个client节点效果就是AP,否则是cp
nacos提供了根据namespace隔离环境的功能,namespace下有group和service,不同的namespace相互隔离,服务互不可见
naocs提供临时实例和非临时实例
临时实例:如果服务宕机,则从服务列表中剔除
非临时实例:如果服务宕机,不从服务列表中剔除
nacos和eurake也有服务注册、服务拉取和心跳检测功能,但存在一些差异
-
zookeeper
zookeeper是基于文件系统和监听通知机制,观察者模式
-
有四种类型的znode:
1、PERSISTENT-持久化目录节点
指client和zookepper断开链接之后,client在zookepper上面创建的节点不会被zookepper删除,还会继续保留
2、PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点
比如,一个user模块部署了多台服务器,在zookepper上面进行注册自己服务,zookepper会对user模块进行创建持久性顺序节点,当第一个节点宕机之后,会按照顺序取下一个节点
3、EPHEMERAL-临时目录节点
client在zookepper上面进注册自己的服务,当服务宕机之后,zookepper会进行删除创建的节点
4、EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点
第四种情况和第二种情况很像,只不过,服务宕机之后就会剔除服务,不会永久保留
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。
zookeeper的选举机制:

三个节点交换信息后,事务ID都为0,那么就需要找节点ID了,最大为3,所以另外两个节点就需要修改投票信息

选举原则:事务ID最大当选,事务ID一致则节点ID最大的当选。
服务器1启动,因当前仅一台服务器,发出的请求没有响应,状态为LOOKING,且默认都将票投给自己;
服务器2启动,此时服务器1与2建立连接,并且myid服务器2大于1,所以服务器2成为leader的票数为2,但任没有达到过半机制,状态都还为LOOKING;
服务器3启动,此时服务器1、2与3建立连接,因服务器3的myid最大,leader投票都给3,达到过半机制(3台服务器,至少2票),所以服务器3成为Leader,服务器1、2成为follower;
服务器4启动,虽然此时myid最大,但集群中已存在leader,所以成为follower;
服务器5与服务器4逻辑一致。
服务集群:

-
优先检查ZXID。ZXID比较大的服务器优先作为leader。
-
如果ZXID相同的话,就比较myid,myid比较大的服务器作为leader。
在ZAB协议中,每个事务都有一个编号ZXID,ZXID由两部分组成,高32位是epoch,低32位为递增计数器
搭建zookeeper的集群时,我们需要手动创建一个myid的文件,这个文件里面我们随便写入一个数字
zookeeper的写流程:

如果事务没有发出去,所有follower都没有收到这个事务,leader故障了。所有的follower都不知道这个事务的存在,根据心跳检测机制,follower发现leader故障,重新选出一个leader。会根据每个节点Zxid来选择,谁的Zxid最大,表示谁的数据最新,自然会被选举成新的leader。如果Zxid都一样,表示在follower故障之前,所有的follower节点数据完全一致,此时选择myid最大的节点成为新的leader,因为有一个固定的选举标准会加快选举流程。新的leader选出来之后,所有节点的数据本身就是一致的,此时就可以对外提供服务。
如果在leader故障之前已经commit,zookeeper依然会根据Zxid或者myid选出数据最新的那个follower作为新的leader。新leader与follower建立FIFO的队列, 先将自身有而follower缺失的事务发送给它,再将这些事务的commit命令发送给 follower,这便保证了所有的follower都保存了所有的事务、所有的follower都处理了所有的消息。
ZAB协议有三种模式:
恢复模式:当集群启动,或leader挂了,zk集群需进入恢复模式,包含两个阶段:leader选举与初始化同步;
广播模式:分为两类,初始化广播和更新广播;
同步模式:分为两类,初始化同步与更新同步。
-
zookeeper数据不一致的场景
1、zk过半成功,剩余未commit的节点
场景:比如5个节点,有三个返回写入成功,则如果有读请求到另两个节点,则会数据不一致。
解决方案:sync API
原理:sync是使client当前连接的Zookeeper服务器,和zk的Leader节点同步(sync)一下数据。
2、leader未发送proposal宕机
这也就是数据同步说过的问题。
leader与follower之间同步数据的步骤如下,类似二阶段提交:
leader接收写请求,发起一个proposal提议,并生成一个全局性的唯一递增ID(zxid),并放入一个FIFO队列;
follower收到proposal提议后,以事务日志的形式写入本地磁盘,并返回ACK给leader;
leader收到过半follower的ACK之后,发起commit给follower提交proposal,当过半提交成功后leader就会commit。
3、leader发送proposal成功,发送commit前宕机
如果发送proposal成功了,但是在将要发送commit命令前宕机了,如果重新进行选举,还是会选择zxid最大的节点作为leader,因此,这个日志并不会被丢弃,会在选举出leader之后重新同步到其他节点当中。
-
zookeeper分布式锁实现
Zookeeper 是基于临时顺序节点以及 Watcher 监听器机制实现分布式锁的
ZooKeeper 的节点监听机制,能避免羊群效应(羊群效应就是一个节点挂掉,所有节点都去监听,然后做出反应,这样会给服务器带来巨大压力。有了临时顺序节点以及节点监听机制,当一个节点挂掉,只有它后面的那一个节点才做出反应)
加锁过程
一把分布式锁通常使用一个 Znode 节点表示;如果锁对应的 Znode 节点不存在,首先创建 Znode 节点代表了一把需要创建的分布式锁。
抢占锁的所有客户端,使用锁的 Znode 节点的子节点列表来表示;如果某个客户端需要占用锁,则创建一个临时顺序的子节点。比如,如果子节点的前缀为 /test/lock/seq-,则第一次抢锁对应的子节点为 /test/lock/seq-000000001,第二次抢锁对应的子节点为 /test/lock/seq-000000002,以此类推。
当客户端创建子节点后,需要进行判断:自己创建的子节点,是否为当前子节点列表中序号最小的子节点。如果是,则加锁成功;如果不是,则监听前一个 Znode 子节点变更消息,等待前一个节点释放锁。
一旦队列中的后面的节点,获得前一个子节点变更通知,则开始进行判断,判断自己是否为当前子节点列表中序号最小的子节点,如果是,则认为加锁成功;如果不是,则持续监听,一直到获得锁。
获取锁后,开始处理业务流程。完成业务流程后,删除自己的对应的子节点,完成释放锁的工作,以方面后继节点能捕获到节点变更通知,获得分布式锁。
-
zookeeper分布式锁和redis分布式锁
优点:ZooKeeper分布式锁(如 InterProcessMutex),除了独占锁、可重入锁,还能实现读写锁,并且可靠性比 Redis 更好。公平锁使用临时有序节点、非公平锁使用临时节点
缺点:ZooKeeper实现的分布式锁,性能并不太高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。而 ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后 Leader 服务器还需要将数据同不到所有的 Follower 机器上,同步之后才返回,这样频繁的网络通信,性能的短板是非常突出的;而 Redis 则是异步复制。
-
nacos

整个服务注册与发现过程,都离不开通讯协议,在1.x的 Nacos 版本中服务端只支持 http 协议,后来为了提升性能在2.x版本引入了谷歌的 grpc,grpc 是一款长连接协议,极大的减少了 http 请求频繁的连接创建和销毁过程,能大幅度提升性能,节约资源。据官方测试,Nacos服务端 grpc 版本,相比 http 版本的性能提升了9倍以上。
创建长连接
Nacos SDK 通过Nacos服务端域名解析出服务端ip列表,选择其中一个ip创建 grpc 连接,并定时检查连接状态,当连接断开,则自动选择服务端ip列表中的下一个ip进行重连。
健康检查请求
在正式发起注册之前,Nacos SDK 向服务端发送一个空请求,服务端回应一个空请求,若Nacos SDK 未收到服务端回应,则认为服务端不健康,并进行一定次数重试,如果都未收到回应,则注册失败。
发起注册
当你查看Nacos java SDK的注册方法时,你会发现没有返回值,这是因为Nacos SDK做了补偿机制,在真实给服务端上报数据之前,会先往缓存中插入一条记录表示开始注册,注册成功之后再从缓存中标记这条记录为注册成功,当注册失败时,缓存中这条记录是未注册成功的状态,Nacos SDK开启了一个定时任务,定时查询异常的缓存数据,重新发起注册。

服务端数据同步(Distro协议):
Nacos SDK只会与服务端某个节点建立长连接,当服务端接受到客户端注册的实例数据后,还需要将实例数据同步给其他节点。Nacos自己实现了一个一致性协议名为Distro,服务注册的时候会触发Distro一次同步,每个Nacos节点之间会定时互相发送Distro数据,以此保证数据最终一致
查询服务实例
服务消费者首先需要调用Nacos SDK的接口来获取最新的服务实例,然后才能从获取到的实例列表中以加权轮询的方式选择出一个实例(包含ip,port等信息),最后再发起调用。
前面已经提到Nacos服务发生上下线、订阅的时候都会推送最新的服务实例列表到当客户端,客户端再更新本地内存中的缓冲数据,所以调用Nacos SDK提供的查询实例列表的接口时,不会直接请求服务端获取数据,而是会优先使用内存中的服务数据,只有内存中查不到的情况下才会发起订阅请求服务端数据

Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式,临时实例心跳不正常会被剔除,非临时实例则不会被剔除
Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式
-
nacos主要的核心功能
1、服务注册:每个服务客户端通过rest方式向服务端进行注册自己的信息
2、服务心跳:每个服务客户端都会维护一个定时心跳,向服务到证明自己是健康的,默认5s发送一次
3、服务同步:服务器集群之间相互进行通讯来保证服务信息的一致性同时提高注册中心的高可用
4、服务发现:客户端有一个定时任务,定时的去注册中心拉取各个服务的信息列表到本地
5、服务健康检查:注册中心定时检查各个服务的健康状态
6、雪崩保护:通过给每个服务实例进行配置阈值,从而实现雪崩保护
7、临时实例:当服务宕机时,注册中心会进行删除注册的服务实例
8、永久实例:即使服务宕机了,服务实例也不会被删除,和前面我们一起讨论的zookepper的持久性节点很像
-
CAP理论
C:一致性
A:可用性
P:容错性
-
分布式事务实现方式
mq:保证数据最终一致性,通过补偿来处理
tcc:seata

2pc:二段提交


-
性能问题。从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态。各个操作数据库的节点此时都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
-
协调者单点故障问题。事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,从而导致参与者节点始终处于事务无法完成的中间状态。
-
丢失消息导致的数据不一致问题。在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就会导致节点间数据的不一致问题。
3pc:CanCommit、PreCommit、DoCommit

相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
-
负载均衡算法
轮询:
随机:
hash:
加权轮询:
-
sentinel限流原理
Slot 插槽
在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:
NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

-
漏桶和令牌桶

漏桶算法的主要思路为:
在nginx层与controller层加一层(即漏桶层),
用于接收nginx收到的大批量的请求,接收的请求的速度是没有控制的,但是如果超过了漏桶层的最大容量则直接抛弃该请求.
漏桶层将大批量的请求以特定的速度转发给controller层.(当然漏桶层可以放置在需要的任何位置)

令牌桶的算法中心逻辑是:
-
以恒定的速度向令牌桶种添加令牌.当令牌桶满时则放弃向令牌桶中添加令牌
-
请求到达controller层时,需要去令牌桶中获取令牌,如果存在令牌,则继续执行,不存在这放弃这次请求
-
GRPC
GRPC是一个高性能、通用的开源RPC框架,基于底层HTTP/2协议标准和协议层Protobuf序列化协议开发,支持众多的开发语言。降级开发者的使用门槛,屏蔽网络协议,调用对端的接口就像是调用本地的函数一样。
RPC

GRPC


gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
gRPC使用protocol buffers作为接口描述语言(IDL)以及底层的信息交换格式
-
基于 HTTP/2 之上的二进制协议(Protobuf 序列化机制);
-
一个连接上可以多路复用,并发处理多个请求和响应;
-
多种语言的类库实现;
-
服务定义文件和自动代码生成(.proto 文件和 Protobuf 编译工具)。
-
RPC 还提供了很多扩展点,用于对框架进行功能定制和扩展,例如,通过开放负载均衡接口可以无缝的与第三方组件进行集成对接(Zookeeper、域名解析服务、SLB 服务等)
GRPC和RPC的区别在哪里?两者之间最明显的区别就在于工作模式不同,RPC主要采用的是客户端和服务端双向沟通的方式,在进行工作的时候RPC需要利用客户端发送信息到服务端,一旦信息被顺利传递,服务端就可以直接开始着手计算结果。GRPC可以直接通过客户端和服务端自动生成功能库。用户可以根据自己的需求来选择适合的模式。在GRPC模式中,客户端调用服务器端的时候,借助客户端向服务器端发送请求并得到响应,与响应一起发送的还有一些数据
消息中间件相关
-
rocketmq的原理

Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的Broker Name,不同的Broker Id来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。
每个Broker与Name Server集群中的所有节点建立长连接,定时(每隔30s)注册Topic信息到所有Name Server。Name Server定时(每隔10s)扫描所有存活broker的连接,如果Name Server超过2分钟没有收到心跳,则Name Server断开与Broker的连接。
Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Producer每隔30s(由ClientConfig的pollNameServerInterval)从Name server获取所有topic队列的最新情况,这意味着如果Broker不可用,Producer最多30s能够感知,在此期间内发往Broker的所有消息都会失败。
Producer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s中扫描所有存活的连接,如果Broker在2分钟内没有收到心跳数据,则关闭与Producer的连接。
Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
Consumer每隔30s从Name server获取topic的最新队列情况,这意味着Broker不可用时,Consumer最多最需要30s才能感知。
Consumer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s扫描所有存活的连接,若某个连接2分钟内没有发送心跳数据,则关闭连接;并向该Consumer Group的所有Consumer发出通知,Group内的Consumer重新分配队列,然后继续消费。
-
kafka的原理

kafka组件:broker、zookeeper、producer、producer、partition、replication 、leader 、follower、consumer
broker:经纪人,每个kafkaserver称为一个broker,,就是一个node,多个borker组成 KafkaCluster;一个机器上可以部署一个或者多个broker,这多个broker连接到相同的ZooKeeper就组成了kafka集群;
zookeeper:信息/注册中心,Kafka 集群能够正常⼯作,需要依赖于 zookeeper,zookeeper 帮助 kafka存储和管理集群信息,旧版的kafka必须配合zookeeper使用,但在新版中kafka2.8.0以后已经丢弃zookeeper;
controller:控制节点,集群中的一个broker作为leader身份来负责管理整个集群,如果controller挂掉,借助zookeeper重新选主。
producer:生产者,就是向kafkabroker发送消息的客户端;
topic:主题,就是一类消息的含义,一个topic中通常放置一类消息。每个topic都有一个或者多个订阅者,也就是消费者consumer,producer将消息推送到topic,由订阅该topic的consumer从topic中拉取消息;
partition:分区,每个topic主题可以有多个分区分担数据的传递,多条路并行,吞吐量大;
topic中的数据被分割为一个或多个partition,每个topic至少有一个partition,当生产者产生数据的时候,根据分配策略(hash取模)选择partition分区,然后将消息追加到指定partition分区的末尾。
replication :副本,每个分区可以设置多个副本,副本之间数据一致。相当于备份,有备胎更可靠;
partition分区可能会损坏,所以得有副本,可以有多个副本。
副本的设置数为N,表示主+备=N个。也就是说设置topic的时候不要设置为 1个partition,1个replicas,这样相当于没有副本,因为副本数包含了分区,如果设置为 2个partition,3个replicas,就表示这个topic有2个分区,每个分区一共有3个,即1个leader,2个follower。
我们将所有的partition分区这样分,有一个leader和多个follower,leader是负责读写数据的,follower只负责备份。
leader & follower:领导者和追随者,也可以叫做主从,上面的这些分区的副本里有1个身份为leader,其他的为follower。leader处理partition的所有读写请求。follower副本不能读,其仅有一个功能,那就是从leader副本拉取消息,尽量让自己跟leader副本的内容一致。consumer只会从leader 中读取数据,当leader 挂掉会有新的follower重新选举为leader。follower会跟leader保持数据一致,当leader挂掉,则从followerer中选举一个作为新的leader,当follower挂掉或卡主或同步数据太慢,leader会把这个follower从“in sync replicsa ”(ISR)列表中删除重新创建一个follower。
consumer:即消费者,就是从kafkabroker获取消息的消费者;
consumer group消费者组:每个consumer属于一个特定的consumer group,将多个消费者集中到一起去处理某一个topic数据,可以更快速的提高消费能力,整个消费者组共享一组偏移量以防止数据被重复度,因为一个topic有多个分区。
offset偏移量:可以唯一的标识一条信息;topic的每一消息都会有一个自增的编号,用于标识顺序和标识消息的偏移量。
偏移量决定读取数据的位置,不会有线程安全的问题,消费者通过偏移量来决定下次读取的消息;
消息被消费之后,并不会马上删除,这样多个业务就可以重复读取kafka中的消息;
某一个偏移量可以通过修改偏移量达到重新读取消息的目的,偏移量由用户控制;
消息最终还是会被删除,默认的时间为1周(7*24小时);
优点
1. 高吞吐量、低延迟
Kafka 每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个 topic 可以分多个 partition , consumer group 对 partition 进行 consume 操作。
2. 可扩展性
Kafka 集群支持热扩展。
3. 持久性、可靠性
消息被持久化到本地磁盘,并且支持数据备份防止数据丢失。
4. 容错性
允许集群中节点失败(若副本数量为 n, 则允许 n-1 个节点失败)
5. 高并发
支持数千个客户端同时读写。

-
kafka高并发写入
页缓存技术 + 磁盘顺序写
Kafka 为了保证磁盘写入性能,首先Kafka是基于操作系统的页缓存来实现文件写入的。
操作系统本身有一层缓存,叫做page cache,是在内存里的缓存,我们也可以称之为os cache,意思就是操作系统自己管理的缓存。
你在写磁盘文件的时候,可以直接写入os cache 中,也就是仅仅写入内存中,接下来由操作系统自己决定什么时候把os cache 里的数据真的刷入到磁盘中。
-
rocketmq和kafka的可靠投递
rocketmq:支持实时刷盘和异步刷盘、同步复制和异步复制、消息重试和消息消费的ack确认
kafka:异步刷盘、异步复制,单机百万/秒,短轮询,写入快,不能重试 acks确认机制
ack确认参数:
At last one:消息绝不会丢,但可能会重复传输(至少一次,ack=all+分区副本大于等于2+ISR应答最小副本数大于等于2)
At most once:消息可能会丢,但绝不会重复传输(最多一次,ack=0)
Exactly once:每条消息肯定会被传输一次且仅传输一次(精确一次,需要幂等性+At last one至少一次相结合,对于一些比较重要的业务场
kafka性吞吐量更高主要是由于Producer端将多个小消息合并,批量发向Broker。kafka采用异步发送的机制,当发送一条消息时,消息并没有发送到broker而是缓存起来,然后直接向业务返回成功,当缓存的消息达到一定数量时再批量发送。
此时减少了网络io,从而提高了消息发送的性能,但是如果消息发送者宕机,会导致消息丢失,业务出错,所以理论上kafka利用此机制提高了io性能却降低了可靠性。
kafka一个topic下面的所有消息都是以partition的方式分布式的存储在多个节点上。同时在kafka的机器上,每个Partition其实都会对应一个日志目录,在目录下面会对应多个日志分段。
rocketmq中的消息主体数据并没有像Kafka一样写入多个文件,而是写入一个文件,这样我们的写入IO竞争就非常小,可以在很多Topic的时候依然保持很高的吞吐量。
-
消息的顺序消费
投递到同一个消息队列中
-
rocketmq多线程消费
/** * 设置消费端线程数固定为20 */
properties.setProperty(PropertyKeyConst.ConsumeThreadNums,"20");
-
rocketmq延时队列

延时消息会被临时存放在延时队列中,到了指定时间才会被放入正常队列中去消费
delayLevelTable定义了延迟级别和延迟时间的对应关系,offsetTable存放延延迟级别对应的队列消费的offset
使用timer定时器启动了一个定时任务,把每个扫描队列封装成一个任务,然后加入到timer中
-
生产者将发送给producer proxy,proxy判断是延迟消息,将其投递到一个缓冲Topic中;
-
delay service启动消费者,用于从缓冲topic中消费延迟消息,以时间为key,存储到rocksdb中;
-
delay service判断消息到期后,将其投递到目标Topic中。
-
消费者消费目标topic中的数据
这种方式的好处是,因为delay service的延迟投递能力是独立于broker实现的,不需要对broker做任何改造,对于任意MQ类型都可以提供支持延迟消息的能力。例如DDMQ对RocketMQ、Kafka都提供了秒级精度的延迟消息投递能力,但是Kafka本身并不支持延迟消息,而RocketMQ虽然支持延迟消息,但不支持秒级精度
-
rocketmq事务消息
2PC(两阶段提交)+ 补偿机制(事务状态回查)的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息

我们可以看到,事务消息主要分为两个流程:
(1)、正常事务消息的发送及提交
a、生产者发送half消息到Broker服务端(半消息);
半消息是一种特殊的消息类型,该状态的消息暂时不能被Consumer消费。当一条事务消息被成功投递到Broker上,但是Broker并没有接收到Producer发出的二次确认时,该事务消息就处于"暂时不可被消费"状态,该状态的事务消息被称为半消息。
b、Broker服务端将消息持久化之后,给生产者响应消息写入结果(ACK响应);
c、生产者根据发送结果执行本地事务逻辑(如果写入失败,此时half消息对业务不可见,本地逻辑不执行);
d、生产者根据本地事务执行结果向Broker服务端提交二次确认(Commit 或是 Rollback),Broker服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;Broker服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接收该消息;
(2)、事务消息的补偿流程
a、在网络闪断或者是应用重启的情况下,可能导致生产者发送的二次确认消息未能到达Broker服务端,经过固定时间后,Broker服务端将会对没有Commit/Rollback的事务消息(pending状态的消息)进行“回查”;
b、生产者收到回查消息后,检查回查消息对应的本地事务执行的最终结果;
c、生产者根据本地事务状态,再次提交二次确认给Broker,然后Broker重新对半事务消息Commit或者Rollback;
-
mq查询消息的方式
messageId,messageKey,扩展api查询、unique_key
-
消息堆积处理方式
灰度发布,扩展机器增加消费效率,优化业务代码缩短消费时间
-
消息的幂等
全局id,消息设置主键id
-
mq怎么支持重复消费
重置offset
-
mq对比

-
rabbitmq

RabbitMQ消息积压的方法:
增加消费者:增加消费者数量可以提高消息处理速度,从而减少消息积压。可以根据消息的类型和优先级分配消费者,使消息得到及时处理。
增加队列:可以增加队列的数量来缓解消息积压。根据消息的类型和优先级,可以将不同类型的消息存储在不同的队列中,从而更好地管理消息流量。
增加RabbitMQ节点:增加RabbitMQ节点可以提高消息处理能力,从而减少消息积压。可以使用RabbitMQ集群来增加节点,从而提高系统可靠性和性能。
消息重试机制:当消息处理失败时,可以将消息重新投递到队列中,并设置重试次数和时间间隔,直到消息被正确处理或超过最大重试次数为止。
设置消息过期时间:可以设置消息的过期时间,当消息在队列中等待时间超过指定时间时,会被自动删除,从而减少消息积压。
使用限流机制:可以使用限流机制来控制消费者的消费速度,避免消息过多导致消费者无法及时处理。可以使用QoS(Quality of Service)机制,设置每个消费者同时处理消息的最大数量,从而保证系统性能和稳定性。
顺序消费:

消息可靠性:
投递:confirm模式,return机制
消费:ack确认机制
五种消息模型:
Simple-简单模型:

Work-工作模型:一个消息只能被一个消费者获取

Fanout-广播模型:
1) 可以有多个消费者
2) 每个消费者有自己的queue(队列)
3) 每个队列都要绑定到Exchange(交换机)
4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
5) 交换机把消息发送给绑定过的所有队列
6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费

Direct-定向模型:
有选择性的接收消息,消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。

Topic-主题模型:
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符

计算机网络
-
输入url到浏览器加载的过程
-
地址栏输入URL
-
DNS 域名解析IP
-
请求和响应数据
-
建立TCP连接(3次握手)
-
发送HTTP请求
-
服务器处理请求
-
返回HTTP响应结果
-
关闭TCP连接(4次挥手)
-
浏览器加载,解析和渲染
-
dns解析过程
客户机--本地域名服务器(缓存)--根域名服务器--所在查询域的主域名服务器(缓存)--子域名服务器...返回
-
零拷贝的数据传输
零拷贝:没有通过cpu搬运数据
mmap+write :把内存数据映射到用户空间
-
计算机网络分为几个层级
应用层:http/https、ftp
表示层
会话层
传输层:tcp、socket是对tcp/ip、udp的封装)
网络层:ip
数据链路层:以太网、WIFI
物理层
-
https和http


http:特点:无连接、无状态、灵活、简单快速
请求报文:由请求行、请求头、空行、请求体四部分组成
响应报文:由状态行、响应头、空行、响应体四部分组成
http1.0协议采用的是请求-应答模式,报文必须是一发一收,就形成了一个先进先出的串行队列,没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求最先处理,就导致如果队首的请求耗时过长,后面的请求就只能处于阻塞状态,这就是著名的队头阻塞问题。解决如下:并发连接和域名分片


HTTPS是在HTTP上建立SSL加密层,并对传输数据进行加密,是HTTP协议的安全版。现在它被广泛用于万维网上安全敏感的通讯,例如交易支付方面。
HTTPS主要作用是:
(1)对数据进行加密,并建立一个信息安全通道,来保证传输过程中的数据安全;
(2)对网站服务器进行真实身份认证
-
tcp和udp
-
TCP是面向连接的,而UDP是无连接的协议。
-
TCP对于传输有用的数据非常可靠,因为它需要确认发送的信息。此外,重新发送丢失的数据包(如果有)。而在UDP的情况下,如果数据包丢失,它不会请求重新传输,目标计算机会收到损坏的数据。因此,UDP 是一种不可靠的协议。
-
与UDP相比,TCP速度较慢,因为TCP在传输数据之前建立连接,并确保数据包的正确传递。另一方面,UDP不承认是否接收了传输的数据。
-
UDP 的标头大小为 8 个字节,TCP 的标头大小是两倍多。TCP 标头大小为 20 字节,TCP 标头包含选项、填充、校验和、标志、数据偏移量、确认号、序列号、源端口和目标端口等。
-
TCP 和 UDP 都可以检查错误,但只有 TCP 可以纠正错误,因为它同时具有拥塞和流量控制。
-
长连接和短连接
短连接的操作步骤是:
建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接
长连接的操作步骤是:
建立连接——数据传输...(保持连接)...数据传输——关闭连接
-
tcp的三次握手和四次挥手

-
客户端发送建立TCP连接的请求报文(SYN=1需要建立TCP连接,seq=x,x为随机生成数值);
第一次握手丢失:客户端没收到返回发起重试
当客户端在1秒后未收到SYN-ACK 报文,客户端将会重新发起SYN报文的动作,在Linux里,一般客户端重传此时设置在5次,并且超时时间第一次是1秒,第二次会变成2秒,第三次会变成4秒,一直到第五次16秒,且每次超时时间是上次超时时间的2倍。若还是没有回应,将断开TCP连接
-
服务端回复客户端发送的TCP连接请求报文(SYN=1,ACK=x+1,seq=y,y为随机生成数值)这里的ack加1可以理解为是确认和谁建立连接;
第二次握手丢失:客户端没收到ack发起重试,服务端没收到ack 也发起重试
是服务端发起建立TCP连接报文。当服务端没有收到第二次握手,服务端将会启动超时重传机制,重新传送SYN-ACK 报文(重传次数默认为5)
-
客户端收到服务端发送的TCP建立验证请求后(SYN=1,ACK=y+1,seq=x+1)。
第三次握手丢失:将会触发超时重传机制
重新传送SYN-ACK 报文,直到第三次握手成功或者达到最大重传次数为止
SYN泛洪攻击:设置超时时间,超时回收资源
syn洪泛攻击是Dos攻击的一种,服务器端的资源分配是在二次握手时分配的,SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,不会向服务端发送ack确认包,所以会大量的占领半连接队列资源,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
syn cookie解决,服务器在第二次握手时不会为第一次握手的SYN创建半开连接,而是生成一个cookie一起发送给客户端,只有客户端在第三次握手发送ACK报文并且验证cookie成功服务器才会创建TCP连接,分配资源

CLOSE_WAIT:等待本地用户的连接终止请求 内核在等待应用进程调用close()函数来关闭连接
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。 最大持续时间默认为 60秒(2*MSL (Maximum Segment Lifetime)),超时后TCP状态转为 CLOSED
TIME_WAIT 两个存在的理由:
1.可靠的实现tcp全双工连接的终止;
2.允许老的重复分节在网络中消逝,防止收到历史数据。
连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。
因为TIME_WAIT 状态会消耗系统资源。如果TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。
第1次挥手:客户端发送一个FIN(FIN=1,seq=x,x由客户端随机生成),用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态;
第一次挥手丢失:发起重试
第2次挥手:服务端收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1(FIN=1,ACK=x+1,seq=y,y由服务端随机生成),服务端进入CLOSE_WAIT状态;close_wait状态可以保证server将剩余数据全部传递完成后,再关闭连接
第二次挥手丢失:发起重试
第3次挥手:服务端发送一个FIN(FIN=1,ACK=x+1,seq=z,z由服务端随机生成),用来关闭服务端到客户端的数据传送,服务端进入LAST_ACK状态;
第三次握手丢失:客户端超过tcp_fin_timeout变closed
第4次挥手:客户端收到FIN后,客户端t进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1(FIN=1,ACK=z+1,seq=h,h为客户端随机生成),服务端进入CLOSED状态,完成四次挥手。
第四次握手丢失:服务端重试,超过最大次数断开
大量的 Timewait 产生会造成文件句柄、内存和端口的占用,由于系统会把过多的 time-wait socket 删除、回收,在网络条件不好的情况下,就可能会导致数据包重复的进行发送
第 1 点就是考虑把Timewait 队列加大。
第 2 点调整 TIME_WAIT 超出时间,可以更快地释放资源。
-
TCP一端故障
TCP有一个保活机制,当一端时间相互直接没有发送数据时,就会触发保活机制。即每隔一段时间都发送一次探测报文给对方,如果几次都不回应的话,就会认为对方已经死亡了,那么就会断开连接。
-
Linux的IO模型

-
同步阻塞IO(bloking IO)
-
同步非阻塞IO(non-blocking IO)
-
多路复用IO(multiplexing IO)
-
信号驱动式IO(signal-driven IO)
-
异步IO(asynchronous IO)
目前流程的多路复用IO实现主要包括四种: select、poll、epoll、kqueue。下表是他们的一些重要特性的比较:
IO模型 | 相对性能 | 关键思路 | 操作系统 | JAVA支持情况 | 优缺点 |
select | 较高 | Reactor | windows/Linux | 支持,Reactor模式(反应器设计模式)。Linux操作系统的 kernels 2.4内核版本之前,默认使用select;而目前windows下对同步IO的支持,都是select模型 | )每次调用 select 都需要将进程加入到所有监视 fd 的等待队列,每次唤醒都需要从每个队列中移除。 这里涉及了两次遍历,而且每次都要将整个 fd_set 列表传递给内核,有一定的开销。 2)当函数返回时,系统会将就绪描述符写入 fd_set 中,并将其拷贝到用户空间。进程被唤醒后,用户线程并不知道哪些 fd 收到数据,还需要遍历一次。 受 fd_set 的大小限制,32 位系统最多能监听 1024 个 fd,64 位最多监听 2048 个 |
poll | 较高 | Reactor | Linux | Linux下的JAVA NIO框架,Linux kernels 2.6内核版本之前使用poll进行支持。也是使用的Reactor模式 | 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降 |
epoll | 高 | Reactor/Proactor | Linux | Linux kernels 2.6内核版本及以后使用epoll进行支持;Linux kernels 2.6内核版本之前使用poll进行支持;另外一定注意,由于Linux下没有Windows下的IOCP技术提供真正的 异步IO 支持,所以Linux下使用epoll模拟异步IO | 没有最大并发连接的限制,能打开的 FD 的上限远大于 1024。 2)效率提升,不是轮询的方式,不会随着 FD 数目的增加效率下降。 3)内存拷贝,利用 mmap() 文件映射内存加速与内核空间的消息传递,即 epoll 使用 mmap 减少复制开销。 4)新增 ET 模式。 |
kqueue | 高 | Proactor | Linux | 目前JAVA的版本不支持 |

-
netty

第一层:Reactor通信调度层。该层的主要职责就是监听网络的连接和读写操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事件、写事件等,将这些事件触发到Pipeline中,再由Pipeline充当的职责链来进行后续的处理。
第二层:职责链Pipeline层。负责事件在职责链中有序的向前(后)传播,同时负责动态的编排职责链。Pipeline可以选择监听和处理自己关心的事件。
第三层:业务逻辑处理层,一般可分为两类:a. 纯粹的业务逻辑处理,例如日志、订单处理。b. 应用层协议管理,例如HTTP(S)协议、FTP协议等。
-
Netty中pipline对异常的传递

我们通常在业务代码中,需要加载自定义节点的最末尾,统一处理pipeline过程中的所有的异常
继承自 ChannelDuplexHandler,表示异常节点既是一个inBound节点又是一个outBound节点
-
防止xss攻击
xss是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
xss攻击流程:
-
攻击者将恶意代码提交到目标网站的数据库中。
-
用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
-
用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
-
恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
Java处理:转义存储:添加XssFilter
前端处理:纯前端渲染、转义HTML、CSP、输入内容长度控制、HTTP-only Cookie
CSP 在 XSS 的防范中可以起到以下的作用:
-
禁止加载外域代码,防止复杂的攻击逻辑。
-
禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
-
禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
-
禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
-
合理使用上报可以及时发现 XSS,利于尽快修复问题
设计模式相关
-
单例模式的应用场景
数据库连接池、共享日志文件、单例bean、线程池等
实现方式:
饿汉式 :指向同一块内存
public class Singleton{
private static Singleton instance=new Singleton();
private Singleton{
}
//对外提供一个静态方法
public static Singleton getSingleton()
{
return instance;
}
}
懒汉式
public class Singleton{
private static Singleton instance=;
private Singleton{
}
//对外提供一个静态方法
public static synchronized Singleton getSingleton()
{
if(null == instance){instance = new Singleton();}
return instance;
}
}
双重检查
双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。
双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因
public class Singleton{
private static Singleton instance;
private Singleton{
}
//对外提供一个静态方法
public static Singleton getSingleton()
{
if(null == instance){
synchronized(Singleton.class){
if(null == instance){intance = new Singleton();}
}}
return instance;
}
}
静态内部类
public class Singleton {
/**
* 静态内部类
**/
private static class SingletonClassInstance{
private static final Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonClassInstance.instance;
}
}
枚举
public enum Singleton{
//枚举元素本身就是单例
INSTANCE;
//添加自己需要的操作,直接通过Singleton.INSTANCE.doSomething()的方式调用即可。方便、简洁又安全。
public void doSomething() {
System.out.println("doSomething");
}
}
算法相关
-
排序算法

n:是数据规模 k:是桶的个数 In-plcae:不占用额外空间 Out-plcae:占用额外空间
冒泡排序(Bubble Sort)
-
它是一种较简单的排序算法。两两比较交换,把最大/小的一个沉底
public class Bubble_sort {
public static void main(String[] args) {
//冒泡排序算法
int[] num=new int[]{10,21,8,16,7,23,12};
//需进行length-1次冒泡
for(int i = 0; i < num.length-1;i++){
for(int j=0;j<num.length-1-i;j++){
if(num[j]>num[j+1]) {
int temp=num[j];
num[j]=num[j+1];
num[j+1]=temp;
}
}
}
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<num.length;i++)
System.out.print(num[i]+" ");
}
}
选择排序(Selection sort)
-
它的基本思想是: 一次选择最大/小的一个往前放
public class SelectionSort {
public static void main(String[] args) {
int[] num = new int[] { 5, 3, 6, 2, 10, 18, 1 };
selectSort(num);
System.out.println("从小到大排序的结果为: "+ Arrays.toString(num));
}
public static void selectSort(int[] num) {
for (int i = 0; i < num.length - 1; i++) {
int minIndex = i; // 用来记录最小值的索引位置,默认值为i
for (int j = i + 1; j < num.length; j++) {
if (num[j] < num[minIndex]) {
minIndex = j; // 遍历 i+1~length 的值,找到其中最小值的位置
}
}
// 交换当前索引 i 和最小值索引 minIndex 两处的值
if (i != minIndex) {
int temp = num[i];
num[i] = num[minIndex];
num[minIndex] = temp;
}
// 执行完一次循环,当前索引 i 处的值为最小值,直到循环结束即可完成排序
}
}
}
插入排序(Insertion Sort)
-
直接插入排序(Straight Insertion Sort)的基本思想是: 从前往后 把当前第n个大的放在第n的位置上
public class InsertionSort {
public static void main(String[] args){
int[] num1 = {2,3,5,1,23,6,78,34};
int[] num2 = sort(num1);
System.out.println("从小到大排序的结果为: "+ Arrays.toString(num2));
}
public static int[] sort(int[] num1){
for(int i=1; i<num1.length; i++){
for(int j=i; j>0; j--){
if(num1[j]<num1[j-1]){
int temp = num1[j-1];
num1[j-1] = num1[j];
num1[j] = temp;
}
}
}
return num1;
}
}
Shell排序(Shell Sort)
-
希尔排序实质上是一种分组插入方法。是在插入排序的基础上,插入排序对几乎已经排好序的数据效率高,线性操作。但插入排序效率低,每次只能挪动一个数据。它的基本思想是: 在插入排序的基础上部分有序,然后再做整体排序。
public class ShellSort {
public static void main(String[] args) {
int[] arr = new int[] { 26, 53, 67, 48, 57, 13, 48, 32, 60, 50 };
shellSortSmallToBig(arr);
System.out.println("从小到大排序的结果为: "+Arrays.toString(arr));
}
public static void shellSortSmallToBig(int[] arr) {
int j = 0;
int temp = 0;
for (int increment = arr.length / 2; increment > 0; increment /= 2) {
for (int i = increment; i < arr.length; i++) {
temp = arr[i];
for (j = i - increment; j >= 0; j -= increment) {
if (temp < arr[j]) {
arr[j + increment] = arr[j];
} else {
break;
}
}
arr[j + increment] = temp;
}
}
}
}
归并排序(Merge Sort)
-
将两个的有序数列合并成一个有序数列,我们称之为"归并"。分而治之,先分为几组,每组有序。
-
每组两个 然后每组四个 八个 十六个
public class MergeSort {
//两路归并算法,两个排好序的子序列合并为一个子序列
public static void merge(int[] a, int left, int mid, int right) {
int[] tmp = new int[a.length];//辅助数组
int p1 = left, p2 = mid + 1, k = left;//p1、p2是检测指针,k是存放指针
while (p1 <= mid && p2 <= right) {
if (a[p1] <= a[p2])
tmp[k++] = a[p1++];
else
tmp[k++] = a[p2++];
}
while (p1 <= mid) tmp[k++] = a[p1++];//如果第一个序列未检测完,直接将后面所有元素加到合并的序列中
while (p2 <= right) tmp[k++] = a[p2++];//同上
//复制回原素组
for (int i = left; i <= right; i++)
a[i] = tmp[i];
}
public static void mergeSort(int[] a, int start, int end) {
if (start < end) {//当子序列中只有一个元素时结束递归
int mid = (start + end) / 2;//划分子序列
mergeSort(a, start, mid);//对左侧子序列进行递归排序
mergeSort(a, mid + 1, end);//对右侧子序列进行递归排序
merge(a, start, mid, end);//合并
}
}
public static void main(String[] args) {
int[] arr = {49, 38, 65, 97, 76, 13, 27, 50};
mergeSort(arr, 0, arr.length - 1);
System.out.println("从小到大排序的结果为: " + Arrays.toString(arr));
}
}
快速排序(Quick Sort)
-
它的基本思想是: 分而治之,找到一个基准去递归。在一个无序数组中取一个数key,每一趟排序的最终目的是:让key的左边的所有数小于key,key的右边都大于或小于key
public class QuickSort {
public static void sort(int a[], int low, int hight) {
int i, j, index;
if (low > hight) {
return;
}
i = low;
j = hight;
index = a[i]; // 用子表的第一个记录做基准
while (i < j) { // 从表的两端交替向中间扫描
while (i < j && a[j] >= index)
j--;
if (i < j)
a[i++] = a[j];// 用比基准小的记录替换低位记录
while (i < j && a[i] < index)
i++;
if (i < j) // 用比基准大的记录替换高位记录
a[j--] = a[i];
}
a[i] = index;// 将基准数值替换回 a[i]
sort(a, low, i - 1); // 对低子表进行递归排序
sort(a, i + 1, hight); // 对高子表进行递归排序
}
public static void quickSort(int a[]) {
sort(a, 0, a.length - 1);
}
public static void main(String[] args) {
int arr[] = { 49, 38, 65, 97, 76, 13, 27, 49 };
quickSort(arr);
System.out.println("从小到大排序的结果为: " + Arrays.toString(arr));
}
}
堆排序
是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
计数排序
将输入数值额外转化为键存在的数组,需要有确定的范围整数
桶排序(Bucket Sort)
-
桶排序(Bucket Sort)的原理很简单,将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
基数排序(Radix Sort)
-
它的基本思想是: 将整数按位数切割成不同的数字,然后按每个位数分别比较。具体做法是: 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列 依次比较个位、十位和百位
public class RadixSort {
public static void main(String[] args) {
//定义整型数组
int[] arr = {21,56,88,195,354,1,35,12,6,7};
//调用基数排序函数
lsd_RadixSort(arr,3);
//输出排序后的数组
System.out.println("从小到大排序的结果为: " + Arrays.toString(arr));
}
public static void lsd_RadixSort(int[] arr,int max) {
//count数组用来计数
int[] count = new int[arr.length];
//bucket用来当桶(在下面你就理解了什么是桶了),放数据,取数据
int[] bucket = new int[arr.length];
//k表示第几位,1代表个位,2代表十位,3代表百位
for(int k=1;k<=max;k++) {
//把count置空,防止上次循环的数据影响
for(int i=0;i<arr.length;i++) {
count[i] = 0;
}
//分别统计第k位是0,1,2,3,4,5,6,7,8,9的数量
//以下便称为桶
//即此循环用来统计每个桶中的数据的数量
for(int i=0;i<arr.length;i++) {
count[getFigure(arr[i],k)]++;
}
//利用count[i]来确定放置数据的位置
for(int i=1;i<arr.length;i++) {
count[i] = count[i] + count[i-1];
}
//执行完此循环之后的count[i]就是第i个桶右边界的位置
//利用循环把数据装入各个桶中,注意是从后往前装
//这里是重点,一定要仔细理解
for(int i=arr.length-1;i>=0;i--) {
int j = getFigure(arr[i],k);
bucket[count[j]-1] = arr[i];
count[j]--;
}
//将桶中的数据取出来,赋值给arr
for(int i=0,j=0;i<arr.length;i++,j++) {
arr[i] = bucket[j];
}
}
}
//此函数返回整型数i的第k位是什么
public static int getFigure(int i,int k) {
int[] a = {1,10,100};
return (i/a[k-1])%10;
}
}
-
平衡二叉树、b树、b+树和b*树
二叉树:链式结构太长,通过二分法保证树结构的有序性
平衡二叉树:平衡算法可以让数据均匀的分布到树里的各个节点,避免树的高度相差太多。通常会保证树的左右两边的节点层级相差不会大于2 例如红黑树O(log n) 左右两个层级的差值不大于1

b树:B树属于多叉树又名平衡多路查找树(查找路径不止两个),数据库索引技术里大量使用着B树和B+树的数据结构

b+树:B+树的非叶子节点不保存具体的数据,而只保存关键字的索引,而所有的数据最终都会保存到叶子节点。因为所有数据必须要到叶子节点才能获取到

1、B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定。
2、B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
3、B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描
B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字和数据,所以在查询这种数据检索的时候会要比B+树快。
B*树又是对B+数的再一次改进,在B+树的构建过程中,为了保持树的平衡,节点的合并拆分是比较耗费时间的,所以B*树就是在如何减少构建中节点合并和拆分的次数,从而提升树的数据插入、删除性能

B*树 与B+树对比
在B+树的基础上因其初始化的容量变大,使得节点空间使用率更高,而又存有兄弟节点的指针,可以向兄弟节点转移关键字的特性使得B*树额分解次数变得更少
-
日访问量最大ip
ip hash取模,分组求最大,然后求出所有分组中最大的那个
-
300w个热词求前10
hashmap + 堆
先对这批海量数据预处理。具体方法是: 维护一个Key为Query字串,Value为该Query出现次数的HashTable,即hash_map(Query,Value),每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内用Hash表完成了统计; 堆排序: 第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。即借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比。所以,我们最终的时间复杂度是: O(N) + N' * O(logK),(N为1000万,N’为300万)
-
鸡兔同笼问题(穷举法)
已知:鸡兔共35只,共94只脚,那么鸡和兔各几只?
public class SameCage {
public static void main(String[] args) {
//循环变量j,控制小鸡的个数: 0到35递增
//循环变量t,控制兔子的个数: 35到0递减
for(int j=0,t=35; j<=35; j++,t--) {//如果有多个小条件,用逗号隔开
//保证脚的数量是94
if(j*2 + t*4 == 94) {
System.out.println("鸡:"+j+", 兔:"+t);
}
}
}
}
-
斐波那契数列
斐波那契数列的前几个数分别为0,1,1,2,3,5…从第三项开始,每一项都等于前两项的和.请接收用户输入的整数n,求出此数列的前n项.
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
public class Faibonacci {
public static void main(String[] args) {
System.out.println("请输入您要测试的数:");
int n = new Scanner(System.in).nextInt();
//判断n是否是不正常的范围
if(n<1){
System.out.println("输入数据有误!!!");
}
//n==1
if(n==1){
System.out.println(0);
}
//n==2
if(n==2){
System.out.println(0+"\t"+1);
}
//n==3
if(n==3){
System.out.println(0+"\t"+1+"\t"+1);
}
//拼接前n项
if(n>3){
System.out.print(0+"\t"+1+"\t"+1+"\t");
}
//循环输出后面的数据
int f1=1;
int f2=1;
int next=0;
for(int i=4;i<=n;i++){
next=f1+f2;
f1=f2;
f2=next;
System.out.print(next+"\t");
}
}
}
-
打印100以内除了尾数为3,5,7的所有数
public class ForContinue {
public static void main(String[] args) {
System.out.println("结果为:");
for(int i=1;i<=100;i++) {
int y = i%10;//100以内的数,通过取余求出尾数
if(y==3 || y==5 || y==7) {
continue;//如果尾数为3 5 7 ,则跳过后面的打印,进行下一轮循环
}
System.out.print(" "+i);
}
}
}
-
回文问题
123321、12321
private static boolean stringJudge(String str) {
for (int i = 0; i < str.length() - i - 1; i++){
if(str.charAt(i) == str.charAt(str.length() - i - 1)){
continue;
}else{
return false;
}
}
return true;
}
}
-
二分法查找
public class dichotomizingSearch {
public static void main(String[] args) {
// 二分法查找
int[] binaryNums = {1, 6, 15, 18, 27, 50};
int findValue = 27;
int binaryResult = binarySearch(binaryNums, 0, binaryNums.length - 1, findValue);
System.out.println("元素第一次出现的位置(从0开始):" + binaryResult);
}
/**
* 二分查找,返回该值第一次出现的位置(下标从 0 开始)
* @param nums 查询数组
* @param start 开始下标
* @param end 结束下标
* @param findValue 要查找的值
* @return int
*/
private static int binarySearch(int[] nums, int start, int end, int findValue) {
if (start <= end) {
// 中间位置
int middle = (start + end) / 2;
// 中间的值
int middleValue = nums[middle];
if (findValue == middleValue) {
// 等于中值直接返回
return middle;
} else if (findValue < middleValue) {
// 小于中值,在中值之前的数据中查找
return binarySearch(nums, start, middle - 1, findValue);
} else {
// 大于中值,在中值之后的数据中查找
return binarySearch(nums, middle + 1, end, findValue);
}
}
return -1;
}
}
-
手写平衡二叉树
红黑树内部有一个可以比较大小的的 Key字段,用于作为红黑树节点 TreeNode(RBTree中的一个内部类)的键值,存在一个 Value的数据项,和一些需要使用到的常量
private class TreeNode {
Key key; // 键值
Value value; // 数据
TreeNode left; // 左节点
TreeNode right; // 右节点
TreeNode parent;// 父节点
int amount; // 子树节点数量
boolean color; // 节点颜色
}
自平衡方法
左旋算法

右旋算法

-
lru实现
lru:最近最少使用


-
基于ip限流
其他相关
-
apollo同步数据的方式
基于http的长轮询和spring扩展的监听机制实现的
-
秒杀场景设计
服务器限流
对用户、ip、接口限流

增加流程门槛
设置验证码/短信验证、增加用户门口(例如会员登记)

获取商品缓存及库存信息
分布式锁避免缓存穿透 setNx没有原子性 用set指令加锁 redission控制锁的重入和续期
-
lockKey:锁的标识
-
requestId:请求id
-
NX:只在键不存在时,才对键进行设置操作。
-
PX:设置键的过期时间为 millisecond 毫秒。
-
expireTime:过期时间
由于该命令只有一步,所以它是原子操作

扣减库存
超大热点商品,针对该商品再做多key拆分,先走弱幂等性的缓存扣减,缓存扣减后,异步往DB写入一条库存流水记录,后续再做缓存与数据库的库存总量同步
加锁处理效率低:redis锁控制并发,数据库SQL语句乐观锁(订单详情维度的sku加锁,进行库存扣减)
数据库扣减资源宝贵:update product set stock=stock-1 where id=product and stock > 0;(数据库的悲观锁是在事务内对当前行加锁、加锁sql需要用到索引,要不然会对表加锁)
redis的incrby:只能保证扣减操作原子性操作、存在负数、库存超卖
lua脚本扣减库存:
StringBuilder lua = new StringBuilder();
lua.append("if (redis.call('exists', KEYS[1]) == 1) then");
lua.append(" local stock = tonumber(redis.call('get', KEYS[1]));");
lua.append(" if (stock == -1) then");
lua.append(" return 1;");
lua.append(" end;");
lua.append(" if (stock > 0) then");
lua.append(" redis.call('incrby', KEYS[1], -1);");
lua.append(" return stock;");
lua.append(" end;");
lua.append(" return 0;");
lua.append("end;");
lua.append("return -1;");
mq处理支付后流程

消息丢失的问题
消息发送过程:消息跟业务做事务、或者存表job去做轮询重发、消息刷盘、消息ack确认
消息做幂等
支付完成发放优惠相关信息
-
Linux常见命令
top:cpu的使用
grep :查找文件匹配信息
命令格式如下:
[root@localhost ~ ] # grep [选项] “搜索内容” 文件名
选项:
-i:忽略大小写
-n:输出行号(显示原始文件中的行号)
-v:反向查找
--color=auto:搜索出的关键字用颜色显示
less:展示文件信息
df-h:硬盘使用
tail:查看文件
-
查询学科成绩前三名
s1.score<s2.score 即表s2中成绩大于s1的人数有几人
select s1.* from score s1 where(select count(1) from score s2 where s1.subject = s2.subject and s1.score < s2.score ) <= 3 order by s1.subject,s1.score desc