java进阶面试题

JVM相关:

jvm中一次完整的GC流程(从ygc到fgc)是怎样的,重点讲讲对象如何晋升到老年代,几种主要的jvm参数等

新生代GC ygc(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
老年代GC fgc(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别那些对象应放在新生代,那些对象应放在老年代中。为了做到这一点,
虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1.
对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,
可以通过参数 -XX:MaxTenuringThreshold 来设置。

比较重要的jvm参数

 -Xss:每个线程的栈大小
  -Xms:初始堆大小,默认物理内存的1/64
  -Xmx:最大堆大小,默认物理内存的1/4
  -Xmn:新生代大小
  -XX:NewSize:设置新生代初始大小
  -XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
  -XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
 -XX:MetaspaceSize:设置元空间大小
  -XX:MaxMetaspaceSize:设置元空间最大允许大小,默认不受限制,JVM Metaspace会进行动态扩展。
垃圾回收统计信息
  -XX:+PrintGC
  -XX:+PrintGCDetails
  -XX:+PrintGCTimeStamps 
  -Xloggc:filename
收集器设置
  -XX:+UseSerialGC:设置串行收集器
  -XX:+UseParallelGC:设置并行收集器
  -XX:+UseParallelOldGC:老年代使用并行回收收集器
  -XX:+UseParNewGC:在新生代使用并行收集器
  -XX:+UseParalledlOldGC:设置并行老年代收集器
  -XX:+UseConcMarkSweepGC:设置CMS并发收集器
  -XX:+UseG1GC:设置G1收集器
  -XX:ParallelGCThreads:设置用于垃圾回收的线程数
并行收集器设置
  -XX:ParallelGCThreads:设置并行收集器收集时使用的CPU数。并行收集线程数。
  -XX:MaxGCPauseMillis:设置并行收集最大暂停时间
  -XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
CMS收集器设置
  -XX:+UseConcMarkSweepGC:设置CMS并发收集器
  -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
  -XX:ParallelGCThreads:设置并发收集器新生代收集方式为并行收集时,使用的CPU数。并行收集线程数。
  -XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
  -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
  -XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
  -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况
  -XX:ParallelCMSThreads:设定CMS的线程数量
  -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
  -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理	
G1收集器设置 
  -XX:+UseG1GC:使用G1收集器
  -XX:ParallelGCThreads:指定GC工作的线程数量
  -XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
  -XX:GCTimeRatio:吞吐量大小,0-100的整数(默认9),值为n则系统将花费不超过1/(1+n)的时间用于垃圾收集
  -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
  -XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
  -XX:G1MaxNewSizePercent:新生代内存最大空间
  -XX:TargetSurvivorRatio:Survivor填充容量(默认50%)
  -XX:MaxTenuringThreshold:最大任期阈值(默认15)
  -XX:InitiatingHeapOccupancyPercen:老年代占用空间超过整堆比IHOP阈值(默认45%),超过则执行混合收集
  -XX:G1HeapWastePercent:堆废物百分比(默认5%)
  -XX:G1MixedGCCountTarget:参数混合周期的最大总次数(默认8)
  • 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms
1.Serial(串行)收集器收集器
Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条
垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束
新生代采用复制算法,老年代采用标记-整理算法。

2.ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
新生代采用复制算法,老年代采用标记-整理算法。

3.Parallel Scavenge收集器
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户
代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,
可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用复制算法,老年代采用标记-整理算法。
4.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用,
它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
初始标记: 暂停所有的其他线程(STW),并记录下直接与root相连的对象,速度很快 ;
并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象
。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,
这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
并发清除: 开启用户线程,同时GC线程开始对未标记的区域做清扫。
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
对CPU资源敏感(会和服务抢资源);
无法处理浮动垃圾(在java业务程序线程与垃圾收集线程并发执行过程中又产生的垃圾,这种浮动垃圾只能等到下一次gc再清理了);
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

5.G1收集器
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。
这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率。

Serial(串行)收集器收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y7oxgsRE-1596716243928)(7E107621F4D64DA683991F7A2F361164)]

ParNew收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OM4wUXG6-1596716243929)(9B2FF104DDF342909C5AC20ABAA18F6A)]

Parallel Scavenge收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TENaY4aO-1596716243930)(CF8215C1AB444B75A5C94631377F7F35)]

CMS收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2qWKfe0O-1596716243931)(FD85460B1D644F43953A6C37506DD4A7)]

G1收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RWBaA4tJ-1596716243932)(844683718D624C78A90FDF3D65C19A33)]

  • 当出现了内存溢出,你怎么排错
jmap -dump:format=b,file=eureka.hprof 13988
也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./   (路径)
用jstack查找死锁,见如下示例,也可以用jvisualvm查看死锁
jstack找出占用cpu最高的堆栈信息
1,使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如4977
2,按H,获取每个线程的内存情况 
3,找到内存和cpu占用最高的线程tid,比如4977 
4,转为十六进制得到 0x1371 ,此为线程id的十六进制表示
5,执行 jstack 4977|grep -A 10 1371,得到线程堆栈信息中1371这个线程所在行的后面10行 
6,查看对应的堆栈信息找出可能存在问题的代码
  • JVM内存模型的相关知识了解多少
本地方法栈(线程私有): 
登记native方法,在Execution Engine执行时加载本地方法库
程序计数器(线程私有): 
就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码)
,由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
方法区(线程共享):
类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,
所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
Java栈(线程私有):
Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)
不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致
堆(线程共享):
虚拟机启动时创建,用于存放对象实例,几乎所有的对象(包含常量池)都在堆上分配内存,当对象无法再该空间申请到内存时将抛出OutOfMemoryError异常。
同时也是垃圾收集器管理的主要区域。可通过 -Xmx –Xms 参数来分别指定最大堆和最小堆

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r68QCkuP-1596716243933)(650F4E1F0AB84820A0850468993577C8)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c9s0i9XG-1596716243934)(6C1C513C96FF47C0A360B1D055C846CE)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GUKARr4p-1596716243934)(309406ACC0E149B7BF890C573A079A48)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eBbCXxtr-1596716243934)(02A21FCC13194D9C827386567A40C960)]

  • 简单说说你了解的类加载器
启动类加载器:负责加载JRE的核心类库,如jre目标下的rt.jar,charsets.jar等
扩展类加载器:负责加载JRE扩展目录ext中JAR类包
系统类加载器:负责加载ClassPath路径下的类包
用户自定义加载器:负责加载用户自定义路径下的类包
全盘负责委托机制:当一个ClassLoader加载一个类时,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入
双亲委派机制:指先委托父类加载器寻找目标类,在找不到的情况下在自己的路径中查找并载入目标类
双亲委派模式优势
沙箱安全机制:自己写的String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再   加载一次

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-86RlLf03-1596716243935)(A97D1F3D6C804FC8A62A31F683752EFC)]

  • JAVA的反射机制
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,
都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
java通常是先有类再有对象,有对象我就可以调用方法或者属性。反射其实是通过Class对象来调用类里面的方法。通过反射可以调用私有方法和私有属性。

网络:

http1.0和http1.1有什么区别


1、HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理

HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。

HTTP 1.1则支持持久连接Persistent Connection, 并且默认使用persistent connection. 在同一个tcp的连接中可以传送多个HTTP请求和响应. 多个请求和响应可以重叠,多个请求和响应可以同时进行. 更加多的请求头和响应头(比如HTTP1.0没有host的字段).

在1.0时的会话方式:

  1. 建立连接
  2. 发出请求信息
  3. 回送响应信息
  4. 关掉连接

HTTP 1.1的持续连接,也需要增加新的请求头来帮助实现,例如,Connection请求头的值为Keep-Alive时,客户端通知服务器返回本次请求结果后保持连接;Connection请求头的值为close时,客户端通知服务器返回本次请求结果后关闭连接。HTTP 1.1还提供了与身份认证、状态管理和Cache缓存等机制相关的请求头和响应头。

请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。例如:一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。 HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容。

2.HTTP 1.1增加host字段

在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。

HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。此外,服务器应该接受以绝对路径标记的资源请求。

3、100(Continue) Status(节约带宽)

HTTP/1.1加入了一个新的状态码100(Continue)。客户端事先发送一个只带头域的请求,如果服务器因为权限拒绝了请求,就回送响应码401(Unauthorized);如果服务器接收此请求就回送响应码100,客户端就可以继续发送带实体的完整请求了。100 (Continue) 状态代码的使用,允许客户端在发request消息body之前先用request header试探一下server,看server要不要接收request body,再决定要不要发request body。

4、HTTP/1.1中引入了Chunked transfer-coding来解决上面这个问题,发送方将消息分割成若干个任意大小的数据块,每个数据块在发送时都会附上块的长度,最后用一个零长度的块作为消息结束的标志。这种方法允许发送方只缓冲消息的一个片段,避免缓冲整个消息带来的过载。

5、HTTP/1.1在1.0的基础上加入了一些cache的新特性,当缓存对象的Age超过Expire时变为stale对象,cache不需要直接抛弃stale对象,而是与源服务器进行重新激活(revalidation)。


TCP三次握手和四次挥手的流程,为什么断开连接要4次,如果握手只有两次,会出现什么

TCP连接建立过程:
1、客户端向服务器发送SYN(同步序列编号 Synchronize Sequence Numbers),其中seq=x(seq 序号 ack 确认号)。
2、服务器收到SYN报文段后,发送SYN+ACK,其中seq=y,确认号=x+1。
3、客户端收到SYN+ACK报文段后,发送ACK,确认号=y+1。服务器收到ACK报文段后,连接建立。
TCP连接断开过程:
1、客户端TCP模块在收到应用程序的通知后,发送FIN,seq=x。
2、服务器收到FIN报文段,发送ACK,确认号=x+1,并且通知应用程序客户端关闭了连接。客户端收到ACK报文段。
3、服务器端的应用程序通知TCP关闭连接,服务器端TCP发送FIN+ACK,seq=y,确认号=x+1(这里ACK只是一般性的捎带ACK,TCP总是这样,以增强健壮性,反正也不费力气,从原理上说,对连接断开不是必须的)。
4、客户端收到FIN+ACK报文段后,发送ACK,确认号y+1。服务器收到ACK报文段后,连接断开。
为什么不去掉第二步,直接进行第三步呢?
因为,第二步中,服务器端通知应用程序并获得反馈信息可能需要可观的时间,这可能涉及人机交互操作,也可能服务器应用层暂时还不想关闭连接。第二步结束后,服务器还可以继续通过这条连接发送数据给客户端,客户端已经不能发送数据了,但是仍然可以回复ACK。第二步中服务器立即发送确认是为了防止客户端重传FIN报文。

当然也有可能双方同时来连接,这个时候通信双方是对等的,比如BGP peer之间就是对等的,翻译成中文为BGP对等体,也算是贴切。如果双发使用非监听端口发起连接,这样最终需要断开其中一条多余的连接。如果双发都使用自己监听的端口发起连接,那就成了四次握手,正好建立起一条双向连接。

也有可能服务器在不想继续为客户端提供服务了,主动断开连接。不过这种情况对于TCP而言仍然是普通的四次挥手。

还有可能客户端和服务器同时断开连接,仍然是发送四个报文,你可以把它看做是四次挥手,但是它不再是普通的四次挥手。因为这4个报文在时间上有重叠。就TCP状态机的迁移而言,也和普通的四次挥手不同。将不再进入FIN-WAIT-2状态。

*SYN:同步标志
 同步序列编号(Synchronize Sequence Numbers)栏有效。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列编号,该序列编号为TCP连接初始端(一般是客户端)的初始序列编号。在这里,可以把TCP序列编号看作是一个范围从0到4,294,967,295的32位计数器。通过TCP连接交换的数据中每一个字节都经过序列编号。在TCP报头中的序列编号栏包括了TCP分段中第一个字节的序列编号。
*ACK:确认标志
 确认编号(Acknowledgement Number)栏有效。大多数情况下该标志位是置位的。TCP报头内的确认编号栏内包含的确认编号(w+1,Figure-1)为下一个预期的序列编号,同时提示远端系统已经成功接收所有数据。
*RST:复位标志
 复位标志有效。用于复位相应的TCP连接。
*URG:紧急标志
 紧急(The urgent pointer) 标志有效。紧急标志置位,
*PSH:推标志
 该标志置位时,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。
*FIN:结束标志
 带有该标志置位的数据包用来结束一个TCP回话,但对应端口仍处于开放状态,准备接收后续数据

TIME_WAIT和CLOSE_WAIT的区别

TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭。
TIME_WAIT:
是主动关闭链接时形成的,等待2MSL时间,约4分钟。主要是防止最后一个ACK丢失。由于TIME_WAIT 的时间会非常长,因此server端应尽量减少主动关闭连接。
CLOSE_WAIT:
是被动关闭连接是形成的。根据TCP状态机,服务器端收到客户端发送的FIN,则按照TCP实现发送ACK,因此进入CLOSE_WAIT状态。
但如果服务器端不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK,则系统中会存在很多CLOSE_WAIT状态的连接。
此时,可能是系统忙于处理读、写操作,而未将已收到FIN的连接,进行close。此时,recv/read已收到FIN的连接socket,会返回0。

说说你知道的几种HTTP响应码

http状态返回代码 1xx(临时响应)
表示临时响应并需要请求者继续执行操作的状态代码。

http状态返回代码 代码   说明
100   (继续)请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。 
101   (切换协议)请求者已要求服务器切换协议,服务器已确认并准备切换。

http状态返回代码 2xx (成功)
表示成功处理了请求的状态代码。

http状态返回代码 代码   说明
200   (成功)  服务器已成功处理了请求。通常,这表示服务器提供了请求的网页。
201   (已创建)  请求成功并且服务器创建了新的资源。
202   (已接受)  服务器已接受请求,但尚未处理。
203   (非授权信息)  服务器已成功处理了请求,但返回的信息可能来自另一来源。
204   (无内容)  服务器成功处理了请求,但没有返回任何内容。
205   (重置内容)服务器成功处理了请求,但没有返回任何内容。
206   (部分内容)  服务器成功处理了部分 GET 请求。

http状态返回代码 3xx (重定向)
表示要完成请求,需要进一步操作。通常,这些状态代码用来重定向。

http状态返回代码 代码   说明
300   (多种选择)  针对请求,服务器可执行多种操作。服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。
301   (永久移动)  请求的网页已永久移动到新位置。服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
302   (临时移动)  服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
303   (查看其他位置)请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。

304   (未修改)自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。
305   (使用代理)请求者只能使用代理访问请求的网页。如果服务器返回此响应,还表示请求者应使用代理。
307   (临时重定向)  服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。

http状态返回代码 4xx(请求错误)
这些状态代码表示请求可能出错,妨碍了服务器的处理。

http状态返回代码 代码   说明
400   (错误请求)服务器不理解请求的语法。
401   (未授权)请求要求身份验证。对于需要登录的网页,服务器可能返回此响应。
403   (禁止)服务器拒绝请求。
404   (未找到)服务器找不到请求的网页。
405   (方法禁用)禁用请求中指定的方法。
406   (不接受)无法使用请求的内容特性响应请求的网页。
407   (需要代理授权)此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
408   (请求超时)  服务器等候请求时发生超时。
409   (冲突)  服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息。
410   (已删除)  如果请求的资源已永久删除,服务器就会返回此响应。
411   (需要有效长度)服务器不接受不含有效内容长度标头字段的请求。
412   (未满足前提条件)服务器未满足请求者在请求中设置的其中一个前提条件。
413   (请求实体过大)服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
414   (请求的 URI 过长)请求的 URI(通常为网址)过长,服务器无法处理。
415   (不支持的媒体类型)请求的格式不受请求页面的支持。
416   (请求范围不符合要求)如果页面无法提供请求的范围,则服务器会返回此状态代码。
417   (未满足期望值)服务器未满足"期望"请求标头字段的要求。

http状态返回代码 5xx(服务器错误)
这些状态代码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错。

http状态返回代码 代码   说明
500   (服务器内部错误)  服务器遇到错误,无法完成请求。
501   (尚未实施)服务器不具备完成请求的功能。例如,服务器无法识别请求方法时可能会返回此代码。
502   (错误网关)服务器作为网关或代理,从上游服务器收到无效响应。
503   (服务不可用)服务器目前无法使用(由于超载或停机维护)。通常,这只是暂时状态。
504   (网关超时)  服务器作为网关或代理,但是没有及时从上游服务器收到请求。
505   (HTTP 版本不受支持)服务器不支持请求中所用的 HTTP 协议版本。 

一些常见的http状态返回代码为:

200 - 服务器成功返回网页
404 - 请求的网页不存在
503 - 服务不可用

架构设计与分布式:

tomcat如何调优,各种参数的意义

    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="500" minSpareThreads="20" maxSpareThreads="50" maxIdleTime="60000"/>
<Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               URIEncoding="UTF-8"
               connectionTimeout="30000"
               enableLookups="false"
               disableUploadTimeout="false"
               connectionUploadTimeout="150000"
               acceptCount="300"
               keepAliveTimeout="120000"
               maxKeepAliveRequests="1"
               compression="on"
               compressionMinSize="2048"
               compressableMimeType="text/html,text/xml,text/javascript,text/css,text/plain,image/gif,image/jpg,image/png" 
               redirectPort="8443" />

maxThreads :Tomcat 使用线程来处理接收的每个请求,这个值表示 Tomcat 可创建的最大的线程数,默认值是 200

minSpareThreads:最小空闲线程数,Tomcat 启动时的初始化的线程数,表示即使没有人使用也开这么多空线程等待,默认值是 10。

maxSpareThreads:最大备用线程数,一旦创建的线程超过这个值,Tomcat 就会关闭不再需要的 socket 线程。

上边配置的参数,最大线程 500(一般服务器足以),要根据自己的实际情况合理设置,设置越大会耗费内存和 CPU,因为 CPU 疲于线程上下文切换,没有精力提供请求服务了,最小空闲线程数 20,线程最大空闲时间 60 秒,当然允许的最大线程连接数还受制于操作系统的内核参数设置,设置多大要根据自己的需求与环境。当然线程可以配置在“tomcatThreadPool”中,也可以直接配置在“Connector”中,但不可以重复配置。

URIEncoding:指定 Tomcat 容器的 URL 编码格式,语言编码格式这块倒不如其它 WEB 服务器软件配置方便,需要分别指定。

connnectionTimeout: 网络连接超时,单位:毫秒,设置为 0 表示永不超时,这样设置有隐患的。通常可设置为 30000 毫秒,可根据检测实际情况,适当修改。

enableLookups: 是否反查域名,以返回远程主机的主机名,取值为:true 或 false,如果设置为false,则直接返回IP地址,为了提高处理能力,应设置为 false。

disableUploadTimeout:上传时是否使用超时机制。

connectionUploadTimeout:上传超时时间,毕竟文件上传可能需要消耗更多的时间,这个根据你自己的业务需要自己调,以使Servlet有较长的时间来完成它的执行,需要与上一个参数一起配合使用才会生效。

acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可传入连接请求的最大队列长度,超过这个数的请求将不予处理,默认为100个。

keepAliveTimeout:长连接最大保持时间(毫秒),表示在下次请求过来之前,Tomcat 保持该连接多久,默认是使用 connectionTimeout 时间,-1 为不限制超时。

maxKeepAliveRequests:表示在服务器关闭之前,该连接最大支持的请求数。超过该请求数的连接也将被关闭,1表示禁用,-1表示不限制个数,默认100个,一般设置在100~200之间。

compression:是否对响应的数据进行 GZIP 压缩,off:表示禁止压缩;on:表示允许压缩(文本将被压缩)、force:表示所有情况下都进行压缩,默认值为off,压缩数据后可以有效的减少页面的大小,一般可以减小1/3左右,节省带宽。

compressionMinSize:表示压缩响应的最小值,只有当响应报文大小大于这个值的时候才会对报文进行压缩,如果开启了压缩功能,默认值就是2048。

compressableMimeType:压缩类型,指定对哪些类型的文件进行数据压缩。

noCompressionUserAgents=“gozilla, traviata”: 对于以下的浏览器,不启用压缩。

如果已经对代码进行了动静分离,静态页面和图片等数据就不需要 Tomcat 处理了,那么也就不需要配置在 Tomcat 中配置压缩了。

-server:启用jdk的server版本。
-Xms:虚拟机初始化时的最小堆内存。
-Xmx:虚拟机可使用的最大堆内存。 #-Xms与-Xmx设成一样的值,避免JVM因为频繁的GC导致性能大起大落
-XX:PermSize:设置非堆内存初始值,默认是物理内存的1/64。
-XX:MaxNewSize:新生代占整个堆内存的最大值。
-XX:MaxPermSize:Perm(俗称方法区)占整个堆内存的最大值,也称内存最大永久保留区域。
  • 常见的缓存策略有哪些,你们项目中用到了什么缓存系统,如何设计的,Redis的使用要注意什么,持久化方式,内存设置,集群,淘汰策略等
一、什么是缓存
1、Cache是高速缓冲存储器 一种特殊的存储器子系统,其中复制了频繁使用的数据以利于快速访问
2、凡是位于速度相差较大的两种硬件/软件之间的,用于协调两者数据传输速度差异的结构,均可称之为 Cache

二、缓存的分类
1、基于web应用的系统架构图



2、在系统架构的不同层级之间,为了加快访问速度,都可以存在缓存

操作系统磁盘缓存->减少磁盘机械操作
数据库缓存->减少文件系统I/O
应用程序缓存->减少对数据库的查询 
Web服务器缓存->减少应用服务器请求
客户端浏览器缓存->减少对网站的访问
三、操作系统缓存
1、文件系统提供的Disk Cache:操作系统会把经常访问到的文件内容放入到内存当中,由文件系统来管理
2、当应用程序通过文件系统访问磁盘文件的时候,操作系统从Disk Cache当中读取文件内容,加速了文件读取速度
3、Disk Cache由操作系统来自动管理,一般不用人工干预,但应当保证物理内存充足,以便于操作系统可以使用尽量多的内存充当Disk Cache,加速文件读取速度
4、特殊的应用程序对文件系统Disk Cache有很高的要求,会绕开文件系统Disk Cache,直接访问磁盘分区,自己实现Disk 
5、Cache策略

Oracle的raw device(裸设备) – 直接抛弃文件系统
MySQL的InnoDB: innodb_flush_method = O_DIRECT

四、数据库缓存
1、重要性

数据库通常是企业应用系统最核心的部分
数据库保存的数据量通常非常庞大
数据库查询操作通常很频繁,有时还很复杂
以上原因造成数据库查询会引起非常频繁的磁盘I/O读取操作,迫使CPU挂起等待,数据库性能极度低下
2、缓存策略
     a、Query Cache

以SQL作为key值缓存查询结果集
一旦查询涉及的表记录被修改,缓存就会被自动删除
设置合适的Query Cache会极大提高数据库性能
Query Cache并非越大越好,过大的Qquery Cache会浪费内存。
MySQL: query_cache_size= 128M
     b、Data Buffer

data buffer是数据库数据在内存中的容器
data buffer的命中率直接决定了数据库的性能
data buffer越大越好,多多益善
MySQL的InnoDB buffer:innodb_buffer_pool_size = 2G
MySQL建议buffer pool开大到服务器物理内存60-80%
五、应用程序缓存
1、对象缓存

由O/R Mapping框架例如Hibernate提供,透明性访问,细颗粒度缓存数据库查询结果,无需业务代码显式编程,是最省事的缓存策略
当软件结构按照O/R Mapping框架的要求进行针对性设计,使用对象缓存将会极大降低Web系统对于数据库的访问请求
良好的设计数据库结构和利用对象缓存,能够提供极高的性能,对象缓存适合OLTP(联机事务处理)应用
2、查询缓存

对数据库查询结果集进行缓存,类似数据库的Query Cache
适用于一些耗时,但是时效性要求比较低的场景。查询缓存和对象缓存适用的场景不一样,是互为补充的
当查询结果集涉及的表记录被修改以后,需要注意清理缓存
3、页面缓存
     a、作用

针对页面的缓存技术不但可以减轻数据库服务器压力,还可以减轻应用服务器压力
好的页面缓存可以极大提高页面渲染速度
页面缓存的难点在于如何清理过期的缓存
    b、分类
         I、动态页面静态化

利用模板技术将访问过一次的动态页面生成静态html,同时修改页面链接,下一次请求直接访问静态链接页面
动态页面静态化技术的广泛应用于互联网CMS/新闻类Web应用,但也有BBS应用使用该技术,例如Discuz!
无法进行权限验证,无法显示个性化信息
可以使用AJAX请求弥补动态页面静态化的某些缺点
        II、Servlet缓存

针对URL访问返回的页面结果进行缓存,适用于粗粒度的页面缓存,例如新闻发布
可以进行权限的检查
OScache提供了简单的Servlet缓存(通过web.xml中的配置)
也可以自己编程实现Servlet缓存
        III、页面内部缓存

针对动态页面的局部片断内容进行缓存,适用于一些个性化但不经常更新的页面(例如博客)
OSCache提供了简单的页面缓存
可以自行扩展JSP Tag实现页面局部缓存

六、web服务器端缓存

基于代理服务器模式的Web服务器端缓存,如squid/nginx
Web服务器缓存技术被用来实现CDN(内容分发网络 content delivery network)
被国内主流门户网站大量采用
不需要编程,但仅限于新闻发布类网站,页面实时性要求不高
七、基于ajax的浏览器缓存

使用AJAX调用的时候,将数据库在浏览器端缓存
只要不离开当前页面,不刷新当前页面,就可以直接读取缓存数据
只适用于使用AJAX技术的页面
1.使用key值前缀来作命名空间
虽然说Redis支持多个数据库(默认32个,可以配置更多),但是除了默认的0号库以外,其它的都需要通过一个额外请求才能使用。所以用前缀作为命名空间可能会更明智一点。

另外,在使用前缀作为命名空间区隔不同key的时候,最好在程序中使用全局配置来实现,直接在代码里写前缀的做法要严格避免,这样可维护性实在太差了。

2.创建一个类似 ”registry” 的key用于标记key使用情况
为了更好的管理你的key值的使用,比如哪一类key值是属于哪个业务的,你通常会在内部wiki或者什么地方创建一个文档,通过查询这个文档,
我们能够知道Redis中的key都是什么作用。
与之结合,一个推荐的做法是,在Redis里面保存一个registry值,这个值的名字可以类似于 __key_registry__ 这样的,这个key对应的value就是你文档的位置,
这样我们在使用Redis的时候,就能通过直接查询这个值获取到当前Redis的使用情况了。

3.注意垃圾回收
Redis是一个提供持久化功能的内存数据库,如果你不指定上面值的过期时间,并且也不进行定期的清理工作,那么你的Redis内存占用会越来越大,
当有一天它超过了系统可用内存,那么swap上场,离性能陡降的时间就不远了。所以在Redis中保存数据时,一定要预先考虑好数据的生命周期,
这有很多方法可以实现。

比如你可以采用Redis自带的过期时间为你的数据设定过期时间。但是自动过期有一个问题,很有可能导致你还有大量内存可用时,
就让key过期去释放内存,或者是内存已经不足了key还没有过期。

如果你想更精准的控制你的数据过期,你可以用一个ZSET来维护你的数据更新程度,你可以用时间戳作为score值,每次更新操作时更新一下score,
这样你就得到了一个按更新时间排序序列串,你可以轻松地找到最老的数据,并且从最老的数据开始进行删除,一直删除到你的空间足够为止。

4.设计好你的Sharding机制
Redis目前并不支持Sharding,但是当你的数据量超过单机内存时,你不得不考虑Sharding的事(注意:Slave不是用来做Sharding操作的,
只是数据的一个备份和读写分离而已)。

所以你可能需要考虑好数据量大了后的分片问题,比如你可以在只有一台机器的时候就在程序上设定一致性hash机制,
虽然刚开始所有数据都hash到一台机器,但是当你机器越加越多的时候,你就只需要迁移少量的数据就能完成了。

5.不要有个锤子看哪都是钉子
当你使用Redis构建你的服务的时候,一定要记住,你只是找了一个合适的工具来实现你需要的功能。而不是说你在用Redis构建一个服务,
这是很不同的,你把Redis当作你很多工具中的一个,只在合适使用的时候再使用它,在不合适的时候选择其它的方法。

redis持久化机制

RDB快照(snapshot)
在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。
你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。
比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集:
# save 60 1000

AOF(append-only file)
快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化,将修改的每一条指令记录进文件
你可以通过修改配置文件来打开 AOF 功能:
# appendonly yes

从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。
这样的话, 当 Redis 重新启时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。
你可以配置 Redis 多久才将数据 fsync 到磁盘一次。
有三个选项:
每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全。
每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
从不 fsync :将数据交给操作系统来处理。更快,也更不安全的选择。
推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

RDB 和 AOF ,我应该用哪一个?
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。

Redis 4.0 混合持久化
重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。AOF在重写(aof文件里可能有太多没用指令,所以aof会定期根据内存的最新数据生成aof文件)时将重写这一刻之前的内存rdb快照文件的内容和增量的 AOF修改内存数据的命令日志文件存在一起,都写入新的aof文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,原子的覆盖原有的AOF文件,完成新旧两个AOF文件的替换;
AOF根据配置规则在后台自动重写,也可以人为执行命令bgrewriteaof重写AOF。 于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

开启混合持久化:
# aof-use-rdb-preamble yes    
混合持久化aof文件结构

redis内存设置

Redis设置最大占用内存,打开redis配置文件,找到如下段落,设置maxmemory参数,maxmemory是bytes字节类型,注意转换。
一般推荐Redis设置内存为最大物理内存的四分之三 修改如下所示:

# In short... if you have slaves attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for slave
# output buffers (but this is not needed if the policy is 'noeviction').
#
# maxmemory <bytes>
maxmemory 268435456

redis集群

在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异常,则会做主从切换,将某一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,且单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率

下载地址:http://redis.io/download
安装步骤:
# 安装gcc
yum install gcc

# 把下载好的redis-5.0.2.tar.gz放在/usr/local文件夹下,并解压
wget http://download.redis.io/releases/redis-5.0.2.tar.gz
tar xzf redis-5.0.2.tar.gz
cd redis-5.0.2

# 进入到解压好的redis-5.0.2目录下,进行编译与安装
make & make install

# 启动并指定配置文件
src/redis-server redis.conf(注意要使用后台启动,所以修改redis.conf里的daemonize改为yes)

# 验证启动是否成功 
ps -ef | grep redis 

# 进入redis客户端 
/usr/local/redis/bin/redis-cli 

# 退出客户端
quit

# 退出redis服务: 
(1)pkill redis-server 
(2)kill 进程号                       
(3)src/redis-cli shutdown 

redis集群搭建  systemctl stop firewalld.service 关闭防火墙
redis集群需要至少要三个master节点,我们这里搭建三个master节点,并且给每个master再搭建一个slave节点,
总共6个redis节点,这里用三台机器部署6个redis实例,每台机器一主一从,搭建集群的步骤如下:
第一步:在第一台机器的/usr/local下创建文件夹redis-cluster,然后在其下面分别创建2个文件夾如下
(1)mkdir -p /usr/local/redis-cluster
(2)mkdir 8001、 mkdir 8004

第一步:把之前的redis.conf配置文件copy到8001下,修改如下内容:
(1)daemonize yes
(2)port 8001(分别对每个机器的端口号进行设置)
(3)dir /usr/local/redis-cluster/8001/(指定数据文件存放位置,必须要指定不同的目录位置,不然会丢失数据)
(4)cluster-enabled yes(启动集群模式)
(5)cluster-config-file nodes-8001.conf(集群节点信息文件,这里800x最好和port对应上)
(6)cluster-node-timeout 5000
   (7)  # bind 127.0.0.1(去掉bind绑定访问ip信息)
   (8)  protected-mode  no   (关闭保护模式)
 (9)appendonly yes
如果要设置密码需要增加如下配置:
 (10)requirepass zhuge     (设置redis访问密码)
 (11)masterauth zhuge      (设置集群节点间访问密码,跟上面一致)

第三步:把修改后的配置文件,copy到8002,修改第2、3、5项里的端口号,可以用批量替换:
:%s/源字符串/目的字符串/g 

第四步:另外两台机器也需要做上面几步操作,第二台机器用8002和8005,第三台机器用8003和8006

第五步:分别启动6个redis实例,然后检查是否启动成功
(1)/usr/local/redis-5.0.2/src/redis-server /usr/local/redis-cluster/800*/redis.conf
(2)ps -ef | grep redis 查看是否启动成功

第六步:用redis-cli创建整个redis集群(redis5以前的版本集群是依靠ruby脚本redis-trib.rb实现)
(1)/usr/local/redis-5.0.2/src/redis-cli -a zhuge --cluster create --cluster-replicas 1
192.168.0.61:8001 192.168.0.62:8002 192.168.0.63:8003 
192.168.0.61:8004 192.168.0.62:8005 192.168.0.63:8006 
代表为每个创建的主服务器节点创建一个从服务器节点

第七步:验证集群:
(1)连接任意一个客户端即可:./redis-cli -c -h -p (-a访问服务端密码,-c表示集群模式,指定ip地址和端口号
)如:/usr/local/redis-5.0.2/src/redis-cli -a zhuge -c -h 192.168.0.61 -p 800*
(2)进行验证: cluster info(查看集群信息)、cluster nodes(查看节点列表)
(3)进行数据操作验证
(4)关闭集群则需要逐个进行关闭,使用命令:
/usr/local/redis/bin/redis-cli -a zhuge -c -h 192.168.0.60 -p 800* shutdown

redis淘汰策略

当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap)。交换会让 Redis 的性能急剧下降,
对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。
在生产环境中我们是不允许 Redis 出现交换行为的,为了限制最大使用内存,Redis 提供了配置参数 maxmemory 来限制内存超出期望大小。 
当实际内存超出 maxmemory 时,Redis 提供了几种可选策略 (maxmemory-policy) 来让用户自己决定该如何腾出新的空间以继续提供读写服务。

noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。
volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。
allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。

volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。如果你只是拿 Redis 做缓存,
那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,
那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰

如何防止缓存雪崩

在正常情况下,一旦miss就去查DB是没有问题的。但是如果大量缓存集中在某一时间段失效,将导致所有请求都去访问后端的DB,DB压力会很大,甚至被压垮,造成雪崩。
场景一
电商系统的某个大促活动的首页,首页有很多新上架的商品。活动开始前,技术团队对缓存做了预热,由于是脚本化预热,
这些商品的Cache数据几乎都是同时创建好,并且过期时间都设置为5分钟。这就会导致这大量的商品数据在5分钟后集中失效。

场景二
cache系统刚上线(或者刚从崩溃中恢复过来),没有对cache进行预热。cache中什么也没有,这时瞬时大流量过来也会产生雪崩。

解决思路:
cache过期时间均匀分布
针对上面的场景一,可以对cache的过期时间做一个均匀分布的处理。比如1-5分钟内,随机分布。
排斥锁
,可以考虑使用排斥锁(mutex)。即第一个线程过来读取cache,发现没有,就去访问DB。
后续线程再过来就需要等待第一个线程读取DB成功,cache里的value变得可用,后续线程返回新的value。伪代码如下:
使用了分布式锁,这当然是考虑到在分布式环境下,读请求会落到集群中的不同应用服务机器上。分布式锁可以选用zookeeper或基于redis的setnx这类原子性操作来实现
加锁时需要用到经典的double-check lock。

排斥锁方案对缓存过期是零容忍的:cache一旦过期,后续所有读操作就必须返回新的value。如果我们稍微放宽点限制:
在cache过期时间T到达后,允许短时间内部分读请求返回旧值,我们就能提出兼顾吞吐率的方案。实际上既然用了cache,
系统就默许了容忍cache和DB的数据短时间的不一致。
限制放宽后,下面我们提出一个优化思路。时间T到达后,cache中的key和value不会被清掉,而只是被标记为过期(逻辑上过期,物理上不过期)
,然后程序异步去刷新cache。而后续部分读线程在前面的线程刷新cache成功之前,暂时获取cache中旧的value返回。一旦cache刷新成功,
后续所有线程就能直接获取cache中新的value。可以看到,这个思路很大程度上减少了排斥锁的使用(虽然并没有完全消除排斥锁)。 
signKey:
既然存放数据的cache不会被清掉,那么就通过别的key也就是代码中的signKey来标记过期。signKey的过期时间一到,就代表实际key逻辑过期。
异步刷新cache时也用到了排斥锁:
这是因为同一时间多个读线程进来都发现signKey已过期,就都要去异步刷新cache,所以这里有必要加上排斥锁。
但注意到isExpired方法中(35-41行),signKey一旦过期,马上把过期时间延后1分钟,这是为了让后续进来的线程先返回旧的value。
这样只有极少一部分读线程去刷新cache。因此需要加排斥锁的线程也并不多。

分布式集群下如何做到唯一序列号

1、通过应用程序生成一个GUID,然后和数据一起插入切分后的集群。
优点是维护简单,实现也容易。缺点是应用的计算成本较大,且GUID的长度比较长,占用数据库存储空间较大,涉及到应用的开发。
说明:主要优势是简单,缺点是浪费存储空间。
2、通过独立的应用程序事先在数据库中生成一系列唯一的 ID,各应用程序通过接口或者自己去读取再和数据一起插入到切分后的集群中。
优点是全局唯一主键简单,维护相对容易。
缺点是实现复杂,需要应用开发。
说明:ID表要频繁查和频繁更新,插入数据时,影响性能。
3、通过【中心数据库服务器】利用数据库自身的自增类型(如 MySQL的 auto_increment 字段),或者自增对象(如 Oracle 的 Sequence)等先生成一个唯一 ID 再和数据一起插入切分后的集群。优点是?好像没有特别明显的优点。缺点是实现较为复杂,且整体可用性维系在这个中心数据库服务器上,一旦这里crash 了,所有的集群都无法进行插入操作,涉及到应用开发。
说明:不推荐。
4、通过集群编号加集群内的自增(auto_increment类型)【两个字段】共同组成唯一主键。优点是实现简单,维护也比较简单,对应用透明。
缺点是引用关联操作相对比较复杂,需要两个字段,主键占用空间较大,在使用 InnoDB 的时候这一点的副作用很明显。
说明:虽然是两个字段,但是这方式存储空间最小,仅仅多了一个smallint两个字节。
5、通过设置每个集群中自增 ID 起始点(auto_increment_offset),将各个集群的ID进行绝对的分段来实现全局唯一。当遇到某个集群数据增长过快后,通过命令调整下一个 ID 起始位置跳过可能存在的冲突。优点是实现简单,且比较容易根据 ID 大小直接判断出数据处在哪个集群,对应用透明。缺点是维护相对较复杂,需要高度关注各个集群 ID 增长状况。
说明:段满了,调整太麻烦。
6、通过设置每个集群中自增 ID 起始点(auto_increment_offset)以及 ID 自增步长(auto_increment_increment),让目前每个集群的起始点错开 1,步长选择大于将来基本不可能达到的切分集群数,达到将 ID 相对分段的效果来满足全局唯一的效果。优点是实现简单,后期维护简单,对应用透明。缺点是第一次设置相对较为复杂。
说明:避免重合需要多种方案结合

设计一个秒杀系统,30分钟没付款就自动关闭交易

Redis是一个分布式缓存系统,支持多种数据结构,我们可以利用Redis轻松实现一个强大的秒杀系统。

我们可以采用Redis 最简单的key-value数据结构,用一个原子类型的变量值(AtomicInteger)作为key,把用户id作为value,
库存数量便是原子变量的最大值。对于每个用户的秒杀,我们使用 RPUSH key value插入秒杀请求, 当插入的秒杀请求数达到上限时,停止所有后续插入。
然后我们可以在台启动多个工作线程,使用 LPOP key 读取秒杀成功者的用户id,然后再操作数据库做最终的下订单减库存操作。
当然,上面Redis也可以替换成消息中间件如ActiveMQ、RabbitMQ等,
也可以将缓存和消息中间件 组合起来,缓存系统负责接收记录用户请求,消息中间件负责将缓存中的请求同步到数据库。

如何做一个分布式锁

使用redis的SETNX
如返回1,则该客户端获得锁,把 lock.foo 的键值设置为时间值表示该键已被锁定,该客户端最后可以通过 DEL lock.foo 来释放该锁。
如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。

用过哪些MQ,怎么用的,和其他mq比较有什么优缺点,MQ的连接是线程安全的吗

RabbitMQ
RabbitMQ 2007年发布,是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。

主要特性:

可靠性: 提供了多种技术可以让你在性能和可靠性之间进行权衡。这些技术包括持久性机制、投递确认、发布者证实和高可用性机制;
灵活的路由: 消息在到达队列前是通过交换机进行路由的。RabbitMQ为典型的路由逻辑提供了多种内置交换机类型。如果你有更复杂的路由需求,
可以将这些交换机组合起来使用,你甚至可以实现自己的交换机类型,并且当做RabbitMQ的插件来使用;
消息集群:在相同局域网中的多个RabbitMQ服务器可以聚合在一起,作为一个独立的逻辑代理来使用;
队列高可用:队列可以在集群中的机器上进行镜像,以确保在硬件问题下还保证消息安全;
多种协议的支持:支持多种消息队列协议;
服务器端用Erlang语言编写,支持只要是你能想到的所有编程语言;
管理界面: RabbitMQ有一个易用的用户界面,使得用户可以监控和管理消息Broker的许多方面;
跟踪机制:如果消息异常,RabbitMQ提供消息跟踪机制,使用者可以找出发生了什么;
插件机制:提供了许多插件,来从多方面进行扩展,也可以编写自己的插件;
使用RabbitMQ需要:
ErLang语言包
RabbitMQ安装包
RabbitMQ可以运行在Erlang语言所支持的平台之上:
Solaris
BSD
Linux
MacOSX
TRU64
Windows NT/2000/XP/Vista/Windows 7/Windows 8
Windows Server 2003/2008/2012
Windows 95, 98
VxWorks
优点:
由于erlang语言的特性,mq 性能较好,高并发;
健壮、稳定、易用、跨平台、支持多种语言、文档齐全;
有消息确认机制和持久化机制,可靠性高;
高度可定制的路由;
管理界面较丰富,在互联网公司也有较大规模的应用;
社区活跃度高;
缺点:
尽管结合erlang语言本身的并发优势,性能较好,但是不利于做二次开发和维护;
实现了代理架构,意味着消息在发送到客户端之前可以在中央节点上排队。此特性使得RabbitMQ易于使用和部署,但是使得其运行速度较慢,
因为中央节点增加了延迟,消息封装后也比较大;
需要学习比较复杂的接口和协议,学习和维护成本较高;
RocketMQ
RocketMQ出自 阿里公司的开源产品,用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一些改进,消息可靠性上比 Kafka 更好。RocketMQ在阿里集团被广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog分发等场景。

主要特性:

是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点;
Producer、Consumer、队列都可以分布式;
Producer向一些队列轮流发送消息,队列集合称为Topic,Consumer如果做广播消费,则一个consumer实例消费这个Topic对应的所有队列,如果做集群消费,则多个Consumer实例平均消费这个topic对应的队列集合;
能够保证严格的消息顺序;
提供丰富的消息拉取模式;
高效的订阅者水平扩展能力;
实时的消息订阅机制;
亿级消息堆积能力;
较少的依赖;
使用RocketMQ需要:

Java JDK
安装git、Maven
RocketMQ安装包
RocketMQ可以运行在Java语言所支持的平台之上。

优点:

单机支持 1 万以上持久化队列
RocketMQ 的所有消息都是持久化的,先写入系统 PAGECACHE,然后刷盘,可以保证内存与磁盘都有一份数据,
访问时,直接从内存读取。
模型简单,接口易用(JMS 的接口很多场合并不太实用);
性能非常好,可以大量堆积消息在broker中;
支持多种消费,包括集群消费、广播消费等。
各个环节分布式扩展设计,主从HA;
开发度较活跃,版本更新很快。
缺点:

支持的客户端语言不多,目前是java及c++,其中c++不成熟;
RocketMQ社区关注度及成熟度也不及前两者;
没有web管理界面,提供了一个CLI(命令行界面)管理工具带来查询、管理和诊断各种问题;
没有在 mq 核心中去实现JMS等接口;
Kafka
Apache Kafka是一个分布式消息发布订阅系统。它最初由LinkedIn公司基于独特的设计实现为一个分布式的提交日志系统( a distributed commit log),,之后成为Apache项目的一部分。Kafka系统快速、可扩展并且可持久化。它的分区特性,可复制和可容错都是其不错的特性。

主要特性:

快速持久化,可以在O(1)的系统开销下进行消息持久化;
高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;
.完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡;
支持同步和异步复制两种HA;
支持数据批量发送和拉取;
zero-copy:减少IO操作步骤;
数据迁移、扩容对用户透明;
无需停机即可扩展机器;
其他特性:严格的消息顺序、丰富的消息拉取模型、高效订阅者水平扩展、实时的消息订阅、亿级的消息堆积能力、定期删除机制;
使用Kafka需要:

Java JDK
Kafka安装包
优点:

客户端语言丰富,支持java、.net、php、ruby、python、go等多种语言;
性能卓越,单机写入TPS约在百万条/秒,消息大小10个字节;
提供完全分布式架构, 并有replica机制, 拥有较高的可用性和可靠性, 理论上支持消息无限堆积;
支持批量操作;
消费者采用Pull方式获取消息, 消息有序, 通过控制能够保证所有消息被消费且仅被消费一次;
有优秀的第三方Kafka Web管理界面Kafka-Manager;
在日志领域比较成熟,被多家公司和多个开源项目使用;
缺点:

Kafka单机超过64个队列/分区,Load会发生明显的飙高现象,队列越多,load越高,发送消息响应时间变长
使用短轮询方式,实时性取决于轮询间隔时间;
消费失败不支持重试;
支持消息顺序,但是一台代理宕机后,就会产生消息乱序;
社区更新较慢;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtaIkoZN-1596716243935)(682FE27021344A52BCAA2CF2FCDE086B)]

  • MQ系统的数据如何保证不丢失
主要ack机制。包括副本集的建立
  • 分布式事务的原理,如何使用分布式事务
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TEuurtVA-1596716243936)(EB88EBB6EFA54CB782B2BBEA839FB9DA)]
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rlaQtJXj-1596716243936)(42B4D10EEFA24411B592CD2500D094D1)]
  • 什么是一致性hash

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ozwR5AeH-1596716243937)(15CDCADC66F7415B8154445D12E50975)]

此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X !一般的,在一致性Hash算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。

综上所述,一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
--------------------- 
作者:Java后端技术 
来源:CSDN 
原文:https://blog.csdn.net/bntX2jSQfEHy7/article/details/79549368 
版权声明:本文为博主原创文章,转载请附上博文链接!

说说你知道的几种HASH算法,简单的也可以

  1. 加法Hash;
所谓的加法Hash就是把输入元素一个一个的加起来构成最后的结果。标准的加法Hash的构造如下:

 static int additiveHash(String key, int prime)
 {
  int hash, i;
  for (hash = key.length(), i = 0; i < key.length(); i++)
   hash += key.charAt(i);
  return (hash % prime);
 }
 这里的prime是任意的质数,看得出,结果的值域为[0,prime-1]。
  1. 位运算Hash;
这类型Hash函数通过利用各种位运算(常见的是移位和异或)来充分的混合输入元素。比如,标准的旋转Hash的构造如下:

 static int rotatingHash(String key, int prime)
 {
   int hash, i;
   for (hash=key.length(), i=0; i<key.length(); ++i)
     hash = (hash<<4)^(hash>>28)^key.charAt(i);
   return (hash % prime);
 }

先移位,然后再进行各种位运算是这种类型Hash函数的主要特点。比如,以上的那段计算hash的代码还可以有如下几种变形:
1.     hash = (hash<<5)^(hash>>27)^key.charAt(i);
2.     hash += key.charAt(i);
        hash += (hash << 10);
        hash ^= (hash >> 6);
3.     if((i&1) == 0)
        {
         hash ^= (hash<<7) ^ key.charAt(i) ^ (hash>>3);
        }
        else
        {
         hash ^= ~((hash<<11) ^ key.charAt(i) ^ (hash >>5));
        }
4.     hash += (hash<<5) + key.charAt(i);
5.     hash = key.charAt(i) + (hash<<6) + (hash>>16) – hash;
6.     hash ^= ((hash<<5) + key.charAt(i) + (hash>>2));
  1. 乘法Hash;
这种类型的Hash函数利用了乘法的不相关性(乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算法,虽然这种算法效果并不好)。比如,

 static int bernstein(String key)
 {
   int hash = 0;
   int i;
   for (i=0; i<key.length(); ++i) hash = 33*hash + key.charAt(i);
   return hash;
 }

jdk5.0里面的String类的hashCode()方法也使用乘法Hash。不过,它使用的乘数是31。推荐的乘数还有:131, 1313, 13131, 131313等等。

使用这种方式的著名Hash函数还有:
 //  32位FNV算法
 int M_SHIFT = 0;
    public int FNVHash(byte[] data)
    {
        int hash = (int)2166136261L;
        for(byte b : data)
            hash = (hash * 16777619) ^ b;
        if (M_SHIFT == 0)
            return hash;
        return (hash ^ (hash >> M_SHIFT)) & M_MASK;
}

以及改进的FNV算法:
    public static int FNVHash1(String data)
    {
        final int p = 16777619;
        int hash = (int)2166136261L;
        for(int i=0;i<data.length();i++)
            hash = (hash ^ data.charAt(i)) * p;
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;
        return hash;
}

除了乘以一个固定的数,常见的还有乘以一个不断改变的数,比如:
    static int RSHash(String str)
    {
        int b    = 378551;
        int a    = 63689;
        int hash = 0;

       for(int i = 0; i < str.length(); i++)
       {
          hash = hash * a + str.charAt(i);
          a    = a * b;
       }
       return (hash & 0x7FFFFFFF);
}

虽然Adler32算法的应用没有CRC32广泛,不过,它可能是乘法Hash里面最有名的一个了。关于它的介绍,大家可以去看RFC 1950规范。
  1. 除法Hash;
除法和乘法一样,同样具有表面上看起来的不相关性。不过,因为除法太慢,这种方式几乎找不到真正的应用。需要注意的是,
我们在前面看到的hash的 结果除以一个prime的目的只是为了保证结果的范围。如果你不需要它限制一个范围的话,
可以使用如下的代码替代”hash%prime”: hash = hash ^ (hash>>10) ^ (hash>>20)。
  1. 查表Hash;
查表Hash最有名的例子莫过于CRC系列算法。虽然CRC系列算法本身并不是查表,但是,查表是它的一种最快的实现方式。
查表Hash中有名的例子有:Universal Hashing和Zobrist Hashing。他们的表格都是随机生成的。
  1. 混合Hash;
混合Hash算法利用了以上各种方式。各种常见的Hash算法,比如MD5、
Tiger都属于这个范围。它们一般很少在面向查找的Hash函数里面使用。


什么是paxos算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zX8gDVEq-1596716243937)(FF2B57B75F5B4C0999310CDB68E8F4FB)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3tNena3-1596716243938)(5468E14810204449B4CF00ED4583619B)]

  • redis和memcached 的内存管理的区别等等
Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。
内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。
性能对比:由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。

1、数据类型支持不同

与Memcached仅支持简单的key-value结构的数据记录不同,Redis支持的数据类型要丰富得多。
最为常用的数据类型主要由五种:String、Hash、List、Set和Sorted Set。Redis内部使用一个redisObject对象来表示所有的key和value。
redisObject最主要的信息如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8HnvgIVR-1596716243938)(48B893A441D1460CA2FAF0EB20059DAD)]

type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的存储方式,
比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,
如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,
当然前提是这个字符串本身可以用数值表示,比如:”123″ “456”这样的字符串。只有打开了Redis的虚拟内存功能,
vm字段字段才会真正的分配内存,该功能默认是关闭状态的。

1)String

常用命令:set/get/decr/incr/mget等;
应用场景:String是最常用的一种数据类型,普通的key/value存储都可以归为此类;
实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。

2)Hash

常用命令:hget/hset/hgetall等
应用场景:我们要存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或者年龄或者生日;
实现方式:Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。如图所示,Key是用户ID, value是一个Map。
这个Map的key是成员的属性名,value是属性值。
这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 
也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据。
当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,
而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
hash

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KiLLrG9A-1596716243939)(7FAA6B030DC64854BE49EAFA86F36A1F)]
3)List

常用命令:lpush/rpush/lpop/rpop/lrange等;
应用场景:Redis list的应用场景非常多,也是Redis最重要的数据结构之一,
比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现;
实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,
不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

4)Set

常用命令:sadd/spop/smembers/sunion等;
应用场景:Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,
set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的;
实现方式:set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

5)Sorted Set

常用命令:zadd/zrange/zrem/zcard等;
应用场景:Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,
并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,
那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。
实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,
排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

2、内存管理机制不同

在Redis中,并不是所有的数据都一直存储在内存中的。这是和Memcached相比一个最大的区别。当物理内存用完时,Redis可以将一些很久没用到的value交换到磁盘。Redis只会缓存所有的key的信息,如果Redis发现内存的使用量超过了某一个阀值,将触发swap的操作,Redis根据“swappability = age*log(size_in_memory)”计算出哪些key对应的value需要swap到磁盘。然后再将这些key对应的value持久化到磁盘中,同时在内存中清除。这种特性使得Redis可以保持超过其机器本身内存大小的数据。当然,机器本身的内存必须要能够保持所有的key,毕竟这些数据是不会进行swap操作的。同时由于Redis将内存中的数据swap到磁盘中的时候,提供服务的主线程和进行swap操作的子线程会共享这部分内存,所以如果更新需要swap的数据,Redis将阻塞这个操作,直到子线程完成swap操作后才可以进行修改。当从Redis中读取数据的时候,如果读取的key对应的value不在内存中,那么Redis就需要从swap文件中加载相应数据,然后再返回给请求方。 这里就存在一个I/O线程池的问题。在默认的情况下,Redis会出现阻塞,即完成所有的swap文件加载后才会相应。这种策略在客户端的数量较小,进行批量操作的时候比较合适。但是如果将Redis应用在一个大型的网站应用程序中,这显然是无法满足大并发的情况的。所以Redis运行我们设置I/O线程池的大小,对需要从swap文件中加载相应数据的读取请求进行并发操作,减少阻塞的时间。
对于像Redis和Memcached这种基于内存的数据库系统来说,内存管理的效率高低是影响系统性能的关键因素。传统C语言中的malloc/free函数是最常用的分配和释放内存的方法,但是这种方法存在着很大的缺陷:首先,对于开发人员来说不匹配的malloc和free容易造成内存泄露;其次频繁调用会造成大量内存碎片无法回收重新利用,降低内存利用率;最后作为系统调用,其系统开销远远大于一般函数调用。所以,为了提高内存的管理效率,高效的内存管理方案都不会直接使用malloc/free调用。Redis和Memcached均使用了自身设计的内存管理机制,但是实现方法存在很大的差异,下面将会对两者的内存管理机制分别进行介绍。
Memcached默认使用Slab Allocation机制管理内存,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。Slab Allocation机制只为存储外部数据而设计,也就是说所有的key-value数据都存储在Slab Allocation系统里,而Memcached的其它内存请求则通过普通的malloc/free来申请,因为这些请求的数量和频率决定了它们不会对整个系统的性能造成影响Slab Allocation的原理相当简单。 如图所示,它首先从操作系统申请一大块内存,并将其分割成各种尺寸的块Chunk,并把尺寸相同的块分成组Slab Class。其中,Chunk就是用来存储key-value数据的最小单位。每个Slab Class的大小,可以在Memcached启动的时候通过制定Growth Factor来控制。假定图中Growth Factor的取值为1.25,如果第一组Chunk的大小为88个字节,第二组Chunk的大小就为112个字节,依此类推。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b232AuBJ-1596716243939)(4F43625B08E64CB484094C425A6AAD20)]

当Memcached接收到客户端发送过来的数据时首先会根据收到数据的大小选择一个最合适的Slab Class,然后通过查询Memcached保存着的该Slab Class内空闲Chunk的列表就可以找到一个可用于存储数据的Chunk。当一条数据库过期或者丢弃时,该记录所占用的Chunk就可以回收,重新添加到空闲列表中。从以上过程我们可以看出Memcached的内存管理制效率高,而且不会造成内存碎片,但是它最大的缺点就是会导致空间浪费。因为每个Chunk都分配了特定长度的内存空间,所以变长数据无法充分利用这些空间。如图 所示,将100个字节的数据缓存到128个字节的Chunk中,剩余的28个字节就浪费掉了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PkRMoyvT-1596716243939)(280F95A2B1CA46D9A1ACADEA4065AE81)]

Redis的内存管理主要通过源码中zmalloc.h和zmalloc.c两个文件来实现的。Redis为了方便内存的管理,在分配一块内存之后,会将这块内存的大小存入内存块的头部。如图所示,real_ptr是redis调用malloc后返回的指针。redis将内存块的大小size存入头部,size所占据的内存大小是已知的,为size_t类型的长度,然后返回ret_ptr。当需要释放内存的时候,ret_ptr被传给内存管理程序。通过ret_ptr,程序可以很容易的算出real_ptr的值,然后将real_ptr传给free释放内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zFqry99f-1596716243940)(1D50DEAAB49B45E183CC443F94EB9DFB)]

Redis通过定义一个数组来记录所有的内存分配情况,这个数组的长度为ZMALLOC_MAX_ALLOC_STAT。数组的每一个元素代表当前程序所分配的内存块的个数,且内存块的大小为该元素的下标。在源码中,这个数组为zmalloc_allocations。zmalloc_allocations[16]代表已经分配的长度为16bytes的内存块的个数。zmalloc.c中有一个静态变量used_memory用来记录当前分配的内存总大小。所以,总的来看,Redis采用的是包装的mallc/free,相较于Memcached的内存管理方法来说,要简单很多。

3、数据持久化支持

Redis虽然是基于内存的存储系统,但是它本身是支持内存数据的持久化的,而且提供两种主要的持久化策略:RDB快照和AOF日志。而memcached是不支持数据持久化操作的。

1)RDB快照

Redis支持将当前数据的快照存成一个数据文件的持久化机制,即RDB快照。但是一个持续写入的数据库如何生成快照呢?Redis借助了fork命令的copy on write机制。在生成快照时,将当前进程fork出一个子进程,然后在子进程中循环所有的数据,将数据写成为RDB文件。我们可以通过Redis的save指令来配置RDB快照生成的时机,比如配置10分钟就生成快照,也可以配置有1000次写入就生成快照,也可以多个规则一起实施。这些规则的定义就在Redis的配置文件中,你也可以通过Redis的CONFIG SET命令在Redis运行时设置规则,不需要重启Redis。
Redis的RDB文件不会坏掉,因为其写操作是在一个新进程中进行的,当生成一个新的RDB文件时,Redis生成的子进程会先将数据写到一个临时文件中,然后通过原子性rename系统调用将临时文件重命名为RDB文件,这样在任何时候出现故障,Redis的RDB文件都总是可用的。同时,Redis的RDB文件也是Redis主从同步内部实现中的一环。RDB有他的不足,就是一旦数据库出现问题,那么我们的RDB文件中保存的数据并不是全新的,从上次RDB文件生成到Redis停机这段时间的数据全部丢掉了。在某些业务下,这是可以忍受的。

2)AOF日志

AOF日志的全称是append only file,它是一个追加写入的日志文件。与一般数据库的binlog不同的是,AOF文件是可识别的纯文本,它的内容就是一个个的Redis标准命令。只有那些会导致数据发生修改的命令才会追加到AOF文件。每一条修改数据的命令都生成一条日志,AOF文件会越来越大,所以Redis又提供了一个功能,叫做AOF rewrite。其功能就是重新生成一份AOF文件,新的AOF文件中一条记录的操作只会有一次,而不像一份老文件那样,可能记录了对同一个值的多次操作。其生成过程和RDB类似,也是fork一个进程,直接遍历数据,写入新的AOF临时文件。在写入新文件的过程中,所有的写操作日志还是会写到原来老的AOF文件中,同时还会记录在内存缓冲区中。当重完操作完成后,会将所有缓冲区中的日志一次性写入到临时文件中。然后调用原子性的rename命令用新的AOF文件取代老的AOF文件。
AOF是一个写文件操作,其目的是将操作日志写到磁盘上,所以它也同样会遇到我们上面说的写操作的流程。在Redis中对AOF调用write写入后,通过appendfsync选项来控制调用fsync将其写到磁盘上的时间,下面appendfsync的三个设置项,安全强度逐渐变强。
appendfsync no 
当设置appendfsync为no的时候,Redis不会主动调用fsync去将AOF日志内容同步到磁盘,所以这一切就完全依赖于操作系统的调试了。
对大多数Linux操作系统,是每30秒进行一次fsync,将缓冲区中的数据写到磁盘上。
appendfsync everysec 
当设置appendfsync为everysec的时候,Redis会默认每隔一秒进行一次fsync调用,将缓冲区中的数据写到磁盘。
但是当这一次的fsync调用时长超过1秒时。Redis会采取延迟fsync的策略,再等一秒钟。也就是在两秒后再进行fsync,
这一次的fsync就不管会执行多长时间都会进行。这时候由于在fsync时文件描述符会被阻塞,所以当前的写操作就会阻塞。
所以结论就是,在绝大多数情况下,Redis会每隔一秒进行一次fsync。在最坏的情况下,两秒钟会进行一次fsync操作。
这一操作在大多数数据库系统中被称为group commit,就是组合多次写操作的数据,一次性将日志写到磁盘。
appednfsync always 
当设置appendfsync为always时,每一次写操作都会调用一次fsync,这时数据是最安全的,当然,由于每次都会执行fsync,所以其性能也会受到影响。
对于一般性的业务需求,建议使用RDB的方式进行持久化,原因是RDB的开销并相比AOF日志要低很多,对于那些无法忍数据丢失的应用,建议使用AOF日志。

4、集群管理的不同

Memcached是全内存的数据缓冲系统,Redis虽然支持数据的持久化,但是全内存毕竟才是其高性能的本质。作为基于内存的存储系统来说,机器物理内存的大小就是系统能够容纳的最大数据量。如果需要处理的数据量超过了单台机器的物理内存大小,就需要构建分布式集群来扩展存储能力。
Memcached本身并不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现Memcached的分布式存储。下图给出了Memcached的分布式存储实现架构。当客户端向Memcached集群发送数据之前,首先会通过内置的分布式算法计算出该条数据的目标节点,然后数据会直接发送到该节点上存储。但客户端查询数据时,同样要计算出查询数据所在的节点,然后直接向该节点发送查询请求以获取数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZgW9ZgiD-1596716243940)(272E15E8176143F484D2207C8D8E5A91)]

相较于Memcached只能采用客户端实现分布式存储,Redis更偏向于在服务器端构建分布式存储。最新版本的Redis已经支持了分布式存储功能。Redis Cluster是一个实现了分布式且允许单点故障的Redis高级版本,它没有中心节点,具有线性可伸缩的功能。下图给出Redis Cluster的分布式存储架构,其中节点与节点之间通过二进制协议进行通信,节点与客户端之间通过ascii协议进行通信。在数据的放置策略上,Redis Cluster将整个key的数值域分成4096个哈希槽,每个节点上可以存储一个或多个哈希槽,也就是说当前Redis Cluster支持的最大节点数就是4096。Redis Cluster使用的分布式算法也很简单:crc16( key ) % HASH_SLOTS_NUMBER。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FHfNaC7b-1596716243941)(BA4FAA7848A647BDA748D0CE54ED4F0D)]

为了保证单点故障下的数据可用性,Redis Cluster引入了Master节点和Slave节点。在Redis Cluster中,每个Master节点都会有对应的两个用于冗余的Slave节点。这样在整个集群中,任意两个节点的宕机都不会导致数据的不可用。当Master节点退出后,集群会自动选择一个Slave节点成为新的Master节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jWOu3ypo-1596716243941)(5D24437F33A3436AB1ABBBCD502FB620)]

java面试直通群

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

happyProgrammerWANG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值