diff --git "a/MD/Java\345\237\272\347\241\200-IO.md" "b/MD/Java\345\237\272\347\241\200-IO.md" new file mode 100644 index 0000000..b5c1ee2 --- /dev/null +++ "b/MD/Java\345\237\272\347\241\200-IO.md" @@ -0,0 +1,45 @@ +同步/异步关注的是消息通信机制 (synchronous communication/ asynchronous communication) 。所谓同步,就是在发出一个调用时,在没有得到结果之前, 该调用就不返回。异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果 + +阻塞/非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。 + +## BIO +Block-IO:InputStream和OutputStream,Reader和Writer。属于同步阻塞模型 + +同步阻塞:一个请求占用一个进程处理,先等待数据准备好,然后从内核向进程复制数据,最后处理完数据后返回 + +![](../images/BIO.png) + +## NIO +NonBlock-IO:Channel、Buffer、Selector。属于IO多路复用的同步非阻塞模型 + +同步非阻塞:进程先将一个套接字在内核中设置成非阻塞再等待数据准备好,在这个过程中反复轮询内核数据是否准备好,准备好之后最后处理数据返回 + +![](../images/NIO-1.png) + +IO多路复用:同步非阻塞的优化版本,区别在于IO多路复用阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的IO系统调用上。换句话说,轮询机制被优化成通知机制,多个连接公用一个阻塞对象,进程只需要在一个阻塞对象上等待,无需再轮询所有连接 + +![](../images/NIO-3.png) + +在Java的NIO中,是基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件(比如:连接打开,数据到达) + +因此,单个线程可以监听多个数据通道,Selector的底层实现是epoll/poll/select的IO多路复用模型,select方法会一直阻塞,直到channel中有事件就绪: + +![](../images/NIO-4.png) + +与BIO区别如下: + +1. 通过缓冲区而非流的方式进行数据的交互,流是进行直接的传输的没有对数据操作的余地,缓冲区提供了灵活的数据处理方式 +2. NIO是非阻塞的,意味着每个socket连接可以让底层操作系统帮我们完成而不需要每次开个线程去保持连接,使用的是selector监听所有channel的状态实现 +3. NIO提供直接内存复制方式,消除了JVM与操作系统之间读写内存的损耗 + +## AIO +Asynchronous IO:属于事件和回调机制的异步非阻塞模型 + +![](../images/AIO.png) + +AIO得到结果的方式: + +* 基于回调:实现CompletionHandler接口,调用时触发回调函数 +* 返回Future:通过isDone()查看是否准备好,通过get()等待返回数据 + +但要实现真正的异步非阻塞IO,需要操作系统支持,Windows支持而Linux不完善 diff --git "a/MD/Java\345\237\272\347\241\200-JVM\345\216\237\347\220\206.md" "b/MD/Java\345\237\272\347\241\200-JVM\345\216\237\347\220\206.md" index 813ec3d..92e8d12 100644 --- "a/MD/Java\345\237\272\347\241\200-JVM\345\216\237\347\220\206.md" +++ "b/MD/Java\345\237\272\347\241\200-JVM\345\216\237\347\220\206.md" @@ -2,7 +2,7 @@ ### Java内存区域的分配 JVM虚拟机内存模型实现规范: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/JVM规范.png) +![](../images/JVM规范.png) 按线程是否共享分为以下区域: @@ -20,7 +20,7 @@ JVM虚拟机内存模型实现规范: 以HotSpot虚拟机实现为例,Java8中内存区域如下: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/JVM1.8.png) +![](../images/JVM1.8.png) 与规范中的区别: @@ -30,9 +30,15 @@ JVM虚拟机内存模型实现规范: ### Java对象不都是分配在堆上 #### 逃逸分析 -逃逸是指在某个方法之内创建的对象除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收。由于其被其它变量引用,由于无法回收,即称为逃逸。 +通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 -逃逸分析技术可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配提高对象分配回收效率,对象占用的空间会随栈帧的出栈而销毁。 +逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。 + +使用逃逸分析,编译器可以对代码做如下优化: + +1. 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 +2. 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。 +3. 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。 ### 类加载机制 #### 加载过程 @@ -44,7 +50,7 @@ JVM虚拟机内存模型实现规范: 5. 初始化(执行类构造器、类变量赋值、静态语句块) #### 类加载器 -启动类加载器:用C++语言实现,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库,无法被Java程序直接引用 +启动类加载器:用C++语言实现,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库,无法被Java程序直接引用 扩展类加载器:用Java语言实现,它负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用 系统类加载器:用Java语言实现,它负责加载系统类路径ClassPath指定路径下的类库,开发者可以直接使用 @@ -60,7 +66,7 @@ JVM虚拟机内存模型实现规范: 3. 为了实现代码热替换,OSGi是为了实现自己的类加载逻辑,用平级查找的逻辑替换掉了向下传递的逻辑。但其实可以不破坏双亲委派逻辑而是自定义类加载器来达到代码热替换。比如[这篇文章](https://www.cnblogs.com/pfxiong/p/4070462.html) ### 内存分配(堆上的内存分配) -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/堆的内存分配.png) +![](../images/堆的内存分配.png) #### 新生代 ##### 进入条件 优先选择在新生代的Eden区被分配。 @@ -85,8 +91,8 @@ GC Roots包括:虚拟机栈中引用的对象、方法区中类静态属性引 老年代因为存活率高、没有分配担保空间,所以使用“标记-清理”或者“标记-整理”算法 复制算法:将可用内存按容量划分为Eden、from survivor、to survivor,分配的时候使用Eden和一个survivor,Minor GC后将存活的对象复制到另一个survivor,然后将原来已使用的内存一次清理掉。这样没有内存碎片。 -标记-清除:首先标记出所有需要回收的对象,标记完成后统一回收被标记的对象。会产生大量碎片,导致无法分配大对象从而导致频繁GC。 -标记-整理:首先标记出所有需要回收的对象,让所有存活的对象向一端移动。 +标记-清除:标记出存在引用链的对象,回收未被标记的对象。会产生大量碎片,导致无法分配大对象从而导致频繁GC。 +标记-整理:标记出存在引用链的对象,让所有存活的对象向一端移动。 #### Minor GC条件 当Eden区空间不足以继续分配对象,发起Minor GC。 @@ -122,11 +128,31 @@ G1将堆划分为多个大小固定的独立区域,根据每次允许的收集 参数:`-XX:+UseG1GC` -### 如何选择垃圾收集器 -1. 需要停顿时间能超过1秒且想要一个可控的吞吐量时,使用并行收集器 -2. 如果停顿时间重要且不超过1秒,使用并发收集器 -3. 如果内存小于100M,使用串行或者JVM自己选 -4. 如果是单核,且没有系统停顿要求,使用串行或者JVM自己选 +### G1的原理,相比CMS的优势 +G1的工作原理: +内存布局: +G1将堆内存划分为多个大小相等的区域(Region),每个区域可以是Eden、Survivor、老年代的一部分或整个Humongous对象(大对象)。这样的划分使得收集可以更细粒度地进行。 +并发标记-整理: +G1使用多阶段的并发标记过程来识别存活对象。首先进行初步标记,然后进行根搜索标记,最后进行重新标记,这些步骤尽可能与应用程序线程并发执行。 +基于目标的收集: +G1根据用户设定的暂停时间目标来决定收集哪些区域。它优先回收垃圾最多(即回收效益最高)的区域,以尽快满足内存需求,这就是“Garbage-First”命名的由来。 +混合收集周期: +G1执行混合垃圾收集,不仅回收年轻代,也回收一部分老年代区域,这样可以更均匀地分散垃圾回收的压力,减少老年代增长导致的Full GC风险。 +空间整合: +G1在回收过程中会进行空间整理,减少内存碎片,这是与CMS的一个重要区别,后者使用标记-清除算法,容易产生碎片。 + +相比CMS的优势: +更好的内存碎片管理: +G1通过复制和整理算法减少了内存碎片,提高了内存使用的效率,而CMS使用标记-清除算法后易造成碎片化。 +可预测的暂停时间: +G1允许用户设定暂停时间目标,通过动态调整收集策略来尽量满足这一目标,使得应用的响应时间更加可预测,适合对延迟敏感的服务。 +自动内存管理: +G1自动管理年轻代和老年代的大小,不需要手动配置比例,降低了调优难度。 +更大的堆支持: +G1设计之初就考虑了大容量堆的管理,能够有效管理数十GB甚至上百GB的堆内存,而CMS在大堆上的表现可能不佳。 +整体性能和吞吐量: +在很多场景下,G1能够提供与CMS相当甚至更好的吞吐量,同时保持较低的暂停时间。 + ## Stop The World Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互 @@ -135,7 +161,64 @@ STW总会发生,不管是新生代还是老年代,比如CMS在初始标记 那么为什么一定要STW?因为在定位堆中的对象时JVM会记录下对所有对象的引用,如果在定位对象过程中,有新的对象被分配或者刚记录下的对象突然变得无法访问,就会导致一些问题,比如部分对象无法被回收,更严重的是如果GC期间分配的一个GC Root对象引用了准备被回收的对象,那么该对象就会被错误地回收。 +## [Java内存模型](https://mp.weixin.qq.com/s/ME_rVwhstQ7FGLPVcfpugQ) +定义:JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性 + +实现:volatile、synchronized、final、concurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字 + +![](../images/j12.jpg) + +主内存:所有变量都保存在主内存中 +工作内存:每个线程的独立内存,保存了该线程使用到的变量的主内存副本拷贝,线程对变量的操作必须在工作内存中进行 + +每个线程都有自己的本地内存共享副本,如果A线程要更新主内存还要让B线程获取更新后的变量,那么需要: + +1. 将本地内存A中更新共享变量 +2. 将更新的共享变量刷新到主内存中 +3. 线程B从主内存更新最新的共享变量 + +## [happens-before](https://www.cnblogs.com/chenssy/p/6393321.html) +我们无法就所有场景来规定某个线程修改的变量何时对其他线程可见,但是我们可以指定某些规则,这规则就是happens-before。特别关注在多线程之间的内存可见性。 + +它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。 + +1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作; +2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作; +3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作; +4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C; +5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作; +6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生; +7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行; +8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始; + ## JVM调优 -https://www.ibm.com/developerworks/cn/java/j-lo-jvm-optimize-experience/index.html +前提:在进行GC优化之前,需要确认项目的架构和代码等已经没有优化空间 + +目的:优化JVM垃圾收集性能从而增大吞吐量或减少停顿时间,让应用在某个业务场景上发挥最大的价值。吞吐量是指应用程序线程用时占程序总用时的比例。暂停时间是应用程序线程让与GC线程执行而完全暂停的时间段 + +对于交互性web应用来说,一般都是减少停顿时间,所以有以下方法: + +1. 如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大 +2. 让大对象进入年老代。可以使用参数-XX:PetenureSizeThreshold 设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配 +3. 设置对象进入年老代的年龄。如果对象每经过一次 GC 依然存活,则年龄再加 1。当对象年龄达到阈值时,就移入年老代,成为老年对象 +4. 使用关注系统停顿的 CMS 回收器 + +基础:https://www.ibm.com/developerworks/cn/java/j-lo-jvm-optimize-experience/index.html + +案例:https://www.wangtianyi.top/blog/2018/07/27/jvmdiao-you-ru-men-er-shi-zhan-diao-you-parallelshou-ji-qi/ + + +## java中的==和equals有什么区别 +==: + +1. ==操作符专门用来比较变量的值是否相同。如果比较的对象是基本数据类型,则比较数值是否相等;如果比较的是引用数据类型,则比较的是对象的内存地址是否相等。 +2. 因为Java只有值传递,所以对于==来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。引用类型对象变量其实是一个引用,它们的值是指向对象所在的内存地址,而不是对象本身。 + +equals: -欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ +1. equals方法常用来比较对象的内容是否相同。 +2. equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。 +3. 未重写equals方法的类:Object中的equals方法实际使用的也是==操作符,比较的是他们的内存地址是否同一地址。 +4. 重写了equals方法的类:实现该类自己的equals方法比较逻辑(一般是比较对象的内容是否相同)。比如: +5. String:比较字符串内容,内容相同这相同; +6. Integer:比较对应的基本数据类型int的值是否相同(==操作符)。 diff --git "a/MD/Java\345\237\272\347\241\200-\345\244\232\347\272\277\347\250\213.md" "b/MD/Java\345\237\272\347\241\200-\345\244\232\347\272\277\347\250\213.md" index d1e07cc..a06a026 100644 --- "a/MD/Java\345\237\272\347\241\200-\345\244\232\347\272\277\347\250\213.md" +++ "b/MD/Java\345\237\272\347\241\200-\345\244\232\347\272\277\347\250\213.md" @@ -2,7 +2,7 @@ ### 线程的生命周期 新建 -- 就绪 -- 运行 -- 阻塞 -- 就绪 -- 运行 -- 死亡 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/j13.png) +![](../images/j13.png) ### 问:你怎么理解多线程的 @@ -40,6 +40,42 @@ notifyAll:唤醒等待队列中等待该对象锁的全部线程,让其竞 **我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。** +#### 锁的种类 +* 公平锁/非公平锁 + +公平锁是指多个线程按照申请锁的顺序来获取锁 +ReentrantLock通过构造函数指定该锁是否是公平锁,默认是非公平锁。Synchronized是一种非公平锁 + +* 可重入锁 + +指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁 +ReentrantLock, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁 +Synchronized,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁 + +* 独享锁/共享锁 + +独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有 +ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁 +读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的 +Synchronized是独享锁 + +* 乐观锁/悲观锁 + +悲观锁在Java中的使用,就是各种锁 +乐观锁在Java中的使用,是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新 + +* 偏向锁/轻量级锁/重量级锁 + +针对Synchronized的锁状态: +偏向锁是为了减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。指一段同步代码一直被一个线程所访问,在无竞争情况下把整个同步都消除掉 +轻量级锁是为了减少无实际竞争情况下,使用重量级锁产生的性能消耗。指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过CAS自旋的形式尝试获取锁,不会阻塞 +重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁 + +* 自旋锁/自适应自旋锁 + +指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU,默认自旋次数为10 +自适应自旋锁的自旋次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定,是虚拟机对锁状况的一个预测 + ### volatile 功能: @@ -47,7 +83,16 @@ notifyAll:唤醒等待队列中等待该对象锁的全部线程,让其竞 2. 禁止 JVM 进行的指令重排序。 ### ThreadLocal -使用`ThreadLocal userInfo = new ThreadLocal()`的方式,让每个线程内部都会维护一个ThreadLocalMap,里边包含若干了 Entry(K-V 键值对),每次存取都会先获取到当前线程ID,然后得到该线程对象中的Map,然后与Map交互。 +1. 定义:ThreadLocal变量每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。 +2. 原理:Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。 +3. Entry的Key为什么要设计成弱引用:如果Key使用强引用:当ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用的话,如果没有手动删除,ThreadLocal就不会被回收,会出现Entry的内存泄漏问题。如果Key使用弱引用:当ThreadLocal的对象被回收了,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value则在下一次ThreadLocalMap调用set,get,remove的时候会被清除。 + + +1. 强引用:我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。 +2. 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。 +3. 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。 +4. 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。 + ### 线程池 #### 起源 @@ -67,6 +112,19 @@ new Thread弊端: * threadFactory:线程工厂,来创建线程 * rejectHandler:当拒绝任务提交时的策略(抛异常、用调用者所在的线程执行任务、丢弃队列中第一个任务执行当前任务、直接丢弃任务) +#### 线程池几个核心参数在CPU密集型、IO密集型业务场景的配置 +核心线程数(Core Pool Size) +CPU 密集型:CPU 密集型任务主要依赖处理器资源,因此推荐将核心线程数设置为等于 CPU 核心数或稍高于 CPU 核心数(例如 CPU核数 + 1),以充分利用硬件资源,减少上下文切换的开销。 +IO 密集型:IO 密集型任务在等待 IO 操作完成时不会占用 CPU,这时可以设置更多的核心线程,通常可以是 2 * CPU核数 + 1 或者更高,以便在等待 IO 期间能有更多线程准备处理后续的计算任务。 + +线程保持存活时间(Keep-Alive Time) +CPU 密集型:通常设置较短的保持存活时间,因为CPU密集型任务一旦完成,线程可能不需要再次激活,过多的空闲线程会浪费资源。 +IO 密集型:可以设置较长的保持存活时间,允许线程在等待下一次 IO 操作时保持一段时间的空闲状态,以备快速响应新的请求。 + +工作队列(Work Queue) +CPU 密集型:可以选择较小的有界阻塞队列,比如 SynchronousQueue,这可以防止过多的线程创建,保持核心线程始终忙碌。 +IO 密集型:可以使用较大的有界队列,如 LinkedBlockingQueue,允许更多的任务在等待,减少线程的创建和销毁。 + #### 创建线程的逻辑 以下任务提交逻辑来自ThreadPoolExecutor.execute方法: @@ -86,7 +144,7 @@ new Thread弊端: #### CountDownLatch 计数器闭锁是一个能阻塞主线程,让其他线程满足特定条件下主线程再继续执行的线程同步工具。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/countdownlatch.png) +![](../images/countdownlatch.png) 图中,A为主线程,A首先设置计数器的数到AQS的state中,当调用await方法之后,A线程阻塞,随后每次其他线程调用countDown的时候,将state减1,直到计数器为0的时候,A线程继续执行。 使用场景: @@ -128,7 +186,7 @@ public class CountDownLatchTest { #### CyclicBarrier 可以让一组线程相互等待,当每个线程都准备好之后,所有线程才继续执行的工具类 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/cyclicbarrier.png) +![](../images/cyclicbarrier.png) 与CountDownLatch类似,都是通过计数器实现的,当某个线程调用await之后,计数器减1,当计数器大于0时将等待的线程包装成AQS的Node放入等待队列中,当计数器为0时将等待队列中的Node拿出来执行。 @@ -139,4 +197,117 @@ public class CountDownLatchTest { 使用场景: 有四个游戏玩家玩游戏,游戏有三个关卡,每个关卡必须要所有玩家都到达后才能允许通过。其实这个场景里的玩家中如果有玩家A先到了关卡1,他必须等到其他所有玩家都到达关卡1时才能通过,也就是说线程之间需要相互等待。 +### 编程题 +交替打印奇偶数 + +```java +public class PrintOddAndEvenShu { + private int value = 0; + + private synchronized void printOdd() { + while (value <= 100) { + if (value % 2 == 1) { + System.out.println(Thread.currentThread() + ": -" + value++); + this.notify(); + } else { + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + } + + } + + private synchronized void printEven() { + while (value <= 100) { + if (value % 2 == 0) { + System.out.println(Thread.currentThread() + ": --" + value++); + this.notify(); + } else { + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + public static void main(String[] args) throws InterruptedException { + PrintOddAndEvenShu print = new PrintOddAndEvenShu(); + Thread t1 = new Thread(print::printOdd); + Thread t2 = new Thread(print::printEven); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + } +} +``` + + +多线程交替打印alibaba: https://blog.csdn.net/CX610602108/article/details/106427979 +我自行实现的简单解法: +```java + +public class Test { + private int currentI = 0; + + private synchronized void printSpace(int inputLength) { + while (true) { + if (currentI == inputLength) { + currentI = 0; + System.out.print(" "); + this.notifyAll(); + } else { + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + private synchronized void printLetter(int i, char c) { + while (true) { + if (i == currentI) { + currentI++; + System.out.print(c); + this.notifyAll(); + } else { + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + private synchronized void print(String input) throws Exception { + Test test = new Test(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + int finalI = i; + Thread tt = new Thread(() -> test.printLetter(finalI, c)); + tt.start(); + } + Thread space = new Thread(() -> test.printSpace(input.length())); + space.start(); + space.join(); + } + + + public static void main(String[] args) throws Exception { + Test test = new Test(); + test.print("alibaba"); + } +} + +``` + 欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ diff --git "a/MD/Java\345\237\272\347\241\200-\351\233\206\345\220\210.md" "b/MD/Java\345\237\272\347\241\200-\351\233\206\345\220\210.md" index 9d04e51..deee610 100644 --- "a/MD/Java\345\237\272\347\241\200-\351\233\206\345\220\210.md" +++ "b/MD/Java\345\237\272\347\241\200-\351\233\206\345\220\210.md" @@ -9,23 +9,36 @@ |扩容之后容量为原来的1.5倍|无| ### HashMap -1. JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题 +1. JDK 1.8 以前 HashMap 的实现是 数组+单链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题 2. 为什么线程不安全?多线程PUT操作时可能会覆盖刚PUT进去的值;扩容操作会让链表形成环形数据结构,形成死循环 3. 容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 size > 16*0.75 时就会发生扩容(容量和负载因子都可以自由调整)。 4. 为什么容量是2的倍数?在根据hashcode查找数组中元素时,取模性能远远低于与性能,且和2^n-1进行与操作能保证各种不同的hashcode对应的元素也能均匀分布在数组中 +### HashMap rehash过程: +1.7: +1. 空间不够用了,所以需要分配一个大一点的空间,然后保存在里面的内容需要重新计算 hash +2. 在准备好新的数组后,map会遍历数组的每个“桶”,然后遍历桶中的每个Entity,重新计算其hash值(也有可能不计算),找到新数组中的对应位置,以头插法插入新的链表 +3. 因为是头插法,因此新旧链表的元素位置会发生转置现象。元素迁移的过程中在多线程情境下有可能会触发死循环(无限进行链表反转),因此HashMap线程不安全 + +1.8: +1. 底层结构为:数组+单链表/红黑树。因此如果某个桶中的链表长度大于等于8了,则会判断当前的hashmap的容量是否大于64,如果小于64,则会进行扩容;如果大于64,则将链表转为红黑树 +2. java1.8+在扩容时,不需要重新计算元素的hash进行元素迁移。而是用原先位置key的hash值与旧数组的长度(oldCap)进行"与"操作。 +3. 如果结果是0,那么当前元素的桶位置不变。 +4. 如果结果为1,那么桶的位置就是原位置+原数组 长度 +5. 值得注意的是:为了防止java1.7之前元素迁移头插法在多线程是会造成死循环,java1.8+后使用尾插法 + ### ConcurrentHashMap原理 [http://www.jasongj.com/java/concurrenthashmap/](http://www.jasongj.com/java/concurrenthashmap/) HashTable 在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。 ConcurrentHashMap 将 hash 表分为 16 个桶(默认值) 最大并发个数就是Segment的个数,默认值是16,可以通过构造函数改变一经创建不可更改,这个值就是并发的粒度,每一个segment下面管理一个table数组,加锁的时候其实锁住的是整个segment #### Java7 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/j3.jpg) +![](../images/j3.jpg) ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。 #### Java8 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/j4.jpg) +![](../images/j4.jpg) 1. 为进一步提高并发性,放弃了分段锁,锁的级别控制在了更细粒度的table元素级别,也就是说只需要锁住这个链表的head节点,并不会影响其他的table元素的读写,好处在于并发的粒度更细,影响更小,从而并发效率更好 2. 使用CAS + synchronized 来保证实现put操作:如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作。如果该put操作使得当前链表长度超过一定阈值,则将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)),插入操作完成之后如果所有元素的数量大于当前容量(默认16)*负载因子(默认0.75)就进行扩容。 diff --git "a/MD/Web\346\241\206\346\236\266-Spring.md" "b/MD/Web\346\241\206\346\236\266-Spring.md" index 2847f9a..ada11b3 100644 --- "a/MD/Web\346\241\206\346\236\266-Spring.md" +++ "b/MD/Web\346\241\206\346\236\266-Spring.md" @@ -30,9 +30,17 @@ Spring是个包含一系列功能的合集,如快速开发的Spring Boot,支 ### IOC(DI) -控制反转:原来是自己主动去new一个对象去用,现在是由容器工具配置文件创建实例让自己用,以前是自己去找妹子亲近,现在是有中介帮你找妹子,让你去挑选,说白了就是用面向接口编程和配置文件减少对象间的耦合,同时解决硬编码的问题(XML) +* 控制反转 -依赖注入:在运行过程中当你需要这个对象才给你实例化并注入其中,不需要管什么时候注入的,只需要写好成员变量和set方法 +由 Spring IOC 容器来负责对象的生命周期和对象之间的关系。IoC 容器控制对象的创建,依赖对象的获取被反转了 +没有 IoC 的时候我们都是在自己对象中主动去创建被依赖的对象,这是正转。但是有了 IoC 后,所依赖的对象直接由 IoC 容器创建后注入到被注入的对象中,依赖的对象由原来的主动获取变成被动接受,所以是反转 + +* 依赖注入 + +组件之间依赖关系由容器在运行期决定,由容器动态的将某个依赖关系注入到组件之中,提升组件重用的频率、灵活、可扩展 +通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现 + +注入方式:构造器注入、setter 方法注入、接口方式注入 ### Spring AOP #### 介绍 diff --git "a/MD/\345\210\206\345\270\203\345\274\217-CAP\347\220\206\350\256\272.md" "b/MD/\345\210\206\345\270\203\345\274\217-CAP\347\220\206\350\256\272.md" index 50272e6..a509eca 100644 --- "a/MD/\345\210\206\345\270\203\345\274\217-CAP\347\220\206\350\256\272.md" +++ "b/MD/\345\210\206\345\270\203\345\274\217-CAP\347\220\206\350\256\272.md" @@ -1,20 +1,63 @@ ## 理论 CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。 -分布式系统的CAP理论:理论首先把分布式系统中的三个特性进行了如下归纳: +* 一致性(C):对某个指定的客户端来说,读操作能返回最新的写操作结果 +* 可用性(A):非故障节点在合理的时间返回合理的响应 +* 分区容错性(P):分区容错性是指当网络出现分区(两个节点之间无法连通)之后,系统能否继续履行职责 -* 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本) -* 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性) -* 分区容错性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。 +CAP理论就是说在分布式系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以考虑最差情况,分区容忍性是一般是需要实现的 -CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。 +虽然 CAP 理论定义是三个要素中只能取两个,但放到分布式环境下来思考,我们会发现必须选择 P(分区容忍)要素,因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证 C,系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A 冲突了,因为 A 要求返回 no error 和 no timeout。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。 -## 个人理解 -分区容错性是指能否对分布式系统中对数据不一致的容忍,那么在当前硬件条件下是肯定会出现不一致的情况,所以需要在一致性和可用性中进行选择。 +BASE理论的核心思想是:BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)的缩写,它接受在短时间内可能存在的不一致性,但保证最终达到一致性。与ACID(原子性、一致性、隔离性、持久性)相比,BASE更注重系统的可用性和可伸缩性,牺牲了严格的即时一致性。 -如果选择一致性,则需要将数据同步更新到所有节点上,每次写操作就都要等待全部节点写成功,会导致可用性的降低。 -如果选择可用性,就需要将数据复制到很多个节点,需要复制的数据很多,复制的过程缓慢,会导致一致性的降低 +## 强一致如何实现 +什么是强一致性:在分布式存储系统中,强一致性是指系统保证任意时刻数据是一致的,即无论在任何节点上进行的操作,都能立即在所有节点上访问到最新的数据。 -BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 +分布式共识算法。其中,Paxos 和 Raft 是两个著名的共识算法,它们可以用于实现分布式系统的强一致性。这些算法通过选举和投票来确保各个节点的操作顺序一致,从而达到强一致性的要求。 +1. Paxos 算法通过多个阶段的提议、投票和决议过程来确保一个值被所有参与者接受。算法包括提议者(Proposer)、接受者(Acceptor)和学习者(Learner)的角色,并且使用一系列编号递增的消息来进行通信,确保在任何时候只有一个提议会被接受。 +2. Raft 使用了明确的领导者(Leader)角色,简化了状态机复制的过程。领导者负责处理所有的客户端请求并维护集群状态的一致性。它通过日志复制、心跳机制以及选举流程确保在任何时刻都有一个有效的领导者,并保持集群内的日志一致性。 +3. Raft 可以看作是对 Paxos 算法的一种工程化改进,它保留了 Paxos 对于分布式一致性问题的核心解决方案,同时通过对算法进行模块化和清晰化设计,极大地降低了开发者理解和实施分布式共识协议的难度 -欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ + +## 数据一致性解决方案 +强一致性(Strong Consistency) +通过事务(如ACID事务)确保每次操作都看到最新的数据状态。这通常需要两阶段提交(2PC)或其他复杂的事务协议,但可能导致性能下降。 + +最终一致性(Eventual Consistency) +允许短暂的数据不一致,但保证在一段时间后所有副本间达到一致。这种模型在分布式系统中广泛使用,如Cassandra和Amazon DynamoDB。 + +分布式事务 +如两阶段提交(2PC)、三阶段提交(3PC)和多阶段提交(MPC)等协议,用于跨节点的事务协调。 + +复制策略 +同步复制:更新在所有副本之间同步完成,保证强一致性,但可能影响性能。 +异步复制:更新在主节点上完成,副本随后更新,牺牲一致性以换取性能。 + +PAXOS/Raft共识算法 +用于在分布式系统中选举领导者并达成一致性决策。 + +幂等性设计 +确保多次执行相同操作不会改变系统状态,有助于防止因重试或网络问题导致的数据不一致 + +## 高可用多活面试题 +解释什么是“多活”架构,并描述其在高可用性中的作用。 +多活架构是指在一个分布式系统中,多个数据中心或节点同时处于活动状态,每个节点都能独立处理请求,从而提高系统的可用性和容灾能力。当某个节点出现故障时,其他节点仍能继续提供服务。 + +描述一下典型的多活架构部署策略,例如如何处理数据同步和流量路由。 +多活架构通常采用数据同步机制(如主从复制、分布式数据库)保持数据一致性,同时使用智能DNS、负载均衡器或路由策略将流量分散到各个活节点。在故障发生时,可以快速切换流量路由,确保服务不中断。 + +如何在多活架构中实现故障检测和自动切换? +通过心跳检测、健康检查、Zookeeper或Consul等服务发现机制监控各个节点的状态,一旦检测到故障,立即触发流量切换策略,将流量导向其他正常运行的节点。 + +谈谈多活架构下的数据一致性挑战及解决方案 +数据一致性是多活架构中的主要挑战。可以采用异步复制、分布式事务(如2PC、Saga)、最终一致性等方法来平衡数据一致性和高可用性。 + +如何设计一个高可用多活系统?请简述关键要素。 +关键要素包括:负载均衡(如DNS轮询、硬件负载均衡器)、数据复制与一致性(如主从复制、分布式数据库)、故障检测与切换机制(如健康检查、心跳检测)、网络连通性保障(如SDN、专线连接)、自动化运维工具(如自动化部署、监控报警)。 + +多活架构面临的主要挑战有哪些?如何解决? +主要挑战包括数据一致性、网络延迟、故障检测与恢复、运维复杂度等。解决方案涉及采用合适的数据同步技术、优化网络架构(如使用CDN、专线)、实施自动化运维工具、以及建立完善的监控和告警体系。 + +在多活架构中,如何处理跨数据中心的事务? +可以采用分布式事务协调服务(如Seata、LRA),或者通过Saga模式、TCC(Try-Confirm-Cancel)模式来处理跨数据中心的长事务,确保事务的原子性和一致性。 diff --git "a/MD/\345\210\206\345\270\203\345\274\217-\344\270\200\350\207\264\346\200\247hash.md" "b/MD/\345\210\206\345\270\203\345\274\217-\344\270\200\350\207\264\346\200\247hash.md" index 88b1da3..4e2b9c2 100644 --- "a/MD/\345\210\206\345\270\203\345\274\217-\344\270\200\350\207\264\346\200\247hash.md" +++ "b/MD/\345\210\206\345\270\203\345\274\217-\344\270\200\350\207\264\346\200\247hash.md" @@ -14,22 +14,22 @@ 一致 Hash 算法是将所有的哈希值构成了一个环,其范围在 `0 ~ 2^32-1`。如下图: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/一致性hash1.jpg) +![](../images/一致性hash1.jpg) 之后将各个节点散列到这个环上,可以用节点的 IP、hostname 这样的唯一性字段作为 Key 进行 `hash(key)`,散列之后如下: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/一致性hash2.jpg) +![](../images/一致性hash2.jpg) 之后需要将数据定位到对应的节点上,使用同样的 `hash 函数` 将 Key 也映射到这个环上。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/一致性hash3.jpg) +![](../images/一致性hash3.jpg) 这样按照顺时针方向就可以把 k1 定位到 `N1节点`,k2 定位到 `N3节点`,k3 定位到 `N2节点`。 ### 容错性 这时假设 N1 宕机了: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/一致性hash4.jpg) +![](../images/一致性hash4.jpg) 依然根据顺时针方向,k2 和 k3 保持不变,只有 k1 被重新映射到了 N3。这样就很好的保证了容错性,当一个节点宕机时只会影响到少少部分的数据。 @@ -37,22 +37,22 @@ 当新增一个节点时: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/一致性hash5.jpg) +![](../images/一致性hash5.jpg) -在 N2 和 N3 之间新增了一个节点 N4 ,这时会发现受印象的数据只有 k3,其余数据也是保持不变,所以这样也很好的保证了拓展性。 +在 N2 和 N3 之间新增了一个节点 N4 ,这时会发现受影响的数据只有 k3,其余数据也是保持不变,所以这样也很好的保证了拓展性。 ## 虚拟节点 到目前为止该算法依然也有点问题: 当节点较少时会出现数据分布不均匀的情况: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/一致性hash6.jpg) +![](../images/一致性hash6.jpg) 这样会导致大部分数据都在 N1 节点,只有少量的数据在 N2 节点。 为了解决这个问题,一致哈希算法引入了虚拟节点。将每一个节点都进行多次 hash,生成多个节点放置在环上称为虚拟节点: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/一致性hash7.jpg) +![](../images/一致性hash7.jpg) 计算时可以在 IP 后加上编号来生成哈希值。 @@ -60,4 +60,15 @@ https://github.com/crossoverJie/JCSprout/blob/master/MD/Consistent-Hash.md +## Redis用了吗? +降低扩容缩容时传统哈希算法重建映射影响范围,一致性哈希不是唯一的解决办法。 + +redis 缓存采用了固定哈希槽的方式维护 Key 和存储节点的关系。每个节点负责一部分槽,槽的总数是固定的 16384,无论节点数怎么变, Key 进行哈希运算再对槽总数取模的值都是不变的。 + +插入数据时,先算出数据应该分配到哪个槽,再去查槽在哪个节点,这样就实现了把 Key 通过哈希算法均匀的分布到大量的槽上。槽分配到哪个节点是人为配置的,只要配置的槽均匀,最终节点上的 Key 就会均匀分布到物理节点,扩容缩容时也只需要迁移部分槽的数据。 + +在一致性哈希算法中,引入虚拟节点其实和固定哈希槽有异曲同工之妙,都是将最终存储的节点和数据的 Key 之间多做一层转换,避免节点总数的变动影响太多映射关系,虽然固定哈希槽不如一致性哈希加虚拟节点灵活,但是考虑到 redis 的存储的数据量级与多数分布式数据库相比差距很大, 固定哈希槽策略对于 redis 已经完全够用了。 + +一致性哈希算法的应用非常广泛,在分布式数据库 Cassandra 、缓存Memcache、分布式服务框架Dubbo中都有应用。 + 欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ diff --git "a/MD/\345\210\206\345\270\203\345\274\217-\344\272\213\345\212\241.md" "b/MD/\345\210\206\345\270\203\345\274\217-\344\272\213\345\212\241.md" index fff0ea3..0272354 100644 --- "a/MD/\345\210\206\345\270\203\345\274\217-\344\272\213\345\212\241.md" +++ "b/MD/\345\210\206\345\270\203\345\274\217-\344\272\213\345\212\241.md" @@ -9,7 +9,7 @@ 如果你要操作别人的服务的库,你必须是通过**调用别的服务的接口**来实现,绝对不允许交叉访问别人的数据库。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/分布式事务1.png) +![](../images/分布式事务1.png) ### TCC 方案 TCC 的全称是:Try、Confirm、Cancel。 @@ -26,7 +26,7 @@ TCC 的全称是:Try、Confirm、Cancel。 但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码很难维护。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/分布式事务2.png) +![](../images/分布式事务2.png) ### 本地消息表 本地消息表其实是国外的 ebay 搞出来的这么一套思想。 @@ -42,7 +42,7 @@ TCC 的全称是:Try、Confirm、Cancel。 这个方案说实话最大的问题就在于**严重依赖于数据库的消息表来管理事务**啥的,会导致如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/分布式事务3.png) +![](../images/分布式事务3.png) ### 可靠消息最终一致性方案 这个的意思,就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。 @@ -56,7 +56,7 @@ TCC 的全称是:Try、Confirm、Cancel。 5. 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。 6. 这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用 RocketMQ 支持的,要不你就自己基于类似 ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/分布式事务4.png) +![](../images/分布式事务4.png) ### 最大努力通知方案 这个方案的大致意思就是: diff --git "a/MD/\345\210\206\345\270\203\345\274\217-\345\215\217\350\260\203\345\231\250.md" "b/MD/\345\210\206\345\270\203\345\274\217-\345\215\217\350\260\203\345\231\250.md" index 2c8d3e0..f82e197 100644 --- "a/MD/\345\210\206\345\270\203\345\274\217-\345\215\217\350\260\203\345\231\250.md" +++ "b/MD/\345\210\206\345\270\203\345\274\217-\345\215\217\350\260\203\345\231\250.md" @@ -1,22 +1,22 @@ ### 分布式协调 这个其实是 zookeeper 很经典的一个用法,简单来说,就好比,你 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上**对某个节点的值注册个监听器**,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 立马就可以收到通知,完美解决。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/zookeeper1.png) +![](../images/zookeeper1.png) ### 分布式锁 举个栗子。对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也**尝试去创建**那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/zookeeper2.png) +![](../images/zookeeper2.png) ### 元数据/配置信息管理 zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持 zookeeper 么? -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/zookeeper3.png) +![](../images/zookeeper3.png) ### HA高可用性 这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个**重要进程一般会做主备**两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/zookeeper4.png) +![](../images/zookeeper4.png) https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/zookeeper-application-scenarios.md diff --git "a/MD/\345\210\206\345\270\203\345\274\217-\346\266\210\346\201\257\351\230\237\345\210\227.md" "b/MD/\345\210\206\345\270\203\345\274\217-\346\266\210\346\201\257\351\230\237\345\210\227.md" index 15eb1b2..02fd880 100644 --- "a/MD/\345\210\206\345\270\203\345\274\217-\346\266\210\346\201\257\351\230\237\345\210\227.md" +++ "b/MD/\345\210\206\345\270\203\345\274\217-\346\266\210\346\201\257\351\230\237\345\210\227.md" @@ -34,7 +34,7 @@ RabbitMQ 如果丢失了数据,主要是因为你消费的时候,刚消费 消息体通过hash分派到队列里,每个队列对应唯一一个消费者。比如下面的示例中,订单号相同的消息会被先后发送到同一个队列中: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/消息顺序性.jpg) +![](../images/消息顺序性.jpg) 在获取到路由信息以后,会根据MessageQueueSelector实现的算法来选择一个队列,同一个订单号获取到的肯定是同一个队列。 @@ -56,7 +56,7 @@ Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的,如果这上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/kafka集群.png) +![](../images/kafka集群.png) https://github.com/doocs/advanced-java#%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97 https://dbaplus.cn/news-73-1123-1.html diff --git "a/MD/\345\210\206\345\270\203\345\274\217-\351\224\201.md" "b/MD/\345\210\206\345\270\203\345\274\217-\351\224\201.md" index 02cb563..a1ced4a 100644 --- "a/MD/\345\210\206\345\270\203\345\274\217-\351\224\201.md" +++ "b/MD/\345\210\206\345\270\203\345\274\217-\351\224\201.md" @@ -71,7 +71,7 @@ public void lock(String key, String request, int timeout) throws InterruptedExce 缺点: 1. 单点故障问题 -2. 锁超时问题:如果A拿到锁之后设置了超时时长,但是业务还未执行完成且锁已经被释放,此时其他进程就会拿到锁从而执行相同的业务 +2. 锁超时问题:如果A拿到锁之后设置了超时时长,但是业务还未执行完成且锁已经被释放,此时其他进程就会拿到锁从而执行相同的业务。如何解决?Redission定时延长超时时长避免过期。为什么不直接设置为永不超时?为了防范业务方没写解锁方法或者发生异常之后无法进行解锁的问题 3. 轮询获取锁状态方式太过低效 ### 基于ZooKeeper diff --git "a/MD/\345\210\206\345\270\203\345\274\217-\351\231\220\346\265\201.md" "b/MD/\345\210\206\345\270\203\345\274\217-\351\231\220\346\265\201.md" new file mode 100644 index 0000000..25a8611 --- /dev/null +++ "b/MD/\345\210\206\345\270\203\345\274\217-\351\231\220\346\265\201.md" @@ -0,0 +1,66 @@ +# 常见算法 +对于限流常见有两种算法: + +1. 漏桶算法 +2. 令牌桶算法 + +漏桶算法比较简单,就是将流量放入桶中,漏桶同时也按照一定的速率流出,如果流量过快的话就会溢出(漏桶并不会提高流出速率)。溢出的流量则直接丢弃。 + +如下图所示: + +![](../images/分布式-限流-2.png) + +漏桶算法虽说简单,但却不能应对实际场景,比如突然暴增的流量。 + +这时就需要用到令牌桶算法:令牌桶会以一个恒定的速率向固定容量大小桶中放入令牌,当有流量来时则取走一个或多个令牌。当桶中没有令牌则将当前请求丢弃或阻塞。 + +![](../images/分布式-限流-1.gif) + +相比之下令牌桶可以应对一定的突发流量. + +# RateLimiter实现 + +对于令牌桶的代码实现,可以直接使用Guava包中的RateLimiter。 + +```java +@Override +public BaseResponse getUserByFeignBatch(@RequestBody UserReqVO userReqVO) { + //调用远程服务 + OrderNoReqVO vo = new OrderNoReqVO() ; + vo.setReqNo(userReqVO.getReqNo()); + + RateLimiter limiter = RateLimiter.create(2.0) ; + //批量调用 + for (int i = 0 ;i< 10 ; i++){ + double acquire = limiter.acquire(); + logger.debug("获取令牌成功!,消耗=" + acquire); + BaseResponse orderNo = orderServiceClient.getOrderNo(vo); + logger.debug("远程返回:"+JSON.toJSONString(orderNo)); + } + + UserRes userRes = new UserRes() ; + userRes.setUserId(123); + userRes.setUserName("张三"); + + userRes.setReqNo(userReqVO.getReqNo()); + userRes.setCode(StatusEnum.SUCCESS.getCode()); + userRes.setMessage("成功"); + + return userRes ; +} +``` + +代码可以看出以每秒向桶中放入两个令牌,请求一次消耗一个令牌。所以每秒钟只能发送两个请求。按照图中的时间来看也确实如此(返回值是获取此令牌所消耗的时间,差不多也是每500ms一个)。 + +使用RateLimiter 有几个值得注意的地方: + +允许先消费,后付款,意思就是它可以来一个请求的时候一次性取走几个或者是剩下所有的令牌甚至多取,但是后面的请求就得为上一次请求买单,它需要等待桶中的令牌补齐之后才能继续获取令牌。 + + +# 其他算法 +1. 固定窗口算法又叫计数器算法,是一种简单方便的限流算法。主要通过一个支持原子操作的计数器来累计 1 秒内的请求次数,当 1 秒内计数达到限流阈值时触发拒绝策略。每过 1 秒,计数器重置为 0 开始重新计数。虽然我们限制了 QPS 为 2,但是当遇到时间窗口的临界突变时,如 1s 中的后 500 ms 和第 2s 的前 500ms 时,虽然是加起来是 1s 时间,却可以被请求 4 次。 +2. 滑动窗口算法在固定窗口的基础上,将一个计时窗口分成了若干个小窗口,然后每个小窗口维护一个独立的计数器。当请求的时间大于当前窗口的最大时间时,则将计时窗口向前平移一个小窗口。平移时,将第一个小窗口的数据丢弃,然后将第二个小窗口设置为第一个小窗口,同时在最后面新增一个小窗口,将新的请求放在新增的小窗口中。同时要保证整个窗口中所有小窗口的请求数目之后不能超过设定的阈值。有效解决了窗口切换时可能会产生两倍于阈值流量请求的问题。 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4b39534226cf4981a4c4632f0cab335e~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp) + + diff --git "a/MD/\345\276\256\346\234\215\345\212\241-\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260.md" "b/MD/\345\276\256\346\234\215\345\212\241-\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260.md" index bd3b963..a909bf8 100644 --- "a/MD/\345\276\256\346\234\215\345\212\241-\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260.md" +++ "b/MD/\345\276\256\346\234\215\345\212\241-\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260.md" @@ -4,7 +4,7 @@ 客户端应用进程向注册中心发起查询,来获取服务的位置。服务发现的一个重要作用就是提供一个可用的服务列表。 ## 原理 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/consul.jpg) +![](../images/consul.jpg) 1. 当User Service启动的时候,会向Consul发送一个POST请求,告诉Consul自己的IP和Port 2. Consul 接收到User Service的注册后,每隔10s(默认)会向User Service发送一个健康检查的请求,检验User Service是否健康(Consul其实支持其他健康检查机制) diff --git "a/MD/\345\276\256\346\234\215\345\212\241-\346\234\215\345\212\241\351\205\215\347\275\256\344\270\255\345\277\203.md" "b/MD/\345\276\256\346\234\215\345\212\241-\346\234\215\345\212\241\351\205\215\347\275\256\344\270\255\345\277\203.md" index 0939de5..640d453 100644 --- "a/MD/\345\276\256\346\234\215\345\212\241-\346\234\215\345\212\241\351\205\215\347\275\256\344\270\255\345\277\203.md" +++ "b/MD/\345\276\256\346\234\215\345\212\241-\346\234\215\345\212\241\351\205\215\347\275\256\344\270\255\345\277\203.md" @@ -1,6 +1,6 @@ 我们的每个服务的配置文件都是在自身代码库中,当服务数量达到一定数量后,管理这些分散的配置文件会成为一个痛点。这节课我么就来解决配置文件管理的痛点 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/config.jpg) +![](../images/config.jpg) Spring Cloud Config的目标是将各个微服务的配置文件集中存储一个文件仓库中(比如系统目录,Git仓库等等),然后通过Config Server从文件仓库中去读取配置文件,而各个微服务作为Config Client通过给Config Server发送请求指令来获取特定的Profile的配置文件,从而为自身的应用提供配置信息。同时还提供配置文件自动刷新功能。 diff --git "a/MD/\345\276\256\346\234\215\345\212\241-\347\275\221\345\205\263.md" "b/MD/\345\276\256\346\234\215\345\212\241-\347\275\221\345\205\263.md" index 0306688..486fbe7 100644 --- "a/MD/\345\276\256\346\234\215\345\212\241-\347\275\221\345\205\263.md" +++ "b/MD/\345\276\256\346\234\215\345\212\241-\347\275\221\345\205\263.md" @@ -5,15 +5,15 @@ API网关。它是系统的单个入口服务器。它类似于面向对象的 ### 流量统一管理、路由 它用于解决微服务过于分散,没有一个统一的出入口进行流量管理、路由映射、监控、缓存、负载均衡的问题。 我们用两张图来解释: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/api-gateway-1.jpg) +![](../images/api-gateway-1.jpg) 如上图所示的mst-user-service, mst-good-service, mst-order-service,这些应用会需要一些通用的功能,比如Authentication, 这些功能过于分散,代码就需要在三个服务中都写一遍,因此需要有一个统一的出入口来管理流量,就像下图 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/api-gateway-2.jpg) +![](../images/api-gateway-2.jpg) ### 设备适配 还可以针对不同的渠道和客户端提供不同的API Gateway,对于该模式的使用由另外一个大家熟知的方式叫Backend for front-end, 在Backend for front-end模式当中,我们可以针对不同的客户端分别创建其BFF。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/api-gateway-3.jpg) +![](../images/api-gateway-3.jpg) ### 协议转换 客户端直接调用微服务可能会使用对网络不友好的协议。内部服务之间可能使用Thrift二进制RPC、gRPC协议。两个协议对浏览器不友好并且最好是在内部使用。应用应该在防火墙之外使用HTTP或者WebSocket之类的协议。 diff --git "a/MD/\346\220\234\347\264\242\345\274\225\346\223\216.md" "b/MD/\346\220\234\347\264\242\345\274\225\346\223\216.md" deleted file mode 100644 index a77072f..0000000 --- "a/MD/\346\220\234\347\264\242\345\274\225\346\223\216.md" +++ /dev/null @@ -1,143 +0,0 @@ -搜索引擎是因为个人项目关系,那个搜索引擎非常简单,但有很多东西可以问到,可选择性学习借鉴,了解到对项目是如何提问的,还有问题的深度 - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/j10.png) - -* 网页采集 -* 内容过滤 -* 信息存储 -* 信息检索与后台管理 - -## 爬虫 -### 原理 -仅仅针对网页的源代码进行下载,不包含信息结构化提取 - -* 下载工具:使用HttlClient下载,放入Vector,用ConcurrentHashMap做爬虫深度控制 -* URL解析逻辑:将网页源代码中的URL过滤出来放入集合 - -首先在下载队列中,需要安排一些源URL,当下载工具把网页源码下载下来之后,从中解析出能继续当成源URL的URL放入下载集合中,直到下载工具把下载集合中的内容都下载完成 - -### 为什么用HttpClient为下载工具 -**简单易用,符合需求场景,不需要更高更强的功能** - -HttpURLConnection:本身的 API 不够友好,所提供的功能也有限 -HttpClient:功能强大 -OkHttp:是一个专注于性能和易用性的 HTTP 客户端。OkHttp 会使用连接池来复用连接以提高效率。OkHttp 提供了对 GZIP 的默认支持来降低传输内容的大小。OkHttp 也提供了对 HTTP 响应的缓存机制,可以避免不必要的网络请求。当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址 - -### 如何保存URL -封装一个包含同步的添加、移除方法LinkedList来作为存储URL的集合 - -### 如何做爬虫深度控制 -用ConcurrentHashMap保存【URL -> 深度】的键值对,在爬虫下载的过程中同时记录下深度,所以需要通过多个线程高频地对这个键值对集合进行操作,还要在增加元素的时候需要进行同步操作以免元素被覆盖,还需要避免多线程reset导致的循环依赖问题 - -### URL如何解析并保存 -在下载到网页源码之后,将其中的所有URL通过Jsoup或者对每行内容进行正则表达式提取出来,如果符合预设值的正则表达式就添加到下载集合中。 - -### 多线程爬虫如何实现 -使用Executors.newFixedThreadPool(),因为我爬虫线程基本固定不会需要销毁重建,并且可以控制线程最大数量,方便日后根据机器性能调整。 - -如果问到线程池的原理,请参[这里](http://www.wangtianyi.top/blog/2018/05/08/javagao-bing-fa-wu-xian-cheng-chi/?utm_source=github&utm_medium=github)。 - -### 抵抗反爬虫策略 -#### 动态页面的加载 -phantomjs + selenium - -#### 如何抓取需要登录的页面 -模拟登录之后将sessionId保存到request header的cookie中 - -#### 如何解决IP限制问题 -买个支持ADSL的拨号服务器,便宜的一个月80 - - -#### 每分钟访问频率 -寻找网站访问频率访问限制漏洞,自己探索不同网站的访问频率限制规则 - -#### 验证码 -图像识别 - -### 如何实现数据库连接池 -1. 连接池的建立。使用外部配置的连接池信息初始化一定数量的连接,结合CopyOnWriteArrayList来存储连接 -2. 连接池的管理。当客户请求数据库连接时,从ThreadLocal或线程共享的连接池查看是否有空闲连接,如果存在空闲连接,则将连接的引用借给客户使用;如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数,如果没达到就重新创建一个连接给请求的客户;如果达到就按设定的最大等待时间进行等待,如果超出最大等待时间,则抛出异常给客户。 -3. 连接池的关闭。当应用程序退出时,关闭连接池中所有的连接,释放连接池相关的资源,该过程正好与创建相反。 - -#### [并发、性能问题](http://0x0010.com/2017/09/performance-promote-of-using-threadlocal-in-hikaricp/) -使用synchronized、lock可解决多线程分发与回收线程问题。 - -但性能问题还是很大,主要是因为**加锁、释放锁、挂起、恢复线程的上下文切换的开销**,所以为了提高性能,可进行以下方面的优化: -避免同步操作,更换为其他操作。使用**ThreadLocal**将线程池的副本保存在每个线程中来减少锁竞争,同时结合**CAS乐观锁**处理连接的分发与回收 - -#### 增加连接超时功能 -参考HikariCP中CurrentBag中borrow方法: - -``` -timeout = timeUnit.toNanos(timeout); - do { - final long start = currentTime(); - // 在一定时间内,阻塞地从SynchronousQueue中试图获取生产好的连接实例 - // 如果队列中没有可以用的连接实例,返回null - // 如果队列中有可以用的实例,那么把状态改成USE并且返回 - final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS); - if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { - return bagEntry; - } - - // timeout循环被减小,直到大于0.01毫秒 - timeout -= elapsedNanos(start); - } while (timeout > 10_000); - -``` - -### TF-IDF -#### 原理 -如果某个词或短语在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。 - -一个词在本文出现次数/本文总词数 -(文章总数/这个词在所有文章中出现次数)取对数 - 相乘 - -#### 目的 -去掉网页内容相同或相似的网页 - -#### 用法 -用TF-IDF(term frequency–inverse document frequency)统计每个文章中少数重要词(排序后的前几个),用MD5算法把每个词算出一个16进制的数全部加在一起然后比较两个总数,如果相同,那么两篇文章中重要的词就相同的,去掉。 - -## Lucene原理 -### 搜索原理 -词项查询(TermQuery) -布尔查询(BooleanQuery) -短语查询(PhraseQuery) -范围查询(RangeQuery) -百搭查询(WildardQuery) -FuzzQuery(模糊) - -### 倒排索引 -不是由记录来确定属性值,而是由属性值来确定记录的位置。 - -#### 构建过程 - -1. Analyzer分词 -2. 根据单词生成索引表,同时得到“词典文件”(词-> 单词ID) -3. 得到“倒排列表”(单词ID -> 文档ID) - -设有两篇文章1和2 -文章1的内容为:Tom lives in Guangzhou,I live in Guangzhou too. -文章2的内容为:He once lived in Shanghai. - -|关键词|文章号|出现频率|出现位置| -|---|---|---|---| -|guangzhou|1|2|3,6| -|live|1|2|2,5| -|live|2|1|2| -|shanghai|2|1|3| -|tom|1|1|1| - -Lucene将上面三列分别作为词典文件、频率文件、位置文件保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。 - -#### 词典文件(HashMap) - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/j7.jpg) - -#### 倒排表 - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/j8.gif) - -欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ diff --git "a/MD/\346\225\260\346\215\256\345\272\223-MySQL.md" "b/MD/\346\225\260\346\215\256\345\272\223-MySQL.md" index 932621a..c2cebbb 100644 --- "a/MD/\346\225\260\346\215\256\345\272\223-MySQL.md" +++ "b/MD/\346\225\260\346\215\256\345\272\223-MySQL.md" @@ -16,6 +16,11 @@ MyISAM相对简单所以在效率上要优于InnoDB。如果系统插入和查 5. 水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的sharding key,为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表; ### [SQL优化](https://www.imooc.com/video/3711): +优化步骤: + +1. 根据慢日志定位慢查询SQL +2. 用explain分析SQL(type和extra字段分析) +3. 修改SQL或加索引(如下) * 对经常查询的列建立索引,但索引建多了当数据改变时修改索引会增大开销 * 使用精确列名查询而不是*,特别是当数据量大的时候 @@ -23,16 +28,39 @@ MyISAM相对简单所以在效率上要优于InnoDB。如果系统插入和查 * 不用NOT IN,因为会使用全表扫描而不是索引;不用IS NULL,NOT IS NULL,因为会使索引、索引统计和值更加复杂,并且需要额外一个字节的存储空间。 问:max(xxx)如何用索引优化? -答:在xxx列上建立索引,因为索引是B+树顺序排列的,锁在下次查询的时候就会使用索引来查询到最大的值是哪个 +答:在xxx列上建立索引,因为索引是B+树顺序排列的,在下次查询的时候就会使用索引来查询到最大的值是哪个 -问:如何对分页进行优化? -答:`SELECT * FROM big_table order by xx LIMIT 1000000,20`,这条语句会查询出1000020条的所有数据然后丢弃掉前1000000条,为了避免全表扫描的操作,在order by的列上加**索引**就能通过扫描索引来查询。但是这条语句会查询还是会扫描1000020条,还能改进成`select id from big_table where id >= 1000000 order by xx LIMIT 0,20`,用ID作为**过滤**条件将不需要查询的数据直接去除。 +问:有个表特别大,字段是姓名、年龄、班级,如果调用`select * from table where name = xxx and age = xxx`该如何通过建立索引的方式优化查询速度? +答:由于mysql查询每次只能使用一个索引,如果在name、age两列上创建联合索引的话将带来更高的效率。如果我们创建了(name, age)的联合索引,那么其实相当于创建了(name, age)、(name)2个索引。因此我们在创建复合索引时应该将最常用作限制条件的列放在最左边,依次递减。其次还要考虑该列的数据离散程度,如果有很多不同的值的话建议放在左边,name的离散程度也大于age。 + +问:优化大数据量的分页查询? +1. 索引优化:确保用于排序和筛选的字段已经建立了索引 +2. 延迟关联:先获取主键,然后再根据主键获取完整的行数据。这样可以减少数据库在排序时需要处理的数据量。通过将 select * 转变为 select id,把符合条件的 id 筛选出来后,最后通过嵌套查询的方式按顺序取出 id 对应的行 +3. 业务限制:查询页数上限 +4. 使用其他数据结构如ES、分表 + +```sql +-- 优化前 +select * +from people +order by create_time desc +limit 5000000, 10; + +-- 优化后 +select a.* +from people a +inner join( + select id + from people + order by create_time desc + limit 5000000, 10 +) b ON a.id = b.id; +``` #### 最左前缀匹配原则 -在mysql建立联合索引时会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。mysql会一直向右匹配直到遇到范围查询(>/5 and d=6,如果建立(a,b,c,d)联合索引,d就用不到索引,如果建立(a,b,d,c)则都能用到索引。 +定义:在检索数据时从联合索引的最左边开始匹配。一直向右匹配直到遇到范围查询(>/ 5 and d = 6,如果建立(a,b,c,d)索引,d是用不到索引的,如果建立(a,b,d,c)索引,则都可以用到,此外a,b,d的顺序可以任意调整 -问:有个表特别大,字段是姓名、年龄、班级,如果调用`select * from table where name = xxx and age = xxx`该如何通过建立索引的方式优化查询速度? -答:由于mysql查询每次只能使用一个索引,如果在name、age两列上创建联合索引的话将带来更高的效率。如果我们创建了(name, age)的联合索引,那么其实相当于创建了(name, age)、(name)三个索引,这是联合索引的特性,与联合索引的实现原理有关,也是最佳左前缀特性。因此我们在创建复合索引时应该将最常用作限制条件的列放在最左边,依次递减。其次还要考虑该列的数据离散程度,如果有很多不同的值的话建议放在左边,name的离散程度也大于age。 +联合索引原理:联合索引是将第一个字段作为非叶子节点形成B+树结构的,在查询索引的时候先根据该字段一步步查询到具体的叶子节点,叶子节点上还会存在第二个字段甚至第三个字段的已排序结果。所以对于(a,b,c,d)这个联合索引会存在(a),(a,b),(a,b,c),(a,b,c,d)四个索引 ## 事务隔离级别 1. 原子性(Atomicity):事务作为一个整体被执行 ,要么全部执行,要么全部不执行; @@ -40,21 +68,70 @@ MyISAM相对简单所以在效率上要优于InnoDB。如果系统插入和查 3. 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行,然后你可以扯到隔离级别; 4. 持久性(Durability):一个事务一旦提交,对数据库的修改应该永久保存。 -[https://www.jianshu.com/p/4e3edbedb9a8](https://www.jianshu.com/p/4e3edbedb9a8) +不同事务隔离级别的问题: +1. 读未提交(Read Uncommitted):在这个级别下,一个事务可以读取到另一个事务尚未提交的数据变更,即脏读(Dirty Read)现象可能发生。这种隔离级别允许最大的并行处理能力,但并发事务间的数据一致性最弱。 +2. 读已提交(Read Committed):在这种级别下,一个事务只能看到其他事务已经提交的数据,解决了脏读的问题。但是,在同一个事务内,前后两次相同的查询可能会返回不同的结果,这被称为不可重复读(Non-repeatable Read)。 +3. 可重复读(Repeatable Read):这是MySQL InnoDB存储引擎默认的事务隔离级别。在这个级别下,事务在整个生命周期内可以看到第一次执行查询时的快照数据,即使其他事务在此期间提交了对这些数据的修改,也不会影响本事务内查询的结果,因此消除了不可重复读的问题。但是,它不能完全避免幻读(Phantom Read),即在同一事务内,前后两次相同的范围查询可能因为其他事务插入新的行而返回不同数量的结果。 + +为什么可重复读不能完全避免幻读: +1. 幻读的定义: 幻读是指在一个事务内,同一个查询语句多次执行时,由于其他事务提交了新的数据插入或删除操作,导致前后两次查询结果集不一致的情况。具体表现为:即使事务A在开始时已经获取了一个数据范围的快照,当它再次扫描这个范围时,发现出现了之前未读取到的新行,这些新行如同“幻影”一般突然出现。 +2. 可重复读与幻读的关系: 在可重复读隔离级别下,对于已存在的记录,MVCC确保事务能够看到第一次查询时的数据状态,因此不会发生不可重复读。但是,当有新的行被插入到符合事务查询条件的范围内时,MVCC本身并不能阻止这种现象的发生,因为新插入的行对于当前事务是可见的(取决于插入事务的提交时间点和当前事务的Read View)。 +3. 为了减轻幻读的影响,InnoDB在执行某些范围查询时会采用间隙锁(Gap Locks)或者Next-Key Locks来锁定索引区间,防止其他事务在这个范围内插入新的记录,但并非所有的范围查询都会自动加上这样的锁,而且这仅限于使用基于索引的操作,非索引字段的范围查询无法通过间隙锁完全避免幻读。 ## 锁表、锁行 -[https://segmentfault.com/a/1190000012773157](https://segmentfault.com/a/1190000012773157) +1. InnoDB 支持表锁和行锁,使用索引作为检索条件修改数据时采用行锁,否则采用表锁 +2. InnoDB 自动给修改操作加锁,给查询操作不自动加锁 +3. 行锁相对于表锁来说,优势在于高并发场景下表现更突出,毕竟锁的粒度小 +4. 当表的大部分数据需要被修改,或者是多表复杂关联查询时,建议使用表锁优于行锁 ### 悲观锁乐观锁、如何写对应的SQL -[https://www.jianshu.com/p/f5ff017db62a](https://www.jianshu.com/p/f5ff017db62a) +悲观锁:在悲观锁机制下,事务认为在它执行期间数据一定会被其他事务修改,因此在读取数据时就立即对其加锁,阻止其他事务对同一数据进行操作,直到当前事务结束并释放锁。SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE +乐观锁:乐观锁并不在读取数据时立即加锁,而是在更新数据时检查自上次读取以来数据是否已被其他事务修改过。这通常通过在表中添加一个版本号字段或者时间戳字段来实现。 + +### 锁的种类 +按功能分类: +1. 共享锁:Shared Locks,简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁。 +2. 独占锁:排他锁,英文名:Exclusive Locks,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁。 + +按粒度分类: +1. 表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做ddl处理时使用。另外表级别还有AUTO-INC锁:在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。这样一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的 +2. 行锁,也称为记录锁,顾名思义就是在记录上加的锁。锁住数据行,这种加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,MySQL一般都是用行锁来处理并发事务。下面是行锁的类型: + - Record Locks:记录锁,当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁; + - Gap Locks:是为了防止插入幻影记录而提出的,不允许其他事务往这条记录前面的间隙插入新记录。在REPEATABLE READ隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC方案解决,也可以采用加锁方案解决,但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁 + - Next-Key Locks:记录锁和一个gap锁的合体。锁住某条记录,又想阻止其他事务在该记录前面的间隙插入新记录 + - Insert Intention Locks:插入意向锁。一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的gap锁,如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交 + - 隐式锁:当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB会跳过加锁环节,这种机制称为隐式锁。隐式锁是InnoDB实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能 + +### MVCC +MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种用于数据库管理系统中的事务处理机制,它在不使用加锁或者只对数据进行有限锁定的情况下,为并发事务提供了一种相对无冲突的访问方式。MVCC有效地避免了脏读、不可重复读等问题,并极大地提高了数据库系统的并发性能 + +原理: +1. 隐式字段:每行记录除了实际的数据列外,还包含额外的系统信息,例如DB_TRX_ID(最近修改该行事务ID)、DB_ROLL_PTR(回滚指针指向undo日志),以及DB_ROW_ID(行ID)等。这些隐藏字段用于跟踪数据的不同版本。 +2. 事务版本号:每个事务都有一个唯一的事务ID,在事务开始时分配。这个ID用来区分不同事务的操作时间点。 +3. Undo日志:当事务对某一行进行修改时,旧版本的数据会被存放在Undo日志中,形成历史版本链。这样当其他事务需要读取旧版本数据时,可以从Undo日志中获取。 +4. Read View:MVCC通过Read View来决定事务可见的行版本。每个读操作都会创建或复用一个Read View,根据Read View的规则判断当前事务能否看到某个版本的数据:如果被读取行的事务ID小于Read View的最低事务ID,则可见;如果大于等于Read View的最高事务ID且未提交,则不可见;否则,检查该行是否在Read View创建之后被其他已提交事务修改过。 +5. 非锁定读:在MVCC中,存在两种类型的读操作:快照读(Snapshot Read)和当前读(Current Read)。快照读就是基于MVCC实现的,它不会阻塞其他事务,并总是返回某一特定时间点的数据视图。而当前读则会获取最新的数据并加锁以确保一致性。 + ### 索引 #### 原理 -我们拿出一本新华字典,它的目录实际上就是一种索引:非聚集索引。我们可以通过目录迅速定位我们要查的字。而字典的内容部分一般都是按照拼音排序的,这实际上又是一种索引:聚集索引。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。 - -使用B+树来构建索引,为什么不用二叉树?红黑树在磁盘上的查询性能远不如B+树。原因是红黑树最多只有两个子节点,所以高度会非常高,导致遍历查询的次数会多,又因为红黑树在数组中存储的方式,导致逻辑上很近的父节点与子节点可能在物理上很远,导致无法使用磁盘预读的局部性原理,需要很多次IO才能找到磁盘上的数据;但B+树一个节点中可以存储很多个索引的key,且将大小设置为一个页,一次磁盘IO就能读取很多个key,且叶子节点之间还加上了下个叶子节点的指针,遍历索引也会很快。 - -B+树的高度如何计算?在Linux里,每个页默认4KB,假设索引的是8B的long型数据,每个key后有个页号4B,还有6B的其他数据(参考《MySQL技术内幕:InnoDB存储引擎》P193的页面数据),那么每个页的扇出系数为4KB/(8B+4B+6B)=227,即每个页可以索引245个key。在高度h=3时,s=227^3=1100万。通常来说,索引树的高度在2~4。 +聚集索引:数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,所以一个表中只能拥有一个聚集索引。叶子结点即存储了真实的数据行,不再有另外单独的数据页 +非聚集索引:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,所以一个表中可以拥有多个非聚集索引。叶子结点包含索引字段值及指向数据页数据行的逻辑指针 + +#### 索引原理 +为什么使用B+树来构建索引? +1. b+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素; +2. b+树查询必须查找到叶子节点,b树只要匹配到即可不用管元素位置,因此b+树查找更稳定 +3. 对于范围查找来说,b+树只需遍历叶子节点链表即可,b树却需要重复地中序遍历 +4. 红黑树最多只有两个子节点,所以高度会非常高,导致遍历查询的次数会多 +5. 红黑树在数组中存储的方式,导致逻辑上很近的父节点与子节点可能在物理上很远,导致无法使用磁盘预读的局部性原理,需要很多次IO才能找到磁盘上的数据。B+树一个节点中可以存储很多个索引的key,且将大小设置为一个页,一次磁盘IO就能读取很多个key +6. 叶子节点之间还加上了下个叶子节点的指针,遍历索引也会很快。 + +B+树的高度如何计算? +1. 假设B+树的高度为2的话,即有一个根结点和若干个叶子结点。这棵B+树的存放总记录数为=根结点指针数 * 单个叶子节点记录行数 +2. 假设一行记录的数据大小为1k,那么单个叶子节点可以存的记录数 =16k/1k =16. +3. 非叶子节点内存放多少指针呢?我们假设主键ID为bigint类型,长度为8字节(int类型的话,一个int就是32位,4字节),而指针大小是固定的在InnoDB源码中设置为6字节,假设n指主键个数即key的个数,n*8 + (n + 1) * 6 = 16K=16*1024B , 算出n约为 1170,意味着根节点会有1170个key与1171个指针 +4. 一棵高度为2的B+树,能存放1171* 16 = 18736条这样的数据记录。同理一棵高度为3的B+树,能存放1171 * 1171 * 16 = 21939856,也就是说,可以存放两千万左右的记录。B+树高度一般为1-3层,已经满足千万级别的数据存储。 #### 优化 如何选择合适的列建立索引? @@ -63,6 +140,48 @@ B+树的高度如何计算?在Linux里,每个页默认4KB,假设索引的 2. 离散度大(不同的数据多)的列使用索引才有查询效率提升 3. 索引字段越小越好,因为数据库按页存储的,如果每次查询IO读取的页越少查询效率越高 +### count1/count*/count字段 的区别 + +1. count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL +2. count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL +3. count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。 + +count(\*),自动会优化指定到那一个字段。所以没必要去count(1),用count(\*),sql会帮你完成优化的 因此:count(1)和count(\*)基本没有差别! + +### explain详解 +1. id:select 查询序列号。id相同,执行顺序由上至下;id不同,id值越大优先级越高,越先被执行。 +2. select_type:查询数据的操作类型,其值如下: + - simple:简单查询,不包含子查询或 union + - primary:包含复杂的子查询,最外层查询标记为该值 + - subquery:在 select 或 where 包含子查询,被标记为该值 + - derived:在 from 列表中包含的子查询被标记为该值,MySQL 会递归执行这些子查询,把结果放在临时表 + - union:若第二个 select 出现在 union 之后,则被标记为该值。若 union 包含在 from 的子查询中,外层 select 被标记为 derived + - union result:从 union 表获取结果的 select +3. table:显示该行数据是关于哪张表 +4. partitions:匹配的分区 +5. type:表的连接类型,其值,性能由高到底排列如下: + - system:表只有一行记录,相当于系统表 + - const:通过索引一次就找到,只匹配一行数据 + - eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常用于主键或唯一索引扫描 + - ref:非唯一性索引扫描,返回匹配某个单独值的所有行。用于=、< 或 > 操作符带索引的列 + - range:只检索给定范围的行,使用一个索引来选择行。一般使用between、>、<情况 + - index:只遍历索引树 + - ALL:全表扫描,性能最差 +注:前5种情况都是理想情况的索引使用情况。通常优化至少到range级别,最好能优化到 ref +6. possible_keys:显示 MySQL 理论上使用的索引,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。如果该值为 NULL,说明没有使用索引,可以建立索引提高性能 +7. key:显示 MySQL 实际使用的索引。如果为 NULL,则没有使用索引查询 +8. key_len:表示索引中使用的字节数,通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好 显示的是索引字段的最大长度,并非实际使用长度 +9. ref:显示该表的索引字段关联了哪张表的哪个字段 +10. rows:根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好 +11. filtered:返回结果的行数占读取行数的百分比,值越大越好 +12. extra:包含不合适在其他列中显示但十分重要的额外信息,常见的值如下: + - using filesort:说明 MySQL 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。出现该值,应该优化 SQL + - using temporary:使用了临时表保存中间结果,MySQL 在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。出现该值,应该优化 SQL + - using index:表示相应的 select 操作使用了覆盖索引,避免了访问表的数据行,效率不错 + - using where:where 子句用于限制哪一行 + - using join buffer:使用连接缓存 + - distinct:发现第一个匹配后,停止为当前的行组合搜索更多的行 + ## 分区分库分表 分区:把一张表的数据分成N个区块,在逻辑上看最终只是一张表,但底层是由N个物理区块组成的,通过将不同数据按一定规则放到不同的区块中提升表的查询效率。 @@ -78,4 +197,23 @@ B+树的高度如何计算?在Linux里,每个页默认4KB,假设索引的 2. 跨库跨表的join问题。在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上,我们无法join位于不同分库的表,也无法join分表粒度不同的表,结果原本一次查询能够完成的业务,可能需要多次查询才能完成。 3. 额外的数据管理负担和数据运算压力。额外的数据管理负担,最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算。 + +## MySQL数据库主从延迟同步方案 +什么事主从延迟:为了完成主从复制,从库需要通过 I/O 线程获取主库中 dump 线程读取的 binlog 内容并写入到自己的中继日志 relay log 中,从库的 SQL 线程再读取中继日志,重做中继日志中的日志,相当于再执行一遍 SQL,更新自己的数据库,以达到数据的一致性。主从延迟,就是同一个事务,从库执行完成的时间与主库执行完成的时间之差 + +1. 「异步复制」:MySQL 默认的复制即是异步的,主库在执行完客户端提交的事务后会立即将结果返给客户端,并不关心从库是否已经接收并处理。这样就会有一个问题,一旦主库宕机,此时主库上已经提交的事务可能因为网络原因并没有传到从库上,如果此时执行故障转移,强行将从提升为主,可能导致新主上的数据不完整。 +2. 「全同步复制」:指当主库执行完一个事务,并且所有的从库都执行了该事务,主库才提交事务并返回结果给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。 +3. 「半同步复制」:是介于全同步复制与全异步复制之间的一种,主库只需要等待至少一个从库接收到并写到 Relay Log 文件即可,主库不需要等待所有从库给主库返回 ACK。主库收到这个 ACK 以后,才能给客户端返回 “事务完成” 的确认。这样会出现一个问题,就是实际上主库已经将该事务 commit 到了存储引擎层,应用已经可以看到数据发生了变化,只是在等待返回而已。如果此时主库宕机,可能从库还没写入 Relay Log,就会发生主从库数据不一致。为了解决上述问题,MySQL 5.7 引入了增强半同步复制。针对上面这个图,“Waiting Slave dump” 被调整到了 “Storage Commit” 之前,即主库写数据到 binlog 后,就开始等待从库的应答 ACK,直到至少一个从库写入 Relay Log 后,并将数据落盘,然后返回给主库 ACK,通知主库可以执行 commit 操作,然后主库再将事务提交到事务引擎层,应用此时才可以看到数据发生了变化。 +4. 一主多从:如果从库承担了大量查询请求,那么从库上的查询操作将耗费大量的 CPU 资源,从而影响了同步速度,造成主从延迟。那么我们可以多接几个从库,让这些从库来共同分担读的压力。 +5. 强制走主库:如果某些操作对数据的实时性要求比较苛刻,需要反映实时最新的数据,比如说涉及金钱的金融类系统、在线实时系统、又或者是写入之后马上又读的业务,这时我们就得放弃读写分离,让此类的读请求也走主库,这就不存延迟问题了。 +6. 并行复制:MySQL在5.6版本中使用并行复制来解决主从延迟问题。所谓并行复制,指的就是从库开启多个SQL线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。MySQL 5.7对并行复制做了进一步改进,开始支持真正的并行复制功能。MySQL5.7的并行复制是基于组提交的并行复制,官方称为enhanced multi-threaded slave(简称MTS),延迟问题已经得到了极大的改善。其核心思想是:一个组提交的事务都是可以并行回放(配合binary log group commit);slave机器的relay log中 last_committed相同的事务(sequence_num不同)可以并发执行。 + +强制走主库之后,如何在处理大数据量的时候保证实时写查: +1. 表结构与索引优化:确保表结构设计合理,尽量避免数据冗余和更新异常。根据查询条件创建合适的索引,特别是对于高频查询字段,可以显著提高读取速度。 +2. 分区表(Partitioning):对于非常大的表,可以考虑使用分区表功能,将大表物理分割成多个小表,根据时间、范围或其他业务逻辑进行划分,这样不仅能提高查询效率,还可以通过并行处理提高写入速度。 +3. 分库分表:当单表数据量过大时,采用水平拆分或垂直拆分策略将数据分布到多个数据库或表中,进一步提升读写性能。 +4. 查询优化:避免全表扫描,尽可能使用覆盖索引。编写高效的SQL语句,减少不必要的计算和排序操作。 +5. 缓存机制:在数据库层面,可以考虑使用内存表或者缓存技术(如Redis)来临时存储最近写入的数据,以便快速响应实时查询请求。 +6. 实时分析型数据库:对于大数据量且实时性要求极高的场景,可以考虑引入实时分析型数据库系统,例如ClickHouse、Druid等,它们设计之初就为实时写入和查询提供了高效的支持。 + 欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ diff --git "a/MD/\346\225\260\346\215\256\345\272\223-Redis.md" "b/MD/\346\225\260\346\215\256\345\272\223-Redis.md" index b567c4c..8b86d69 100644 --- "a/MD/\346\225\260\346\215\256\345\272\223-Redis.md" +++ "b/MD/\346\225\260\346\215\256\345\272\223-Redis.md" @@ -5,13 +5,51 @@ Redis是一个键值对数据库,数据库中的键值对由字典保存。每 字典的键是一个字符串对象。字典的值则可以是包括【字符串、列表、哈希表、集合或有序集】在内的任意一种 Redis 类型对象。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/redis键空间.png) +![](../images/redis键空间.png) 上图展示了一个包含 number 、 book 、 message 三个键的数据库 —— 其中 number 键是一个列表,列表中包含三个整数值; book 键是一个哈希表,表中包含三个键值对; 而 message 键则指向另一个字符串: -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/redis数据类型.png) +![](../images/redis数据类型.png) -不同的数据类型的具体实现请看: https://redisbook.readthedocs.io/en/latest/index.html#id3 +不同的数据类型的具体实现(压缩列表、跳表必看)请看: https://redisbook.readthedocs.io/en/latest/index.html#id3 + +在线文档:http://118.25.23.115/ + +压缩链表原理: +在内存中是连续存储的,但是不同于数组,为了节省内存,ziplist的每个元素所占的内存大小可以不同 +ziplist将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点 + +与跳表应用场景: +当zset满足以下两个条件的时候,使用ziplist: +1. 保存的元素少于128个 +2. 保存的所有元素大小都小于64字节 + +redis数据过期策略: +定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除 +惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除 +定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key + +内存淘汰策略: +no-eviction:当内存不足以容纳新写入数据时,新写入操作会报错 +allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key +allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key +volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key +volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key +volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除 + +Redis 是单线程吗? +1. Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发生数据给客户端」这个过程是由一个线程(主线程)来完成的但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的: +2. Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务; +3. Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。 + +Redis 采用单线程为什么还这么快? +1. Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构 +2. Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题 +3. Redis 采用了I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。 + +Redis 6.0 之后为什么引入了多线程? +1. 随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。所以为了提高网络请求处理的并行度 +2. 对于读写命令,Redis 仍然使用单线程来处理,Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。 ## 集群模式 来源: @@ -20,18 +58,18 @@ https://www.cnblogs.com/leeSmall/p/8398401.html https://docs.aws.amazon.com/zh_cn/AmazonElastiCache/latest/red-ug/CacheNodes.NodeGroups.html ### 主从 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/redis主从.png) +![](../images/redis主从.png) 用一个redis实例作为主机,其余的实例作为从机。主机和从机的数据完全一致,主机支持数据的写入和读取等各项操作,而从机则只支持与主机数据的同步和读取。因而可以将写入数据的命令发送给主机执行,而读取数据的命令发送给不同的从机执行,从而达到读写分离的目的。 问题是主从模式如果所连接的redis实例因为故障下线了,没有提供一定的手段通知客户端另外可连接的客户端地址,因而需要手动更改客户端配置重新连接。如果主节点由于故障下线了,那么从节点因为没有主节点而同步中断,因而需要人工进行故障转移工作。为了解决这两个问题,在2.8版本之后redis正式提供了sentinel(哨兵)架构。 ### 哨兵 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/redis哨兵.png) +![](../images/redis哨兵.png) 由Sentinel节点定期监控发现主节点是否出现了故障,当主节点出现故障时,由Redis Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。 ### 集群 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/redis集群.png) +![](../images/redis集群.png) redis主从或哨兵模式的每个实例都是全量存储所有数据,浪费内存且有木桶效应。为了最大化利用内存,可以采用集群,就是分布式存储。集群将数据分片存储,每组节点存储一部分数据,从而达到分布式集群的目的。 @@ -84,8 +122,22 @@ redis主从或哨兵模式的每个实例都是全量存储所有数据,浪费 ## 持久化 RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。 -AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。 +1. save:save 是需要占用主线程资源的,会阻塞主线程。 +2. bgsave:使用的是子进程写,不会占用主线程的资源(fork会阻塞主线程,但是copyonwrite过程是纳秒级别相对较快,aof是毫秒级别)。redis使用了操作系统的写时复制技术(COPY -ON-WRITE COW)bgsave子进程是由主进程fork生成的,可以共享主进程的内存,如果主进程有写入命令时候,主进程会把写入的那块内存页先复制一份给fork使用,这样就不影响主进程的写操作,可以继续修改原来的数据,避免了对正常业务的影响。 + + +AOF 持久化记录服务器执行的所有写操作命令,aof日志的重写是由后台程序bgrewriteaof子进程完成的,这也是为了避免阻塞主线程,导致数据性能下降。Redis 还可以在后台对 AOF 文件进行重写(rewrite重写机制就是把这条键值对的多次操作压缩成一次操作,这样就大大减少了aof日志文件的大小。),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。 +1. always 同步回写:每条命令执行完成后,回立即将日志写回磁盘。always回写回占用主线程资源,每次回写都是一个慢速的罗盘操作,基本上可以避免数据的丢失,会对主线程产生影响,如果你对redis数据的准确性要求非常高,而写入和读取占比不高的话,可以采用这种策略。 +2. everysec 每秒回写:每个命令执行完后回先将日志写入aof文件的内存缓冲区,每隔一秒把缓冲区中的命令写入磁盘。每秒回写采用一秒回写一次的策略,避免了同步回写的性能开销,虽然减少了对系统性能的影响,但是如果1秒内产生了大量的写操作,在aof缓冲区积压了很多日志,这时候还没来得及写入aof日志文件就发生了宕机,就会造成数据的丢失。 +3. no 操作系统控制的回写:每个写命令执行完后只是先把日志写入aof文件的内存缓冲区,何时回写磁盘由操作系统决定。采用操作系统控制的回写,在写完aof缓冲区后就可以继续执行命令,但是如果系统发生宕机了,也同样会造成数据的丢失。 + Redis 还可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。 -但实际上持久化会对Redis的性能造成非常严重的影响,如果一定需要保存数据,那么数据就不应该依靠缓存来保存,建议使用其他方式如数据库。所以Redis的持久化意义不大。 +## 渐进式rehash +以下是哈希表渐进式 rehash 的详细步骤: +1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。 +2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。 +3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。 +4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。 +渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。 欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ diff --git "a/MD/\347\247\222\346\235\200\346\236\266\346\236\204.md" "b/MD/\347\247\222\346\235\200\346\236\266\346\236\204.md" index 55d37a8..1c451c9 100644 --- "a/MD/\347\247\222\346\235\200\346\236\266\346\236\204.md" +++ "b/MD/\347\247\222\346\235\200\346\236\266\346\236\204.md" @@ -1,47 +1,59 @@ -## 解决思路 -尽量将请求拦截在数据库的上游,因为一旦大量请求进入数据库,性能会急剧下降 +## 业务上适当规避 +1. 根据某些规则对部分用户直接返回没抢到。比如有些用户曾经被系统识别为恶意用户、垃圾用户、僵尸用户,直接告诉用户已经抢完 +2. 分散不同客户端打开活动入口的时间。比如将1秒内的流量分散到10秒 -## 架构和实现细节 +## 技术上硬核抗压 +1. 限流策略。比如在压力测试中我们测到系统QPS达到了极限,那么超过的部分直接返回已经抢完,通过Nginx的lua脚本可以查redis看到QPS数据从而可以动态调节 +2. 异步削峰。对Redis中的红包预减数量,立即返回抢红包成功请用户等待,然后把发送消息发给消息队列,进行流量的第二次削峰,让后台服务慢慢处理 +3. 服务逻辑。比如业务逻辑是使用事务控制对数据库的创建订单记录,减库存的操作,那么创建操作要放到减库存操作之前,从而避免减数量update的行锁持有时间 +4. 机器配置。当然是服务器机器配置约高越好,数据库配置越猛越好,高并发抢红包主要是CPU与网络IO的负载较高,要选择偏向CPU与网络IO性能的机器 + +### 架构和实现细节 * 前端模块(页面静态化、CDN、客户端缓存) * 排队模块(Redis、队列实现异步下单) * 服务模块(事务处理业务逻辑、避免并发问题) -* 防刷模块(验证码、限IP) +* 防刷模块(验证码、限制用户频繁访问) -## 模块解析 -### 前端模块 +### 模块解析 +#### 前端模块 1. 页面静态化,将后台渲染模板的方式改成使用HTML文件与AJAX异步请求的方式,减少服务端渲染开销,同时将秒杀页面提前放到CDN 2. 客户端缓存,配置Cache-Control来让客户端缓存一定时间页面,提升用户体验 3. 静态资源优化,CSS/JS/图片压缩,提升用户体验 -### 排队模块 +#### 排队模块 1. 对Redis中的抢购对象预减库存,然后立即返回抢购成功请用户等待,这里利用了Redis将大部分请求拦截住,少部分流量进入下一阶段 2. 如果参与秒杀的商品太多,进入下一阶段的流量依然比较大,则需要使用消息队列,Redis过滤之后的请求直接放入到消息队列,让消息队列进行流量的第二次削峰 -### 服务模块 +#### 服务模块 1. 消息队列的消费者,业务逻辑是使用事务控制对数据库的下订单,减库存操作,且下订单操作要放到减库存操作之前,可以避免减库存update的行锁持有时间 -### 防刷模块 +#### 防刷模块 1. 针对恶意用户写脚本去刷,在Redis中保存用户IP与商品ID进行限制 2. 针对普通用户疯狂的点击,使用JS控制抢购按钮,每几秒才能点击一次 3. 在后台生成数学计算型的验证码,使用Graphics、BufferedImage实现图片,ScriptEngineManager计算表达式 -### 异常流程的处理 +#### 异常流程的处理 1. 如果在秒杀的过程中由于服务崩溃导致秒杀活动中断,那么没有好的办法,只能立即尝试恢复崩溃服务或者申请另寻时间重新进行秒杀活动 2. 如果在下订单的过程中由于用户的某些限制导致下单失败,那么应该回滚事务,立即告诉用户失败原因 -## 难点+坑+复盘优化 -### 难点 -1. 理解整个架构设计的思路,围绕这个思路进行思考有什么方式可以做到,在开发过程中多进行压力测试反馈优化 -2. 代码中异常情况的处理与业务上应急预案的准备 +### 总结 +#### 原则 +业务优化思路:业务上适当规避 +技术优化思路:尽量将请求拦截在数据库的上游,因为一旦大量请求进入数据库,性能会急剧下降 +架构原则:合适、简单、演化(以上内容是最终版本,初版可以说没有用到队列,直接使用缓存-数据库这样的架构) + +#### 难点 +1. 如何将高并发大流量一步步从业务和技术方面有条不紊地应对过来 +2. 如何在代码中处理好异常情况以及应急预案的准备 -### 坑 +#### 坑 1. 以上的解决方案能通过利用Redis与消息队列集群来承载非常高的并发量,但是运维成本高。比如Redis与消息队列都必须用到集群才能保证稳定性,会导致运维成本太高。所以需要有专业的运维团队维护。 2. 避免同一用户同时下多个订单,需要写好业务逻辑或在订单表中加上用户ID与商品ID的唯一索引;避免卖超问题,在更新数量的sql上需要加上>0条件 -### 优化 +#### 优化 1. 将7层负载均衡Nginx与4层负载均衡LVS一起使用进一步提高并发量 2. 以上是应用架构上的优化,在部署的Redis、消息队列、数据库、虚拟机偏向选择带宽与硬盘读写速度高的 3. 提前预热,将最新的静态资源同步更新到CDN的所有节点上,在Redis中提前加载好需要售卖的产品信息 -4. 使用分布式限流减少Redis访问压力,在Nginx中配置并发连接数与速度限制,但如果有很多不同的活动同时进行则不适用 +4. 使用分布式限流减少Redis访问压力,在Nginx中配置并发连接数与速度限制 欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ diff --git "a/MD/\347\256\227\346\263\225-\344\272\214\345\217\211\346\240\221-\345\244\232\345\217\211\346\240\221\344\270\255\346\234\200\351\225\277\347\232\204\350\277\236\347\273\255\345\272\217\345\210\227.md" "b/MD/\347\256\227\346\263\225-\344\272\214\345\217\211\346\240\221-\345\244\232\345\217\211\346\240\221\344\270\255\346\234\200\351\225\277\347\232\204\350\277\236\347\273\255\345\272\217\345\210\227.md" new file mode 100644 index 0000000..6ce2af7 --- /dev/null +++ "b/MD/\347\256\227\346\263\225-\344\272\214\345\217\211\346\240\221-\345\244\232\345\217\211\346\240\221\344\270\255\346\234\200\351\225\277\347\232\204\350\277\236\347\273\255\345\272\217\345\210\227.md" @@ -0,0 +1,49 @@ +二叉树:https://cheonhyangzhang.gitbooks.io/leetcode-solutions/content/solutions-501-550/549-binary-tree-longest-consecutive-sequence-ii.html + +多叉树:https://gist.github.com/thanlau/34bd056dd0b1662d5fd483e157a27dfb +```java +/** + * Definition for a multi tree node. + * public class MultiTreeNode { + * int val; + * List children; + * MultiTreeNode(int x) { val = x; } + * } + */ +public class Solution { + /** + * @param root the root of k-ary tree + * @return the length of the longest consecutive sequence path + */ + int max = 0; + + public int longestConsecutive3(MultiTreeNode root) { + // Write your code here + if (root == null){ + return 0; + } + helper(root); + return max; + } + + public int[] helper(MultiTreeNode root) { + int up = 0; + int down = 0; + if (root == null){ + return new int[]{down, up}; + } + for ( MultiTreeNode node : root.children ){ + int[] h = helper(node); + if (node.val == root.val + 1){ + down = Math.max(down, h[0] + 1); + } + if (node.val + 1 == root.val){ + up = Math.max(up, h[1] + 1); + } + } + max = Math.max(max, down + up + 1); + return new int[]{down, up}; + } +} + +``` diff --git "a/MD/\347\256\227\346\263\225-\345\205\266\344\273\226-\344\272\214\345\215\201\350\277\233\345\210\266\347\233\270\345\212\240.md" "b/MD/\347\256\227\346\263\225-\345\205\266\344\273\226-\344\272\214\345\215\201\350\277\233\345\210\266\347\233\270\345\212\240.md" new file mode 100644 index 0000000..d8e0106 --- /dev/null +++ "b/MD/\347\256\227\346\263\225-\345\205\266\344\273\226-\344\272\214\345\215\201\350\277\233\345\210\266\347\233\270\345\212\240.md" @@ -0,0 +1,17 @@ +在二十进制中,我们除了使用数字0-9以外,还使用字母a-j(表示10-19),给定两个二十进制整数,求它们的和。 输入是两个二十进制整数,且都大于0,不超过100位; 输出是它们的和(二十进制),且不包含首0。我们用字符串来表示二十进制整数。 +``` +/* +二十进制相加 +sample +input + +1234567890+abcdefghij +99999jjjjj+9999900001 + +output + +bdfi02467j +iiiij00000 +*/ +``` + diff --git "a/MD/\347\256\227\346\263\225-\346\225\260\347\273\204-\345\277\253\351\200\237\346\216\222\345\272\217-\347\254\254k\345\244\247\344\270\252\346\225\260.md" "b/MD/\347\256\227\346\263\225-\346\225\260\347\273\204-\345\277\253\351\200\237\346\216\222\345\272\217-\347\254\254k\345\244\247\344\270\252\346\225\260.md" index 3251d38..20fbdb9 100644 --- "a/MD/\347\256\227\346\263\225-\346\225\260\347\273\204-\345\277\253\351\200\237\346\216\222\345\272\217-\347\254\254k\345\244\247\344\270\252\346\225\260.md" +++ "b/MD/\347\256\227\346\263\225-\346\225\260\347\273\204-\345\277\253\351\200\237\346\216\222\345\272\217-\347\254\254k\345\244\247\344\270\252\346\225\260.md" @@ -1,19 +1,23 @@ https://leetcode.com/problems/kth-largest-element-in-an-array/ -Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 as one sorted array. +Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element. -Note: +Example 1: -* The number of elements initialized in nums1 and nums2 are m and n respectively. -* You may assume that nums1 has enough space (size that is greater or equal to m + n) to hold additional elements from nums2. -Example: ``` -Input: -nums1 = [1,2,3,0,0,0], m = 3 -nums2 = [2,5,6], n = 3 +Input: [3,2,1,5,6,4] and k = 2 +Output: 5 +``` + +Example 2: -Output: [1,2,2,3,5,6] ``` +Input: [3,2,3,1,2,4,5,5,6] and k = 4 +Output: 4 +``` + +Note: +You may assume k is always valid, 1 ≤ k ≤ array's length. 快排的每一次排序就是找到一个第N大的数,修改快排的递归逻辑。算法复杂度为O(N)。 diff --git "a/MD/\347\263\273\347\273\237\350\256\276\350\256\241-\345\234\250AWS\344\270\212\346\211\251\345\261\225\345\210\260\346\225\260\347\231\276\344\270\207\347\224\250\346\210\267\347\232\204\347\263\273\347\273\237.md" "b/MD/\347\263\273\347\273\237\350\256\276\350\256\241-\345\234\250AWS\344\270\212\346\211\251\345\261\225\345\210\260\346\225\260\347\231\276\344\270\207\347\224\250\346\210\267\347\232\204\347\263\273\347\273\237.md" deleted file mode 100644 index 7bd0978..0000000 --- "a/MD/\347\263\273\347\273\237\350\256\276\350\256\241-\345\234\250AWS\344\270\212\346\211\251\345\261\225\345\210\260\346\225\260\347\231\276\344\270\207\347\224\250\346\210\267\347\232\204\347\263\273\347\273\237.md" +++ /dev/null @@ -1,319 +0,0 @@ -## 第一步 :弄清用例与约束 - -> 收集需求和问题的范围 -> 通过问问题来弄清用例与约束 -> 讨论假设 - -我们假定以下用例 - -### 用例 -解决这个问题需要采用迭代的方法: - -1. 基准/负载测试 -2. 瓶颈检测 -3. 评估替代方案来解决瓶颈 -4. 重复以上 - -这是将基本设计升级为可扩展设计的良好模式 - -除非你有AWS的背景或者正在申请AWS的相关职位,否则在AWS上的实现细节不需要了解。然而**大部分在这里讨论的原理可以应用到除了AWS以外更通用的地方** - -#### 我们将问题约束到如下范围 -* **用户**发送读或写请求 - * **服务**处理,存储用户数据然后返回结果 -* **服务**需要从少量用户发展到数百万用户 - * 在我们升级架构来处理大量用户请求时,讨论通用的扩展模式 -* **服务**需要高可用 - -### 约束和假设 - -#### 状态假设 - -* 流量分布不均 -* 需要关系型数据 -* 从单个用户扩展到千万级用户 - * 用户增加的标识: - * 用户数+ - * 用户数++ - * 用户数+++ - * ... - * 一千万用户 - * 每月10亿次写入 - * 每月1000亿次读取 - * 100:1读写比 - * 每次写入1KB内容 - -#### 计算方式 - -**如果你想做一个大致估算,请向你的面试官表明以下数据:** - -* 每月1TB数据写入 - * 每次写入1KB数据 * 每月10亿次写入 - * 3年有3TB数据写入 - * 假设大多数写入是新的内容而不是已有内容的更新 -* 平均每秒400次写入 -* 平均每秒40000次读取 - -方便的转换公式: - -* 每月有250万秒 -* 每秒一个请求 = 每月250万个请求 -* 每秒40个请求 = 每月1亿个请求 -* 每秒400个请求 = 每月10亿个请求 - -## 第二步:创建高层设计 - -> 大致写出包含所有重要组件的高层设计 - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/aws1.png) - -## 第三步:设计核心组件 - -> 深入每个核心组件的细节 - -### 用例:用户发送读或写的请求 - -#### 目标 - -* 对于仅仅的1-2个用户,你只需要一个基本的配置 - * 简单的单体应用 - * 当需要的时候垂直缩放 - * 监控来确定瓶颈 - -#### 从单体应用开始 - -* EC2上的**服务器** - * 存储用户数据 - * [**MySQL数据库**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9Frdbms) - -使用**垂直扩展**: - -* 选择更好性能的机器 -* 密切关注监控指标以确定如何扩大规模 - * 使用基本监控来确定瓶颈:CPU,内存,IO,网络等 - * CloudWatch, top, nagios, statsd, graphite等 -* 垂直缩放可能会很昂贵 -* 没有故障转移措施 - -*替代方案和其他细节:* - -* **垂直扩展**的替代是[**水平扩展**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E6%B0%B4%E5%B9%B3%E6%89%A9%E5%B1%95) - -#### 从SQL开始,考虑NoSQL - -约束里我们需要关系型数据。我们在开始的时候可以在单机上用**MySQL数据库**. - -*替代方案和其他细节:* - -* [关系型数据库](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9Frdbms) -* 使用[SQL还是NoSQL](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#sql-%E8%BF%98%E6%98%AF-nosql)的原因 - -#### 分配公网静态IP - -* 弹性IP提供一个重启之后不会更改的公网端口 -* 有效的帮助故障转移,只需要将域名指向新IP - -#### 使用DNS - -使用Route 53添加**DNS**将域名映射到实例的公共IP - -*替代方案和其他细节:* - -* [DNS](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%9F%9F%E5%90%8D%E7%B3%BB%E7%BB%9F) - -#### 保护web服务器 - -* 开启必要的端口 - * 允许web服务器对于以下端口回复: - * 80 - HTTP - * 443 - HTTPS - * 22 - SSH(白名单) - * 阻止web服务器进行出站连接 - -*替代方案和其他细节:* - -* [安全](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%AE%89%E5%85%A8) - -## 第四步:扩展设计 - -> 鉴于约束条件,确定并解决瓶颈 - -### 用户数+ - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/aws2.png) - -#### 假设 - -我们的用户数正在增加并且在我们单体应用上的负载也在增加。我们的**基准/负载测试**和**瓶颈**指向了**MySQL数据库**占用更多内存和CPU资源,同时用户内容正在填满磁盘空间 - -到目前为止我们可以通过**水平扩展**解决问题。但不幸的是已经变得非常昂贵并且**MySQL数据库**和**web服务器**无法独立扩展 - -#### 目标 - -* 减轻单体应用的负载并且允许独立扩展 - * 将静态内容分开存储到**AWS对象存储** - * 移动**MySQL数据库**到独立的服务上 -* 缺点 - * 这些改变将增加复杂度并且需要**Web服务器**指向**对象存储**和**MySQL数据库** - * 新组件额外的安全措施 - * AWS的费用将会增加但应该与自己管理类似系统成本进行权衡 - -#### 分离存储静态内容 - -* 考虑使用S3作为**对象存储** - * 高扩展和可靠性 - * 服务端加密 -* 移动静态内容到S3 - * 用户文件 - * JS - * CSS - * 图片 - * 视频 - -#### 移动MySQL数据库到独立的服务 - -* 考虑使用RDS服务管理**MySQL数据库** - * 扩展和管理简单 - * 多个可用区 - * 静态加密 - -#### 保护系统 - -* 在传输和静止时加密数据 -* 使用虚拟私有网络 - * 为单个**Web服务器**创建公共子网以便可以发送和接收网上的流量 - * 为其他组件创建私有网络,组织外部访问 - * 每个组件仅仅对白名单IP开放端口 - -### 用户数++ - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/aws3.png) - -#### 假设 - -我们的**基准/负载测试**和**瓶颈检测**表明我们的单体**Web服务器**在高峰期出现瓶颈,导致回应慢,在某些情况下宕机。随着服务的成熟,我们希望提高可用性和冗余度 - -#### 目的 - -* 以下目标尝试解决**Web服务器**的扩展问题 - * 基于**基准/负载测试**和**瓶颈检测**,你可能只需要实现这些技术中的一个或者两个 -* 使用[**水平扩展**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E6%B0%B4%E5%B9%B3%E6%89%A9%E5%B1%95)处理不断增加的负载并解决单体故障 - * 添加[**负载均衡器**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E5%99%A8) - * ELB是高可用的 - * 如果你想配置自己的**负载均衡器**, 在多个可用区配置[主-主](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%8F%8C%E5%B7%A5%E4%BD%9C%E5%88%87%E6%8D%A2active-active)或[主-备](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%B7%A5%E4%BD%9C%E5%88%B0%E5%A4%87%E7%94%A8%E5%88%87%E6%8D%A2active-passive)可以提高可用性 - * 在**负载均衡器**上关闭SSL去减少在后端服务器上的计算负载并简化证书管理 - * 使用多个**Web服务器**分布到多个区域 - * 使用多个[**主从故障切换**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6)模式的**MySQL**实例来增进冗余度 -* 将**Web服务器**和[**应用服务器**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%BA%94%E7%94%A8%E5%B1%82)分开 - * 独立扩展和配置这两层 - * **Web服务器**可以作为[**反向代理服务器**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86web-%E6%9C%8D%E5%8A%A1%E5%99%A8) - * 比如你可以添加**应用服务器**处理**读API**而其他**应用服务器**处理**写API** -* 移动静态(和一些动态)内容到[**CDN**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%86%85%E5%AE%B9%E5%88%86%E5%8F%91%E7%BD%91%E7%BB%9Ccdn)比如CloudFount去减少负载和延迟 - -### Users+++ - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/aws4.png) - -**注意:** 为了避免过于混乱,没有显示**内部负载均衡器** - -#### 假设 - -我们的**基准/负载测试**和**瓶颈检测**表明我们的读请求很多(100:1读写比),我们的数据库因为大量读取请求导致性能不佳 - -#### 目标 - -* 以下目标尝试去解决在**MySQL数据库**上的问题 - * 基于**基准/负载测试**和**瓶颈检测**,你可能只需要实现这些技术中的一个或者两个 -* 移动以下数据到[**内存缓存**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E7%BC%93%E5%AD%98),比如Elasticache去减少负载和延迟: - * 在**MySQL**中经常读取的内容 - * 首先,在实现**内存缓存**之前试图配置**MySQL数据库**的缓存看是否足以解决瓶颈 - * 来自**Web服务器**的session数据 - * **Web服务器**变成无状态服务,允许**自动缩放** - * 从内存读取1MB需要250微秒,而SSD需要4倍的时间,从硬盘读取需要80倍时间 -* 添加[**MySQL只读副本**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6)来减少主服务器的负载 -* 添加更多**Web服务器**和**应用服务器**来提升响应 - -#### 添加MySQL只读副本 - -* 除了增加和扩展**内存缓存**外, **MySQL只读副本**也能帮助减轻**MySQL主节点**的负载 -* 添加**Web服务器**的逻辑来分开读写数据 -* 在**MySQL只读副本**前添加**负载均衡器**(图里没画) - -### 用户数++++ - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/aws5.png) - -#### 假设 - -我们的**基准/负载测试**和**瓶颈检测**表明在正常工作时间内流量激增,在用户离开办公室时显著下降。我们认为我们可以根据实际负载自动调整服务器来降低成本。我们是个小公司,因此我们希望尽可能多地**自动缩放** - -#### 目标 - -* 添加**自动缩放**来根据需求提供实例数量 - * 跟上流量的高峰 - * 通过关闭未使用的实例来减少费用 -* DevOps自动化 - * Chef, Puppet, Ansible等 -* 继续监控指标以解决瓶颈 - * **主机级别** - 查看单个EC2实例 - * **汇总级别** - 查看负载均衡器统计信息 - * **日志分析** - CloudWatch, CloudTrail, Loggly, Splunk, Sumo - * **外部网站性能** - Pingdom或New Relic - * **处理通知和时间** - PagerDuty - * **错误报告** - Sentry - -#### 添加自动缩放 - -* 考虑AWS的托管服务**自动缩放** - * 为每个**Web服务器**和**应用服务器**创建一个组, 每个组放到多个可用区中 - * 设置最小和最大实例数 - * 通过CloudWatch触发向上和向下扩展 - * 一段时间内的指标: - * CPU负载 - * 延迟 - * 网络流量 - * 自定义指标 - * 缺点 - * 自动缩放可能会带来复杂性 - * 系统可能需要一段时间才能适当扩展以满足不断增长的需求,或者在需求下降时缩小规模 - -### Users+++++ - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/aws6.png) - -**注意:** **自动缩放**组未在图中显示 - -#### 假设 - -随着服务继续朝着约束中的数字增长, **基准/负载测试**和**瓶颈检测**继续迭代来发现和解决新的瓶颈 - -#### 目标 - -由于问题的限制,我们将继续解决扩展问题: - -* 如果我们的**MySQL数据库**开始变得非常大,我们可能会考虑只将有限时间段的数据存储在数据库中,同时将其余数据存储在Redshift等数据仓库中 - * 像Redshift这样的数据仓库可以轻松处理每月1TB的新内容 -* 每秒平均读取请求4万次,读取常用数据的流量可以通过扩展**内存缓存**来解决,这对于处理不均匀分布的流量和流量峰值也很有用 - * **SQL只读副本**可能在处理缓存未命中时遇到问题,我们可能需要采用其他SQL扩展模式 -* 对于单个**SQL写服务**来说,每秒400次平均写入次数(可能更高的峰值)可能很难,同时也表明需要额外的缩放技术 - -SQL扩展模式包括: - -* [联合](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E8%81%94%E5%90%88) -* [分片](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%88%86%E7%89%87) -* [非规范化](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E9%9D%9E%E8%A7%84%E8%8C%83%E5%8C%96) -* [SQL调优](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#sql-%E8%B0%83%E4%BC%98) - -为了进一步解决高读取和写入请求,我们还应考虑将适当的数据移动到[**NoSQL数据库**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#nosql),例如DynamoDB - -我们可以进一步分离[**应用服务器**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%BA%94%E7%94%A8%E5%B1%82)来允许独立的缩放。不需要实时完成的批处理和计算可以使用**队列**和**工作程序**[**异步**](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%BC%82%E6%AD%A5)完成: - -* 例如,在照片服务中,照片上传和缩略图创建可以分开: - * **客户端**上传图片 - * **应用程序服务器**放一个任务到**队列** - * **工作服务**从**队列**中拉取到任务: - * 创建缩略图 - * 上传到**数据库** - * 存储缩略图到**对象存储** - -欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ diff --git "a/MD/\347\263\273\347\273\237\350\256\276\350\256\241-\351\253\230\345\271\266\345\217\221\346\212\242\347\272\242\345\214\205.md" "b/MD/\347\263\273\347\273\237\350\256\276\350\256\241-\351\253\230\345\271\266\345\217\221\346\212\242\347\272\242\345\214\205.md" new file mode 100644 index 0000000..a4ca649 --- /dev/null +++ "b/MD/\347\263\273\347\273\237\350\256\276\350\256\241-\351\253\230\345\271\266\345\217\221\346\212\242\347\272\242\345\214\205.md" @@ -0,0 +1,28 @@ +## 题目 +总共有10亿个红包,在某个时间一起来抢红包,如何设计 + +## 分析 +主要考察的是如何设计高并发系统,但实际上存在一定变通处理方式,不一定全在技术上 + +通常在考虑系统QPS时,应当按业务上的极限QPS作为系统必须承担的QPS设计,比如10亿个红包,因为用户量巨大,极限QPS是可能是10亿 + +但是一般来说几万QPS已经是比较高的并发了,就需要比较大的集群和高并发架构来处理了,所以不可能真正实现10亿的并发架构,而是通过一些变通的方法来处理,比如在业务上做一些处理规避掉部分流量 + +但尽可能地需要实现高并发架构,思路是将大部分流量拦截在系统承载能力低的模块之前 + +## 回答 +#### 业务上适当规避 +在相应法律法规、规章制度、活动说明、用户体验允许的情况下,可以做以下处理 + +1. 根据某些规则对部分用户直接返回没抢到。比如有些用户曾经被系统识别为恶意用户、垃圾用户、僵尸用户,直接告诉用户已经抢完 +2. 分散不同客户端打开活动入口的时间。比如将1秒内的10亿流量分散到10秒,那么平均每秒只有1亿了 +3. 增加客户端入口点击门槛。比如需要手机摇一摇、画一个图案才能触发抢红包的接口 + +#### 技术上硬核抗压 +网关是会接触实打实10亿流量的地方,也是拦截掉最多无效流量的地方,同理,缓存也是 + +1. 限流策略。比如在压力测试中我们测到系统1亿QPS达到了极限,那么超过的部分直接返回已经抢完,通过Nginx的lua脚本可以查redis看到QPS数据从而可以动态调节 +2. 作弊拦截。通过对UA、IP规则直接将抢红包的作弊流量拦截掉 +3. 异步削峰。对Redis中的红包预减数量,立即返回抢红包成功请用户等待,然后把发送消息发给消息队列,进行流量的第二次削峰,让后台服务慢慢处理 +4. 服务逻辑。比如业务逻辑是使用事务控制对数据库的创建红包记录,减红包数量的操作,那么创建操作要放到减数量操作之前,从而避免减数量update的行锁持有时间 +5. 机器配置。当然是服务器机器配置约高越好,数据库配置越猛越好,高并发抢红包主要是CPU的负载较高,要选择偏向CPU性能的机器 diff --git "a/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\346\216\222\345\272\217\347\256\227\346\263\225.md" "b/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\346\216\222\345\272\217\347\256\227\346\263\225.md" index aa6638b..f4d852c 100644 --- "a/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\346\216\222\345\272\217\347\256\227\346\263\225.md" +++ "b/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\346\216\222\345\272\217\347\256\227\346\263\225.md" @@ -1,7 +1,7 @@ ## 快速排序 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/j5.jpg) +![](../images/j5.jpg) -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/quicksort.gif) +![](../images/quicksort.gif) 首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序 @@ -13,9 +13,9 @@ 1. 根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大)。 2. 每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/j11.png) +![](../images/j11.png) -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/heapsort.gif) +![](../images/heapsort.gif) 堆排序过程的最好和最坏时间复杂度是O(nlog2n) diff --git "a/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\346\223\215\344\275\234\347\263\273\347\273\237.md" "b/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\346\223\215\344\275\234\347\263\273\347\273\237.md" new file mode 100644 index 0000000..ad29904 --- /dev/null +++ "b/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\346\223\215\344\275\234\347\263\273\347\273\237.md" @@ -0,0 +1,19 @@ +## 线程和进程区别 +1. 进程是资源分配的最小单位,线程是程序执行的最小单位 +2. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。线程是共享进程的数据空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多 +3. 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。 + +## 硬链接与软链接的区别 +每个文件都是创建了一个指针指向inode(代表物理硬盘的一个区块),可以通过`ls -i`查看。 +硬链接是创建另一个文件,也通过创建指针指向inode。当你删除一个文件/硬链接时,它会删除一个到底层inode的指针。当inode的所有指针都被删除时,才会真正删除文件。 +软连接是另外一种类型的文件,保存的是它指向文件的全路径,访问时会替换成绝对路径 + +## 查看某个进程中的线程 +`ps -T -p ` + +## 查看某个文件夹中每个文件夹的大小 +`du --max-depth=1 -h` + +## CPU负载的含义 +一段时间内CPU正在处理和等待处理的进程总数与CPU最大处理进程数的比例。对于多核CPU来说最大LOAD是最大的核心数量。 +使用`top`和`top -Hp`查找到CPU占用比较大的线程,进而使用`jstack`来排查Java程序的问题 \ No newline at end of file diff --git "a/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\347\275\221\347\273\234\351\200\232\344\277\241\345\215\217\350\256\256.md" "b/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\347\275\221\347\273\234\351\200\232\344\277\241\345\215\217\350\256\256.md" index 3779974..e1f12d9 100644 --- "a/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\347\275\221\347\273\234\351\200\232\344\277\241\345\215\217\350\256\256.md" +++ "b/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\347\275\221\347\273\234\351\200\232\344\277\241\345\215\217\350\256\256.md" @@ -19,7 +19,7 @@ HTTP是TCP/IP协议中的【应用层】协议。它不涉及数据包传输, ## TCP TCP是一种面向连接的、可靠的、基于字节流的TCP/IP协议中的【传输层】协议 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/tcp.png) +![](../images/tcp.png) ### 建立连接:三次握手 1. 建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。 @@ -51,20 +51,37 @@ TCP的滑动窗口是动态的,应用程序在需要(如内存不足)时 * TCP对应的协议(FTP/SMTP/HTTP),UDP(DNS) ## HTTPS -HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性,它大幅增加了中间人攻击的成本。 +HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中被窃取、改变,确保数据的完整性,它大幅增加了中间人攻击的成本。 加密过程: 1. Hello - 握手开始于客户端发送Hello消息。包含服务端为了通过SSL连接到客户端的所有信息,包括客户端支持的各种密码套件和最大SSL版本。服务器也返回一个Hello消息,包含客户端需要的类似信息,包括到底使用哪一个加密算法和SSL版本。 -2. 证书交换 - 现在连接建立起来了,服务器必须证明他的身份。这个由SSL证书实现,像护照一样。SSL证书包含各种数据,包含所有者名称,相关属性(域名),证书上的公钥,数字签名和关于证书有效期的信息。客户端检查它是不是被CA验证过的且根据数字签名验证内容是否被修改过。注意服务器被允许需求一个证书去证明客户端的身份,但是这个只发生在敏感应用。 -3. 密钥交换 - 先使用RSA非对称公钥加密算法(客户端生成一个对称密钥,然后用SSL证书里带的服务器公钥将该对称密钥加密。随后发送到服务端,服务端用服务器私钥解密,到此,握手阶段完成。)或者DH交换算法在客户端与服务端双方确定一将要使用的密钥,这个密钥是双方都同意的一个简单,对称的密钥,这个过程是基于非对称加密方式和服务器的公钥/私钥的。 -4. 加密通信 - 在服务器和客户端加密实际信息是用到对称加密算法,用哪个算法在Hello阶段已经确定。对称加密算法用对于加密和解密都很简单的密钥,这个密钥是基于第三步在客户端与服务端已经商议好的。与需要公钥/私钥的非对称加密算法相反。 - -## Socket -Socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,是操作系统对外开放的接口 - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/socket.jpg) - -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/socket.png) - -欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ +2. 证书交换 - 现在连接建立起来了,服务器必须通过SSL证书证明他的身份。SSL证书包含各种数据,包含所有者名称,相关属性(域名),证书上的公钥,数字签名和关于证书有效期的信息。客户端检查它是不是被CA验证过的且根据数字签名验证内容是否被修改过。注意服务器被允许需求一个证书去证明客户端的身份,但是这个只发生在敏感应用。 +3. 密钥交换 - 使用RSA非对称公钥加密算法(客户端生成一个对称密钥,然后用SSL证书里带的服务器公钥将该对称密钥加密。随后发送到服务端,服务端用服务器私钥解密,到此,握手阶段完成。)或者DH交换算法在客户端与服务端双方确定将要使用的密钥。这个密钥是双方都同意的一个简单,对称的密钥。这个过程是基于非对称加密方式和服务器的公钥/私钥的。 +4. 加密通信 - 在服务器和客户端加密实际信息是用到对称加密算法,用哪个算法在Hello阶段已经确定。对称加密算法是对于加密和解密都很简单的密钥。这个密钥是基于第三步在客户端与服务端已经商议好的。与需要公钥/私钥的非对称加密算法相反。 + +## socket线程安全? +总结来说,虽然系统级别的socket操作本身具备一定程度的并发安全性,但在多线程程序中对socket进行读写时仍需要开发者采取适当的同步策略以保证线程安全。 + +1. 系统调用级别:在操作系统层面,对同一个socket文件描述符(fd)进行读写操作的系统调用如send、recv、write和read等,通常是线程安全的。这意味着多个线程可以同时调用这些函数而不会导致内核数据结构的混乱。 +2. 应用程序级别:然而,在应用程序中直接对同一个socket进行并发读写时,并非完全线程安全。不同的线程如果不加控制地同时对同一socket进行读写,可能会遇到以下问题: + - 数据包边界混淆:TCP是流式传输协议,没有消息边界的概念,两个线程如果并发发送或接收,可能导致接收到的数据边界与发送时不一致。 + - 竞争条件:当多个线程尝试修改socket的状态或缓冲区时,可能会产生未定义的行为,例如,一个线程正在发送数据时,另一个线程关闭了socket,这可能导致不可预测的结果。 + - 同步问题:如果没有适当的锁或其他同步机制来保护共享状态,那么不同线程之间对于何时开始和结束I/O操作的协调就可能出现问题。 +3. 最佳实践:为了在多线程环境下正确且安全地使用socket,通常建议采取以下措施: + - 互斥访问:使用线程同步机制,如互斥锁(mutex),确保任何时候只有一个线程在执行读写操作。 + - 生产者-消费者模型:设计成一个线程专门负责接收数据并放入队列,另一个线程从队列取出数据并发送,通过队列实现线程间的通信和同步。 + - 事件驱动编程:在某些场合下,如使用异步I/O(如epoll/kqueue等)、IOCP(Windows平台)或者事件循环机制,可以避免直接的并发访问,从而在单个线程内处理多个socket连接,减少线程间同步开销。 + +## java socket跟socket有什么区别 +1. 抽象层次: + - 一般意义上的Socket:通常指的是计算机网络编程中的一个抽象概念,是操作系统提供的一种用于进程间通信的机制,通过它可以在网络中不同主机上的进程之间进行双向的数据传输。它是TCP/IP协议栈的一部分,在TCP或UDP等传输层协议之上提供接口。 + - Java Socket:在Java编程语言中,java.net.Socket和java.net.ServerSocket类是对底层操作系统Socket接口的封装,提供了面向对象的方式来创建、管理和使用网络连接。程序员可以通过Java Socket API实现跨网络的客户端-服务器通信。 +2. API形式: + - 通用Socket:在不同的编程语言或环境中,对Socket的操作可能需要直接调用系统API函数(如C语言中的socket()、bind()、listen()、accept()、send()、recv()等)。 + - Java Socket:Java为开发者提供了一套高级且易于使用的API,如Socket socket = new Socket(host, port)来建立到指定主机和端口的连接,以及socket.getInputStream()和socket.getOutputStream()来获取输入输出流以读写数据。 +3. 平台无关性: + - 通用Socket:直接操作系统提供的Socket API时,代码往往具有一定的平台相关性,即在不同的操作系统上可能需要采用不同的API及调用方式。 + - Java Socket:由于Java语言的跨平台特性,其封装的Socket API能够在支持Java的所有平台上运行,使得基于Java Socket编写的网络程序具有良好的可移植性。 + +综上所述,Java Socket是在Java编程环境下对通用Socket概念的具体实现和抽象,为开发者提供了一个更加便捷、安全且平台无关的方式来处理网络通信问题。 diff --git "a/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\350\256\276\350\256\241\346\250\241\345\274\217.md" index 598171e..34aaa51 100644 --- "a/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -9,6 +9,9 @@ http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern +## 状态模式 +https://www.cnblogs.com/xyzq/p/11090344.html + ## 装饰器模式 在保持原有功能接口不变的基础上动态扩展功能的模式。 Java IO包中就使用了该模式,InputStream有太多的实现类如FileInputStream,如果要在每个实现类上加上几种功能如缓冲区读写功能Buffered,则会导致出现ileInputStreamBuffered, StringInputStreamBuffered等等,如果还要加个按行读写的功能,类会更多,代码重复度也太高,你说改原来的接口也行啊,但是这样就是改变接口的内容了,现在我想做到不更改以前的功能,动态地增强原有接口。 @@ -17,14 +20,14 @@ Java IO包中就使用了该模式,InputStream有太多的实现类如FileInpu 缺点: 会引入很多小类,从而增加使用复杂度,多层装饰的时候很容易导致不知道如何使用 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/装饰器模式.png) +![](../images/装饰器模式.png) ## 策略模式 一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。 比如利用策略模式优化过多 if else 代码,将这些if else算法封装成一个一个的类,任意地替换 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/策略模式.png) +![](../images/策略模式.png) 整体思路如下: @@ -42,7 +45,7 @@ https://juejin.im/post/5c5172d15188256a2334a15d ## 观察者模式 当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 -![](https://github.com/xbox1994/2018-Java-Interview/raw/master/images/观察者模式.jpg) +![](../images/观察者模式.jpg) 观察者模式使用三个类 Subject、Observer 和 Client。Subject 对象带有绑定观察者到 Client 对象和从 Client 对象解绑观察者的方法。我们创建 Subject 类、Observer 抽象类和扩展了抽象类 Observer 的实体类。 diff --git "a/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" deleted file mode 100644 index ef655bb..0000000 --- "a/MD/\351\200\232\347\224\250\345\237\272\347\241\200-\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ /dev/null @@ -1,16 +0,0 @@ -## 线程和进程区别 -1. 进程是资源分配的最小单位,线程是程序执行的最小单位。 -2. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。 -3. 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。 - -## 进程间通信方式 -1. 管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 -2. 有名管道: 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。 -3. 信号量: 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 -4. 消息队列: 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 -5. 信号: 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 -6. 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。 -7. 套接字: 套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。 - -欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ - \ No newline at end of file diff --git a/README.md b/README.md index d0863a8..c8e9559 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,68 @@ -本项目是本人于2018年参加BATJ等其他公司电话、现场面试之后总结出来的针对Java面试的知识点或真题,每个点或题目都是在面试中被问过的。 +本项目是本人参加BAT等其他公司电话、现场面试之后总结出来的针对Java面试的知识点或真题,每个点或题目都是在面试中被问过的。 除开知识点,一定要准备好以下套路: -1. **个人介绍**,需要准备1分钟和5分钟两个版本,包括学习经历、工作经历、项目经历、个人优势、一句话总结。一定要自己背得滚瓜烂熟,张口就来 -2. **抽象概念回答方式**,当面试官问你是如何理解多线程的时候,你要知道从定义、来源、实现、问题、优化、应用方面系统性地回答 -3. **项目所占的比重是非常大的**,至少与知识点的比例是五五开,所以必须针对简历中的两个以上的项目,形成包括【架构和实现细节】,【正常流程和异常流程的处理】,【难点+坑+复盘优化】三位一体的组合拳 +1. **个人介绍**,需要准备一个1分钟的介绍,包括学习经历、工作经历、项目经历、个人优势、一句话总结。一定要自己背得滚瓜烂熟,张口就来 +2. **抽象概念**,当面试官问你是如何理解多线程的时候,你要知道从定义、来源、实现、问题、优化、应用方面系统性地回答 +3. **项目强化**,至少与知识点的比例是五五开,所以必须针对简历中的两个以上的项目,形成包括【架构和实现细节】,【正常流程和异常流程的处理】,【难点+坑+复盘优化】三位一体的组合拳 4. **压力练习**,面试的时候难免紧张,可能会严重影响发挥,通过平时多找机会参与交流分享,或找人做压力面试来改善 5. **表达练习**,表达能力非常影响在面试中的表现,能否简练地将答案告诉面试官,可以通过给自己讲解的方式刻意练习 +6. **重点针对**,面试官会针对简历提问,所以请针对简历上写的所有技术点进行重点准备 ### Java基础 -* [JVM原理](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/Java基础-JVM原理.md) -* [集合](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/Java基础-集合.md) -* [多线程](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/Java基础-多线程.md) -* [问题排查](http://www.wangtianyi.top/blog/2018/07/20/javasheng-chan-huan-jing-xia-wen-ti-pai-cha/?utm_source=github&utm_medium=github) +* [JVM原理](MD/Java基础-JVM原理.md) +* [集合](MD/Java基础-集合.md) +* [多线程](MD/Java基础-多线程.md) +* [IO](MD/Java基础-IO.md) +* [问题排查](https://www.wangtianyi.top/article/2018-07-20-javasheng-chan-huan-jing-xia-wen-ti-pai-cha/?utm_source=github&utm_medium=github) ### Web框架、数据库 -* [Spring](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/Web框架-Spring.md) -* [MySQL](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/数据库-MySQL.md) -* [Redis](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/数据库-Redis.md) +* [Spring](MD/Web框架-Spring.md) +* [MySQL](MD/数据库-MySQL.md) +* [Redis](MD/数据库-Redis.md) ### 通用基础 -* [进程与线程](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/通用基础-进程与线程.md) -* [网络通信协议](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/通用基础-网络通信协议.md) -* [排序算法](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/通用基础-排序算法.md) -* [常用设计模式](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/通用基础-设计模式.md) -* [从URL到看到网页的过程](http://www.wangtianyi.top/blog/2017/10/22/cong-urlkai-shi-,ding-wei-shi-jie/?utm_source=github&utm_medium=github) +* [操作系统](MD/通用基础-操作系统.md) +* [网络通信协议](MD/通用基础-网络通信协议.md) +* [排序算法](MD/通用基础-排序算法.md) +* [常用设计模式](MD/通用基础-设计模式.md) +* [从URL到看到网页的过程](https://www.wangtianyi.top/article/2017-10-22-cong-urlkai-shi-,ding-wei-shi-jie/?utm_source=github&utm_medium=github) ### 分布式 -* [CAP理论](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/分布式-CAP理论.md) -* [锁](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/分布式-锁.md) -* [事务](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/分布式-事务.md) -* [消息队列](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/分布式-消息队列.md) -* [协调器](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/分布式-协调器.md) -* [ID生成方式](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/分布式-ID生成方式.md) -* [一致性hash](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/分布式-一致性hash.md) +* [CAP理论](MD/分布式-CAP理论.md) +* [锁](MD/分布式-锁.md) +* [事务](MD/分布式-事务.md) +* [消息队列](MD/分布式-消息队列.md) +* [协调器](MD/分布式-协调器.md) +* [ID生成方式](MD/分布式-ID生成方式.md) +* [一致性hash](MD/分布式-一致性hash.md) +* [限流](MD/分布式-限流.md) ### 微服务 -* [微服务介绍](http://www.wangtianyi.top/blog/2017/04/16/microservies-1-introduction-to-microservies/?utm_source=github&utm_medium=github) -* [服务发现](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/微服务-服务注册与发现.md) -* [API网关](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/微服务-网关.md) -* [服务容错保护](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/微服务-服务容错保护.md) -* [服务配置中心](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/微服务-服务配置中心.md) -### 算法(头条必问) -* [数组-快速排序-第k大个数](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/算法-数组-快速排序-第k大个数.md) -* [数组-对撞指针-最大蓄水](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/算法-数组-对撞指针-最大蓄水.md) -* [数组-滑动窗口-最小连续子数组](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/算法-数组-滑动窗口-最小连续子数组.md) -* [数组-归并排序-合并有序数组](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/算法-数组-归并排序-合并有序数组.md) -* [链表-链表反转-链表相加](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/算法-链表-反转链表-链表相加.md) -* [链表-双指针-删除倒数第n个](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/算法-链表-双指针-删除倒数第n个.md) -* [二叉树-递归-二叉树反转](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/算法-二叉树-递归-二叉树反转.md) -* [动态规划-连续子数组最大和](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/算法-动态规划-连续子数组最大和.md) -* [数据结构-LRU淘汰算法](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/算法-数据结构-LRU淘汰算法.md) +* [微服务介绍](https://www.wangtianyi.top/article/2017-04-16-microservies-1-introduction-to-microservies/?utm_source=github&utm_medium=github) +* [服务发现](MD/微服务-服务注册与发现.md) +* [API网关](MD/微服务-网关.md) +* [服务容错保护](MD/微服务-服务容错保护.md) +* [服务配置中心](MD/微服务-服务配置中心.md) +### 算法 +* [数组-快速排序-第k大个数](MD/算法-数组-快速排序-第k大个数.md) +* [数组-对撞指针-最大蓄水](MD/算法-数组-对撞指针-最大蓄水.md) +* [数组-滑动窗口-最小连续子数组](MD/算法-数组-滑动窗口-最小连续子数组.md) +* [数组-归并排序-合并有序数组](MD/算法-数组-归并排序-合并有序数组.md) +* [数组-顺时针打印矩形](https://www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a) +* [数组-24点游戏](https://leetcode.cn/problems/24-game/description/) +* [链表-链表反转-链表相加](MD/算法-链表-反转链表-链表相加.md) +* [链表-双指针-删除倒数第n个](MD/算法-链表-双指针-删除倒数第n个.md) +* [链表-双指针-重排链表](https://leetcode.cn/problems/reorder-list/description/) +* [二叉树-递归-二叉树反转](MD/算法-二叉树-递归-二叉树反转.md) +* [二叉树-递归-多叉树中最长的连续序列](MD/算法-二叉树-多叉树中最长的连续序列.md) +* [二叉树-trie树](https://leetcode.cn/problems/implement-trie-prefix-tree/description/) +* [动态规划-连续子数组最大和](MD/算法-动态规划-连续子数组最大和.md) +* [数据结构-LRU淘汰算法](MD/算法-数据结构-LRU淘汰算法.md) +* [其他-二十进制相加](MD/算法-其他-二十进制相加.md) +* [有序数组中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/solution/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-s-114/) +* [数组中的k个最小值](https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/zui-xiao-de-kge-shu-by-leetcode-solution/) ### 项目举例 -* [秒杀架构](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/秒杀架构.md) -* [搜索引擎](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/搜索引擎.md) +* [秒杀架构](MD/秒杀架构.md) ### 系统设计 -* [从面试者角度设计一个系统设计题](http://www.wangtianyi.top/blog/2018/08/31/xi-tong-she-ji-mian-shi-ti-:zong-he-kao-cha-mian-shi-zhe-de-da-zhao/?utm_source=github&utm_medium=github) -* [系统设计题答题套路](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E4%B8%80%E4%B8%AA%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E7%9A%84%E9%9D%A2%E8%AF%95%E9%A2%98) -* [在AWS上扩展到数百万用户的系统](https://github.com/xbox1994/2018-Java-Interview/blob/master/MD/系统设计-在AWS上扩展到数百万用户的系统.md) - -欢迎光临[我的博客](http://www.wangtianyi.top/?utm_source=github&utm_medium=github),发现更多技术资源~ +* [系统设计-高并发抢红包](MD/系统设计-高并发抢红包.md) +* [系统设计-在AWS上扩展到数百万用户的系统](https://www.wangtianyi.top/article/2019-03-06-zai-awsshang-kuo-zhan-dao-shu-bai-mo-yong-hu-de-xi-tong/?utm_source=github&utm_medium=github) +* [系统设计-从面试者角度设计一个系统设计题](https://www.wangtianyi.top/article/2018-08-31-xi-tong-she-ji-mian-shi-ti-zong-he-kao-cha-mian-shi-zhe-de-da-zhao/?utm_source=github&utm_medium=github) +### 智力题 +* [概率p输出1,概率1-p输出0,等概率输出0和1](https://blog.csdn.net/qq_29108585/article/details/60765640) +* [判断点是否在多边形内部](https://www.cnblogs.com/muyefeiwu/p/11260366.html) diff --git a/images/AIO.png b/images/AIO.png new file mode 100644 index 0000000..e5ca3f8 Binary files /dev/null and b/images/AIO.png differ diff --git a/images/BIO.png b/images/BIO.png new file mode 100644 index 0000000..b4c5010 Binary files /dev/null and b/images/BIO.png differ diff --git a/images/NIO-1.png b/images/NIO-1.png new file mode 100644 index 0000000..3aa9418 Binary files /dev/null and b/images/NIO-1.png differ diff --git a/images/NIO-3.png b/images/NIO-3.png new file mode 100644 index 0000000..de4a7e6 Binary files /dev/null and b/images/NIO-3.png differ diff --git a/images/NIO-4.png b/images/NIO-4.png new file mode 100644 index 0000000..af53c3b Binary files /dev/null and b/images/NIO-4.png differ diff --git a/images/pay_wx.png b/images/pay_wx.png new file mode 100644 index 0000000..0956481 Binary files /dev/null and b/images/pay_wx.png differ diff --git a/images/wechat_qrcode.jpg b/images/wechat_qrcode.jpg new file mode 100644 index 0000000..fd1a042 Binary files /dev/null and b/images/wechat_qrcode.jpg differ diff --git a/images/wxg.png b/images/wxg.png new file mode 100644 index 0000000..6f41830 Binary files /dev/null and b/images/wxg.png differ diff --git a/images/wxg2.jpg b/images/wxg2.jpg new file mode 100644 index 0000000..97a90a7 Binary files /dev/null and b/images/wxg2.jpg differ diff --git a/images/wxg2.png b/images/wxg2.png new file mode 100644 index 0000000..199aed7 Binary files /dev/null and b/images/wxg2.png differ diff --git "a/images/\345\210\206\345\270\203\345\274\217-\351\231\220\346\265\201-1.gif" "b/images/\345\210\206\345\270\203\345\274\217-\351\231\220\346\265\201-1.gif" new file mode 100644 index 0000000..b9d493b Binary files /dev/null and "b/images/\345\210\206\345\270\203\345\274\217-\351\231\220\346\265\201-1.gif" differ diff --git "a/images/\345\210\206\345\270\203\345\274\217-\351\231\220\346\265\201-2.png" "b/images/\345\210\206\345\270\203\345\274\217-\351\231\220\346\265\201-2.png" new file mode 100644 index 0000000..679167c Binary files /dev/null and "b/images/\345\210\206\345\270\203\345\274\217-\351\231\220\346\265\201-2.png" differ diff --git "a/images/\347\237\245\350\257\206\346\230\237\347\220\203.JPG" "b/images/\347\237\245\350\257\206\346\230\237\347\220\203.JPG" new file mode 100644 index 0000000..d6b59ee Binary files /dev/null and "b/images/\347\237\245\350\257\206\346\230\237\347\220\203.JPG" differ