经典问题

HashMap相关

HashMap容量为何设置2的N次方。

索引均匀分布,如果求膜运算效率比位运算低,当容量一定是2^n时,h & (length - 1) == h % length,所以设置成2的N次方

HaspMap扩容是怎样扩容的。

HashMap中有一个加载因子,默认是0.75。也就是说当数组中的使用情况达到75%的时候将会进行扩容。扩容过程就是用一个新的数组(原数组长度的2倍,这样就会依然保持容量为2的N次方),代替原来的数组,完成扩容。

HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化。

HashMap底层使用链表+数组实现,将key值经过hash算法得到hash值,然后求出索引index,将数据放入对应的Entry[index]中去,如果hash值相同,就在Entry[index]组成链表。JDK8中把引入了红黑树,对链表进行了优化

HashMap,HashTable,ConcurrentHashMap的区别。

HashMap不是线程安全;HashTable线程安全但是效率低下,采用synchronize加锁实现, HashTable不允许key和value为null;ConcurrentHashMap也是线程安全,效率较高,采用分段式加锁的方式实现。 将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

极高并发下HashTable和ConcurrentHashMap哪个性能更好,为什么,如何实现的。

当然是ConcurrentHashMap性能更好。HashTable实现线程安全就是使用synchronize加锁实现的,整个就像一把大锁。在1.7中ConcurrentHashMap使用的是分段锁, ConcurrentHashMap的主干是个Segment数组, Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。 所以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。1.8版本ConcurrentHashMap再次做了改变,1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现。

HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。

HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。具体原理https://coolshell.cn/articles/9606.html

Jvm虚拟机

JVM内存结构

方法区

用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

堆内存

堆区是Java虚拟机所管理的内存中最大的一块,主要存储对象的实例。

虚拟机栈

生命周期与线程是同步的,每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出入口等信息,每个方法的调用到执行完成的过程就是一个栈帧入栈到出栈的过程。

本地方法栈

与虚拟机栈执行的基本相同,唯一的区别就是虚拟机栈是执行Java方法的,本地方法栈是执行native方法的。

程序计数器

是一块较小的内存空间,用来指定当前线程执行字节码的行数,每个线程计数器都是私有的,因为每个线程都需要记录执行的行数 ,如果线程执行是一个Java方法的时候,计数器记录的是虚拟机字节码指令的地址;当执行的是Native的方法的时候,计数器指令为空;该内存区域是Java虚拟机唯一没有规定任何OutOfMemoryError的区域。

JVM方法栈的工作过程,方法栈和本地方法栈有什么区别。

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

本地方法栈和虚拟机栈基本一致,不过本地方法栈执行的是native方法。

JVM的栈中引用如何和堆中的对象产生关联。

在栈中存储局部变量,堆中存放实例对象,栈中变量对应堆中实例对象的一个引用地址,这样产生了关联。

逃逸分析技术

当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,比如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。 3种常见的指针逃逸场景。分别是 全局变量赋值,方法返回值,实例引用传递。

我们知道java对象是在堆里分配的,在调用栈中,只保存了对象的指针。当对象不再使用后,需要依靠GC来遍历引用树并回收内存,如果对象数量较多,将给GC带来较大压力,也间接影响了应用的性能。减少临时对象在堆内分配的数量,无疑是最有效的优化方法。

GC的常见算法

1.引用计数法。2.可达性分析法(GC Roots对象(1)虚拟机(JVM)栈中引用对象(2)方法区中的类静态属性引用对象(3)方法区中常量引用的对象(final 的常量值)(4)本地方法栈JNI的引用对象)

1.标记-清理算法。2.复制法。3.标记-整理算法。4.分代收集算法

CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势。

CMS是一款优秀的收集器,主要优点:并发收集、低停顿。缺点: 无法处理浮动垃圾,内存碎片化

CMS回收过程:初始标记(STW initial mark)-并发标记(Concurrent marking)-重新标记(STW remark)-并发清理(Concurrent sweeping)

初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。

并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。

重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。

并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:

1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。

3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,

5、G1运作步骤:

1、初始标记;2、并发标记;3、最终标记;4、筛选回收

上面几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短,并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

垃圾回收Minor GC、Major GC 、Full GC

堆被分解为较小的三个部分。具体分为:新生代、老年代、持久代。

每次 Minor GC 会清理年轻代的内存。Major GC 是清理老年代。Full GC 是清理整个堆空间—包括年轻代和老年代。实际上Major GC 和Full GC比这想象的要复杂

Java有没有主动触发GC的方式

System.gc(); 或者 Runtime.getRuntime().gc();

常用的JVM调优参数

Java类加载的过程

(1)加载

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。

(2)链接 将java类的二进制代码合并到jvm的运行状态之中的过程

2.1 验证

确保加载的类信息符合jvm规范,没有安全方面的问题。

2.2 准备

正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。

2.3 解析

虚拟机常量池内的符号引用替换为直接引用的过程。(比如String s ="aaa",转化为 s的地址指向“aaa”的地址)

(3)初始化

初始化阶段是执行类构造器方法的过程。类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先初始化其父类的初始化。虚拟机会保证一个类的构造器方法在多线程环境中被正确加锁和同步。当访问一个java类的静态域时,只有真正声明这个静态变量的类才会被初始化。

双亲委派模型的过程以及优势

启动类加载器 Bootstrap ClassLoader:加载<JAVA_HOME>\lib目录下核心库 扩展类加载器 Extension ClassLoader:加载<JAVA_HOME>\lib\ext目录下扩展包 应用程序类加载器 Application ClassLoader: 加载用户路径(classpath)上指定的类库

双亲委派模型要求除顶层启动类加载器外其余类加载器都应该有自己的父类加载器;类加载器之间通过复用关系来复用父加载器的代码。

过程:实现双亲委派模型的代码都集中在java.lang.ClassLoader的loadClass()方法中: 首先会检查请求加载的类是否已经被加载过; 若没有被加载过: 递归调用父类加载器的loadClass(); 父类加载器为空后就使用启动类加载器加载; 如果父类加载器和启动类加载器均无法加载请求,则调用自身的加载功能。

优势: Java类伴随其类加载器具备了带有优先级的层次关系,确保了在各种加载环境的加载顺序。 保证了运行的安全性,防止不可信类扮演可信任的类。

强软弱虚引用

1.强引用(StrongReference)

强引用就是指在程序代码之中普遍存在的,类似于Object obj = newObject()这类的引用。

上面的式子创建了一个Object对象,并将这个对象的(强)引用存到变量obj中。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

2.软引用(SoftReference)

软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常,利用这个特性,我们可以用软引用来做缓存,如果用强引用做缓存,会导致缓存数据始终保存在内存中,这就导致要由我们自己来决定什么时候缓存数据不再需要并且手动清除,进而让垃圾回收器回收,这就使得我们需要人为的决定该清理哪个对象。

3.弱引用(WeakReference)

弱引用也是用来描述非必须对象的,但是它的强度比软引用更若弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作之时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

4.虚引用(PhantomReference)

虚引用也称为幽灵引用或者幻影引用,与软引用,弱引用不同,虚引用指向的对象十分脆弱,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁。

引用队列(ReferenceQueue):

当对象改变其可达性状态时,对该对象的引用就可能会被置于引用队列中。这些队列被垃圾回收器用来与我们的代码沟通有关对象可达性变化的情况。这些队列是探测可达性变化的最佳方式,尽管我们也可以通过检查get方法的返回值是不是null来探测对象的可达性变化,引用对象在构造时可以与特定队列建立关联。

引用队列中的虚引用可以用来确定对象何时被回收。我们不可能通过虚引用来访问任何对象,即使该对象通过其他方式是可达的也是如此,因为虚引用的get方法总是返回null,事实上,用虚引用来查找要回收的对象是最安全的方法,因为弱引用和软引用在对象被确定可能会被终结之后就插入到队列中,而虚引用则是在对象被终结之后才插入到队列中的,即在该对象可以执行某些操作的之后插人队列的,所以它是绝对安全的。如果可以的话,应该总是使用虚引用,因为其他引用会存在finalize方法使将被终结对象重新复活的可能性。

并发编程

volatile

特性: 1.可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。 2.原子性。对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

内存语义:1.当写一个volatile变量时,JMM会将本地变量中对应的共享变量值刷新到主内存中;2.当读一个volatile变量时,JMM会将线程本地变量存储的值,置为无效值,线程接下来将从主内存中读取共享变量。

内存语义的实现:编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

内存语义:锁的获取和volatile的读有相同的内存语义,锁的释放和volatile的写有相同的内存语义。

线程

线程状态:初始化(NEW)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIME_WAITING)、终止(TERMINATED)

中断interrupt、interrupted和isInterrupted的区别

interrupt()方法来停止线程,不会马上终止,它仅仅是在当前的线程中打了一个停止的标记。即不会影响线程的正常运行,只是该线程多了一个停止的标记而已。

interrupted()测试的是当前线程是否处于中断状态,是一个静态方法,在一次执行后具有将状态标志清除为false的状态。即连续两次调用该方法后,第二次调用则会返回false

isInterrupted()测试的是调用该方法的线程是否处于中断状态,是一个实例方法,不会清除状态标志

wait,notify,notifyAll

等待/唤醒。 是定义在Object类的实例方法,用于控制线程状态,三个方法都必须在synchronized 同步关键字所限定的作用域中调用

wait()与sleep()的区别

1.首先sleep()是Thread()类的方法,而wait()是Object类的方法

2.sleep()方法是休眠,阻塞线程的同时仍然会持有锁,也就是说它休眠期间其他线程仍然无法获得锁,同时sleep()休眠时自动醒的;而调用wait()方法时,则自动释放锁,也就是其他线程可以获得锁,而且wait()是无法自动醒的,只有通过notify()或 notifyAll()才行。

AQS的理解

源码解读

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

CountDownLatch与CyclicBarrier、 Semaphore

CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行。

CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。

线程池的种类,区别和使用场景

newCachedThreadPool:

  • 底层:返回ThreadPoolExecutor实例,corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列)

  • 通俗:当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。

  • 适用:执行很多短期异步的小程序或者负载较轻的服务器

newFixedThreadPool:

  • 底层:返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列

  • 通俗:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)

  • 适用:执行长期的任务,性能好很多

newSingleThreadExecutor:

  • 底层:FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列

  • 通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)

  • 适用:一个任务一个任务执行的场景

NewScheduledThreadPool:

  • 底层:创建ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列

  • 通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构

  • 适用:周期性执行任务的场景

线程池任务执行流程:

  1. 当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。

  2. 当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行

  3. 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务

  4. 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理

  5. 当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程

  6. 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

备注:

一般如果线程池任务队列采用LinkedBlockingQueue队列的话,那么不会拒绝任何任务(因为队列大小没有限制),这种情况下,ThreadPoolExecutor最多仅会按照最小线程数来创建线程,也就是说线程池大小被忽略了。

如果线程池任务队列采用ArrayBlockingQueue队列的话,那么ThreadPoolExecutor将会采取一个非常负责的算法,比如假定线程池的最小线程数为4,最大为8所用的ArrayBlockingQueue最大为10。随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间。

这个算法背后的理念是:该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;这时第二个节流阀—最大线程数就起作用了。

框架

代理

Spring aop 动态代理

Spring事务隔离级别和传播特性

传播行为

事务的第一个方面是传播行为。传播行为定义关于客户端和被调用方法的事务边界。Spring定义了7中传播行为。

传播行为

意义

PROPAGATION_MANDATORY

表示该方法必须运行在一个事务中。如果当前没有事务正在发生,将抛出一个异常

PROPAGATION_NESTED

表示如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于封装事务进行提交或回滚。如果封装事务不存在,行为就像PROPAGATION_REQUIRES一样。

PROPAGATION_NEVER

表示当前的方法不应该在一个事务中运行。如果一个事务正在进行,则会抛出一个异常。

PROPAGATION_NOT_SUPPORTED

表示该方法不应该在一个事务中运行。如果一个现有事务正在进行中,它将在该方法的运行期间被挂起。

PROPAGATION_SUPPORTS

表示当前方法不需要事务性上下文,但是如果有一个事务已经在运行的话,它也可以在这个事务里运行。

PROPAGATION_REQUIRES_NEW

表示当前方法必须在它自己的事务里运行。一个新的事务将被启动,而且如果有一个现有事务在运行的话,则将在这个方法运行期间被挂起。

PROPAGATION_REQUIRED

表示当前方法必须在一个事务中运行。如果一个现有事务正在进行中,该方法将在那个事务中运行,否则就要开始一个新事务。

隔离级别

声明式事务的第二个方面是隔离级别。隔离级别定义一个事务可能受其他并发事务活动活动影响的程度。另一种考虑一个事务的隔离级别的方式,是把它想象为那个事务对于事物处理数据的自私程度。

在一个典型的应用程序中,多个事务同时运行,经常会为了完成他们的工作而操作同一个数据。并发虽然是必需的,但是会导致一下问题:

  • 脏读(Dirty read)-- 脏读发生在一个事务读取了被另一个事务改写但尚未提交的数据时。如果这些改变在稍后被回滚了,那么第一个事务读取的数据就会是无效的。

  • 不可重复读(Nonrepeatable read)-- 不可重复读发生在一个事务执行相同的查询两次或两次以上,但每次查询结果都不相同时。这通常是由于另一个并发事务在两次查询之间更新了数据。

  • 幻影读(Phantom reads)-- 幻影读和不可重复读相似。当一个事务(T1)读取几行记录后,另一个并发事务(T2)插入了一些记录时,幻影读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。

在理想状态下,事务之间将完全隔离,从而可以防止这些问题发生。然而,完全隔离会影响性能,因为隔离经常牵扯到锁定在数据库中的记录(而且有时是锁定完整的数据表)。侵占性的锁定会阻碍并发,要求事务相互等待来完成工作。

考虑到完全隔离会影响性能,而且并不是所有应用程序都要求完全隔离,所以有时可以在事务隔离方面灵活处理。因此,就会有好几个隔离级别。

隔离级别

含义

ISOLATION_DEFAULT

使用后端数据库默认的隔离级别。

ISOLATION_READ_UNCOMMITTED

允许读取尚未提交的更改。可能导致脏读、幻影读或不可重复读。

ISOLATION_READ_COMMITTED

允许从已经提交的并发事务读取。可防止脏读,但幻影读和不可重复读仍可能会发生。

ISOLATION_REPEATABLE_READ

对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻影读仍可能发生。

ISOLATION_SERIALIZABLE

完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。

spring事务管理方式

spring支持编程式事务管理和声明式事务管理两种方式。

编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

MyBatis 缓存机制

MyBatis 的缓存分为一级缓存和二级缓存,都是基于HashMap实现

  • 一级缓存是 SqlSession 级别的缓存

  • 二级缓存是 mapper 级别的缓存,多个 SqlSession 共享

mybatis的一级缓存是SqlSession级别的缓存,在操作数据库的时候需要先创建SqlSession会话对象,在对象中有一个HashMap用于存储缓存数据,此HashMap是当前会话对象私有的,别的SqlSession会话对象无法访问。 二级缓存是mapper级别的缓存,也就是同一个namespace的mappe.xml,当多个SqlSession使用同一个Mapper操作数据库的时候,得到的数据会缓存在同一个二级缓存区域。

二级缓存默认是没有开启的。需要在setting全局参数中配置开启二级缓存

<settings>
        <setting name="cacheEnabled" value="true"/>默认是false:关闭二级缓存
<settings>

数据库

sql的left join 、right join 、inner join之间的区别

left join(左联接) 返回包括左表中的所有记录和右表中联结字段相等的记录 right join(右联接) 返回包括右表中的所有记录和左表中联结字段相等的记录 inner join(等值连接) 只返回两个表中联结字段相等的行

数据库引擎

InnoDB和MyISAM是许多人在使用MySQL时最常用的两个表类型,这两个表类型各有优劣,视具体应用而定。基本的差别为:MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持。MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快,但是不提供事务支持,而InnoDB提供事务支持已经外部键等高级数据库功能。

数据库调优

1、缓存

2、当只需要一条数据时使用LIMIT 1

3、避免select * ,取之所需

4、为每张表设置一个id作为其主键

5、选择正确的存储引擎

6、拆为搜索的字段建立索引

7、LIKE关键字的使用尽量避免,即使需要也使用“KEY%”百分号在后面的这种

8、适当可考虑分库分表

Last updated