面试题总结

一.HashMap

  1.1,常见的数据结构:数组结构,链表结构,哈希表结构
        各自的数据结构的特点:
                <1> 数组结构: 存储区间连续、内存占用严重、空间复杂度大

                         优点:随机读取和修改效率高,原因是数组是连续的(随机访问性强,查找速度快)
                         缺点:插入和删除数据效率低,因插入数据,这个位置后面的数据在内存中都要往后移动,且大小固定不易动态扩展。
                <2> 链表结构:存储区间离散、占用内存宽松、空间复杂度小

                         优点:插入删除速度快,内存利用率高,没有固定大小,扩展灵活
                         缺点:不能随机查找,每次都是从第一个开始遍历(查询效率低)
                <3> 哈希表结构:结合数组结构和链表结构的优点,从而实现了查询和修改效率高,插入和删除效率也高的一种数据结构

1.2,HashMap的默认初始长度是多少?如何扩容,什么时候开始扩容?

        HashMap的默认初始长度是16,自动拓展和手动初始化时,长度必须是2的幂,即2^n (每次扩容都是以2的整数次幂扩容

        原因:选择16是为了服务于从Key映射到index的Hash算法,在性能和内存的使用上取平衡,实现一个尽量均匀分布的Hash函数,选取16,是通过位运算的方法进行求取的。

        当hashmap中的元素个数size超过数组长度*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置。

1.3,HashMap为什么线程不安全,如何让它线程安全?

       1.3.1 在jdk1.7中(死循环、数据丢失),多线程并发情况下死循环是因为并发 HashMap 扩容导致的

        并发扩容导致死循环的第一步

                线程 T1 和线程 T2 要对 HashMap 进行扩容操作,此时 T1 和 T2 指向的是链表的头结点元素 A,而 T1 和 T2 的下一个节点,也就是 T1.next 和 T2.next 指向的是 B 节点,
        并发扩容导致死循环的第二步

                线程 T2 时间片用完进入休眠状态,而线程 T1 开始执行扩容操作,一直到线程 T1 扩容完成后,线程 T2 才被唤醒,扩容之后可知线程 T1 执行之后,因为是头插法,所以 HashMap 的顺序已经发生了改变,但线程 T2 对于发生的一切是不可知的,所以它的指向元素依然没变,T2 指向的是 A 元素,T2.next 指向的节点是 B 元素。

       并发扩容导致死循环的第三步
                当线程 T1 执行完,而线程 T2 恢复执行时,死循环就建立

        因为 T1 执行完扩容之后 B 节点的下一个节点是 A,而 T2 线程指向的首节点是 A,第二个节点是 B,这个顺序刚好和 T1 扩完容完之后的节点顺序是相反的。T1 执行完之后的顺序是 B 到 A,而 T2 的顺序是 A 到 B,这样 A 节点和 B 节点就形成死循环了,这就是 HashMap 死循环导致的原因。

        1.3.2,在jdk1.8中对HashMap进行了优化(数据覆盖),在发生hash碰撞,不再采用头插法方式(头部插添加数据),而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全,jdk1.8中HashMap中put操作的主函数, 如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。

        1.3.3,HashMap线程不安全解决方案

                    使用线程安全容器 ConcurrentHashMap 替代(推荐使用此方案)。

                1.3.3.1,ConcurrentHashMap为什么线程安全?

                        <1>,ConcurrentHashMap的key和Value都不能为null

                        <2>,JDK1.7中ConcurrentHashMap使用的锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

                        <3>,JDK1.8放弃了锁分段的做法,采用CAS(CAS:在判断数组中当前位置为null的时候,使用CAS来把这个新的Node(节点)写入数组中对应的位置)和synchronized方式处理并发。以put操作为例,CAS方式确定key的数组下标,synchronized保证链表节点的同步效果。

                        <4>,JDK1.8的做法有什么好处呢?

                                减少内存开销
                                        假设使用可重入锁,那么每个节点都需要继承AQS,但并不是每个节点都需要同步支持,只有链表的头节点(红黑树的根节点)需要同步,这无疑消耗巨大内存。
                                获得JVM的支持
                                        可重入锁毕竟是API级别的,后续的性能优化空间很小。synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。

特别说明

使用线程安全容器 Hashtable 替代(性能低,不建议使用)。
使用 synchronized 或 Lock 加锁 HashMap 之后,再进行操作,相当于多线程排队执行(比较麻烦,也不建议使用)。

1.4,HashMap底层是哈希表结构

        1.4.1,HashMap中的put()和get()的实现原理:

            1.4.1.1,map.put(k,v)实现原理
                        <1>,首先将k,v封装到Node对象当中(节点)。
                        <2>,然后它的底层会调用K的hashCode()方法得出hash值。
                        <3>,通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
           1.4.1.2,map.get(k)实现原理

                <1>,先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
                <2>,通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

1.5,HashMap为何随机增删、查询效率都很高的原因是?
        增删是在链表上完成的,而查询只需扫描部分,则效率高。
        HashMap集合的key,会先后调用两个方法,hashCode and equals方法,这这两个方法都需要重写。

1.6,为什么放在hashMap集合key部分的元素需要重写equals方法?
        因为equals方法默认比较的是两个对象的内存地址 

 1.7、HashMap红黑树原理分析

        相比 jdk1.7 的 HashMap 而言,jdk1.8最重要的就是引入了红黑树的设计红黑树除了插入操作慢其他操作都比链表快,当hash表的单一链表长度超过 8 个的时候,数组长度大于64,链表结构就会转为红黑树结构。当红黑树上的节点数量小于6个,会重新把红黑树变成单向链表数据结构。
        为什么要这样设计呢?好处就是避免在最极端的情况下链表变得很长很长,在查询的时候,效率会非常慢。
        红黑树查询:其访问性能近似于折半查找,时间复杂度 O(logn);
        链表查询:这种情况下,需要遍历全部元素才行,时间复杂度 O(n);
        简单的说,红黑树是一种近似平衡的二叉查找树,其主要的优点就是“平衡“,即左右子树高度几乎一致,以此来防止树退化为链表,通过这种方式来保障查找的时间复杂度为 log(n)。

        关于红黑树的内容,主要有以下几个特性: 

                <1>,每个节点要么是红色,要么是黑色,但根节点永远是黑色的;

                <2>,每个红色节点的两个子节点一定都是黑色;

                <3>,红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色);

                <4>,从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;

                <5>,所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);
                在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件 3 或条件 4,需要通过调整使得查找树重新满足红黑树的条件。

二.并发编程

        读多,用缓存抗,本地缓存catch,redis等

        写多,并行转串行,使用mq等消息中间或者队列做异步或者排队处理

三.多线程,线程池

        3.1,什么情况下需要用多线程?

                (1) 连续的操作,需要花费忍无可忍的过长时间才可能完成
                (2) 并行计算
                (3) 为了等待网络、文件系统、用户或其他I/O响应而耗费大量的执行时间

                3.1.1, 程序包含复杂的计算任务时
                        主要是利用多线程获取更多的CPU时间(资源)。
                3.1.2, 处理速度较慢的外围设备
                        比如:打印时。再比如网络程序,涉及数据包的收发,时间因素不定。使用独立的线程处理这些任务,可使程序无需专门等待结果。
                3.1.3, 程序设计自身的需要
                        windows系统是基于消息循环的抢占式多任务系统,为使消息循环系统不至于阻塞,程序需要多个线程的来共同完成某些任务。

        3.2,多线程的缺点:
                <1>,如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换.
                <2>,更多的线程需要更多的内存空间
                <3>,线程中止需要考虑对程序运行的影响.
                <4>,通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生(                                        线程死锁:线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请                                  对方的资源,所以这两个线程就会互相等待而进入死锁状态。)

        3.3,多线程产生死锁必须具备以下四个条件:

                <1>,互斥条件:该资源任意一个时刻只由一个线程占用。

                <2>,请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

                <3>,不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,                                                           只有自己使用完毕后才释放资源。

                <4>,循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

        3.4,多线程死锁解决方案:

                <1>,破坏互斥条件这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
                
<2>,破坏请求与保持条件一次性申请所有的资源。
                
<3>,破坏不剥夺条件 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
                
<4>,破坏循环等待条件靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

        3.5,创建线程的方式

                <1>,继承Thread类创建线程类 (1)定义Thread类的子类,并重写该类的run方法, 该run方法的方法体就代表了线程要完成的任务。

                <2>,通过Runnable接口创建线程类 (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

        3.6,线程的生命周期

                新建,就绪,运行,阻塞,死亡

        3.7,线程唤醒与阻塞的常用方法

                如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。以下是详细的唤醒方法:

                <1>,sleep() 方法:

                        sleep(毫秒),指定以毫秒为单位的时间,使线程在该时间内进入线程阻塞状态,期间得不到cpu的时间片,等到时间过去了,线程重新进入可执行状态。(暂停线程,不会释放锁)

                <2>,suspend() 和 resume() 方法:

                        挂起和唤醒线程,suspend e()使线程进入阻塞状态,只有对应的resume
e()被调用的时候,线程才会进入可执行状态。(不建议用,容易发生死锁)

                <3>, yield() 方法:

                        会使得线程放弃当前分得的cpu时间片,但此时线程仍然处于可执行状态,随时可以再次分得cpu时间片。yield()方法只能使同优先级的线程有执行的机会。调用
yield()的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。(暂停当前正在执行的线程,并执行其他线程,且让出的时间不可知)

                <4>,wait() 和 notify() 方法(可以实现线程之间通讯)

                        两个方法搭配使用,wait()使线程进入阻塞状态,调用notify()时,线程进入可执行状态。wait()内可加或不加参数,加参数时是以毫秒为单位,当到了指定时间或调用notify()方法时,进入可执行状态。(属于Object类,而不属于Thread类,wait()会先释放锁住的对象,然后再执行等待的动作。由于wait()所等待的对象必须先锁住,因此,它只能用在同步化程序段或者同步化方法内,否则,会抛出异常IllegalMonitorStateException.)

                <5>,join()方法(解决线程顺序问题)

                        也叫线程加入。是当前线程A调用另一个线程B的join()方法,当前线程转A入阻塞状态,直到线程B运行结束,线程A才由阻塞状态转为可执行状态。

特别说明

        以上是Java线程唤醒和阻塞的五种常用方法,不同的方法有不同的特点,其中wait()
notify()是其中功能最强大、使用最灵活的方法,但这也导致了它们效率较低、较容易出错的特性,因此,在实际应用中应灵活运用各种方法,以达到期望的目的与效果!

        3.8,线程之间信方式(四种)

                <1>, 同步(多个线程通过synchronized关键字这种方式来实现线程间的通信)

                <2>,while轮询的方式(比较消耗cpu 因为你不断的在检查是否符合,当然你可以使用睡眠,每隔几秒检查一次,这样就不准确了

                <3>,wait/notify机制( wait()方法是Object 类的方法,它的作用是使当前执行wait()方法的线程等待,在wait()所在的代码行处暂停执行,并释放锁,直到接到通知或中断。

                       notify()方法用来通知那些可能等待该锁的其他线程,如果有多个线程等待,则按照执行wait方法的顺序发出一次性通知(一次只能通知一个!),使得等待排在第一顺序的线程获得锁。需要说明的是,执行notify方法后,当前线程并不会立即释放锁,要等到程序执行完,即退出synchronized同步区域后。

                <4>,管道通信(在JDK中提供了几个管道流类可以使线程间进行通信:                                                1、PipedInputStream和PipedOutputStream ;                                                                            2、PipedReader和PipedWriter)

                <5>,使用 volatile 关键字(基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。)可解决多线程安全非原子性的问题

        3.9,线程池的创建方式

                <1>,newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;

                <2>,newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
                <3>,newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
                <4>,newSingleThreadScheduledExecutor():创建单线程池,返回                                 ScheduledExecutorService,可以进行定时或周期性的工作调度;

                <5>,newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
                <6>,newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,                                    Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,                           利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
                <7>,ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是                                                                              对ThreadPoolExecutor的封装。

        3.10,线程池的 7 大参数

                <1>,corePoolSize:核心线程数。(是指线程池中长期存活的线程数)

                <2>,maximumPoolSize:最大线程数。(线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。)

                <3>,keepAliveTime:空闲线程存活时间。(空闲线程存活时间,当线程池中没有任务时,会销毁一些线程,销毁的线程数=maximumPoolSize(最大线程数)-corePoolSize(核心线程数)。)

                <4>,TimeUnit:时间单位。(空闲线程存活时间的描述单位)

                <5>,BlockingQueue:线程池任务队列。(阻塞队列:线程池存放任务的队列,用来存储线程池的所有待执行任务。)

                <6>,ThreadFactory:创建线程的工厂。(线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。)

                <7>,RejectedExecutionHandler:拒绝策略。(当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。)

                3.10.1,拒绝策略有以下 4 种:

                        <1>,AbortPolicy:拒绝并抛出异常。

                        <2>,CallerRunsPolicy:使用当前调用的线程来执行此任务。

                        <3>,DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。

                        <4>,DiscardPolicy:忽略并抛弃当前任务。

                线程池的默认策略是 AbortPolicy (拒绝并抛出异常)。

        3.11,多线程为什么会不安全?

                1.线程之间是抢占式执行的(根本原因,线程不安全的万恶之源)

                        抢占式执行,导致两个线程里面的操作先后顺序无法确定 这种随机性是导致线程不安全的根本原因 (无力改变,操作系统的内核实现)

                2.多个线程修改同一个变量

                3.原子性(可以用volatile 解决)
                        像 ++ 这样的操作,本质上是三个步骤(LOAD,ADD,SAVE),是一个"非原子" 的操作,像 = 操作,本质上就是一个步骤,认为是一个"原子" 操作 (可通过加锁方式解决,变成原子的)

                4.内存可见性(与编译器优化有关)

                        一个内存修改,一个内存读取,由于编译器的优化,可能把中间环节的 SAVE 和 LOAD 操作去掉了,此时读取的线程可能是未修改的结果(可以用volatile 解决)

                5.指令重排序(也与编译器优化有关)

        编译器会自动调整执行指令的顺序,以达到提高执行效率的效果,前提是需要保证最终效果不变,但是在多线程下,会影响结果

        3.12,多线程不安全解决方案

                        1.将变量放在方法中,也就是局部变量【操作系统会为每个线程分配属于它自己的内存空间,通常称为栈内存,其它线程无权访问,而局部变量在栈内存中】

                        2.变量在类中的方法外 ,即成员变量,这时可以利用ThreadLocal【原理:多个线程访问同一共享变量时,ThreadLocal类为每个线程提供一份该变量的副本,各个线程拥有一份属于自己的变量副本,操作修改的是各自的变量副本,而不会相互影响。】

                        3.变量+final,即变成常量(只能读,不能修改)

                        4.从锁着手【公共区域(堆内存)的数据,要被多个线程操作时,为了确保数据的安全(或一致)性,需要在数据旁边放一把锁,要想操作数据,得先获取锁】

                        方法一:悲观锁【认定数据一定不安全,不管怎样,想访问数据就需要锁,没锁的访问不了】

                        方法二:乐观锁【在高并发时可以用悲观锁,但在低并发时,数据被意外修改的概率很低,(假如只有一个线程)再用悲观锁(获得锁、释放锁)可能就会造成浪费,这时可以用乐观锁即CAS】

                        乐观锁:假如一个线程操作数据,做到一半,休息了,就记录下数据的值,等回来继续做时,先将记录的数据与当前数据对比,如果一样就继续干,不一样就重新做。

        3.13,线程池的工作流程

        3.14,常用线程池特性和应用场景

                1.FixedThreadPool
                        FixedThreadPool是一个固定大小的线程池,它的核心线程数和最大线程数都是固定的。如果当前线程池中的线程数小于核心线程数,则会创建新的线程来处理任务。如果线程池中的线程数达到了核心线程数,而任务队列中还有任务等待执行,则任务会被放入任务队列中等待执行。如果任务队列已满,而线程数还没有达到最大线程数,则会创建新的线程来处理任务。如果线程数已经达到了最大线程数,而任务队列中还有任务等待执行,则会采取拒绝策略来处理任务。

FixedThreadPool适用于执行长时间的任务,需要固定数目的线程来保证任务能够及时得到处理。

                2.CachedThreadPool
                        CachedThreadPool是一个可缓存的线程池,它的核心线程数为0,最大线程数为无限大。如果有任务需要执行,则会创建新的线程来处理任务。如果有线程在60秒内没有被使用,则会被回收。如果有大量的短时间任务需要执行,则CachedThreadPool是一个比较好的选择。

CachedThreadPool的缺点是当任务的数量超过了线程池的处理能力时,可能会出现任务等待的情况。

                3.SingleThreadPool
SingleThreadPool是一个只有一个线程的线程池。它的核心线程数和最大线程数都是1。如果有多个任务需要执行,则会将它们放入任务队列中等待执行。如果当前线程正在执行任务,而任务队列中还有任务等待执行,则新任务会被放入任务队列中等待执行。

SingleThreadPool适用于需要按顺序执行任务的场景,因为它保证了任务的执行顺序。

                4.ScheduledThreadPool
ScheduledThreadPool是一个定时执行任务的线程池。它可以按照固定的时间间隔或者延迟时间来执行任务。ScheduledThreadPool适用于需要按照一定的时间间隔或者延迟来执行任务的场景,例如定时备份、定时任务等。

        3.15,对于线程池大小的设定,我们需要考虑的问题有:

                        1.CPU个数        

                        2.内存大小         

                        3.任务类型,是计算密集型(CPU密集型)还是I/O密集型 是否需要一些稀缺资源,像数据库连接这种等等

                有种简单的估算方式,设N为CPU个数

                        1.对于CPU密集型的应用,线程池的大小设置为N+1

                        2.对于I/O密集型的应用,线程池的大小设置为2N+1

这种设置方式适合于一台机器上的应用的类型是单一的,并且只有一个线程池,实际情况还需要根据实际的应用进行验证。
在I/O优化中,以下的估算公式可能更合理:

  • 最佳线程数量 = ((线程等待时间+线程CPU时间)/ 线程CPU时间)* CPU个数

由公式可得,线程等待时间所占比例越高,需要越多的线程。 
线程CPU时间所占比例越高,所需的线程数越少。

 四.Spring Bean

        4.1,什么是Spring Bean?

                Bean是构成用户应用程序主干的对象。

                Bean由Spring IOC(控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。)容器管理。

                Bean由Spring IOC容器实例化,配置,装配和管理。

                Bean是基于用户提供给容器的配置元数据创建的。

        4.2,Spring Bean的创建过程

                注解类变成Spring Bean为例,Spring会扫描指定包下面的Java类,然后将其变成beanDefinition对象,然后Spring会根据beanDefinition来创建bean,特别要记住一点,Spring是根据beanDefinition来创建Spring Bean的。

        4.3,Spring Bean的生命周期

                1.实例化(Instantiation)
                2.初始化
                3.使用和销毁(estruction)

        在 Spring 中,Bean默认都是单例的,同一个容器的单例只会被创建一次,后续再获取 bean 时,直接从单例缓存 singletonObjects 中进行获取。而且因为单例缓存是公共变量,所以对它进行操作的时候,都进行了加锁操作,避免了多线程并发修改或读取的覆盖操作。

   @order注解是spring-core包下的一个注解,@Order的作用是定义Spring IOC容器中Bean的执行顺序的优先级(这里的顺序也可以理解为存放到容器中的先后顺序)。

        4.4,Spring Boot自动装配的原理

                Spring Boot的核心理念是简化Spring应用的搭建和开发过程,提出了约定大于配置和自动装配的思想。开发Spring项目通常要配置xml文件,当项目变得复杂的时候,xml的配置文件也将变得极其复杂。为了解决这个问题,我们将一些常用的通用的配置先配置好,要用的时候直接装上去,不用的时候卸下来,这些就是Spring Boot框架在Spring框架的基础上要解决的问题。

                Spring Boot的自动装配得益于Spring推出了JavaConfig的这种模式,Java开发人员可以通过@Configuration + @Bean的方式向Spring IOC容器注入Bean。每个场景启动器其实都添加了一个这样的JavaConfig,只要这些JavaConfig配置类能被加入到IOC容器,那么自然而然的就可以自动注入JavaConfig提供的Bean。

五.Mybatis

        5.1,Mybaits漏洞之SQL注入

                  SQL注入漏洞:注入攻击的本质,是把用户输入的数据当做代码执行。

                                                1.是用户能够控制输入

                                                 2.第二个是原本程序要执行的代码,拼接了用户输入的数据。

  解决方案:简单的说就是利用 #{} 将参数做"预编译处理",其实原理是将参数字符串化。

        5.2,Mybatis的一级缓存和二级缓存

                1.一级缓存Mybatis的一级缓存是指SQLSession,一级缓存的作用域是SQlSession,Mabits默认开启一级缓存;

                2.Mybatis默认是没有开启二级缓存的。

                3.一级缓存 Mybatis的一级缓存是指SQLSession,一级缓存的作用域是SQlSession, Mabits默认开启一级缓存。 在同一个SqlSession中,执行相同的SQL查询时;第一次会去查询数据库,并写在缓存中,第二次会直接从缓存中取。 当执行SQL时候两次查询中间发生了增删改的操作,则SQLSession的缓存会被清空。

                每次查询会先去缓存中找,如果找不到,再去数据库查询,然后把结果写到缓存中。 Mybatis的内部缓存使用一个HashMap,key为hashcode+statementId+sql语句。Value为查询出来的结果集映射成的java对象。 SqlSession执行insert、update、delete等操作commit后会清空该SQLSession缓存。

                4.二级缓存 二级缓存是mapper级别的,Mybatis默认是没有开启二级缓存的。 第一次调用mapper下的SQL去查询用户的信息,查询到的信息会存放代该mapper对应的二级缓存区域。 第二次调用namespace下的mapper映射文件中,相同的sql去查询用户信息,会去对应的二级缓存内取结果。

六.数据库

        6.1,数据库锁种类:

                1.共享锁,表示对数据进行读操作;

                2.排他锁,表示对数据进行写操作; 

                3.行锁,对一行记录加锁,只影响一条记录;

                4.意向锁,为了在一个事务中揭示下一行将要被请求锁的类型。

       6.2,数据库垂直拆分和水平拆分

                    垂直拆分

                     就是把不同的表拆到不通的数据库中。按照业务将表进行分类,分布到不同的数据库上,专库专用,这样的也就把数据压力分到到不通数据库上面。

                     水平拆分

                      垂直拆分遇到瓶颈后,可以使用水平拆分。

                      相对于垂直拆分的区别是:水平拆分就是把同一个表拆到不同的数据库中。水平拆分不是将表的数据做分类,二十按照某个字段的

                      某种规则来分散到多个库之中,每个表中包含一部分数据。

        6.3,MySQL数据库常用的搜索引擎有哪些,区别是什么?

                MySQL数据库常见的引擎有 innodb 和myisam两种。采用不同的数据存储文件管理数据。

                myisam引擎:创建一张表对应三个文件,依次存放表的结构,表的数据和表的索引;

                innodb引擎:建一张表对应两个文件:依次表示表的结构,表的数据信息和索引信息。但较为特殊的是:所有的innodb引擎创建的表的数据统一存放在 /var/lib/mysql/ibdata1文件中。如果数据量很大,MySQL会自动的创建ibdata2,ibdata3,…,便于管理。MySQL数据库,建议选用innodb引擎,来支持事务。

        6.4,mysql的索引

                1.普通索引(没有任何限制,用于加速查询)

                2.唯一索引 (索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一)

                3.主键索引(是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引。)

                4.组合索引(指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合。)

        6.5,数据库表中,什么字段适合加索引

                主键、外键、where、group by、order by

                1. 表的主键、外键必须有索引

                2. 数据量超过300的表应该有索引

                3. 经常与其他表进行连接的表,在连接字段上应该建立索引

                4. 经常出现在where字句中的字段,特别是大表的字段,应该建立索引

                5. 索引应该建在选择型高的字段(指能够过滤掉更多不需要的记录)上

                6. 索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引

                7.频繁进行数据操作的表,不要建立太多的索引;(在表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。)

        6.6,数据库表中,什么情况下索引会失效

                1.如果条件中有or,即使其中有条件带索引也不会使用(要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引)

                2.like查询是以%开头,可以是%结束,例如 name like ‘张%’,这种情况索引不会失效。(如果匹配字符串的第一个字符为“%”,索引不会起作用。只有“%”不在第一个位置索引才会起作用。)

                3.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。

                4.is null或者is not null 也会导致无法使用索引。

                5.在MYSQL使用不等于(<,>,!=)的时候无法使用索引,会导致索引失效。

                6.不在索引列上做任何操作(计算,函数,(自动或者手动)类型装换),会导致索引失效而导致全表扫描。

        6.7,如何判断sql有没有走索引

                1.对比加索引前后的SQL查询情况

        解释Explain得到的结果 

                1.type 反应查询语句的性能
                        type 的信息很明显地体现出是否用到了索引:

                        type 结果值从好到坏依次是:

                        system > const> eq_ref> ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

一般来说,得保证查询至少达到 range 级别,最好能达到 ref 级别,否则就可能出现性能问题。

                2. possible_keys: SQL查询时用到的索引。
                        可以看到,没加索引时,possible_keys 的值为 NULL,加了索引后的值为 address,即用到了索引address(索引默认为(column_list)中的第一个列的名字).

                3.key 显示SQL实际决定查询结果使用的键(索引)。如果没有使用索引,值为NULL
                        可以看到,没加索引时,key 的值为 NULL,加了索引后的值为 address,即决定查询结果用到了索引address

                4.rows 显示MySQL认为它执行查询时必须检查的行数
                        可以看到,没加索引时,rows 的值为17,即数据表student中所有数据,说明没加索引时的SQL查询是全表扫描;

                        加了索引后,rows 的值为6,数据库表中address以“北京市”开头的一共也就6条,SQL在执行查询操作时,一共也检查了6行,不必进行全表扫描查询,可以很容易得出结论:加索引的SQL查询性能远高于不加索引的情况。

        6.8,数据库慢查询优化

                1.索引没起到作用的情况

                         <1>,使用LIKE关键字的查询语句。在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引不会起作用。只有“%”不在第一个位置索引才会起作用。
                         <2>,使用多列索引的查询语句。MySQL可以为多个字段创建索引,一个索引最多可以包括16个字段。对于多列索引,只有查询条件使用了这些字段中的第一个字段时,索引才会被使用。

                2.优化数据库结构

                         <1>,将字段很多的表拆解成多个表。

                         <2>,设置中间表。

                3.分解关联查询         

                          很多高性能的应用都会对关联查询进行分解,就是可以对每一个表进行一次单表查询,然后将查询结果在应用程序中进行关联,很多场景下这样会更高效。 

                4.优化limit分页

                        在系统中需要分页的操作通常会使用limit加上偏移量的方法实现,同时加上合适的order by 子句。如果有对应的索引,通常效率会不错,否则MySQL需要做大量的文件排序操作。

                        一个非常令人头疼问题就是当偏移量非常大的时候,例如可能是limit 10000,20这样的查询,这是mysql需要查询10020条然后只返回最后20条,前面的10000条记录都将被舍弃,这样的代价很高。

                       优化此类查询的一个最简单的方法是尽可能的使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候这样做的效率会得到很大提升。                 

        6.9,创建索引的实践

               1.无需一开始就建立索引。

                可以等到业务场景明确后,或者是数据量超过 1 万、查询变慢后,再针对需要查询、排序或分组的字段创建索引。创建索引后可以使用 EXPLAIN 命令,确认查询是否可以使用索引。

                2.尽量索引轻量级的字段。

                比如能索引 int 字段就不要索引 varchar 字段。索引字段也可以是部分前缀,在创建的时候指定字段索引长度。针对长文本的搜索,可以考虑使用 Elasticsearch 等专门用于文本搜索的索引数据库。

                3.尽量不要在 SQL 语句中 SELECT *。

                应该 SELECT 必要的字段,甚至可以考虑使用联合索引来包含我们要搜索的字段(即索引覆盖),既能实现索引加速,又可以避免回表的开销。                                                                         

        6.10,索引数据结构

                Mysql索引数据结构有hash和b+tree,hash由数组和链表组成。hash不支持范围查找。

                B-Tree由红黑树(从左到右为小中大)变化而来,不同的是btree一个节点里面有多个节点,并且节点含有数据。B+tree(有冗余节点)是B-tree的变种,,详细区别看图。

在mysql启动时会把数据库索引加载到内存中,后续索引到的数据也会缓存到内存中

        6.11,数据库隔离级别

                 MySQL的默认隔离级别是可重复读(REPEATABLE READ)。这个级别保证了事务在同一个行的读取是一致的,在事务提交之前,其他事务无法看到这个事务中进行的修改。

                1.读未提交 Read uncommitted
                        就是可以读到未提交的内容。因此,在这种隔离级别下,查询是不会加锁的,也由于查询的不加锁,所以这种隔离级别的一致性是最差的,可能会产生“脏读”、“不可重复读”、“幻读”。如无特殊情况,基本是不会使用这种隔离级别的

                2.读已提交 Read committed 就是只能读到已经提交了的内容。这是各种系统中最常用的一种隔离级别

                3.可重复读 Repeatable read
                        在同一个事务中,前后两次读到的数据是一致的;就是专门针对“不可重复读”这种情况而制定的隔离级别,在这个级别下,普通的查询同样是使用的“快照读”,但是,和“读提交”不同的是,当事务启动时,就不允许进行“修改操作(Update)”了,而“不可重复读”恰恰是因为两次读取之间进行了数据的修改

                4.序列化 Serializable 这是数据库最高的隔离级别,这种级别下,事务“串行化顺序执行”,也就是一个一个排队执行。

        6.12,数据库隔离级别所产生的问题

                                        脏读        不可重复读         幻读
                读未提交         √                 √                         √
                读已提交         ×                 √                         √
                可重复读         ×                 ×                         √
                序列化             ×                 ×                         ×

                脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

                不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。重点在于修改。

                幻读:是值在一个事务中,相同的条件第一次和第二次读出来的记录数不一致,另外一个事务新增或删除了一条记录,这条记录属于第一个事务读取的数据行内。重点在于新增或删除。

        6.13,聚簇索引和非聚簇索引的区别:

        1.聚簇索引
                按照主键来建的索引一定是聚簇索引,其他列也可以建聚簇索引。
                索引和数据一起构建索引,非叶子结点保存key,叶子结点保存行数据。
                一张表一定只有一个聚簇索引,因为聚簇索引代表了文件的实际存储形式,一张表在磁盘上只会有一种存储结构。
        2.非聚簇索引(辅助索引)
                数据与索引分开存储,叶子结点保存了行数据的主键。
                拿到主键之后再去聚簇索引查找得到行数据。

                使用聚集索引可以减少访问磁盘的次数。
                因为聚集索引的叶子结点保存了行数据,每次访问磁盘不只是把对应的行数据加载进内存,而是一块包含了多行(连续主键)数据加载进内存。

                如果没有聚簇索引,非聚簇索引的叶子结点直接保存行数据在磁盘位置。每次查找都需要去磁盘取数据。

                在修改行数据时,主键不变,聚簇索引就不要维护,只需要维护非聚簇索引就可以了。 

        6.14,回表            

                1.如果是通过主键索引来查询数据,例如 select * from user where id=100,那么此时只需要搜索主键索引的 B+Tree 就可以找到数据。

                2.如果是通过非主键索引来查询数据,例如 select * from user where username='javaboy',那么此时需要先搜索 username 这一列索引的 B+Tree,搜索完成后得到主键的值,然后再去搜索主键索引的 B+Tree,就可以获取到一行完整的数据。

        对于第二种查询方式而言,一共搜索了两棵 B+Tree,第一次搜索 B+Tree 拿到主键值后再去搜索主键索引的 B+Tree,这个过程就是所谓的回表。

        从上面的分析中我们也能看出,通过非主键索引查询要扫描两棵 B+Tree,而通过主键索引查询只需要扫描一棵 B+Tree,所以如果条件允许,还是建议在查询中优先选择通过主键索引进行搜索。

        6.15,如何提高mysql插入速度(添加主键)

                1.MySQL是一种常用的关系型数据库管理系统,它的性能对于许多应用程序来说至关重要。在许多情况下,我们需要向MySQL中插入大量数据。如果不加以优化,这个过程可能会变得非常缓慢。在本文中,我们将探讨如何通过添加主键来提高MySQL插入速度。

                2.为什么添加主键可以提高MySQL插入速度?

                   主键是一种用于唯一标识表中每行数据的标识符。在MySQL中,每个表都应该有一个主键。如果没有主键,MySQL将不得不全表扫描以查找匹配的行。这将导致插入速度变慢。当表中有主键时,MySQL可以使用更快的算法来查找匹配的行,从而提高插入速度。

七.AOP

        AOP的思想 其思想是,不去动原来的代码,而是基于原来代码产生代理对象,通过代理的方法去包装原来的方法,就完成了对以前方法的增强。换句话说,AOP的底层原理就是动态代理的实现。

        面向切面编程,是通过代理的方式为程序添加统一功能,集中解决一些公共问题。

        7.1,AOP切面应用场景

                      自定义异常处理,Synchronization 同步,事务,Debugging 调试

        7.2,什么情况下会失效

                final 修饰的、static 修饰的 、private 修饰的和构造方法

                AOP主要是通过继承父类,加强其子类的方式来实现对原方法的加强。

                不能被子类重写的方法都会导致aop失效

        7.3,多切面下如何控制切面顺序

                可以通过@order注解来进行指定,值越小,优先级越高,没有指定@order注解,优先级最低

八.分布式事务seata

   @GlobalTransactional (在事务发起方添加注解)

          AT模式需要建立UNDO_LOG(是 AT 模式中的核心部分他是在 RM 部分完成的 , 在每一个数据库单元处理时均会生成一条 undoLog 数据.

CREATE TABLE `undo_log` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`branch_id` bigint(20) NOT NULL,

`xid` varchar(100) NOT NULL,

`context` varchar(128) NOT NULL,

`rollback_info` longblob NOT NULL,

`log_status` int(11) NOT NULL,

`log_created` datetime NOT NULL,

`log_modified` datetime NOT NULL,

`ext` varchar(100) DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) )

ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

二阶段提交

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

写隔离 

  • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
  • 拿不到 全局锁 ,不能提交本地事务。
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

读隔离

        在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。

        如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

        SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

        出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

        AT 分支事务的业务逻辑:

update product set name = 'GTS' where name = 'TXC';

本地事务什么是提交? 

 5.插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。

6.提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。

7.本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。

8.将本地事务提交的结果上报给 TC。

九. redis

       9.1,redis的五种数据类型

                1.String数据类型(最基本的类型)

                2.List数据类型(有序,可以重复)

                    应用场景:

                        场景一:日志记录
                                        队列的特点是先进先出,后进后出.我们可以使用 lpush 命令从队列的左边放入,然后利用rpop命令从右边取出,这样就模拟实现了队列.可以用来记录日志等.之前我们公司的由于机器比较少,没有空间安装MQ,就是使用list结构来记录日志的,可以做一个定时任务,单线程从队列中将数据取出,既能顺序消费又缓解了数据库的写入压力
                        场景二:抽奖,抢票
                                        list 是线程安全的,所有的pop操作是原子性的,适用于抽奖,抢票等场景,用来防止超卖问题.这里重点解释一下抽奖:主要是分为三步
                        1.全部奖品打散放入list中
                        2.调用pop命令从list中取出
                        3.将中奖记录写入到数据库
                        场景三:流量消峰
                                        将所有的请求全部放到list中,然后开启多个线程来处理后续请求,减轻服务器压力,用来处理一些高并发场景.
常用命令
                        lpush 从左边放入 rpush 从右边放入
                        lpop 从左边取出 rpop 从右边取出
                        range 按照顺序取出,可以指定数据所在的下标

                3.Hash数据类型(散列类型)

                        应用场景:

                                1.由于hash数据类型的key-value的特性,用来存储关系型数据库中表记录,是redis中哈希类型最常用的场景。一条记录作为一个key-value,把每列属性值对应成field-value存储在哈希表当中,然后通过key值来区分表当中的主键。

                                2.经常被用来存储用户相关信息。优化用户信息的获取,不需要重复从数据库当中读取,提高系统性能。

                4.set数据类型(无序集合与hash存储结构完全相同,仅存储键,不存储值(nil),并且值式不允许重复的。也就是只有键没有值的hash)(使用场景:用户关注列表,商品标签)

                5.SortedSet数据类型(zset、有序集合,在set的存储结构基础上添加可排序字段。)。

        9.2,redis持久化的几种方式 (RDB和AOF两种方式

                        RDB:根据指定的规则“定时”将内存中的数据存储在硬盘上,生成的快照(使用save命令)

                        RDB优势

                        1.RDB是一个非常紧凑(compact小型)的文件,它保存了redis 在某个时间点上的数据集,这种文件非常适合用于进行备份和灾难恢复。

                         2.生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。

                         3.RDB 在恢复大数据集时的速度比AOF的恢复速度要快。

                        RDB劣势:

                               1.RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,频繁执行成本过高

                               2.在一定间隔时间做一次备份,所以如果redis意外down掉的话,最后一次快照之后的修改数据会被丢失(数据有丢失)。

                        AOF:每次执行命令后将命令本身记录下来,每次执行命令都会将命令写入到aof文件中

                                问题1:数据都是实时持久化到磁盘吗?

                                虽然每次执行更改Redis数据库内容的操作时,AOF都会将命令记录在AOF文件中,但是事实上,由于操作系统的缓存机制,数据并没有真正地写入硬盘,而是进入了aof_buf缓存中。在默认情况下系统每30秒会执行一次同步操作。以便将aof_buf缓存中的内容真正地写入磁盘中。

                                在这30秒的过程中如果系统异常退出则会导致aof_buf缓存中的数据丢失。这个时候就需要Redis在写入AOF文件后主动要求系统将aof_buf缓存内容同步到磁盘中。在redis.conf中通过如下配置来设置同步机制。

                                fork()出一个子进程,通过子进程将aof_buf缓存中的数据同步到磁盘中

                                问题2:文件越来越大,怎么办?

                                AOF持久化是Redis不断将写命令记录到 AOF 文件中,随着Redis不断的运行,AOF 的文件会越来越大,为了解决这个问题,Redis新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。

                                AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。

                        在启动时,Redis会逐个执行AOF文件中的命令来将硬盘中的数据载入到内存中,载入的速度相对于RDB会慢一些

                9.3,Redis的单机、主从、哨兵、集群四种模式

                        1.单机模式

                单机模式是最简单的部署模式,Redis将数据存储在单个节点上。这个节点包括一个Redis进程和一个持久化存储。单机模式非常适合小型应用程序或者开发和测试环境,因为它提供了一个简单的方法来存储和检索数据,同时具有低延迟和高性能。

                2.主从模式

            主从模式是在单机模式的基础上添加了数据备份的功能。在主从模式中,Redis节点被分为主节点和从节点。主节点负责处理所有的写操作,而从节点则复制主节点的数据,并负责处理读操作。主从模式的主要优点是提高了可靠性和可扩展性。如果主节点发生故障,可以使用从节点来恢复数据。

                3.哨兵模式

                哨兵模式是在主从模式的基础上添加了故障检测和自动故障转移的功能。在哨兵模式中,一个或多个哨兵进程监视Redis节点的运行状况。如果主节点发生故障,哨兵会检测到这一情况并自动将其中一个从节点提升为新的主节点。这个过程是自动的,所以不需要人为干预。哨兵模式提高了Redis集群的可靠性,确保即使主节点发生故障,Redis服务也能够继续运行。

                4.集群模式

                集群模式是在多个Redis节点之间分配数据,提供更高的可扩展性和容错能力。在集群模式中,数据被分配到多个Redis节点上,每个节点处理自己的数据。当一个节点失效时,集群会自动将这个节点的数据迁移到其他节点上。集群模式在Redis大规模部署中非常有用,因为它可以轻松扩展和缩小Redis集群,而不会影响到整个系统的性能和可靠性。

十.SpringCloud安全框架Hdiv

        Hdiv Security 是支持应用程序自我保护的先驱,是同类产品中的第一款产品,可在整个软件开发生命周期 (SDLC) 中提供针对安全漏洞和业务逻辑缺陷的保护。

hdiv社区版,默认能够做哪些安全防护
1.1 SQL注入攻击 (SQL Injection)
1.2 代码执行攻击 (Code execution)
1.3 跨站脚本攻击(XSS)

包含简单的跨站脚本攻击, 图片攻击,script标签攻击,eval攻击 

十一.SpringCloud的Feign

        业务流程比较复杂不建议使用Feign

        Feign是对服务端和客户端通用接口的封装,让代码可以复用做到统一管理。

十二.MQ消息积压的问题和解决方案

        12.1,为什么会出现消息积压的问题?

                其实本质针对的场景,可能你的消费端出了问题,不消费了;或者消费的速度极其慢。接着就坑爹了,可能你的消息队列集群的磁盘都快写满了,都没人消费,这个时候怎么办?或者是这整个就积压了几个小时,你这个时候怎么办?或者是你积压的时间太长了,导致比如 RabbitMQ 设置了消息过期时间后就没了怎么办?

        所以就这事儿,其实线上挺常见的,一般不出,一出就是大问题。一般常见于,举个例子,消费端每次消费之后要写 mysql,结果 mysql 挂了,消费端 hang 那儿了,不动了;或者是消费端出了个什么岔子,导致消费速度极其慢。

        12.2,如何解决?

                1.大量消息在 mq 里积压了几个小时了还没解决,临时紧急扩容

                先修复 consumer (消费者)的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。

                新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
                然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
                接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
                等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

        12.3,Mq 中的消息过期失效了

                假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢

                这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上 12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。

                假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

        12.4,mq都快写满了

                           如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。

        12.5,对于 RocketMQ,官方针对消息积压问题,提供了解决方案。

                1. 提高消费并行度
                2. 批量方式消费
                3. 跳过非重要消息

        12.6,​Mq的6种模式

                1.简单模式

                一个生产者一个消费者

                生产则发送消息,消费者拿到消息,这个过程就结束了。

                ​2.work模式

                一个生产者多个消费者

                生产则发送一条消息,只能被一个消费者拿到。

                如果生产者发送多条消息,消费者拿到的消息是不会重复的。

                3.订阅模式

                和work模式类似,一个生产者多个消费者,但是中间多了个交换机(exchange),一条消息可以被多个消费者获取。

                生产者将消息发给交换机,交换机把消息分配给“已绑定”的消费者,前提是消费者和交换机绑定。

                4.路由模式

                和订阅模式类似,同样是一个生产者多个消费者,中间多了个交换机(exchange),一条消息可以被多个消费者获取。但是他在传递消息的时候多设置了一个key,消费者拿消息的时候也设置一个或多个key,key匹配才能拿消息。

                例如:一号消费者设置的是insert,二号消费者设置的key是insert和update。假如生产者发送了一个key为insert的时候,2个消费者都能拿到数据。如果生产者发送了一个key为update,那么这时候只有2号消费者能拿到数据。

                5.通配符(话题)模式(Topics

                和路由模式类似,同样是一个生产者多个消费者,中间多了个交换机(exchange),一条消息可以被多个消费者获取。同样是传key,但是他的key是可以模糊匹配的,*匹配一个单词,#匹配0或者多个单词。

                例如:一号消费者设置的是item.#,二号消费者设置的key是item.*。一号消费者可以获取所有item开头的消息,二 号就只能匹配item.后面加一个词的消息。例如:消息是item.insert他可以获取,但是item.insert.update他就不能获取了。

十二.Spring事务的7种传播行为

        什么叫事务传播行为?

        事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

十三.消息中间kafka,mq之间的区别和应用场景,kafka怎么保证消息顺序及它为什么是无序的?

        1.在架构模型方面

                RabbitMQ遵循AMQP协议,RabbitMQ的broker由Exchange,Binding,queue组成,其中exchange和binding组成了消息的路由键;客户端Producer通过连接channel和server进行通信,Consumer从queue获取消息进行消费(长连接,queue有消息会推送到consumer端,consumer循环从输入流读取数据)。rabbitMQ以broker为中心;有消息的确认机制。

                kafka遵从一般的MQ结构,producer,broker,consumer,以consumer为中心,消息的消费信息保存的客户端consumer上,consumer根据消费的点,从broker上批量pull数据;无消息确认机制。

        2.在吞吐量

                rabbitMQ在吞吐量方面稍逊于kafka,他们的出发点不一样,rabbitMQ支持对消息的可靠的传递,支持事务,不支持批量的操作;基于存储的可靠性的要求存储可以采用内存或者硬盘。

                kafka具有高的吞吐量,内部采用消息的批量处理,zero-copy机制,数据的存储和获取是本地磁盘顺序批量操作,具有O(1)的复杂度,消息处理的效率很高。

        3.在可用性方面

                rabbitMQ支持miror的queue,主queue失效,miror queue接管。

                kafka的broker支持主备模式。

        4.在集群负载均衡方面

                rabbitMQ的负载均衡需要单独的loadbalancer进行支持。

                kafka采用zookeeper对集群中的broker、consumer进行管理,可以注册topic到zookeeper上;通过zookeeper的协调机制,producer保存对应topic的broker信息,可以随机或者轮询发送到broker上;并且producer可以基于语义指定分片,消息发送到broker的某分片上。

十四.jvm根节点,根可达算法

          jvm根节点有哪些?

                   (1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

                   (2). 方法区中的类静态属性引用的对象。

                   (3). 方法区中常量引用的对象。

                   (4). 本地方法栈中JNI(Native方法)引用的对象。

十五.Spring

        1.什么是循环依赖?

                循环依赖,其实就是循环引用,就是两个或者两个以上的 bean 互相引用对方,最终形成一个闭环,如 A 依赖 B,B 依赖 C,C 依赖 A。

        2.三种循环依赖的情况

                ①构造器的循环依赖:这种依赖spring是处理不了的,直接抛出BeanCurrentlylnCreationException异常。
                ②单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖,能处理。
                ③非单例循环依赖:无法处理。原型(Prototype)的场景是不支持循环依赖的,通常会走到AbstractBeanFactory类中下面的判断,抛出异常。

 

                Spring 在实例化对象的之后,就会为其创建一个 Bean 工厂,并将此工厂加入到三级缓存中。
                因此,Spring 一开始提前暴露的并不是实例化的 Bean,而是将 Bean 包装起来的 ObjectFactory。为什么要这么做呢?
                这实际上涉及到 AOP,如果创建的 Bean 是有代理的,那么注入的就应该是代理 Bean,而不是原始的 Bean。但是 Spring 一开始并不知道 Bean 是否会有循环依赖,通常情况下(没有循环依赖的情况下),Spring 都会在完成填充属性,并且执行完初始化方法之后再为其创建代理。但是,如果出现了循环依赖的话,Spring 就不得不为其提前创建代理对象,否则注入的就是一个原始对象,而不是代理对象。因此,这里就涉及到应该在哪里提前创建代理对象?
                Spring 的做法就是在 ObjectFactory 中去提前创建代理对象。它会执行 getObject() 方法来获取到 Bean。

        3.Spring 为什么要用单例与多例

                之所以用单例,是因为没必要每个请求都新建一个对象,这样子既浪费CPU又浪费内存; 可以保证系统中一个类只有一个实例而且该实例和外界通信,解约资源,便于维护;当前需要频繁访问一个对象,可以用单例,避免创建过多的垃圾

                之所以用多例,是为了防止并发问题;即一个请求改变了对象的状态,此时对象又处理另一个请求,而之前请求对对象状态的改变导致了对象对另一个请求做了错误的处理;

                用单例和多例的标准只有一个: 当对象含有可改变的状态时(更精确的说就是在实际应用中该状态会改变),则多例,否则单例;
                Spring的bean默认注入是单例的,它在Spring容器初始化的时候创建对象;

十六.linux查询服务器内存和cpu占用命令,查询日志关键字命令 

        1.查询服务器内存 free -m
        2.查询日志关键字 tail -fn 1000 test.log | grep '关键字'
        3.查询日志尾部最后10行的日志  tail -n 10 test.log
        4.查询10行之后的所有日志   tail -n +10 test.log
        5.循环实时查看最后1000行记录(最常用的) tail -fn 10 test.log

十七.高并发情况下的幂等设计

           容易出现的问题:

                (1)订单接口, 不能多次创建订单
                (2)支付接口, 重复支付同一笔订单只能扣一次钱
                (3)支付宝回调接口, 可能会多次回调, 必须处理重复回调
                (4)普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次

           常用的解决方法:
                (1)唯一索引 – 防止新增脏数据
                (2)token机制 – 防止页面重复提交
                (3)悲观锁 – 获取数据的时候加锁(锁表或锁行)
                (4)乐观锁 – 基于版本号version实现, 在更新数据那一刻校验数据
                (5)分布式锁 – redis(jedis、redisson)或zookeeper实现
                (6)状态机 – 状态变更, 更新数据时判断状态


十八.ZooKeeper的应用场景?

        Zookeeper(分布式应用程序协调服务软件)

                分布式锁

                       创建分布式锁的原理
                        1.多个Jvm同时在Zookeeper上创建相同的临时节点(lockPath)。

                        2.因为临时节点路径保证唯一性,只要谁能够创建成功谁就能够获取锁,就可以开始执行业务逻辑;同时只要这个会话结束就节点就会消失,这样下一轮其他Jvm也有获取锁的机会。

                        3.如果节点已经给其他请求创建的话,当前的请求实现等待。

                释放锁的原理
                        因为我们采用临时节点,当前节点成功,表示获取锁成功;正常执行完业务逻辑之后调用Session关闭连接方法,当前节点会删除。其他正在等待请求的Jvm采用事件监听,如果当前节点被删除的话,又重新进入到获取锁的流程;

                综上所述:临时节点+事件通知 = zk分布式锁。

                zk分布式锁的实现
                        首先我们可以将失信分布式锁的一些必要的步骤抽象出来,这样的话我们就不仅可以用zookeeper来实现,也可以用其他的比如redis,数据库之类的来实现分布式锁。

                 zk应用场景

                分布式日志收集系统

                注册中心

十九.单例的优缺点

        单例模式的主要优点如下:      
                  (1) 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。      
                  (2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。      
                  (3) 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。  
        单例模式的主要缺点如下:        
                  (1) 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
     
                  (2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。        
                  (3) 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。  
        适用场景        
                在以下情况下可以考虑使用单例模式:      
                (1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。      
                (2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

        单例的创建方式:

                1.饿汉式:类初始化的时候,会立即加载该对象,线程天生安全,调用效率高。

                2.懒汉式:类初始化时,不会初始化该对象,真正需要使用的时候才会去创建该对象,具备懒加载功能。

                3.静态内部类方式:结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。

                        优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。

                        劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。

                4.枚举单例:使用枚举实现单例模式,实现简单、调用效率高,枚举本身就是单例,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞,缺点是没有延迟加载。

                5.双重检测方式(因为JVM本身重排序的原因,可能会出现多次的初始化)

二十.Java常用的设计模式有以下几种:

        结构性模式:

                1.适配器模式:常用于将一个新接口适配旧接口,在我们业务代码中经常有新旧接口适配需求,可以采用该模式。

                2.桥接模式:即将抽象和抽象的具体实现进行解耦,这样可以使得抽象和抽象的具体实现可以独立进行变化,这个模式,其实我们每天都在用到,但是你可能却浑然不知。只要你用到面向接口编程,其实都是在用桥接模式。

                3.组合模式:让客户端看起来在处理单个对象和对象的组合是平等的,换句话说,某个类型的方法同时也接受自身类型作为参数。组合模式常用于递归操作的优化上,比如每个公司都有个boss系统,都会有什么菜单的功能。比如一级菜单下有二级菜单,二级菜单又有三级菜单。删除一级菜单的时候需要不断删除子菜单,那么这个设计模式你可以试试。总之,凡是有级联操作的,你都可以尝试这个设计模式。

                4.装饰者模式:动态的给一个对象附加额外的功能,因此它也是子类化的一种替代方法。该设计模式在JDK中广泛运用,我们常用的AOP,既有动态代理,也有装饰者的味道。

                5.门面模式: 即为一组组件,接口,抽象或子系统提供简化的接口。我们每天使用的SLFJ日志就是门面日志,比如我们使用Dubbo,向外提供的服务就尽量采用门面模式,然后服务在调用各种service做聚合。

                6.享元模式:  使用缓存来减少对小对象的访问时间,只要用到了缓存,基本都是在使用享元模式。map缓存几个对象,基本上都运用了享元的思想。

                7.代理模式:代理模式用于向较简单的对象代替创建复杂或耗时的对象。代理模式用得很广泛,比如Spring AOP、Hibernate数据查询、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等

        创建模式

                1.抽象工厂模式:抽象工厂模式提供了一个协议来生成一系列的相关或者独立的对象,而不用指定具体对象的类型。它使得应用程序能够和使用的框架的具体实现进行解耦。即抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。

                2.建造者模式:用于通过定义一个类来简化复杂对象的创建,该类的目的是构建另一个类的实例。构建器模式还允许实现Fluent接口。比如订单系统大部分项目都有,订单对象就是一个复杂对象,我们就可以采用建造者模式来做。

                3.工厂方法:只是一个返回实际类型的方法。

                4.原型模式:使得类的实例能够生成自身的拷贝。如果创建一个对象的实例非常复杂且耗时时,就可以使用这种模式,而不重新创建一个新的实例,你可以拷贝一个对象并直接修改它。比如我们业务代码,经常要各种DTO、BO、DO、VO转换,其实就可以参考原型设计模式的思想来做。

                5.单例模式:用来确保类只有一个实例。Joshua Bloch在Effetive Java中建议到,还有一种方法就是使用枚举。数据库连接池,网站计数器,日志计录器等,Spring的bean,默认就是单例级别的

        行为模式

                1.责任链:通过把请求从一个对象传递到链条中下一个对象的方式来解除对象之间的耦合,直到请求被处理完毕。链中的对象是同一接口或抽象类的不同实现。凡是带有Filter关键词的,基本都在用这个设计模式。用到拦截器的地方基本都在用这个设计模式。

                2.命令模式:将命令包装在对象中,以便可以将其存储,传递到方法中,并像任何其他对象一样返回。命令模式使用频率较高,和策略模式比较像,具体区别可以搜索一下。如果用过Activiti工作流引擎的朋友可以看一下里面的源码,很多地方都用到了命令模式。

                3.解释器模式:此模式通常描述为该语言定义语法并使用该语法来解释该格式的语句。

                4.迭代器模式:提供一个统一的方式来访问集合中的对象。

                5.中介者模式:通过使用一个中间对象来进行消息分发以及减少类之间的直接依赖。比如MQ,其实就是在用中介者模式。

                6.备忘录模式:生成对象状态的一个快照,以便对象可以恢复原始状态而不用暴露自身的内容。比如Date对象通过自身内部的一个long值来实现备忘录模式。这个在业务中使用得不多,其中一种场景是,你要把数据丢到MQ,但是MQ暂时不可用,那么你把数据暂存到DB,后面再轮询丢到MQ。

                7.空对象模式:它允许您抽象空对象的处理。

                8.观察者模式:用于为组件提供一种灵活地向感兴趣的接收者广播消息的方式。基本上用到ZK的地方,都是在用观察者模式,比如分布式锁,比如服务发现等。

                9.状态模式:允许您在运行时根据内部状态轻松更改对象的行为。比如我们常见的订单状态或者各种XX状态,都可以用得上。

                10.策略模式:使用这个模式来将一组算法封装成一系列对象。通过调用这些对象可以灵活的改变程序的功能。常用于优化大量的if else

     11.模板方法模式:让子类可以重写方法的一部分,而不是整个重写,你可以控制子类需要重写那些操作。业务代码中经常遇到有很多相同的部分,我们可以做一个抽象类,子类来实现差异化

                12.访问者模式:提供一个方便的可维护的方式来操作一组对象。它使得你在不改变操作的对象前提下,可以修改或者扩展对象的行为。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值