大家好,又见面了,我是你们的朋友全栈君。
关键:
(1)客户端的请求参数上加一个请求时间timestamp
原理:服务器获取请求的timestamp,然后比较自身系统时间,如果相差超过设定时间就是超时,该请求无效
作用:就算第三方截取了该请求,它也只能在设定时间内进行重放攻击
(2)客户端的请求参数上加一个随机字符串string
原理:服务端获取请求的随机字符串string,然后查看是否在设定时间内别的请求使用过该string,被使用过就判定无效
作用:加上timestamp,就造成短时间内一个请求只能使用一次,因为就算请求被拦截,它请求成功一次后,第二次复制重放时就因为随机字符串被使用而被拒绝
如果你的系统不到足够复杂的程度不要考虑使用微服务。当业务不复杂,团队规模不大的时候,单块架构比微服务架构具有更高的生产率因为建立微服务架构需要额外的开销来支持和管理微服务,从而降低生产率;但是随着业务复杂性的增加和团队规模的扩大,单块架构比微服务架构的生产率下降更趋明显。当复杂性达到一个临界点,微服务架构的生产率会优于单块架构,因为微服务的松散耦合自治特性减缓了生产率的下降趋势。
所以我们在做项目时,一定要根据自己的团队和业务复杂性来判断,何时应用微服务。
优点:
针对特定服务发布,影响小,风险小,成本低
频繁发布版本,快速交付需求
低成本扩容,弹性伸缩,适应云环境
微服务往往是增加了协同开发效率和后续分组件的可维护性和代码的可阅读性,对性能的提升没有多大
缺点:
分布式系统的复杂性
部署,测试和监控的成本问题
分布式事务和CAP的相关问题
使用一个固定的服务器专门保持session,其他服务器共享,Session保持(会话保持)是我们见到最多的名词之一,通过会话保持,负载均衡进行请求分发的时候保证每个客户端固定的访问到后端的同一台应用服务器。会话保持方案在所有的负载均衡都有对应的实现。而且这是在负载均衡这一层就可以解决Session问题。
会话复制在Tomcat上得到了支持,它是基于IP组播(multicast)来完成Session的复制,Tomcat的会话复制分为两种:
全局会话复制:利用Delta Manager复制会话中的变更信息到集群中的所有其他节点。
非全局复制:使用Backup Manager进行复制,它会把Session复制给一个指定的备份节点。
主要是因为会话复制不适合大的集群。根据笔者在生产的实践案例,当时是在集群超过6个节点之后就会出现各种问题,不推荐生产使用。
Session存放到哪里?
对于Session来说,肯定是频繁使用的,虽然你可以把它存放在数据库中,但是真正生产环境中我更推荐存放在性能更快的分布式KV数据中,例如:Memcached和Redis。
会话保持的缺点: ①负载不均衡了 ②没有彻底解决问题
会话复制的缺点: 集群超过6个节点就会出现一系列的问题
会话共享:会话数据共享在Nosql(Redis)数据库中分享。
当服务器创建完session对象后,会把session对象的id以cookie形式返回给客户端。这样,当用户保持当前浏览器的情况下再去访问服务器时,会把session的id传给服务器,服务器根据session的id来为用户提供相应的服务
session是服务端存储,cookie是浏览器端存储
Cookie是把用户的数据写给用户的浏览器。
Session技术把用户的数据写到用户独占的session中。
Session对象由服务器创建,开发人员可以调用request对象的getSession方法得到session对象。
线性表是由N个元素组成的有序系列,也是最常见的一种数据结构,重点有数组和链表
4.1.1 数组
数组是一种存储单元连续,用来存储固定大小元素的线性表。数组静态分配内存,在内存中连续,利用下标定位,时间复杂度为O(1),插入或删除元素的时间复杂度O(n)。Java中对应的集合实现,比如ArrayList
4.1.2 链表
链表又分为单链表和双链表,是在物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中指针的链接次序实现的。链表动态分配内存,不连续,定位元素时间复杂度O(n),插入或删除元素时间复杂度O(1)。Java中对应的集合实现,比如linkdeList.
4.1.3 数组和链表的优缺点
数组随机访问性强(通过下标进行快速定位),查找速度快,但插入和删除效率低(插入和删除需要移动数据);可能浪费内存(因为是连续的,所以每次申请数组之前必须规定数组的大小,如果大小不合理,则可能会浪费内存),内存空间要求高,必须有足够的连续内存空间;数组大小固定,不能动态拓展。
链表插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素);内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间),大小没有固定,拓展很灵活。不能随机查找,必须从第一个开始遍历,查找效率低。
4.1.4 单链表和双链表的特点和优缺点
特点:单链表只有一个指向下一结点的指针,也就是只能next;双链表除了有一个指向下一结点的指针外,还有一个指向前一结点的指针,可以通过prev()快速找到前一结点,顾名思义,单链表只能单向读取。
优缺点:1、删除单链表中的某个结点时,一定要得到待删除结点的前驱,得到该前驱有两种方法,第一种方法是在定位待删除结点的同时一路保存当前结点的前驱。第二种方法是在定位到待删除结点之后,重新从单链表表头开始来定位前驱。尽管通常会采用方法一。但其实这两种方法的效率是一样的,指针的总的移动操作都会有2*i次。而如果用双向链表,则不需要定位前驱结点。因此指针总的移动操作为i次。
2、查找时也一样,我们可以借用二分法的思路,从head(首节点)向后查找操作和last(尾节点)向前查找操作同步进行,这样双链表的效率可以提高一倍。
可是为什么市场上单链表的使用多余双链表呢?
从存储结构来看,每个双链表的节点要比单链表的节点多一个指针,而长度为n就需要 n*length(这个指针的length在32位系统中是4字节,在64位系统中是8个字节) 的空间,这在一些追求时间效率不高应用下并不适应,因为它占用空间大于单链表所占用的空间;这时设计者就会采用以时间换空间的做法,这时一种工程总体上的衡量。
4.2.1 栈
栈是一种运算受限的线性表,重点掌握其先进后出的特点。表的末端叫栈顶,基本操作有push(进栈)和pop(出栈)。Java中的stack就是简单的栈实现
4.2.2 队列
队列也是一种操作受限的线性表,重点掌握其先进先出的特点。表的前端只允许进行删除的操作,表的后端进行插入的操作。进行插入操作的端称为队尾,进行删除操作的称为队头。Java中很多queue的实现,消息中间件的队列本质也是基于此。
非线性结构里面,树是非常非常重要的一种数据结构。基于本身的结构优势,尤其在查找领域,应用广泛,其中又以二叉树最为重要。
4.3.1二叉搜索树
二叉搜索树又叫二叉查找树,又叫二叉排序树。性质如下:1 若左子树不空,则左子树上所有结点的值均小于它根节点的值;2 若右子树不空,则右子树上所有结点的值均大于它的根节点的值;3 左右子树也分别为二叉排序树;4 没有键值相等的结点
4.3.2 平衡二叉树
平衡二叉树又叫AVL树。性质如下:它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1
4.3.3 红黑树
红黑树是一种特殊的平衡二叉树,它保证在最坏情况下基本动态集合操作的事件复杂度为O(log n)。红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。平衡二叉树追去绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本.
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。 2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
线程的通信是指线程之间以何种机制来交换信息。
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区.。
线程栈包含了当前线程执行的方法调用相关信息;还包含了当前方法的所有本地变量信息。一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。即使两个线程执行的是同一段代码,它们也会各自在自己的线程栈中创建本地变量,因此,每个线程中的本地变量都会有自己的版本。
所有原始类型(boolean,byte,short,char,int,long,float,double)的本地变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的本地变量,一个线程可以传递一个副本给另一个线程,当它们之间是无法共享的。
堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量(类的变量)还是方法中的本地变量(局部变量),它都会被存储在堆区。对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。 对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。
现代计算机一般都有2个以上CPU,而且每个CPU还有可能包含多个核心。因此,如果我们的应用是多线程的话,这些线程可能会在各个CPU核心中并行运行。
在CPU内部有一组CPU寄存器,也就是CPU的储存器。CPU操作寄存器的速度要比操作计算机主存快的多。在主存和CPU寄存器之间还存在一个CPU缓存,CPU操作CPU缓存的速度快于主存但慢于CPU寄存器。某些CPU可能有多个缓存层(一级缓存和二级缓存)。计算机的主存也称作RAM,所有的CPU都能够访问主存,而且主存比上面提到的缓存和寄存器大很多。
当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存,进而在读取CPU缓存到寄存器。当CPU需要写数据到主存时,同样会先flush寄存器到CPU缓存,然后再在某些节点把缓存数据flush到主存。
当多个线程同时操作同一个共享对象时,如果没有合理的使用volatile和synchronization关键字,一个线程对共享对象的更新有可能导致其它线程不可见。
想象一下我们的共享对象存储在主存,一个CPU中的线程读取主存数据到CPU缓存,然后对共享对象做了更改,但CPU缓存中的更改后的对象还没有flush到主存,此时线程对共享对象的更改对其它CPU中的线程是不可见的。最终就是每个线程最终都会拷贝共享对象,而且拷贝的对象位于不同的CPU缓存中。下图展示了上面描述的过程。左边CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中: 要解决共享对象可见性这个问题,我们可以使用java volatile关键字。 Java’s volatile keyword. volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile原理是基于CPU内存屏障指令实现的
如果多个线程共享一个对象,如果它们同时修改这个共享对象,这就产生了竞争现象。如下图所示,线程A和线程B共享一个对象obj。假设线程A从主存读取Obj.count变量到自己的CPU缓存,同时,线程B也读取了Obj.count变量到它的CPU缓存,并且这两个线程都对Obj.count做了加1操作。此时,Obj.count加1操作被执行了两次,不过都在不同的CPU缓存中。如果这两个加1操作是串行执行的,那么Obj.count变量便会在原始值上加2,最终主存中的Obj.count的值会是3。然而下图中两个加1操作是并行的,不管是线程A还是线程B先flush计算结果到主存,最终主存中的Obj.count只会增加1次变成2,尽管一共有两次加1操作。 要解决上面的问题我们可以使用java synchronized代码块。synchronized代码块可以保证同一个时刻只能有一个线程进入代码竞争区,synchronized代码块也能保证代码块中所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会flush到主存,不管这些变量是不是volatile类型的。
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution Engine(执行引擎);两个组件为Runtime Data Area(运行时数据区)、Native Interface(本地接口)。
Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载Class文件到Runtime Data Area中的Method Area。
Execution Engine(执行引擎):执行Classes中的指令。
Native Interface(本地接口):与Native Libraries交互,是其它编程语言交互的接口。
Runtime Data Area(运行时数据区域):这就是我们常说的JVM的内存。
加载过程:首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,
而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中,长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的发生场景。内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。
内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。
1 年老代堆空间被占满 异常: java.lang.OutOfMemoryError: Java heap space
这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。如上图所示,所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。
解决:
这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。如果在for语句里面频繁创建对象就容易造成内存泄漏,如 for(int i=0;i<100;i++){ Student s=new Student()};
2 持久代被占满 异常:java.lang.OutOfMemoryError: PermGen space Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。
解决:1. -XX:MaxPermSize=16m 2. 换用JDK。比如JRocket。
3 堆栈溢出 异常:java.lang.StackOverflowError
说明:这个就不多说了,一般就是递归没返回,或者循环调用造成
4 线程堆栈满 异常:Fatal: Stack size too small
说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
5 系统内存被占满 异常:java.lang.OutOfMemoryError: unable to create new native thread
说明:这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。
解决:
1. 重新设计系统减少线程数量。
2. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。
在Java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
一般有两种方法来判断:引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
新生代收集器(基本采用复制算法):serial收集器; ParNew收集器–是Serial收集器的多线程版本; Parallel Scaverge收集器
老年代收集器(基本采用标记整理算法):Serial Old收集器–是Serial收集器的老年代版本;Parallel Old–是Parallel Scavenge收集器的老年代版本;CMS收集器
新生代+老年代收集器:G1 收集器; ZGC 收集器
JVM 调优的工具:
JDK 自带了很多监控工具,都位于 JDK 的BIN目录下,其中最常用的是 Jconsole 和 Jvisualvm 这两款视图监控工具。
Jconsole:用于对 JVM 中的内存、线程和类等进行监控;
Jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
常用的 JVM 调优的参数:
-Xms2g:初始化大小为 2G;
-Xmx2g:最大内存为 2G;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印GC信息;
-XX:+PrintGCDetails:打印GC详细信息。
交易链过长,无限递归,死循环
同步与异步是用户线程与内核的交互方式,同步指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;异步是用户线程发起IO请求后仍然继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞与非阻塞是用户线程调用内核IO的操作方式,阻塞是IO操作需要彻底完成后才返回到用户空间;非阻塞是IO操作被调用后立即返回给用户一个状态值,无需等待IO操作彻底完成。
7.2.1阻塞IO
阻塞IO是指调用了read后,必须等待数据到达,并且复制到了用户空间,才能返回,否则整个线程一直在等待。所以阻塞IO的问题就是,线程在读写IO的时候不能干其它的事情。
7.2.2非阻塞IO
非阻塞IO在调用read后,可以立刻返回,然后问操作系统,数据有没有在内核空间准备好,如果准备好了,就可以read出来了。因为不知道什么时候准备好,要保证实时性,就得不断的轮询。
7.2.3 IO多路复用(非阻塞IO)
在使用非阻塞IO的时候,如果每个线程访问网络后都不停的轮询,那么这个线程就被占用了,那跟阻塞IO也没什么区别了。每个线程都轮询自己的socket,这些线程也不能干其它的事情。如果能有一个专门的线程去轮询所有的socket,如果数据准备好,就找一个线程处理,这就是IO多路复用。当然轮询的线程也可以不用找其他线程处理,自己处理就行,例如redis就是这样的。
IO多路复用,能够让一个或几个线程去管理很多个(可以成千上万)socket连接,这样连接数就不再受限于系统能启动的线程数。我们把select轮询抽出来放在一个线程里, 用户线程向其注册相关socket或IO请求,等到数据到达时通知用户线程,则可以提高用户线程的CPU利用率.这样, 便实现了异步方式。
a、丢失的原因:因为网络传输的不稳定性,当生产者在向MQ发送消息的过程中,MQ没有成功接收到消息,但是生产者却以为MQ成功接收到了消息,不会再次重复发送该消息,从而导致消息的丢失。
b、解决办法: 有两个解决办法:事务机制和confirm机制,最常用的是confirm机制。
事务机制:
RabbitMQ 提供了事务功能,生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。伪代码如下:
// 开启事务
channel.txSelect
try {
// 这里发送消息
} catch (Exception e) {
channel.txRollback
// 这里再次重发这条消息
}
// 提交事务
channel.txCommit;
confirm机制:
RabbitMQ可以开启 confirm 模式,在生产者那里设置开启 confirm 模式之后,生产者每次写的消息都会分配一个唯一的 id,如果消息成功写入 RabbitMQ 中,RabbitMQ 会给生产者回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,生产者可以发送。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么可以重发。
注意:RabbitMQ的事务机制是同步的,很耗型能,会降低RabbitMQ的吞吐量。confirm机制是异步的,生成者发送完一个消息之后,不需要等待RabbitMQ的回调,就可以发送下一个消息,当RabbitMQ成功接收到消息之后会自动异步的回调生产者的一个接口返回成功与否的消息。
a、丢失的原因:RabbitMQ接收到生产者发送过来的消息,是存在内存中的,如果没有被消费完,此时RabbitMQ宕机了,那么再次启动的时候,原来内存中的那些消息都丢失了。
b、解决办法:开启RabbitMQ的持久化。当生产者把消息成功写入RabbitMQ之后,RabbitMQ就把消息持久化到磁盘。结合上面的说到的confirm机制,只有当消息成功持久化磁盘之后,才会回调生产者的接口返回ack消息,否则都算失败,生产者会重新发送。存入磁盘的消息不会丢失,就算RabbitMQ挂掉了,重启之后,他会读取磁盘中的消息,不会导致消息的丢失。
c、持久化的配置:
第一点是创建 queue 的时候将其设置为持久化,这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
第二个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
注意:持久化要起作用必须同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。
a、丢失的原因:如果RabbitMQ成功的把消息发送给了消费者,那么RabbitMQ的ack机制会自动的返回成功,表明发送消息成功,下次就不会发送这个消息。但如果就在此时,消费者还没处理完该消息,然后宕机了,那么这个消息就丢失了。
b、解决的办法:简单来说,就是必须关闭 RabbitMQ 的自动 ack,可以通过一个 api 来调用就行,然后每次在自己代码里确保处理完的时候,再在程序里 ack 一把。这样的话,如果你还没处理完,不就没有 ack了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索,在经常需要进行检索的字段上创建索引;一个表的索引数最好不要超过6个,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定,避免在索引上使用计算.
程序中通常是根据用户的输入来动态执行SQL,这时应该尽量使用参数化SQL,这样不仅可以避免SQL注入漏洞攻击,最重要数据库会对这些参数化SQL进行预编译,这样第一次执行的时候DBMS会为这个SQL语句进行查询优化并且执行预编译,这样以后再执行这个SQL的时候就直接使用预编译的结果,这样可以大大提高执行的速度。
每次执行SQL的时候都要建立网络连接、进行权限校验、进行SQL语句的查询优化、发送执行结果,这个过程 是非常耗时的,因此应该尽量避免过多的执行SQL语句,能够压缩到一句SQL执行的语句就不要用多条来执行。
简化SQL语句的重要方法就是采用临时表暂存中间结果,但是,临时表的好处远远不止这些,将临时结果暂存在临时表,后面的查询就在tempdb中了,这可以避免程序中多次扫描主表,也大大减少了程序执行中“共享锁”阻塞“更新锁”,减少了阻塞,提高了并发性能。
但是也得避免频繁创建和删除临时表,以减少系统表资源的消耗。
尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
9.6 LIMIT语句尽量要跟order by或者 distinct.这样可以避免做一次full table scan
9.7 .如果想要清空表的所有纪录,建议用truncate table tablename而不是delete from tablename.
1 事务开始和提交
只在必要的情况下才使用事务begin translation 和commit,SQL语句默认就是一个事务,在该语句执行完成后也是默认commit的,在保证数据一致性的前提下,egin translation套住的SQL语句越少越好!有些情况下可以采用触发器同步数据。
2 用varchar/nvarchar 代替 char/nchar
尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些
3 用where字句替换HAVING字句
避免使用HAVING字句,因为HAVING只会在检索出所有记录之后才对结果集进行过滤,而where则是在聚合前 刷选记录,如果能通过where字句限制记录的数目,那就能减少这方面的开销。HAVING中的条件一般用于聚合函数的过滤
4 用union all替换union
当SQL语句需要union两个查询结果集合时,即使检索结果中不会有重复的记录,如果使用union这两个结果集 同样会尝试进行合并,然后在输出最终结果前进行排序,因此如果可以判断检索结果中不会有重复的记录时候,应该用union all,这样效率就会因此得到提高。
5 慎用like做查询条件
双百分号不会用到索引,单百分号会用到,一般用右边百分号比较多,比如:‘name%’。
索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引
6 其他
查询条件不用is null的判断
设计模式实例分享可参考博客地址:java设计模式实例分享_higherzjm的专栏-CSDN博客
为其他对象提供一种代理以控制对这个对象的访问。 从结构上来看和Decorator模式类似,但Proxy是控制,更像是一种对功能的限制,而Decorator是增加职责。 spring的Proxy模式在aop中有体现,比如JdkDynamicAopProxy和Cglib2AopProxy。AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。Spring AOP就是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,Spring AOP会使用Cglib,这时候Spring AOP会使用Cglib生成一个被代理对象的子类来作为代理,如下图所示:
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。spring中Observer模式常用的地方是listener的实现。如ApplicationListener。
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。spring中Observer模式常用的地方是listener的实现。如ApplicationListener。 观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。
Spring 的事件流程总结:
定义一个事件: 实现一个继承自 ApplicationEvent,并且写相应的构造函数;
定义一个事件监听者:实现 ApplicationListener 接口,重写 onApplicationEvent() 方法;
使用事件发布者发布消息: 可以通过 ApplicationEventPublisher 的 publishEvent() 方法发布消息。当调用 DemoPublisher 的 publish() 方法的时候,比如 demoPublisher.publish(“你好”) ,控制台就会打印出:接收到的信息是:你好 。
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。 spring中在实例化对象的时候用到Strategy模式
形象的称为就是一个接口在不同场景下有多个实现类,客户端根据不同场景调用对应的实现类。
和工厂模式的比较:
1.工厂模式封装的是对象,策略模式封装的是算法
2.工厂模式可能需要将工厂和产品都暴露给调用方,因为调用方可能会用到产品的不同方面。但是策略模式,只需要将context暴露给调用方,其内部算法对调用方不可见,不需要将算法子类暴露出去,因为这些算法,本质上都是完成同一件事的不同方法。
装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面
在我们的项目中遇到这样一个问题:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。我们以往在spring和hibernate框架中总是配置一个数据源,因而sessionFactory的dataSource属性总是指向这个数据源并且恒定不变,所有DAO在使用sessionFactory的时候都是通过这个数据源访问数据库。但是现在,由于项目的需要,我们的DAO在访问sessionFactory的时候都不得不在多个数据源中不断切换,问题就出现了:如何让sessionFactory在执行数据持久化的时候,根据客户的需求能够动态切换不同的数据源?我们能不能在spring的框架下通过少量修改得到解决?是否有什么设计模式可以利用呢? 首先想到在spring的applicationContext中配置所有的dataSource。这些dataSource可能是各种不同类型的,比如不同的数据库:Oracle、SQL Server、MySQL等,也可能是不同的数据源:比如apache 提供的org.apache.commons.dbcp.BasicDataSource、spring提供的org.springframework.jndi.JndiObjectFactoryBean等。然后sessionFactory根据客户的每次请求,将dataSource属性设置成不同的数据源,以到达切换数据源的目的。spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。基本上都是动态地给一个对象添加一些额外的职责。
适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
spring AOP中的适配器模式
我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter 。Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return之前)等等。每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptor、AfterReturningAdviceAdapter、AfterReturningAdviceInterceptor。Spring预定义的通知要通过对应的适配器,适配成 MethodInterceptor接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor 负责适配 MethodBeforeAdvice)。
spring MVC中的适配器模式
在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。
为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:
if(mappedHandler.getHandler() instanceof MultiActionController){
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
}else if(…){
}
假如我们再增加一个 Controller类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。
保证一个类仅有一个实例,并提供一个访问它的全局访问点。 spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是是任意的java对象。
形象的称为就是一个接口在不同场景下有多个实现类,客户端根据不同场景调用对应的实现类。
通常由应用程序直接使用new创建新的对象,为了将对象的创建和使用相分离,采用工厂模式,即应用程序将对象的创建及初始化职责交给工厂对象。
一般情况下,应用程序有自己的工厂对象来创建bean.如果将应用程序自己的工厂对象交给Spring管理,那么Spring管理的就不是普通的bean,而是工厂Bean
又叫做静态工厂方法(StaticFactory Method)模式,但不属于23种GOF设计模式之一。
简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。
spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定
模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。
私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的用户使用。当Maven需要下载构件的时候,它从私服请求,如果 私服上不存在该构件,则从外部远程仓库下载,缓存在私服上之后,再为Maven的下载请求提供服务。 私服的好处:a、节省自己的外网带宽;b、加速Maven构建;c、部署自己内部的第三方构件;d、提高稳定性,增强控制; e、降低中央仓库的负荷。
PROPAGATION_REQUIRED–支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择(大部分项目默认配置)。
PROPAGATION_SUPPORTS–支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY–支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW–新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED–以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER–以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED–如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
default:使用底层数据库的默认隔离级别对于大多数的数据库来说,默认的隔离级别都是read_commted
read_commted:只允许事务读取已经被其他事务提交的变更,可以避免脏读,但不可以避免不可重复读和幻读
read_uncommted:允许事务读取未被其他事务提交的变更,脏读,不可重复读,幻读都会出现。
repeatable_read: 可避免脏读和不可重复读,但幻读问题仍然存在
serlalizable:所有问题都可以避免,但性能十分低下。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。存在竞争的线程不安全,不存在竞争的线程就是安全的
1、synchronized同步方法;2、synchronized同步代码块;
3、wait与notify,这两个方法并不是在Thread类中的,而是输出Object类。这也意味着任何对象都可以调用这2个方法。
4、使用特殊域变量(volatile)实现线程同步,保证此变量对所有线程的可见性,指一条线程修改了这个变量的值,新值对于其他线程来说是可见的,通过新值立即同步到主内存和每次使用前从主内存刷新机制保证了可见性。禁止指令重排序保证了有序性,但并不是多线程安全的,它不保证原子性。Volatile变量最好是那种只有一个线程修改变量,多个线程读取变量的地方。也就是对内存可见性要求高,而对原子性要求低的地方。银行卡余额为例,多个人取钱一定要确保余的实时可见性;
5、使用重入锁实现线程同步;
6、使用局部变量实现线程同步(ThreadLocal),ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。simpleDateFormat为例,ThreadLocal给每个线程维护一个自己的simpleDateFormat对象,这个对象在线程之间是独立的,互相没有关系的。这也就避免了线程安全问题ThreadLocal给每个线程维护一个自己的simpleDateFormat对象,这个对象在线程之间是独立的,互相没有关系的。这也就避免了线程安全问题;
7、使用阻塞队列实现线程同步
8、使用原子变量实现线程同步
同一个线程再次进入同步代码的时候.可以使用自己已经获取到的锁,这就是可重入锁。java里面内置锁(synchronize)和Lock(ReentrantLock)都是可重入的。这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
1.Synchronized
Synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
2.ReentrantLock
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。
1 悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。一般多写的场景下用悲观锁就比较合适
项目中悲观锁的实现,通常依靠数据库提供的锁机制实现,比如mysql的排他锁,select … for update来实现悲观锁。使用悲观锁,需要关闭mysql的自动提交功能,将 set autocommit = 0;mysql中的行级锁是基于索引的,如果sql没有走索引,那将使用表级锁把整张表锁住。
具体操作过程更新数据之前for update查询数据进行锁表,以免别其他事务修改
2 乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。乐观锁适用于写比较少的情况下(多读场景)
项目中乐观锁的实现通常不依靠数据库提供的锁机制,需要我们自已实现,实现方式一般是记录数据版本,一种是通过版本号,一种是通过时间戳。给表加一个版本号或时间戳的字段,读取数据时,将版本号一同读出,数据更新时,将版本号加1。当我们提交数据更新时,判断当前的版本号与第一次读取出来的版本号是否相等。如果相等,则予以更新,否则认为数据过期,拒绝更新,让用户重新操作。更新带条件也是一种乐观锁的实现,以减库存为例update order set num=num-1 where num=9,这边的9要求等于你更新前查询到的值一样,这样避免在多并发的过程中数量被改了,比如改到num为0的时候,再减去1就是负数了
1 读锁
共享锁(S锁)又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S 锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
2 写锁
排他锁(X锁)又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
各排序的时间复杂度:
时间复杂度概念:
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
理论:冒泡排序,是通过每一次遍历获取最大/最小值,将最大值/最小值放在尾部/头部,然后除开最大值/最小值,剩下的数据在进行遍历获取最大/最小值;
代码实现:
public static void main(String[] args) {
int arr[] = {8, 5, 3, 2, 4};
//冒泡
for (int i = 0; i < arr.length; i++) {
//外层循环,遍历次数
for (int j = 0; j < arr.length - i - 1; j++) {
//内层循环,升序(如果前一个值比后一个值大,则交换)
//内层循环一次,获取一个最大值
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
排序过程(红色:移动的数据):
5,3,2,4,8 || 3,2,4,5,8 || 2,3,4,5,8 || 2,3,4,5,8 || 2,3,4,5,8
理论:将第一个值看成最小值,然后和后续的比较找出最小值和下标,交换本次遍历的起始值和最小值,每次遍历的时候,将前面找出的最小值,看成一个有序的列表,后面的看成无序的列表,然后每次遍历无序列表找出最小值。
代码实现:
public static void main(String[] args) {
int arr[] = {6, 5, 3, 2, 4};
//选择
for (int i = 0; i < arr.length; i++) {
//默认第一个是最小的。
int min = arr[i];
//记录最小的下标
int index = i;
//通过与后面的数据进行比较得出,最小值和下标
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]) {
min = arr[j];
index = j;
}
}
//然后将最小值与本次循环的,开始值交换
int temp = arr[i];
arr[i] = min;
arr[index] = temp;
//说明:将i前面的数据看成一个排好的队列,i后面的看成一个无序队列。每次只需要找无需的最小值,做替换
}
}
排序过程(红色:移动的数据):
2,5,3,6,4 || 2,3,5,6,4 || 2,3,4,6,5 || 2,3,4,5,6 || 2,3,4,5,6
理论:默认从第二个数据开始比较。如果第二个数据比第一个小,则交换。然后在用第三个数据比较,如果比前面小,则插入(狡猾)。否则,退出循环。说明:默认将第一数据看成有序列表,后面无序的列表循环每一个数据,如果比前面的数据小则插入(交换)。否则退出。
代码实现:
public static void main(String[] args) {
int arr[] = {7, 5, 3, 2, 4};
//插入排序
for (int i = 1; i < arr.length; i++) {
//外层循环,从第二个开始比较
for (int j = i; j > 0; j--) {
//内存循环,与前面排好序的数据比较,如果后面的数据小于前面的则交换
if (arr[j] < arr[j - 1]) {
int temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
} else {
//如果不小于,说明插入完毕,退出内层循环
break;
}
}
}
}
排序过程(红色:有序,黑色:无序):
5,7,3,2,4||3,5,7,2,4||2,3,5,7,4||2,3,4,5,7
理论:基本上和插入排序一样的道理,不一样的地方在于,每次循环的步长,通过减半的方式来实现。说明:基本原理和插入排序类似,不一样的地方在于。通过间隔多个数据来进行插入排序。
代码实现:
public static void main(String[] args) {
int arr[] = {7, 5, 3, 2, 4, 1, 8, 9, 6};
//希尔排序(插入排序变种版)
for (int i = arr.length / 2; i > 0; i /= 2) {
//i层循环控制步长
for (int j = i; j < arr.length; j++) {
//j控制无序端的起始位置
for (int k = j; k > 0 && k - i >= 0; k -= i) {
if (arr[k] < arr[k - i]) {
int temp = arr[k - i];
arr[k - i] = arr[k];
arr[k] = temp;
} else {
break;
}
}
}
//j,k为插入排序,不过步长为i
}
}
排序过程(步长4/2/1):
4,1,3,2,6,5,8,9,7
3,1,4,2,6,5,7,9,8
1,2,3,4,5,6,7,8,9
理论:
a、确认列表第一个数据为中间值,第一个值看成空缺(低指针空缺)。b、然后在剩下的队列中,看成有左右两个指针(高低)。c、开始高指针向左移动,如果遇到小于中间值的数据,则将这个数据赋值到低指针空缺,并且将高指针的数据看成空缺值(高指针空缺)。然后先向右移动一下低指针,并且切换低指针移动。d、当低指针移动到大于中间值的时候,赋值到高指针空缺的地方。然后先高指针向左移动,并且切换高指针移动。重复c、d操作。e、直到高指针和低指针相等时退出,并且将中间值赋值给对应指针位置。f、然后将中间值的左右两边看成行的列表,进行快速排序操作。
代码实现:
public static void main(String[] args) {
int arr[] = {7, 5, 3, 2, 4, 1, 8, 9, 6};
//快速排序
int low = 0;
int high = arr.length - 1;
quickSort(arr, low, high);
}
public static void quickSort(int[] arr, int low, int high) {
//如果指针在同一位置(只有一个数据时),退出
if (high - low < 1) {
return;
}
//标记,从高指针开始,还是低指针(默认高指针)
boolean flag = true;
//记录指针的其实位置
int start = low;
int end = high;
//默认中间值为低指针的第一个值
int midValue = arr[low];
while (true) {
//高指针移动
if (flag) {
//如果列表右方的数据大于中间值,则向左移动
if (arr[high] > midValue) {
high--;
} else if (arr[high] < midValue) {
//如果小于,则覆盖最开始的低指针值,并且移动低指针,标志位改成从低指针开始移动
arr[low] = arr[high];
low++;
flag = false;
}
} else {
//如果低指针数据小于中间值,则低指针向右移动
if (arr[low] < midValue) {
low++;
} else if (arr[low] > midValue) {
//如果低指针的值大于中间值,则覆盖高指针停留时的数据,并向左移动高指针。切换为高指针移动
arr[high] = arr[low];
high--;
flag = true;
}
}
//当两个指针的位置相同时,则找到了中间值的位置,并退出循环
if (low == high) {
arr[low] = midValue;
break;
}
}
//然后出现有,中间值左边的小于中间值。右边的大于中间值。
//然后在对左右两边的列表在进行快速排序
quickSort(arr, start, low -1);
quickSort(arr, low + 1, end);
}
复制代码
排序过程(青色:中间值,蓝色:确认位置的数据,红色:移动的数据):
6,5,3,2,4,1,7,9,8
1,5,3,2,4,6,7,9,8
1,5,3,2,4,6,7,9,8
1,4,3,2,5,6,7,9,8
1,2,3,4,5,6,7,9,8
1,2,3,4,5,6,7,9,8
1,2,3,4,5,6,7,8,9
理论:a、将列表按照对等的方式进行拆分,b、拆分小最小快的时候,在将最小块按照原来的拆分,进行合并 c、合并的时候,通过左右两块的左边开始比较大小。小的数据放入新的块中 d、说明:简单一点就是先对半拆成最小单位,然后将两半数据合并成一个有序的列表。
代码实现:
public static void main(String[] args) {
int arr[] = {7, 5, 3, 2, 4, 1,6};
//归并排序
int start = 0;
int end = arr.length - 1;
mergeSort(arr, start, end);
}
public static void mergeSort(int[] arr, int start, int end) {
//判断拆分的不为最小单位
if (end - start > 0) {
//再一次拆分,知道拆成一个一个的数据
mergeSort(arr, start, (start + end) / 2);
mergeSort(arr, (start + end) / 2 + 1, end);
//记录开始/结束位置
int left = start;
int right = (start + end) / 2 + 1;
//记录每个小单位的排序结果
int index = 0;
int[] result = new int[end - start + 1];
//如果查分后的两块数据,都还存在
while (left <= (start + end) / 2 && right <= end) {
//比较两块数据的大小,然后赋值,并且移动下标
if (arr[left] <= arr[right]) {
result[index] = arr[left];
left++;
} else {
result[index] = arr[right];
right++;
}
//移动单位记录的下标
index++;
}
//当某一块数据不存在了时
while (left <= (start + end) / 2 || right <= end) {
//直接赋值到记录下标
if (left <= (start + end) / 2) {
result[index] = arr[left];
left++;
} else {
result[index] = arr[right];
right++;
}
index++;
}
//最后将新的数据赋值给原来的列表,并且是对应分块后的下标。
for (int i = start; i <= end; i++) {
arr[i] = result[i - start];
}
}
}
复制代码
排序过程:
5,7,3,2,4,1,6
5,7,2,3,4,1,6
2,3,5,7,4,1,6
2,3,5,7,1,4,6
2,3,5,7,1,4,6
1,2,3,4,5,6,7
1,首先找瓶颈,看问题点出在哪里,哪里给性能上面拖了后腿;
2,假如一个接口里面做的事情太多,看能不能功能拆分;
3,拆分达到极限了,考虑代码处理速度上慢的原因是操作数据库原因还是代码逻辑问题;
4,要是代码逻辑处理速度原因,建议使用多线程,开启多个线程同步处理;
5,要是数据库查询速度和频率原因,建议使用缓存,将需要经常查询数据库的数据缓存在内存中,这样对缓存做管理即可
6,缓存是对数据库的查询操作比较多的情况下。假如是对数据库的插入和更新操作比较多的情况下,建议考虑批量操作
总结:
代码的处理速度问题,优化到极限的情况下,首先采用多线程去处理
数据库查询频繁问题,优化到极限的情况下,首先采用缓存的方式去处理
数据库插入和更新频繁问题,优化到极限的情况下,考虑批量操作方式处理
在tomcat中的catalina.sh文件配置,如JAVA_OPTS=’-Xms1024m -Xmx2048m -XX: PermSize=256M -XX:MaxNewSize=256m -XX:MaxPermSize=256m’。-Xms java虚拟机初始化时的最小内存;-Xmx java虚拟机可使用的最大内存;-XX: PermSize 内存永久保留区域-XX:MaxPermSize 内存最大永久保留区域。根据不同的配置硬件设备可适当调整参数的大小,内存大、cpu核数多,可对相应的参数设置过大,反之设置适当偏小。
在Tomcat conf中server.xml文件中配置,如
<Connector executor=”tomcatThreadPool” port=”8181″ protocol=”HTTP/1.1″ maxHttpHeaderSize=”8192″ minProcessors=”100″ maxProcessors=”1000″ enableLookups=”false” useSendfile=”false” compression=”on” compressionMinSize=”2048″ acceptCount=”1000″ connectionTimeout=”30000″ redirectPort=”8443″ URIEncoding=”utf-8″/>
maxHttpHeaderSize=”8192″: 此选项用于配置:来自于客户端请求的Request和Response的HTTP header 的最大长度,以字节计算。如果不设置,该属性默认值为4096(4K),这里增加到了8192(8K);minProcessors:最小空闲连接线程数,用于提高系统处理性能,默认值为 10;maxProcessors:最大连接线程数,即:并发处理的最大请求数,默认值为 75; acceptCount:允许的最大连接数,应大于等于 maxProcessors ,默认值为 100 ;enableLookups:是否反查域名,取值为: true 或 false 。为了提高处理能力,应设置为 false;connectionTimeout:网络连接超时,单位:毫秒。设置为 0 表示永不超时,这样设置有隐患的。通常可设置为30000 毫秒(也就是30秒)。;redirectPort=”8443″:这里系统默认的,它指定转发端口,如果当前只支持non-SSL请求,在需要安全通信的场所,将把客户请求转发至SSL的redirectPort端口。其中和最大连接数相关的参数为maxProcessors 和 acceptCount 。如果要加大并发连接数,应同时加大这两个参数。web server允许的最大连接数还受制于操作系统的内核参数设置,通常 Windows 是 2000 个左右, Linux 是1000 个左右。
在Tomcat conf中server.xml文件中配置,如 <Connector … compression=”on” compressionMinSize=”2048″ compressableMimeType=”text/html,text/xml,text/javascript,text/css,text/plain” connectionTimeout=”20000″ />
compression 打开压缩功能 ; compressionMinSize 启用压缩的输出内容大小,这里面默认为2KB ;compressableMimeType 压缩类型; connectionTimeout 定义建立客户连接超时的时间. 如果为 -1, 表示不限制建立客户连接的时间
如果有一个特别大的访问量到数据库上时,往往查询速度会变得很慢,所以我们需要进行优化。优化从三个方面考虑:SQL语句优化、主从复制,读写分离,负载均衡、数据库分库分表。
一、SQL查询语句优化
1、使用索引
建立索引可以使查询速度得到提升,我们首先应该考虑在where及order by,group by涉及的列上建立索引。
2、借助explain(查询优化神器)选择更好的索引和优化查询语句
SQL 的 Explain 通过图形化或基于文本的方式详细说明了 SQL 语句的每个部分是如何执行以及何时执行的,以及执行效果。通过
对选择更好的索引列,或者对耗时久的SQL语句进行优化达到对查询速度的优化。
3、任何地方都不要使用SELECT * FROM语句。
4、不要在索引列做运算或者使用函数
5、查询尽可能使用limit来减少返回的行数
6、使用查询缓存,并将尽量多的内存分配给MYSQL做缓存
二、主从复制,读写分离,负载均衡
目前大多数的主流关系型数据库都提供了主从复制的功能,通过配置两台(或多台)数据库的主从关系,可以将一台数据库服务器的数据更新同步到另一台服务器上。网站可以利用数据库这一功能,实现数据库的读写分离,从而改善数据库的负载压力。一个系统的读操作远远多于写操作,因此写操作发向master,读操作发向slaves进行操作(简单的轮询算法来决定使用哪个slave)。
利用数据库的读写分离,Web服务器在写数据的时候,访问主数据库(master),主数据库通过主从复制将数据更新同步到从数据库(slave),这样当Web服务器读数据的时候,就可以通过从数据库获得数据。这一方案使得在大量读操作的Web应用可以轻松地读取数据,而主数据库也只会承受少量的写入操作,还可以实现数据热备份,可谓是一举两得。
三、数据库分表、分区、分库
1、分表
通过分表可以提高表的访问效率。有两种拆分方法:
垂直拆分
在主键和一些列放在一个表中,然后把主键和另外的列放在另一个表中。如果一个表中某些列常用,而另外一些不常用,则可以采用垂直拆分。
水平拆分
根据一列或者多列数据的值把数据行放到两个独立的表中。
2、分区
分区就是把一张表的数据分成多个区块,这些区块可以在一个磁盘上,也可以在不同的磁盘上,分区后,表面上还是一张表,但是数据散列在多个位置,这样一来,多块硬盘同时处理不同的请求,从而提高磁盘I/O读写性能。实现比较简单,包括水平分区和垂直分区。
3、分库
分库是根据业务不同把相关的表切分到不同的数据库中,比如web、bbs、blog等库。
分库解决的是数据库端 并发量的问题。分库和分表并不一定两个都要上,比如数据量很大,但是访问的用户很少,我们就可以只使用分表不使用分库。如果数据量只有1万,而访问用户有一千,那就只使用分库。
注意:分库分表最难解决的问题是统计,还有跨表的连接(比如这个表的订单在另外一张表),解决这个的方法就是使用中间件,比如大名鼎鼎的MyCat,用它来做路由,管理整个分库分表,乃至跨库跨表的连接
在一个高并发系统中 如果突然出现一个应用或者说一个服务突然变得很慢,应该怎么排查?
这个是考线上排查问题能力,没有标准答案,作为开发,假设这种情景出现你怎么诊断问题?
首先:想知道,在实际情况下,怎么知道【一个应用或者说一个服务突然变得很慢】?调用访问的时候会发现的,对于业务流程比较熟悉很重要,先能够初步圈出,可能出现问题的地方,服务监控是必须的,做业务必须要知道自己服务的状态。
1、首先就是想看日志,后来想想看日志确实不太可行,并发量太大的情况下,查日志会很慢,(看日志,pstack strace gdb)。 2、应该从上到下看。—-网络,系统,应用。任何一个环节都有可能有问题,首先看网络监控情况,然后看系统(内存,cpu,负载 )情况。 3、把线程dump出来,用jstack dump 线程调用链,看线程卡到哪里了。然后定位代码,看看赌在哪个函数,是不是调用了重函数。当然先定位到具体应用,再dump。 4、看接口故障率,然后看数据库有没有锁。 5、先看日志,排除了磁盘满了,网络慢,然后看进程,具体到当时几个线程,堵在哪儿。 6、分布式服务一般都是服务治理的,可以看整个调用链的时间图。 7、看监控,监控只是能找到问题 但是不能找到问题原因,什么请求、返回、错误、慢速,这些都是要监控的,告警也要配置好,有问题第一时间知道。 8、假如是网络问题那就找临近的防火墙看对应服务器的并发连接数是否新建连接/半开连接超高,或者有突发流量挤占了资源。同时查对应服务器的磁盘I/O情况和对应那个应用的进程内存占用曲线是否有突发。高并发应用我理解上日志肯定是没法去看的,因为日志量太巨大了。 9、集群或多主机的系统,需要根据监控 看各个主机的CPU 内存 接口IO等,这些能分析一些,如果能得出具体的主机 再分析哪个应用的CPU 内存 异常dump 等。如果涉及到接口机 可以看接口的失败率,应用的话需要排查log了,有时候主机空间不足 内存不足、硬件故障也会导致问题。 10、例子:在银行做安全运维时候,那帮服务器的和网银的一天到晚就说这是网络问题,这绝对网络瘫了。。然后查一圈发现他们一个什么java的问题,说是有个印度小哥把java runtime装了最高版本。但是系统不支持,必须重新装回低版本,就好了。 11、如果是分布式部署多台机器,别的机器没问题,但这台机器有问题。那么更有可能是因为网络或者磁盘导致的,一般这些资源都有zabbox这样的监控。看日志是肯定要的。 12、如果这些机器都是虚拟机的话也有可能是控制器系统的问题。 13、扩展: 阿里,负载均衡和流量清洗系统强大,每年网购高峰期和那几个“节”一过0点的突发流量,很考验负载均衡系统的部署策略。高峰限流是关键,不能冲垮系统,但高峰一限流。。购物车就无法结账了。(异步结帐) 京东,双12 的时候。负载均衡也会出问题。有时候,负载不均衡的情况出现,有针对应用的限流和熔断的,也有针对URL的限流和熔断机制。
另外:DDOS攻击、负载均衡算法没设计好、CDN、DNS等都有可能
ACID,是指在数据库管理系统(DBMS)中事务所具有的四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。
1、原子性: 整个事务中的所有操作,要么全部完成,要么全部不完成.
2、一致性:
在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。比如在一个银行事务中,需要从存储账户扣除款项,然后在支付账户中增加款项。 如果在这个中转的过程发生了失败,那么绝对不能让数据库只执行其中一个账户的操作,因为这样会导致数据处于不一致的状态
3、隔离性:
两个事务的执行是互不干扰的,一个事务不可能看到其他事务运行时,中间某一时刻的数据,比如A事务支持某添加表数据,该事务未commit之前,其他事务是看不到数据的变好的。
4、持久性: 在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾,像springCloud拥有了可用性和分区容错性的优点,缺就缺在一致性的问题,所以出现了springCloud分布式事物的其他支持方案,比如seaTa和Tx_lcn的分布式食物支持。
1 替换DBCP和c3p0,提供了一个高效,功能强大,可扩展性的数据库连接池; 2 可以监控数据库访问性能,druid内置提供了一个功能强大的StatFilter插件,能够详细统计 SQL的执行性能,这对于线上分析数据库访问性能有帮助; 3 数据库密码加密,直接把密码写在配置文件中 ,这是不好的行为,容易导致安全问题,DruidDruiver和DruidDatasource都支持passwordCallback; 4 SQL执行日志,Druid提供了不同的logFIlter,能够支持Common-logging、log4j和JDKLog,可以按需选择相应的LogFilter,监控应用的数据库访问情况; 5 扩展JDBC,如果要多JDBC层有编程的需求,可以提供Druid提供的Filter机制编写JDBC层的扩展插件。
1 缓存穿透:
用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透
解决方案:
(1)布隆过滤器,对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。
或者 把已存在数据的key存在布隆过滤器中。当有新的请求时,先到布隆过滤器中查询是否存在,如果不存在该条数据直接返回;如果存在该条数据再查询缓存查询数据库。
(2)缓存空对象 当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
2 缓存击穿
指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
3 缓存雪崩
缓存层出现了错误,不能正常工作了。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
解决方案:
(1)redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
(2)限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
(3)数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
4 Redis 布隆过滤器简介
Redis中布隆过滤器的数据结构就是一个很大的位数组和几个不一样的无偏哈希函数(能把元素的哈希值算得比较平均,能让元素被哈希到位数组中的位置比较随机)。如下图,A、B、C就是三个这样的哈希函数,分别对“OneMoreStudy”和“万猫学社”这两个元素进行哈希,位数组的对应位置则被设置为1:
向布隆过滤器中添加元素时,会使用多个无偏哈希函数对元素进行哈希,算出一个整数索引值,然后对位数组长度进行取模运算得到一个位置,每个无偏哈希函数都会得到一个不同的位置。再把位数组的这几个位置都设置为1,这就完成了bf.add
命令的操作。
向布隆过滤器查询元素是否存在时,和添加元素一样,也会把哈希的几个位置算出来,然后看看位数组中对应的几个位置是否都为1,只要有一个位为0,那么就说明布隆过滤器里不存在这个元素。如果这几个位置都为1,并不能完全说明这个元素就一定存在其中,有可能这些位置为1是因为其他元素的存在,这就是布隆过滤器会出现误判的原因。
22 软件程序性能问题分析 1 看程序中是否有大量循环,分析能否共用 2 看程序循环中是否包含RPC的调用,分析能否抽离到循环外调用RPC 3 看程序循环中是否包含数据库的增删改查操作,分析能否抽离到循环外进行操作 4 分析程序中业务的增删改单个操作能否进行批量处理 5 分析数据库业务查询是否存在低效能SQL,如查询条件是否建索引,是否有用临时表解决复杂的关联查询 6 idea可使用sonarlint插件进行代码分析
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/157041.html原文链接:https://javaforall.cn