JDK1.7中底层是数组+链表,JDK1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
JDK1.7中链表插入使用的是头插法,JDK1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
JDK1.7中哈希算法比较复杂,存在各种右移与异或运算,JDK1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而JDK1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源
1、根据Key通过哈希算法与与运算得出数组下标
2、如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放入该位置
3、如果数组下标位置元素不为空,则要分情况讨论
3.2.1、如果是红黑树Node,则将key和value封裝为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value
3.2.2、如果此位置上的Node对象是链表节点,则将key和value封装为一个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于等于8,那么则会将该链表转成红黑树
3.2.3、将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束put方法
JDK1.7中HashMap的transfer函数如下:在扩容操作的时候,他会重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。【头插法】
会将链表的顺序翻转,这也是形成死循环的关键点。
线程判断是否出现hash碰撞的时候,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
JDK1.7
1、先生成新数组
2、遍历老数组中的每个位置上的链表上的每个元素
3、取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
4、将元素添加到新数组中去
5、所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
JDK1.8
1、先生成新数组
2、遍历老数组中的每个位置上的链表或红黑树
3、如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4、如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
5、所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
JDK1.7
1、JDK1.7的ConcurrentHashMap是基于Segment分段实现的
2、每个Segment相对于一个小型的HashMap
3、每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
4、先生成新的数组,然后转移元素到新数组中
5、扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
JDK1.8
1、JDK1.8的ConcurrentHashMap不再基于Segment实现
2、当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
3、如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
4、ConcurrentHashMap是支持多个线程同时扩容的
5、扩容之前也先生成一个新的数组
6、在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作
插入的时候如果数组元素使用了红黑树,取消了分段锁设计,synchronize替代了Lock锁。
为什么这样优化?
避免冲突严重时链表过长,提高查询效率,时间复杂度从O(N)提高到O(logN)
我们写Java代码,用txt就可以写,但是写出来的Java代码,想要运行,需要先编译成字节码,那就需要编译器,而JDK中就包含了编译器javac,编译之后的字节码,想要运行,就需要一个可以执行字节码的程序,这个程序就是JVM(Java虚拟机),专门用来执行Java字节码的。
如果我们要开发Java程序,那就需要JDK,因为要编译Java源文件。
如果我们只想运行已经编译好的Java字节码文件,也就是*.class文件,那么就只需要JRE。
另外,JVM在执行Java字节码时,需要把字节码解释为机器指令,而不同操作系统的机器指令是有可能不一样的,所以就导致不同操作系统上的JVM是不一样的,所以我们在安装JDK时需要选择操作系统。JAVA跨平台,但JVM不垮平台
JDK中包含了JRE,JRE中包含了JVM。
另外,JM是用来执行Java字节码的,所以凡是某个代码编译之后是Java字节码,那就都能在JVM上运行,比如Apache Groovy, Scala and Kotlin 等等。
分配内存一般有两种解决方案:
1、CAS:对分配内存空间的动作做同步处理,采用CAS机制,配合失败重试的方式保证更新操作的线程安全性。【注意⚠️:在每次分配时都需要进行同步控制,这种是比较低效的】
2、TLAB:每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中再分配,当这部分区域用完之后,再分配新的"私有"内存
我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况,也就是说数组里存储的是引用类型)。
String 真正不可变有下面几点原因:
在Java中,每个对象都可以调用自己的hashCode()方法得到自己的哈希值(hashCode),相当于对象的指纹信息,通常来说世界上没有完全相同的两个指纹,但是在Java中做不到这么绝对,但是我们仍然可以利用hashCode来做一些提前的判断,比如:
如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象
如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象,也可能是两个对象
如果两个对象相等,那么他们的hashCode就一定相同
在Java的一些集合类的实现中,在比较两个对象是否相等时,会根据上面的原则,会先调用对象的hashCode()方法得到hashCode进行比较,如果hashCode不相同,就可以直接认为这两个对象不相同,如果hashCode相同,那么就会进一步调用equals()方法进行比较。而equals()方法,就是用来最终确定两个对象是不是相等的,通常equals方法的实现会比较重,逻辑比较多,而hashCode()主要就是得到一个哈希值,实际上就一个数字,相对而言比较轻,所以在比较两个对象时,通常都会先根据hashCode想比较一下。
所以我们就需要注意,如果我们重写了equals()方法,那么就要注意hashCode()方法,一定要保证能遵守上述规则。
ArrayList就是一个数组,默认是一个长度为0的空数组,第一次add时会进行扩容,第一次扩容的长度是DEFAULT_CAPACITY=10,之后按照oldCapacity + (oldCapacity >> 1)即1.5倍的的增速进行扩容。ArrayList不像HashMap,它没有所谓的负载因子,它只有在elementData装不下时才会进行扩容。ArrayList只会扩容,不会缩容,即使clear了,数组的长度依然不会变。
1、底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
2、由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同
3、ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
1、浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
2、深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象
1、强引用:发生 gc 的时候不会被回收。
2、软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
3、弱引用:有用但不是必须的对象,在下一次GC时会被回收。
4、虚引用:主要用来跟踪对象垃圾回收的活动过程。虚引用必须和引用队列关联使用。虚引用的作用主要是管理堆外内存(堆外内存=操作系统内存-jvm内存),当发生GC,虚引用就会被回收,并且会把回收的通知放到ReferenceQueue中。
1、首先CopyOnWriteArrayList内部也是用过数组来实现的,CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
2、并且,写操作会加锁,防止出现并发写入丢失数据的问题
3、写操作结束之后会把原数组指向新数组
4、CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景
编译器(javac)将Java源文件(.java)文件编译成为字节码文件(.class),可以做到一次编译到处运行,windows上编译好的class文件,可以直接在linux上运行,通过这种方式做到跨平台,不过Java的跨平台有一个前提条件,就是不同的操作系统上安装的JDK或JRE是不一样的,虽然字节码是通用的,但是需要把字节码解释成各个操作系统的机器码是需要不同的解释器的,所以针对各个操作系统需要有各自的JDK或JRE。
采用字节码的好处,一方面实现了跨平台,另外一方面也提高了代码执行的性能,编译器在编译源代码时可以做一些编译期的优化,比如锁消除、标量替换、方法内联等。
JDK自带有三个类加载器: BootstrapClassLoader、 ExtClassLoader、 AppClassLoader.
BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件
ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类
AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件
JVM中存在三个默认的类加载器:
AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BootstrapClassLoader.
JVM在加载一个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会先使用ExtClassLoader的loadClass方法来加载类,同样ExtClassLoader的loadClass方法中会先使用BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接返回,如果
BootstrapClassLoader没有加载到,那么ExtClassLoader就会自己尝试加载该类,如果没有加载到,那么刚会由AppClassLoader来加载这个类
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自己进行加载
打破双亲委派机继承ClassLoader类,还要重写loadClass和findClass方法。打破双亲委派,其实就是重写这个方法,来用我们自己的方式来实现即可
对于还在正常运行的系统:
1、可以使用jmap来查看JVM中各个区域的使用情况
2、可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
3、可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
4、通过各个命令的结果,或者jvisualvm等工具来进行分析
5、首先,初步猜测频繁发送fullGC的原因,如果频繁发生fullGC但是又一直没有出现内存溢出,那么表示fullGC实际上是回收了很多对象了,所以这些对象最好能在youngGC过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullGC减少,则证明修改有效
6、同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存
对于已经发生了OOM的系统:
1、一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件
(-XX:+HeapDumpOnOutofMemoryError XX:HeapDumpPath=/usr/local/base)
2、我们可以利用jsisualvm等工具来分析dump文件
3、根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
4、然后再进行详细的分析和调试
1、首先把字节码文件内容加载到方法区
2、然后再根据类信息在堆区创建对象活,就会在Suvivor区来回拷贝,每移动一次,年龄加1
3、对象首先会分配在堆区中年轻代的Eden区,经过一次Minor GC后,对象如果存活,就会进入Suvivor区。在后续的每次Minor GC中,如果对象一直存活
4、当年龄超过15后,对象依然存活,对象就会进入老年代
5、如果经过Full GC,被标记为垃圾对象,那么就会被GC线程清理掉
一个对象的GC年龄是存储在对象头里面的,而一个java对象在JVM的内存布局是由三个部分来组成的。分别是对象头、实例数据、对其填充。而在对象头里面,有4个bit位来存储GC年龄。而4个bit位能够存储的最大数值为15,所以从这个角度来说,JVM分代年龄之所以设置成15次,是因为他最大能够存储的数值是15
1、引用计数算法:这种方式是给堆内存当中的每个对象记录一个引用个数,引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。
2、可达性算法:这种方式是在内存中,从根对象向下一直找引用,找到的对象就不是垃圾,没找到的对象就是垃圾。
1、标记清除算法:
2、复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。
3、标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。
1、ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
2、ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap, Map的key为ThreadLocal对象,Map的value为需要缓存的值
3、如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadlLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引|用指向 ThreadLocalMap, ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Enty对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove()方法,手动清除Entry对象
4、ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)
造成死锁的几个原因:
1、一个资源每次只能被一个线程使用
2、一个线程在阻塞等待某个资源时,不释放己占有资源
3、一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
4、若干线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
1、要注意加锁顺序,保证每个线程按同样的顺序进行加锁
2、要注意加锁时限,可以针对锁设置一个超时时间
3、要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
1、如果此时线程池中的线程数量小于corePoolsize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务
2、如果此时线程池中的线程数量等于corePoolsize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列
3、如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务
4、如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务
5、当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数
如果希望核心线程也要回收,可以设置allowCoreThreadTimeOut这个属性为true,一般情况下我们不会去回收核心线程。
因为线程池本身就是实现线程的复用,而且这些核心线程在没有任务要处理的时候是处于阻塞状态并没有占用CPU资源。
当线程池中的核心线程都在忙时,如果继续往线程池中添加任务,那么任务会先放入队列,队列满了之后,才会新开线程。这就相当于,一个公司本来有10个程序员,本来这10个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是一开始这些需求只会增加在待开发列表中,然后这10个程序员加班加点的从待开发列表中获取需求并进行处理,但是某一天待开发列表满了,公司发现现有的10个程序员是真的处理不过来了,所以就开始新招员工了。
首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lock0)方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,并没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。
1、tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
2、lock()表示阻塞
加锁,线程会阻塞直到加到锁,方法没有返回值
CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用
CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒。
对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。
1、偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
2、轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
3、如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4、自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和喚醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行
中,相对而言没有使用太多的操作系统资源,比较轻量。
1、Synchronized是一个关键字。ReentrantLock是一个类,只是实现了Lock接口
2、Synchronized会自动的加锁与释放锁,不会造成死锁。ReentrantLock需要程序员手动加锁与释放锁,如果使用不当没有unLock()释放锁就会造成死锁。
3、Synchronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
4、Synchronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
5、Synchronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
6、Synchronized底层有一个锁升级的过程
7、通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
8、synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁
1、AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架
2、在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不用的意义
3、在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1,释放锁state就减1
通常,我们认为Spring有两大特性IOC和AOP,那到底该如何理解IOC呢?
对于很多初学者来说,IOC这个概念给人的感觉就是我好像会,但是我说不出来。
那么IOC到底是什么,接下来来说说我的理解,实际上这是一个非常大的问题,所以我们就把它拆细了来回答,lOC表示控制反转,那么:
1、什么是控制?控制了什么?
2、什么是反转?反转之前是谁控制的?反转之后是谁控制的?如何控制的?
3、为什么要反转?反转之前有什么问题?反转之后有什么好处?
这就是解决这一类大问题的思路,大而化小。
那么,我们先来解决第一个问题:什么是控制?控制了什么?
我们在用Spring的时候,我们需要做什么:
1、建一些类,比如UserService、 OrderService
2、用一些注解,比如@Autowired
但是,我们也知道,当程序运行时,用的是具体的UserService对象、OrderService对象,那这些对象是什么时候创建的?谁创建的?包括对象里的属性是什么时候赋的值?谁赋的?所有这些都是我们程序员做的,以为我们只是写了类而已,所有的这些都是Spring做的
这就是控制:
1、控制对象的创建
2、控制对象内属性的赋值
如果我们不用Spring,那我们得自己来做这两件事,反过来,我们用Spring,这两件事情就不用我们做了,我们要做的仅仅是定义类,以及定义哪些属性需要Spring来赋值(比如某个属性上加@Autowired),而这其实就是第二个问题的答案,这就是反转,表示一种对象控制权的转移。
那反转有什么用,为什么要反转?
如果我们自己来负责创建对象,自己来给对象中的属性赋值,会出现什么情况?
比如,现在有三个类:
1、A类,A类里有一个属性C c;
2、B类,B类里也有一个属性C c;
3、C类
现在程序要运行,这三个类的对象都需要创建出来,并且相应的属性都需要有值,那么除了定义这三个类之外,我们还得写:
1、A a = new A();
2、B b = new B();
3、C c = new C();
4、a.c = c;
5、b.c =c;
这五行代码是不用Spring的情况下多出来的代码,而且,如果类在多一些,类中的属性在多一些,那相应的代码会更多,而且代码会更复杂。所以我们可以发现,我们自己来控制比交给Spring来控制,我们的代码量以及代码复杂度是要高很多的,反言之,将对象交给Spring来控制,减轻了程序员的负担。
总结一下,lOC表示控制反转,表示如果用Spring,那么Spring会负责来创建对象,以及给对象内的属性赋值,也就是如果用Spring,那么对象的控制权会转交给Spring
单例模式表示JVM中某个类的对象只会存在唯一一个。
而单例Bean并不表示JVM中只能存在唯一的某个类的Bean对象。
【 经典单例模式代码 双检锁 ➕ volatile 】
public class Singleton {
private volatile static Singleton singleton; // 定义对象进行volatile修饰
private Singleton ( ){}
public static Singleton getSingleton(){
if(singleton == null){// 第一步 常规的非空判断,没有对象才会去创建对象
synchronized (Singleton.class){
// 第二次判空 - 防止对象被创建多次,虽然只有一个线程能拿到锁,但是多个线程很有可能已经进入了if代码块,
// 此时正在等待,假设a、b两个线程。a先拿到锁,一旦线程a释放,线程b会立即获得锁,然后又进行对象创建,这样对象会被创建多次。
if (singleton == null) {
singleton = new Singleton(); // 第三步 在指令层面不是一个原子操,所以进行volatile修饰
}
}
}
return singleton;
}
}
多个事务方法相互调用时,事务如何在这些方法间传播,方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。
1、REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
2、SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
3、MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
4、REQUIRES_ NEW:创建一个新事务,如果存在当前事务,则挂起该事务。
5、NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
6、NEVER:不使用事务,如果当前事务存在,则抛出异常
7、NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)
1、方法内的自调用:Spring事务是基于AOP的,只要使用代理对象调用某个方法时,Spring事务才能生效,而在一个方法中调用使用this.xxx()调用方法时,this并不是代理对象,所以会导致事务失效。
AopContext.currentProxy() + @EnableAspectJAutoProxy(exposeProxy = true)
2、方法是private的:Spring事务会基于CGLIB来进行AOP,而CGLIB会基于父子类来失效,子类是代理类,父类是被代理类,如果父类中的某个方法是private的,那么子类就没有办法重写它,也就没有办法额外增加Spring事务的逻辑。
3、方法是final的:原因和private是一样的,也是由于子类不能重写父类中的final的方法
4、单独的线程调用方法:当Mybatis或JdbcTemplate执行SQL时,会从ThreadLocal中去获取数据库连接对象,如果开启事务的线程和执行SQL的线程是同一个,那么就能拿到数据库连接对象,如果不是同一个线程,那就拿到不到数据库连接对象,这样,Mybatis或JdbcTemplate就会自己去新建一个数据库连接用来执行SQL,此数据库连接的autocommit为true,那么执行完SQL就会提交,后续再抛异常也就不能再回滚之前已经提交了的SQL
了
5、没加@Configuration注解:如果用SpringBoot基本没有这个问题,但是如果用的Spring,那么可能会有这个问题,这个问题的原因其实也是由于
Mybatis或JdbcTemplate会从ThreadLocal中去获取数据库连接,但是ThreadLocal中存储的是一个MAP,MAP的key为DataSource对象,value为连接对象,而如果我们没有在AppConfig 上添加@Configuration注解的话,会号致MAP中存的DataSource对象和Mybatis和jdbcTemplate中的DataSource对象不相等,从而也拿不到数据库连接,导致自己去创建数据库连接了。
6、异常被吃掉:如果Spring事务没有捕获到异常,那么也就不会回滚了,默认情况下Spring会捕获RuntimeException和Error。
7、类没有被Spring管理
8、数据库不支持事务
Spring中一个Bean的创建大概分为以下几个步骤:
1、解析xml配置或注解配置的类
通过BeanDefinitionRegistry接口里的.registry()方法来加载得到BeanDefinition,然后注入到BeanFactory中
2、通过BeanDefinition反射(或者构造方法创建?)创建Bean对象;
3、对Bean对象成员变量进行属性填充
4、回调实现了Aware接口的方法,如BeanNameAware;【aware:adj: 有意识的】
5、调用BeanPostProcessor的初始化前置方法;
6、调用init初始化方法;
7、调用BeanPostProcessor的初始化后置方法,此处会进行AOP;
8、将创建的Bean对象放入容器(一个Map)中;------- 创建好的bean( 循环依赖遍是在这一步发生的问题)
9、业务使用Bean对象
10、Spring容器关闭时调用DisposableBean的destory()方法;
Spring本身并没有针对Bean做线程安全的处理,所以:
1、如果Bean是无状态的,那么Bean则是线程安全的
2、如果Bean是有状态的,那么Bean则不是线程安全的
另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。
基本上,Spring不允许使用有状态的bean。如果您使用Singleton-您有问题-下一次调用将"看到"旧数据。如果使用Prototype-对该bean的任何方法的每次调用都将导致一个新的类实例,这又不是您想要的。
解决方案似乎是-每当需要状态类时都使用new关键字。
或使用剩余的请求或会话范围,但是这些仅在Web容器中可用。
BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicationContext继承了 BeanFactory,所以
ApplicationContext拥有BeanFactory所有的特点,也是一个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如
EnvironmentCapable、 MessageSource、 ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功
能,这是BeanFactory所不具备的
因为Spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有是被代理对象调用时,那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会失效的。
同时如果某个方法是private的,那么@Transactional也会失效,因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactianal失效
1、@SpringBootApplication注解:这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:
a、@SpringBootConfiguration:这个注解实际就是一个@Configuration,表示启动类也是一个配置类
b、@EnableAutoConfiguration:向Spring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean
c、@ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
2、@Bean注解:用来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象
3、@Controller、 @Service、 @ResponseBody、 @Autowired都可以说
1、首先,SpringBoot在启动时会先创建一个Spring容器
2、在创建Sprng容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成一个启动Tomcat的Bean
3、Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat
优点:
1、基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在XML 里,解除 sql 与程序代码的耦合,便于统一管
理;提供XML 标签,支持编写动态 SQL 语句,并可重用。
2、与JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;
3、很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要JDBC 支持的数据库 MyBatis 都支持)
4、能够与 Spring 很好的集成;
5、提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
缺点:
1、SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL 语句的功底有一定要求。
2、SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
// TODO 一二级缓存
1、#{}是预编译处理、是占位符,${}是字符串替换、是拼接符。
2、Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement 来赋值;
3、Mybatis在处理${}时,会将sql中的${}替换成变量的值,调用Statement 来赋值;
4、使用#{}可以有效的防止 SQL 注入,提高系统安全性。
索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
索引的原理:就是把无序的数据变成有序的查询
1、把创建了索引的列的内容进行排序
2、对排序结果生成倒排表
3、在倒排表内容上拼上数据地址链
4、在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据
1、模糊查询. 以“%abc”开头的 LIKE 语句
2、数据类型–隐式转换
3、函数,对索引字段使用内部函数
4、索引不存储空值,所以尽量对字段设置一定要not null,否则失效。(索引也是按一页16KB进行存储的)
5、运算,也就是说对索引列进行加减乘除等运算,会导致索引失效
6、最左原则,联合索引,索引顺序非常重要,如果不是按照最左列开始进行查找也会失效
---- 当然,mysql8有个索引跳跃扫描,见下面描述
7、快,全表扫描,数据量不多的时候,innodb会觉得全表扫描比索引更快,也不会使用索引!
MySQL 8.0.13开始支持 index skip scan :索引跳跃扫描。该优化方式支持那些SQL在不符合组合索引最左前缀的原则的情况,优化器依然能组使用组合索引。
Extra列中显示增加了Using index for skip scan,表示用到了索引跳跃扫描的优化逻辑
查询更快、占用空间更小
1、适合索引的列是出现在where子句中的列,或者连接子句中指定的列
2、基数较小的表,索引效果较差,没有必要在此列建立索引
3、使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空问,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配。
4、不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。
5、定义有外键的数据列一定要建立索引。
6、更新频繁字段不适合创建索引
7、若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
8、尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
9、对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。
10、对于定义为text、image和bit的数据类型的列不要建立索引。
事务基本特性ACID分别是:
1、原子性指的是一个事务中的操作要么全部成功,要么全部失败。
2、一致性指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证
2、隔离性指的是一个事务的修改在最终提交前,对其他事务是不可见的。
3、持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中。
隔离性有4个隔离级别,分别是:
read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。
用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。
read commit 读已提交,两次读取结果不一致,叫做不可重复读。
不可重复读解决了脏读的问题,他只会读取已经提交的事务。∑用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。
repeatable read 可重复复读,这是mysql的默认级别,就是每次读取结果都一样,但是有可能产生幻读。
serializable 串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。
MVCC (Multi-Version Concurrency Control,多版本并发控制)指的就是在使用READ COMMITTD、 REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程.可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能.READ COMMITTD.
REPEATABLE READ这两个隔离级别的一个很大不同就是:生成Readview的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了.
MyISAM:
InnoDb:
列名 | 描述 |
---|---|
id | 查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一的id值,某子查询会被优化为join 查询,那么出现的id会一样 |
select_type | SELECT关键字对应的那个查询的类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 针对单表的查询方式(全表扫描、索引) |
possible_keys | 可能用到的索引 |
key | 实际上用到的索引 |
key_len | 实际用到的索引长度 |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows | 预估的需要读取的条数 |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
Extra | 一些额外的信息,比如排序等 |
索引覆盖就是一个SQL在执行时,可以利用索引来快速查找, 并且此SQL所要查询的字段在当前索引对应的字段中都包含了, 那么就表示此SQL走完索引后不用回表了,所需要的字段都在当前索引的叶子节点上存在,可以直接作为结果返回了
当一个SQL想要利用索引是,就一定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b,c三个字段建立了一个联合索引,那么在写一个sql时就一定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左往右去比较大小进行排序的,所以如果想要利用B+树进行快速查找也得符合这个规则
1、Record lock:
行锁,行锁是基于索引实现,因为通过索引访问数据
2、Gap lock:间隙锁:
锁定一个范围,不包括记录本身。比如:当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,它用于锁定的索引之间的间隙,但是不会包含记录本身。比如语句select * from user where age>1 and age<10 for update
,将会锁住age在(1,10)的范围区间,此时其他事务对该区间的操作都会被阻塞。这种锁机制就是所谓的间隙锁(Next-Key锁)。它存在的主要目的有一个是为了解决幻读问题,因为RR作为InnoDB的默认事物隔离级别,是存在幻读问题的。
3、Next-key lock:
(序列号加锁机制 record+gap) 临键锁,锁定一个范围,包含记录本身
Innodb通过Buffer Pool, LogBuffer, Redo Log, Undo Log来实现事务,
以一个update语句为例:
1、Innodb在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool中
2、执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
3、针对updlate语句生成一个RedoLog对象,并存入LogBuffer中
4、针对update语句生成undolog日志,用于事务回滚
5、如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中
6、如果事务回滚,则利用undolog日志进行回滚
B树的特点:
1、节点排序
2、一个节点了可以存多个元素,多个元素也排序了
B+树的特点:
1、拥有B树的特点
2、叶子节点之间有指针
3、非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序
Mysq/索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。
exists:exists对外表用loop逐条查询,每次查询都会查看exists的条件语句,当exists里的条件语句能够返回记录行时(无论记录行是的多少,只要能返回),条件就为真,返回当前loop到的这条记录;反之,如果exists里的条件语句不能返回记录行,则当前loop到的这条记录被丢弃,exists的条件就像一个bool条件,当能返回结果集则为true,不能返回结果集则为false
in:in查询相当于多个or条件的叠加
一个是大表,则子查询表大的用exists,子查询表小的用in
按锁粒度分类:
1、行锁:锁某行数据,锁粒度最小,并发度高
2、表锁:锁整张表,锁粒度最大,并发度低
3、间隙锁:锁的是一个区间
还可以分为:
1、共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
2、排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写
还可以分为:
1、乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
2、悲观锁:上面所的行锁、表锁等都是悲观锁
在事务的隔离级别实现中,就需要利用锁来解决幻读
1、检查是否走了索引,如果没有则优化SQL利用索引
2、检查所利用的索引,是否是最优索引
3、检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
4、检查表中数据是否过多,是否应该进行分库分表了
5、检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源
我们通常说表太大,另外一层意思就是数据太多了,导致索引的效果都不明显了,只能分表了。所以我们在讨论什么是大表时,需要站在Mysql索引角度来分析,到底多大数据量时是大表。
上图是(mnodb中的一个主键索引1,也就是一颗B+树,树中的每个节点是一个Innodb Page,大小默认为16kb,叶子节点中的每个节点主要存储的就是一条条数据,非叶子节点中存储的是主键和页地址。
所以,我们可以来计算一下,如果B+树的高度为2,能存多少条数据。
所以1页中,也就是一个节点中,可以存:
所以如果B+树的高度为2,那么叶子节点就有1638个,所以能存的行数据条数为:1638*16=26208条记录。
如果B+树的高度为3,那么第一层一个节点,第二层1638个节点,第三层16381638个节点,最终数据条数为:16381638*16 = 42,928,704。也就是差不多4000多万条数据
如果主键的类型为bigint,一个主键占8个字节,所以高度为3时,能存:
也就是2000多万行数据。
所以,我们可以通过这种方式来判断一个表的数据量是不是过多(B+树的高度一般不建议超过三层,因为B+树的数据都是存在磁盘中的,树太高了,进行IO的次数就表多了,整体效率也就降低了)。如果表中实际的数据总条数超过了3层能存放的数据量,那么这个表就是大表了,此时索引的效率就不高了,就需要进行分表了。
Redis基于Reactor模式开发了网络事件处理器、文件事件处理器 fileeventhandler。它是单线程的,所以 Redis才叫做单线程的模型,它采用IO多路复用 机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了 Redis内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个Socket、IO多路复用程序、文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)
多个 Socket 可能并发的产生不同的事件,IO多路复用程序会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中有序、同步取出一个Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。
然后一个 Socket 的事件处理完之后,IO多路复用程序才会将队列中的下一个 Socket 给事件分派器。文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理
单线程快的原因:
Redis DataBase,在指定的时间问隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
优点:
缺点:
Append Only File,以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录
优点:
缺点:
AOF文件比RDB更新频率高,优先使用AOF还原数据,AOF比RDB更安全也更大,RDB性能比AOF好,如果两个都配了优先加载AOF。
Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情況下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了情性过期和定期过期两种过期策略。
MULT命令的执行,标识着一个事务的开始。MULTI命令会将客户端状态的 flags 属性中打开 REDIS_MULTI 标识来完成的。
当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为MULTI、 EXEC、 WATCH、
DISCARD中的一个,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复
如果客户端发送的命令为 EXEC、 DISCARD、 WATCH、 MULTI 四个命令的其中一个,那么服务器立即执行这个命令。
如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。
首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态 (redisClient)的 flags 属性关闭 REDIS_MULTI 标识,并且返回错误信息给客户端。如果正确,将这个命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复
事务队列是按照FIFO的方式保存入队的命令
客户端发送 EXEC 命令,服务器执行 EXEC 命令逻辑。
Redis 不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。
Redis 事务不支持检查那些程序员自己逻辑错误。例如对 String 类型的数据库键执行对 HashMap 类型的操作!
WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放
到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值nil。
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。
UNWATCH命令可以取消watch对所有key的监控。
通过执行slaveof命令或设置slaveof选项,让一个服务器去复制另一个服务器的数据。主数据库可以进行读写操作,当写操作号致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
1、复制偏移量:执行复制的双方,主从节点,分别会维护一个复制偏移量offset
2、复制积压缓冲区:主节点内部维护了一个固定长度的、先进先出(FIFO)队列 作为复制积压缓冲区,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
3、服务器运行ID(runid):每个Redis节点,都有其运行ID,运行ID由节点在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来。从节点Redis断开重连的时候,就是根据运行ID来判断同步的进度:
如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
Redis的主从复制是提高Redis的可靠性的有效措施
1、集群启动时,主从库间会先建立连接,为全量复制做准备
2、主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照RDB
3、在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则redis的的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性,主库会在内存中用专门的replication buffer,记录RDB文件生成收到的所有写操作。
4、最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成RDB文件发送后,就会把此时replication buffer中修改操作发送给从库,从库再执行这些操作。这样一来,主从库就实现同步了
5、后续主库和从库都可以处理客户端读操作,写操作只能交给主库处理,主库接收到写操作后,还会将写操作发送给从库,实现增量同步
Redis的数据结构有:
1、字符串:可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID
2、哈希表:可以用来存储一些key-value对,更适合用来存储对象
3、列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据
4、集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能
5、有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能
setNx是一个天然的原子性操作
缓存中存放的大多都是热点数据,目的就是防止请求可以直接从缓存中获取到数据,而不用访问Mysql
1、缓存穿透:假如某一时刻访问redis的大量key都在redis中不存在(比如黑客故意伪造一些乱七八糟的key),那么也会给数据造成压力,这就是缓存穿透,解决方案是使用布隆过濾器,它的作用就是如果它认为一个key不存在,那么这个key就肯定不存在,所以可以在缓存之前加一层布隆过滤器来拦截不存在的key
2、缓存击穿:和缓存雪崩类似,缓存雪崩是大批热点数据失效,而缓存击穿是指某一个热点key突然失效,也导致了大量请求直接访问Mysql数据库,这就是缓存击穿,解决方案就是考虑这个热点key不设过期时间或用分布式锁
3、缓存雪崩:如果缓存中某一时刻大批热点数据同时过期,那么就可能导致大量请求直接访问Mysql了,解决办法就是在过期时间上增加一点随机值,另外如果搭建一个高可用的Redis集群也是防止缓存雪崩的有效手段
RDB:
Redis DataBase 将某一个时刻的内存快照 (Snapshot),以二进制的方式写入磁盘。
手动触发:
save命令,使 Redis 处于阻塞状态,直到 RDB 持久化完成,才会响应其他客户端发来的命令,所以在生产环境一定要慎用
bgsave命令,fork出一个子进程执行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了
自动触发:
save mn:在m 秒内,如果有 n 个键发生改变,则自动触发持久化,通过bgsave执行,如果设置多个、只要满足其一就会触发,配置文件有默认配置(可以注释掉)
flushall:用于清空redis所有的数据库,flushdb清空当前redis所在库数据(默认是0号数据库),会清空RDB文件,同时也会生成dump.rdb、内容为空
主从同步:全量同步时会自动触发bgsave命令,生成rdb发送给从节点
优点:
缺点:
AOF:
Append Only File 以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,调操作系统命令进程刷盘
1、所有的写命令会追加到 AOF 缓冲中。
2、AOF 缓冲区根据对应的策略向硬盘进行同步操作。
3、随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩目的。
4、当 Redis 重启时,可以加载 AOF 文件进行数据恢复。同步策略:
每秒同步:异步成,效率非常高,一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失
每修改同步:同步持久化,每次发生的数据变化都会被立即记录到磁盘中,最多丢一条 不同步:由操作系统控制,可能丢失较多数据
优点:
1、数据安全
2、通过 append 模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis- check-aof 工具解决数据一致性问题。
3、AOF 机制的 rewrite 模式。定期对AOF文件进行重写,以达到压缩的目的
缺点:
1、AOF 文件比 RDB 文件大,且恢复速度慢。
2、数据集大的时候,比 rdb 启动效率低。
3、运行效率没有RDB高
对比:
由于不能同时满足CAP,所以出现了BASE理论:
1、BA: Basically Available,表示基本可用,表示可以允许一定程度的不可用,比如由于系统故障,请求时间变长,或者由于系统故障导致部分非核心功能不可用,都是允许的
2、S: Soft state:表示分布式系统可以处于一种中间状态,比如数据正在同步…
3、E: Eventually consistent,表示最终一致性,不要求分布式系统数据实时达到一致,允许在经过一段时间后再达到一致,在达到一致过程中,系统也是可用的
RPC,表示远程过程调用,对于Java这种面试对象语言,也可以理解为远程方法调用,RPC调用和HTTP调用是有区别的,RPC表示的是一种调用远程方法的方式,可以使用HTTP协议、或直接基于TCP协议来实现RPC,在Java中,我们可以通过直接使用某个服务接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以,有一种说法是RPC协议是HTTP协议之上的一种协议,也是可以理解的。参考理解:外语书和英语书的关系
在开发中,我们通常会需要一个唯一ID来标识数据,如果是单体架构,我们可以通过数据库的主键,或直接在内存中维护一个自增数字来作为ID都是可以的,但对于一个分布式系统,就会有可能会出现ID冲突,此时有以下解决方案:
uuid,这种方案复杂度最低,但是会影响存储空间和性能
利用单机数据库的自增主键,作为分布式ID的生成器,复杂度适中,ID长度较之uuid更短,但是受到单机数据库性能的限制,并发量大的时候,此方案也不是最优方案
利用redis、zookeeper的特性来生成id,比如redis的自增命令、zookeeper的顺序节点,这种方案和单机数据库(mysql)相比,性能有所提高,可以适当选用
雪花算法,一切问题如果能直接用算法解决,那就是最合适的,利用雪花算法也可以生成分布式ID,底层原理就是通过某台机器在某一毫秒内对某一个数字自增,这种方案也能保证分布式架构中的系统id唯一,但是只能保证趋势递增。业界存在tinyid、leaf等开源中间件实现了雪花算法。
在单体架构中,多个线程都是属于同一个进程的,所以在线程并发执行时,遇到资源竞争时,可以利用ReentrantLock、synchronized等技术来作为锁,来控制共享资源的使用。
而在分布式架构中,多个线程是可能处于不同进程中的,而这些线程并发执行遇到资源竞争时,利用ReentrantLock、synchronized等技术是没办法來控制多个进程中的线程的,所以需要分布式锁,意思就是,需要一个分布式锁生成器,分布式系统中的应用程序都可以来使用这个生成器所提供的锁,从而达到多个进程中的线程使用同一把锁。
目前主流的分布式锁的实现方案有两种:
zookeeper:利用的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高一致性,因为zookeeper保证的是
CP,所以由它实现的分布式锁更可靠,不会出现混乱
redis:利用redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是高可用,因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(一旦redlis中的数据出现了不一致),可能会出现多个客户端同时加到锁的情况
在分布式系统中,一次业务处理可能需要多个应用来实现,比如用户发送一次下单请求,就涉及到订单系统创建订单、库存系统减库存,而对于一次下单,订单创建与减库存应该是要同时成功或同时失败的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这类问题,就需要用到分布式事务。常用解决方案有:
1、本地消息表:创建订单时,将减库存消息加入在本地事务中,一起提交到数据库存入本地消息表,然后调用库存系统,如果调用成功则修改本地消息状态为成功,如果调用库存系统失败,则由后台定时任务从本地消息表中取出未成功的消息,重试调用库存系统
2、消息队列:目前RocketMQ中支持事务消息,它的工作原理是:
a、生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的
b、再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
c、并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功
d、一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
e、如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理
3、Seata:阿里开源的分布式事务框架,支持AT、TCC等多种模式,底层都是基于两阶段提交理论来实现的
ZAB协议是Zookeeper用来实现一致性的原子广播协议,该协议描述了Zookeeper是如何实现一致性的,分为三个阶段:
但值得注意的是,Zookeeper只是尽量的在达到强一致性,实际上仍然只是最终一致性的。
可以利用Zookeeper的临时节点和watch机制来实现注册中心的自动注册和发现,另外Zookeeper中的数据都是存在内存中的,并且Zookeeper底层采用了nio,多线程模型,所以Zookeeper的性能也是比较高的,所以可以用来作为注册中心,但是如果考虑到注册中心应该是注册可用性的话,那么Zookeeper则不太合适,因为Zookeeper是CP的,它注重的是一致性,所以集群数据不一致时,集群将不可用,所以用Redis、Eureka、Nacos来作为注册中心将更合适。
对于Zookeeper集群,整个集群需要从集群节点中选出一个节点作为Leader,大体流程如下:
1、集群中各个节点首先都是观望状态(LOOKING),一开始都会投票给自己,认为自己比较适合作为leader
2、然后相互交互投票,每个节点会收到其他节点发过来的选票,然后pk,先比较zxid,zxid大者获胜,zxid如果相等则比较myid,myid大者获胜
3、一个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投给zxid或myid更大的节点,并将选票放入自己的投票箱中,并
将新的选票发送给其他节点
4、如果pk是平局则将接收到的选票放入自己的投票箱中
5、如果pk赢了,则忽略所接收到的选票
6、当然一个节点将一张选票放入到自己的投票箱之后,就会从投票箱中统计票数,看是否超过一半的节点都和自己所投的节点是一样的,如果超过半
数,那么则认为当前自己所投的节点是leader
7、集群中每个节点都会经过同样的流程,pk的规则也是一样的,一旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是一样的,所以各个节点最终选出来的leader也是一样的,这样集群的leader就选举出来了
Dubbo中的架构设计是非常优秀的,分为了很多层次,并且每层都是可以扩展的,比如:
1、采用无状态服务,抛弃session
2、存入cookie(有安全风险)
3、服务器之间进行 Session 同步,这样可以保证每个服务器上都有全部的 Session 信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失
4、IP 绑定策略
使用 Nginx(或其他复杂均衡软硬件)中的P 绑定策略,同一个 P 只能在指定的同一个机器访问,但是这样做失去了负载均衡的意义,当挂掉一台服务器的时候,会影响一批用户的使用,风险很大
5、使用 Redis 存储
把 Session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次 Redis,但是这种方案带来的好处也是很大的:
第一位符号位固定为0,41位时间戳,10位workld,12位序列号,位数可以有不同实现。
优点:每个毫秒值包含的ID值很多,不够可以变动位数來增加,性能佳(依赖worklc的实现)。时间戳值在高位,中间是固定的机器码,自增的序列在低
位,整个ID是趋势递增的。能够根据业务场景数据库节点布置灵活调整bit位划分,灵活度高。
缺点:强依赖于机器时钟,如果时钟回拨,会导致重复的ID生成,所以一般基于此的算法发现时钟回拨,都会抛异常处理,阻止ID生成,这可能导致服务不可用。
映射:将查询条件的字段与分区键进行映射,建一张单独的表维护(使用覆盖索引)或者在缓存中维护
基因法:分区键的后x个bit位由查询字段进行hash后占用,分区键直接取x个bit位获取分区,查询字段进行hash获取分区,适合非分区键查询字段只有一个的情况
冗余:查询字段冗余存储
hash算法:根据key进行hash函数运算、结果对分片数取模,确定分片 适合固定分片数的场景,扩展分片或者减少分片时,所有数据都需要重新计
算分片、存储
一致性hash:将整个hash值得区间组织成一个闭合的圆环,计算每台服务器的hash值、映射到圆环中,使用相同的hash算法计算数据的hash值,映射到圆环,顺时针寻找,我到的第一个服务器就是数据存铺的服务器。新增及减少节点时只会影响节点到他逆时针最近的一个服务器之间的值 存在hash环倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决
hash slot: 将数据与服务器隔离开,数据与slot映射,slot与服务器映射,数据进行hash决定存放的slot,新增及删除节点时,将slot进行迁移即可
1、当服务A调用服务B,服务B调用C,此时大量请求突然请求服务A,假如服务A本身能抗住这些请求,但是如果服务C抗不住,导致服务C请求堆积,从而服务B请求堆积,从而服务A不可用,这就是服务雪崩,解决方式就是服务降级和服务熔断。
2、服务限流是指在高并发请求下,为了保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流是非常重要的。
1、服务熔断是指,当服务A调用的某个服务B不可用时,上游服务A为了保证自己不受影响,从而不再调用服务B,直接返回一个结果,减轻服务A和服务B的压力,直到服务B恢复。
2、服务降级是指,当发现系统压力过载时,可以通过关闭某个服务,或限流某个服务来減轻系统压力,这就是服务降级。
相同点:
不同点:熔断是下游服务故障触发的,降级是为了降低系统负载
拆分微服务的时候,为了尽量保证微服务的稳定,会有一些基本的准则:
1、微服务之间尽量不要有业务交叉。
2、微服务之前只能通过接口进行服务调用,而不能绕过接口直接访问对方的数据。
3、高内聚,低耦合。
什么是DDD:在2004年,由Eric Evans提出了,DDD是面对软件复杂之道。Domain-Driven- Design -Tackling Complexity in the Heart of Software
大泥团:不利于微服务的拆分。大泥团结构拆分出来的微服务依然是泥团机构,当服务业务逐渐复杂,这个泥团又会膨胀成为大泥团。
DDD只是一种方法论,没有一个稳定的技术框架。DDD要求领域是跟技术无关、跟存储无关、跟通信无关。
所谓中台,就是将各个业务线中可以复用的一些功能抽取出来,剥离个性,提取共性,形成一些可复用的组件。大体上,中台可以分为三类 业务中台、数据中台和技术中台。大数据杀熟-数据中台中台跟DDD结合:DDD会通过限界上下文将系统拆分成一个一个的领域,而这种限界上下文,天生就成了中台之间的逻辑屏障。DDD在技术与资源调度方面都能够给中台建设提供不错的指导。DDD分为战略设计和战术设计。上层的战略设计能够很好的指导中台划分,下层的战术设计能够很好的指导微服务搭建。
select模型,使用的是数组来存储Socket连接文件描述符,容量是固定的,需要通过轮询来判断是否发生了IO事件
poll模型,使用的是链表来存储Socket连接文件描述符,容量是不固定的,同样需要通过轮询来判断是否发生了IO事件
epoll模型,epol和poll是完全不同的,epoll是一种事件通知模型,当发生了IO事件时,应用程序才进行IO操作,不需要像poll模型那样主动去轮询
跨域是指浏览器在发起网络请求时,会检查该请求所对应的协议、域名、端口和当前网页是否一致,如果不一致则浏览器会进行限制,比如在www.baidu.com的某个网页中,如果使用ajax去访问www.jd.com是不行的,但是如果是img、 iframe、script等标签的src属性去访问则是可以的,之所以浏览器要做这层限制,是为了用户信息安全。但是如果开发者想要绕过这层限制也是可以的:
1、response添加header,比如resp。setHeader(“Access-Control-Allow-Origin“,“大“);表示可以访问所有网站,不受是否同源的限制
2、jsonp的方式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的
3、后台自己控制,先访问同域名下的接口,然后在接口中再去使用HTTPClient等工具去调用目标接口
4、网关,和第三种方式类似,都是交给后台服务来进行跨域访问
零拷贝指的是,应用程序在需要把内核中的一块区域数据转移到另外一块内核区域去时,不需要经过先复制到用户空间,再转移到目标内核区域去了,币直接实现转移。
首先我们可以看到Redis的空间实际上比我们MySQL少的多,那么Redis如何能够筛选出热点数据,这道题主要考察的是Redis的数据淘汰策略(这里有个误区,很多人容易混淆把数据淘汰策略当做数据过期策略),在Redis 4.0之后是为我们提供了8种淘汰策略,4.0之前则是提供的6种,主要是新增了LFU算法。其实说说是有8种,但是真正意义上是5种,针对random、 Iru、ffu是提供了两种不同数据范围的策略,一种是针对设置了过期时间的,一种是没有设置过期时间的。具体的五种策略分别为:
noeviction 选择这种策略则代表不进行数据淘汰,同时它也是redis中默认的淘汰策略,当缓存写满时redis就不再提供写服务了,写请求则直
接返回失败。
random 随机策略这块则是分为两种,一种是volatile,这种是设置了过期时间得数据集,而另外一种是allkeys,这种是包含了所有的数据,
当我们缓存满了的时候,选用这种策略就会在我们的数据集中进行随机删除。
volatile-ttl 这种策略是针对设置了过期时间的数据,并且按照过期时间的先后顺序进行删除,越早过期的越先被删除
图灵课堂
Iru 这里的Iru策略和我们上面random策略一样也是提供了两种数据集进行处理,LRU算法全程为(最近最少使用)简单一句话来概括就是“如
果数据最近被访问过,那么将来被访问的几率也就越高”。这种算法其实己经比较符合我们的实际业务需求了,但是还是存在一些缺陷。
Ifu 最后一种策略就是我们的LFU算法,它是在我么LRU算法基础上增加了请求数统计,这样能够更加精准的代表我们的热点数据。
我们再回看我们的这个问题,我们能很清楚的知道,我们需要的策略是LFU算法。选择volatile还是allkeys就要根据具体的业务需求了。
首先普及下幂等的概念
“在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数”那么在我们的实际业务场景中幂等是一个非常高频的场景,比如:
那么我们有那些方案可以解决我们的幂等性问题呢?
a、服务端提供获取 Token 的接口,请求前客户端调用接口获取 Token
b、然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间),
c、将 Token 返回到客户端,在执行业务请求带上该 Token
d、服务端接收到请求后根据 Token 到 Redis 中查找该 key 是否存在(注意原子性),
e、如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。
我们来分析一下这道面试题,这道题主要是偏实际应用的
缓存可以提升性能,减轻数据库压力,在获取这部分好处的同时,它却带来了一些新的问题,缓存和数据库之间的数据一致性问题。
想必大家在工作中只要用了咱们缓存势必就会遇到过此类问题,那这道题该如何回答呢?
首先我们来看看一致性:
强一致性:任何一次读都能读到某个数据的最近一次写的数据。
弱一致性:数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
延迟双删
延时双删策略是分布式系纷中毁据库存馆和缓存数据保特一致性的常用策略。但它不是强一致。
实现思路:也是非常简单的,先删除缓存然后更新DB在最后延迟 N 秒去再去执行一次缓存删除
弊端:小概率会出现不一致情况、耦合程度高
通过MQ进行重试删除
binlog异步删除
据说以前有个典故,矿工开矿前,会先放一只金丝雀下去,看金丝雀是否能活下来,用来探测是否有毒气,金丝雀发布也是由此得名。
灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度,而我们平常所说的金丝雀部署也就是灰度发布的一种方式。
首先我们来讲讲实现一个分布式锁我们需要考虑那些东西?
一张图带你弄懂Redisson分布式锁原理
这道面试题是考察面试者对死锁的了解程度,在这个角度来提问也是很有趣。首先,我们如果要实现死锁的话有四个必要的条件:
MySQL 8.0 支持了 Invisible Indexes 隐藏索引 这个特性,可以把某个索引设置为对优化器不可见,生成查询计划时便不使用这个索引了,但这个索引还是被正常维护的,例如表数据变更后还是会更新索引。
我们都知道维护一个索引带来的成本是很高的,当数据量越大的时候成本越高。不知道大家有没有遇到过一种场景就是做数据库优化时,我们可能会去删除一些多余的索引,我们要去线上禁用掉一个索引然后去观察对查询效率的一个影响,在8.0之前我们可能只能将数据导出到一个新的环境,然后再去删除掉这个索引,如果观察发现影响面比较大,那么就不能去删。还有一种方案是直接删线上的索引不行再加回来,稍微体量大一点的公司都是不被允许的。
那么在8.0 之后我们就不用这么麻烦了,官方为我们提供了一个新的功能叫隐藏索引,或者你可以理解成为灰度索引,它的作用类似于回收站功能。虽然我删除了某个文件但是这个文件其实还在。如果碰到上面的这种场景,就可以直接使用这个隐藏索引。
alter table xxx alter index xxx invisible;
alter table xxx alter index xxx visible;
import java.lang.reflect.Field;
public class Test {
public static void main(String [] args) {
C c= new C();
try {
Field f = C.class.getDeclaredField("a");
f.setAccessible(true);
Integer i= (Integer)f.get(c);
System.out.println(i);
} catch (Exception e) {}
}
}
class C {
private Integer a = 6;
}
那么private修饰符不是失去了它原本的意义了吗?
其实不尽然,本身private修饰符是基于OOP思想下封装概念的一种体现,对于使用者而言是一种约束,并不是一种安全机制。就好比说,你在道路上行 驶,导航时不时提醒你限速,这是道路交通法的规定,但是救护车碰上情况是不是也会超速对吧,这个就是这两者的差异。
你在用spring的loC的时候,你知道你要“注入”,不管它是不是private的,都能够注入进去对吧。
如果你按照遵守这套规则,开发者在不考虑bug的情况下可以保证不出问题,否则就很有可能在你意想不到的地方带来灾难性的后果。