在前后端分离的情况下,假如在springboot没配置cors的情况下,就会出现这样的问题。
Access to XMLHttpRequest at 'http://localhost:8080/api' from origin 'http://localhost:63342' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
那很简单,只要重写一下WebMvcConfigurer
下的addCorsMappings
方法就可以了,真就这么容易就好了,草
@Overridepublic void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE") .allowCredentials(true);}
根据以往的经验,一般请求链如下,通过浏览器然后通过nginx反向代理到springboot应用,按照从前来一直都没问题
web browser --> nginx --> springboot
但是我自己写了一个ajax在本地测试,发现是没有问题的,但是推送到测试服务器供前端测试的时候却不行
$.ajax({ method: "POST", url: "http://localhost:8080/api", }).then(function (value) { console.log(value); })
然后前端那边提醒说到有自定义的请求头,然后我再测试,发现我本地也不行了
$.ajax({ method: "POST", url: "http://localhost:8080/api", headers:{ "token":"abc" } }).then(function (value) { console.log(value); })
然后再修改springboot里的配置,在通过上面的ajax测试,发现成了,然后高兴推送到测试服务器,结果我再测试发现依然不行
@Overridepublic void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE") .allowedHeaders("token") .allowCredentials(true);}
然后因为往常nginx也没有配置,发现也没有问题,然后这次居然不行,然后把目光投向k8s的nginx-ingress
接着google了一番,发现Ingress
需要配置几个Annotation
最后
apiVersion: extensions/v1beta1kind: Ingressmetadata: name: demo namespace: demo annotations: nginx.ingress.kubernetes.io/enable-cors: "true" nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS" nginx.ingress.kubernetes.io/cors-allow-origin: "*" nginx.ingress.kubernetes.io/cors-allow-headers: "token" nginx.ingress.kubernetes.io/cors-allow-credentials: "true"spec: rules: - host: api.demo.com http: paths: - path: / backend: serviceName: demo-service servicePort: 80
最后通过上述操作,终于可以了
]]>垃圾收集器目前来说有:
新生代收集器
老年代收集器
描述:如同名字一样,先标记后收集
缺点: 效率问题,标记、收集两个过程效率都不高。另一个是空间问题,标记清除后会产生大量不连续的内存碎片,导致以后程序需要分配较大对象时,无法找到足够内存而不得不触发另一次垃圾收集动作
描述:将可用内存按照容量分为大小相等的两块,当这一块内存用完了,就将还存活的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉。
优点:进行垃圾收集时,对整个半区进行内存回收,内存分配也不用考虑内存碎片等复杂情况
缺点:将内存缩小了为原来的一半,代价过大
现在的商业虚拟机都采用该收集算法来回收新生代,由于新生代中对象98%是“朝生夕死”的,所以不需要1:1比例划分内存空间,所以分为一块较大的Eden空间以及两块较小的Survivor空间,每次使用eden以及其中一块Survivor
回收过程:
将Eden以及Survivor中还存活的对象一次性复制到另外一块Survivor空间中,最后清理掉刚才用过的Survivor空间
我们没办法确保每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。
分配担保:
在垃圾收集过程中,如果另外一块Survivor没有足够空间存放上一次新生代收集下来的对象,则通过分配担保机制进入老年代。
担保机制:
主要讲JDK1.6之后的。
如果老年代可用的连续内存大小大于新生代对象总大小或者对象历次晋升的平均大小(也就是每次经过MinorGC之后还存活的对象的平均大小),那么就进行MinorGC,否则就进行Full GC。
复制-收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中对象100%存活的极端情况,所以老年代一般不能直接选用这种算法。
根据老年代的特点,标记过程仍然与(标记-清除)一样,但是清除步骤换成整理,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
当代商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
并行的多线程的收集器,看似跟ParNew是一样的,但是是有区别的。
Parallel Scavenge收集器架构中本身有PS MarkSweep收集器进行老年代收集,并非直接使用Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现非常接近
Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即
吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量
允许值是一个大于0的毫秒数
参数是一个大于0且小于100的整数,也就是垃圾收集时间占总时间比率,相当于是吞吐量的倒数
打开这个参数后不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适停顿时间或最大的吞吐量
Parallel Old 是Parallel Scavenge Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
在没有Parallel Old收集器的时候,如果选择了Parallel Scavenge作为新生代收集器,那么老年代出了Serial Old收集器之外别无选择
直到Parallel Old收集器出现后,“吞吐量优先”的收集器才有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge加Parallel Old收集器
单线程
Serial新生代使用复制算法
Serial old老年代使用标记整理算法
Serial Old收集器意义在于给client模式下的虚拟机使用。
在Server模式下,有两大用途:1、在JDK1.5以及之前版本与Parallel Scavenge收集器配合使用,另一种就是作为CMS收集器后备预案,在并发收集发生Concurrent Mode Failure时使用
ParNew收集器其实就是Serial收集器的多线程版本,是Server模式下的首选新生代收集器(主要是因为只有ParNew收集器能与CMS老年代收集器配合工作)
不幸的是,CMS无法与Parallel Scavenge新生代收集器配合使用。
所以如果选择CMS作为老年代收集器,新生代只能选择ParNew跟Serial收集器其中一个
所以使用-XX:+UseConcMarkSweepGc
的选项后ParNew也是默认的新生代收集器
可以使用
-XX:ParallelGCThreads
参数限制垃圾收集的线程数
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
从名字Mark Sweep可以看出来,CMS也是基于标记-清除算法实现的,但是比前面几种收集器来说更复杂一点。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
并发标记阶段就是进行GC Roots Tracing的过程
重新标记是为了修正并发标记期间用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段停顿的时间一般比初始标记阶段稍长一些,但远比并发标记的时间短
其中初始标记、重新标记仍然需要”Stop The World”。
由于整个过程中耗时最长的并发标记和并发清除过程线程都可以与用户线程一起工作,所以,总体上说,CMS收集器的内存回收过程是与用户线程一起并发执行的
CMS的3个明显缺点。
在默认情况下,CMS默认启动的回收线程计算方式如下
启动的回收线程数 = (CPU数量+3)/4
也就是在4核的情况下,会启动1条回收线程,在并发回收时回收线程会占用不少于25%的CPU资源,不过随着CPU的数量增加会下降。
但是,当CPU不足4个的时候,比如2个,那么则会占用不少于50%的运算能力去执行收集线程,这是无法忍受的。
因此虚拟机提供了一种称为“增量式并发收集器”(I ncremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,原理就是在并发回收的时候,用户线程跟回收线程交替执行,虽然回收的时间更长,但是对用户程序影响变小的。**但是实践证明,该增量CMS收集器效果很一般,以被声明
deprecated
,不再提倡用户使用了
浮动垃圾:
由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然还会有新的垃圾不断产生,这一部分垃圾在标记过程之后,CMS无法在当次收集中处理它们,只好留待下次GC时再清理掉。这部分垃圾就叫做浮动垃圾。
因为在垃圾收集阶段用户线程还需要运行,那也是需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间踢空并发收集时程序运行作使用。
这部分预留的空间可以通过-XX:CMSInitiatingOccupancyFraction
的值进行修改。JDK1.6中,默认值为92%
当CMS运行期间预留的内存无法满足程序需要,就会出现一次”Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,不过这样等待的时间就长了。
当碎片过多时,会出现尽管老年代还有很大的空间剩余,却无法找到足够大的连续空间来分配当前对象,不得不提前出发一次full GC。
为此,CMS提供了一个参数 -XX:+UseCMSCompactAtFullCollection
用于开启CMS对空间进行整理,用于在CMS在顶不住要进行Full GC时开启内存碎片的合并整理过程,不过,整理过程是无法并发的,空间碎片问题没有了,不过停顿时间不得不变长了。
但是,虚拟机设计者又提供了另外一个参数-XX:CMSFullGCsBeforeCompaction
这个参数用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认为0,表示每次进入Full GC时都进行碎片整理)
G1是一款面向服务端应用的垃圾收集器,与其他GC收集器相比,G1具备如下特点:
G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STW停顿的时间,部分其他收集器原本需要停顿java线程执行的GC动作,G1仍然可以通过并发的方式让java程序继续执行
虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活一段时间、熬过多次GC的旧对象以获取更好的收集效果
G1整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于”复制”算法实现的。但不管怎么说,G1在运作期间不会产生内存碎片空间,收集后能提供规整的可用内存
G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。
在G1之前的其他收集器的范围都是整个新生代或者老年代,而G1不再是这样。在使用G1收集器时,java堆的内存布局就与其他收集器有很大差别,它将java堆划分为多个大小相等的独立区域(Region)。虽然还有新生代老年代的概念,但是不再是物理隔离了,他们都是一部分Region的集合。
G1建立可预测停顿时间模型的原因:
因为G1可以有计划地避免在整个java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这种使用Region划分内存空间以及优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
在G1中,Region之间的对象引用以及其他收集器中的新生代与老年代之前的对象引用,虚拟机都是使用Remembered Set来避免扫描全堆的。
G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全表扫描也不会有遗漏
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下几个步骤:
现在有一个很简单的景象,把arthas复制进去,并且运行arthas-demo
FROM openjdk:8-jdk-alpineCOPY --from=hengyunabc/arthas:latest /opt/arthas /opt/arthasCMD ["java","-jar","/opt/arthas/arthas-demo.jar"]
然后进入容器运行arthas,会提示Unable to get pid of LinuxThreads manager thread
这个错误
/opt/arthas # java -jar arthas-boot.jar [INFO] arthas-boot version: 3.1.3[INFO] Found existing java process, please choose one and hit RETURN.* [1]: 1 /opt/arthas/arthas-demo.jar1[INFO] arthas home: /opt/arthas[INFO] Try to attach process 1[ERROR] Start arthas failed, exception stack trace: com.sun.tools.attach.AttachNotSupportedException: Unable to get pid of LinuxThreads manager threadat sun.tools.attach.LinuxVirtualMachine.<init>(LinuxVirtualMachine.java:86)at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:78)at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:250)at com.taobao.arthas.core.Arthas.attachAgent(Arthas.java:82)at com.taobao.arthas.core.Arthas.<init>(Arthas.java:28)at com.taobao.arthas.core.Arthas.main(Arthas.java:120)[ERROR] attach fail, targetPid: 1/opt/arthas #
google了下,发现只有在arthas的github中的issue有讨论下,那里提到解决方案有两种
docker run --init my-app
但是由于我的docker镜像是跑在k8s上的,不由得我来run,所以放弃这种方案
最简单的方案就是如下
COPY --from=hengyunabc/arthas:latest /opt/arthas /opt/arthasRUN apk add --no-cache tiniENTRYPOINT ["/sbin/tini", "--"]CMD ["java","-jar","/opt/arthas/arthas-demo.jar"]
现在查看进程会发现pid为1的进程已经变成tini了
/opt/arthas # ps -efPID USER TIME COMMAND 1 root 0:00 /usr/bin/tini -- java -jar /opt/arthas/arthas-demo.jar 7 root 0:00 java -jar /opt/arthas/arthas-demo.jar 19 root 0:00 sh 55 root 0:00 ps -ef/opt/arthas #
再去运行arthas
/opt/arthas # java -jar arthas-boot.jar [INFO] arthas-boot version: 3.1.3[INFO] Found existing java process, please choose one and hit RETURN.* [1]: 7 /opt/arthas/arthas-demo.jar1[INFO] arthas home: /opt/arthas[INFO] Try to attach process 7[INFO] Attach process 7 success.[INFO] arthas-client connect 127.0.0.1 3658 ,---. ,------. ,--------.,--. ,--. ,---. ,---. / O \ | .--. ''--. .--'| '--' | / O \ ' .-' | .-. || '--'.' | | | .--. || .-. |`. `-. | | | || |\ \ | | | | | || | | |.-' | `--' `--'`--' '--' `--' `--' `--'`--' `--'`-----' wiki https://alibaba.github.io/arthas tutorials https://alibaba.github.io/arthas/arthas-tutorials version 3.1.3 pid 7 time 2019-11-13 08:20:51 [arthas@7]$
]]>下面两种是比较极端的算法,最佳置换算法是一种理想化的算法,具有最好的性能但是实际上无法实现。而先进先出算法是最直观的算法,由于与通常页面的使用规律不符,可能是性能最差的算法。
选择的淘汰页面将是以后永不使用的,或许是最长时间不再被访问的页面。
该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰。
LRU页面置换算法是根据页面调入内存后的使用情况作出决策的,LRU是选择最久未使用的页面予以淘汰。
该算法赋予每个页面一个访问字段,用来记录一个页面上次被访问以来所经历的时间t。当淘汰一个页面时,选择现有页面中其t值最大的,即最近最久未使用的页面予以淘汰。
LRU虽然是一种比较好的算法,但要求系统有较多的支持硬件。为了了解一个进程在内存中的各个页面各有多少时间未被进程访问,以及如何快速地知道哪一页是最近最久未使用的页面,须有寄存器和栈两类硬件之一的支持
1) 寄存器
为了记录某进程在内存中各页的使用情况,须为每个在内存中的页面配置一个移位寄存器,可表示为:
R=Ra-1Ra-2Ra-3···R2R1R0
当进程访问某物理块时,要将相应寄存器的Ra-1
位置设置成1。此时定时信号将没隔一定时间(例如100ms)将寄存器右移一位。
这里可能有点抽象,我理解是如下。就是每隔一段时间,将寄存器右移一位,从R7 -> R6,然后判断对应的页是否被使用,如果使用了则记为1,未使用则记为0,则通过统计这8个寄存器中的记录(R值),如:
00010011
跟00000101
等总共8个值,明显在00010011
跟00000101
比较的话,后者比前者小,则说明后者更久未使用
时间 | 0 | 100ms | 200ms |
---|---|---|---|
页号 | 1 | 1 | 1 |
寄存器 | R7:0 | R6:1 | R5:1 |
如果把n位寄存器的数看作是一个整数,那么具有最小数值的寄存器所对应的页面就是最近最久未使用的页面。
2) 栈
利用一个特殊的栈保存当前使用的各个页面的页面号,每当进程访问某页面时,便将该页面的页号从栈中移出,将它压入栈顶。
假设一进程,分为5个物理块,所访问的页面号序列未:
4,7,0,7,1,0,1,2,1,2,6
当第四次访问时,因为7已存在在栈中,注意这里的操作是,直接将栈中的7移除了,然后再把第4次访问的7放如栈顶
在LFU算法中采用了移位寄存器方式,刚上面的LRU的访问图完全相同。但是是将Ra-1Ra-2···R2R1
求和。
应该指出,这种算法并不能真正反映出页面的使用情况,因为在每一时间间隔内,只是用寄存器的一位来记录页的使用情况,因此,在该时间间隔内,对某页访问一次和访问1000次是完全等效的。
虽然LRU是一种较好的算法,但由于要求比较多的硬件支持,使得其实现所需的成本较高,故实际应用中,大多采用LRU近似算法,Clock算法就是用的比较多的一种LRU近似算法
为每页设置一位访问位,将内存中的所有页面都通过链接指针连接成一个循环队列,当某页被访问时,其访问位被置1。置换算法在选择一页淘汰时,只需检查页的访问位,如果是0则置换出,如果是1则置为0,再按照FIFO算法检查下一个页面,当其访问位仍为1时,则再返回到队首去检查第一个页面。
但因为该算法只有一位访问位,只能用它表示该页是否已经使用过,而置换时是将未使用过的页面换出去,故又把该算法成为最近未用算法或NRU(Not Recently Used)算法。
在改进型Clock算法中,除须考虑页面的使用情况外,还需再增加一个因素——置换代价。这样,选择页面置换出时,既要是未使用过的页面,又要是未修改过的页面。
由访问位A和修改位M可以组合成下面四种类型的页面:
该算法的执行过程可分为以下三步:
该算法与简单Clock算法比较,可减少磁盘的I/O操作次数,但为找到一个可置换的页,可能须经过几轮扫描,换言之,实现该算法本身的开销将有所增加
PBA算法的主要特点是:
为了能显著降低页面换进、换出的频率,在内存中设置了如下两个链表
该链表是一个空闲物理块链表,是系统掌握的空闲物理块,用于分配给频繁发生缺页的进程,以降低该进程的缺页率。当这样的进程需要读入一个页面时,便可利用空闲物理块链表中的第一个物理块来装入该页。
当下次再次请求该页面时,不需要再从外存中读入,而直接在该链表中取出
该链表是已修改的页面所形成的链表。跟上述原理几乎一样,只是这个链表用于存放被置换出去已修改的页,这样可以减少写到外存的频率,也可以减低从外存载入到内存的频率。
]]>简称内存或主存。
寄存器具有与处理机相同的速度,完全能与CPU协调工作。
用户程序要在系统中运行,必须先将它装入内存,然后再将其转变为一个可以执行的程序,通常要经过一以下几个步骤。
对换的类型
每次对换时,都是将一定数量的程序或数据换入或还出内存。根据每次对换时所对换的数量,可将对换分为如下两类:
根据在离散分配时所分配地址空间的基本单位不同,又可将离散分配分为以下三种:
地址变换机构的任务实际上只是将逻辑地址中的页号转换为内存中的物理块号。
当进程要访问某个逻辑地址中的数据时,分为以下几个步骤
其实就是一个缓存而已。
高速的寄存器,称为“联想寄存器”或“快表”,这个寄存器具备并行查寻能力。
当cpu给出有效地址后,将该地址送入联想寄存器即快表,若能直接找到对应的物理地址,直接送入物理地址寄存器中,如果未找到,再到内存中查找,送入物理内存寄存器的同时再将此有效地址跟物理地址的映射存到快表中。如果此时快表已满,则OS必须找到一个老且认为已经不再需要的页表项,将它换出。
由于成本关系,快表不可能做的很大,通常只存放16-512个页表项。
在传统的OS中,进程是作为独立调度和分派的基本单位,因而进程是能独立运行的基本单位。
引入线程的OS中,以把线程作为调度和分派的基本单位,因而线程是能独立运行的基本单位。当线程切换时,仅需保存和设置少量的寄存器内容,切换代价远低于进程。
引入线程的OS中,不仅进程之间可以并发执行,而且一个进程中的多个线程之间亦可并发执行。
进程可以拥有资源,并作为系统中拥有资源的一个基本单位。然而线程本身并不拥有系统资源,而是仅有一点必不可少的、能保证独立运行的资源。
线程除了拥有自己的少量资源外,还允许多个线程共享该进程所有的资源。
进程间的独立性比线程间的独立性高的多,这是因为,为了防止进程间彼此干扰和破坏,每个进程都拥有一个独立的地址空间和其它资源,除了共享变量外,不允许其它进程访问。
但是,同一个进程中的不同线程往往是为了提高并发性以及进行相互之间的合作而创建的,他们共享进程的内存地址空间和资源。
在创建或撤销进程时,系统都要为之分配和回收进程控制块、分配或回收其它资源,如内存空间和I/O设备等。类似地,进程切换时,涉及到进程的上下文切换,而线程的切换代价远低于进程。
在多处理机系统中,对于传统的进程,即单线程进程,不管有多少个处理机,该进程只能运行在一个处理机上,但对于多线程进程,可以将一个进程中的多个线程分配到多个处理机上执行。
与进程一样,各个线程之间也存在共享资源和相互合作的制约关系,致使线程在运行时也具有间断性。所以,线程在运行时具有如下三种基本状态:
线程状态之间的转换和进程之间的转换是一样的
跟进程控制块(PCB)一样,线程也有这样一个线程控制块TCB。
如同每个进程有一个进程控制块一样,系统也为每个线程配置了一个线程控制块TCB,将所有用于控制和管理线程的信息记录在线程控制块中。线程控制块通常有这样几项:
①线程标识符,为每个线程赋予一个唯一的线程标识符;
②一组寄存器,包括程序计数器PC、状态寄存器和通用寄存器的内容;
③线程运行状态,用于描述线程正处于何种运行状态:
④优先级,描述线程执行的优先程度;
⑤线程专有存储区,用于线程切换时存放现场保护信息,和与该线程相关的统计信息等;
⑥信号屏蔽,即对某些信号加以屏蔽:
⑦堆栈指针,在线程运行时,经常会进行过程调用,而过程的调用通常会出现多重嵌套的情况,这样,就必须将每次过程调用中所使用的局部变量以及返回地址保存起来。
为此,应为每个线程设置一个堆栈,用它来保存局部变量和返回地址。相应地,在TCB中,也须设置两个指向堆栈的指针:指向用户自己堆栈的指针和指向核心栈的指针。前者是指当线程运行在用户态时,使用用户自己的用户栈来保存局部变量和返回地址,后者是指当线程运行在核心态时使用系统的核心栈。
目前,高级通信机制可归为四大类:共享存储器系统、管道通信系统、消息传递系统以及客户机-服务器系统
相互通信的进程共享某些数据结构和共享存储区,进程之间能够通过这些空间进行通信。因此,可以分成以下两种类型:
管道,指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又叫pipe文件。
为了协调双方的通信,管道机制必须提供以下三方面的协调能力。
怎么感觉有点像go的channel呢
该机制中,进程不必借助任何共享存储区或数据结构,而是以格式化的消息(message)为单位,将通信的数据封装在消息中,并利用操作系统提供的一组通信命令(原语),在进程间进行消息传递,完成进程间的数据交换。
该机制又可以分为两类:
在客户机-服务器系统的通信机制下,在网络环境的各种应用领域已成为当前主流的通信实现机制,主要的实现方法分为三类:套接字、远程过程调用、和远程方法调用
套接字可以分为两类
RPC引入一个存根(stub)的概念:在本地客户端,每个能够独立运行的远程过程都拥有一个客户存根(client stubborn),本地进程调用远程过程实际是调用该过程关联的存根;与此类似,在每个远程进程所在的服务器端,其所对应的实际可执行进程也存在一个服务器存根(stub)与其关联。本地客户存根与对应的远程服务器存根一般也是处于阻塞状态,等待消息。
实际上,远程过程调用的主要步骤是:
在Innodb 1.0.x版本开始引入了新的文件格式,以前支持的Compact
和Redundant
格式称为Antelope
文件格式,新的文件格式称为Barracuda
文件格式,Barracuda
文件格式拥有两种新的行记录格式:Compressed
和Dynamic
。
在新的两种记录格式对于放在BLOB中的数据采用了完全的行溢出方式。
如下图可知,数据页中只存放20个字节的指针,实际的数据都存放在Off Page中,而之前的Compact
和Redundant
两种格式会存放768个前缀字节
Compressed
行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储
在多字节字符集类型下,char类型被明确视为变长字符类型,对于未能占满长度的字符还是填充0x20。
]]>在多字节字符集的情况下,CHAR和VARCHAR的实际行存储基本是没有区别的。
关于进程同步有一系列经典问题,比较代表性的有
假定生产者消费者之间的公用缓冲池具有n个缓冲区,互斥信号量mutex实现诸进程对缓冲池的互斥使用,empty、full表示缓冲池中空缓冲区和满缓冲区的数量。
假设:缓冲池未满,生产者可将消息送入缓冲池;缓冲池未空,消费者则可以从缓冲池取走一个消息。
// in为进队列的下标,out为消费队列的下标int in=0,out=0;// 循环队列item buffer[n];// mutex互斥锁,empty:剩余可用的队列空间,full当前已有的产品数量semaphore mutex=1,empty=n,full=0;void proceducer(){ do{ // 生产了一个产品nextp producer an item nextp; ... // 如果是empty为0,则进程会被放进等待的队列中 wait(empty); // 获得一个锁的东西,比如此时有消费者在buffer中获取产品,则也会被放进等待队列中,直到被释放 wait(mutex); // 已取得锁,可以将产品放进in的位置 buffer[in]=nextp; // 将该buffer当循环队列使用,计算下次放进去队列的下标 in:=(in+1)%n; // 释放该锁,使消费者可以获得该互斥锁 signal(mutex); // full表示有多少个产品,signal表示对full++,此时多了一个产品,则唤醒(通知)在等待队列中的消费者可以进行消费了 signal(full); }while(true);}void consumer(){ do { // 判断队列是否为空,为空则将进程放进阻塞队列中,直到proceducer()有新的产品被生产了,然后通知该阻塞的队列的进程,也就本进程了 wait(full); // 获取操作队列的锁 wait(mutex); // 获取产品 nextc=buffer[out]; // 循环队列,获取下一个出队列的下标 out=(out+1)%n; // 释放队列的锁,通知生产者可以操作队列了 signal(mutex); // empty++,队列增加了一个位置,通知生产在阻塞队列的进程可以生产了 signal(empty); // 消费。。 consumer the item in nextc; } while(true);}// main void main(){ cobegin proceducer(); consumer(); coend}
几乎与上述描述一致,这里用AND信号量解决
// in为进队列的下标,out为消费队列的下标int in=0,out=0;// 循环队列item buffer[n];// mutex互斥锁,empty:剩余可用的队列空间,full当前已有的产品数量semaphore mutex=1,empty=n,full=0;void proceducer(){ do{ // 创建新产品 producer an item nextp; // AND信号量,这里复习一下Swait是什么 // 一个死循环,然后判断是否所有条件都满足(大于等于1),只要有其中一个条件不满足,就会将该进程放进等待或者阻塞队列中 // 当满足条件了,会对swait中的参数进行减一操作,对empty减一,就是空闲数-1 Swait(empty,mutex); // 把新生产的产品放进去队列 buffer[in]=nextp; // 计算下标 in:=(in+1)%n; // AND信号量的操作,也复习一下 // 对Ssignal中的所有参数进行加一操作 // 并唤醒等待或阻塞的进程 Ssignal(mutex,full); } while(true);}void consumer(){ do { // 上述一样,当full(未消费的产品数)大于等于1 // 且mutex互斥锁没有被占用时 // 取得资源的使用权,否则阻塞 Swait(full,mutex); nextc=buffer[out]; out=(out+1)%n; // 对mutex跟empty进行加一操作,表示释放资源 // 并且唤醒在等待empty跟mutex的进程 Ssignal(mutex,empty); consumer the item in nextc; }while(true);}
在利用管程方法来解决生产者—消费者的问题时,首先为他们建立一个管程,并命名为producerconsumer,简称pc,其中包含两个过程
对于条件变量notfull和notempty,分别有两个过程cwait和csignal对它们进行操作
Monitor producerconsumer{ item buffer[N]; int in,out; // 条件变量 // notfull,没有空余的区域 // notempty,队列为空 condition notfull,notempty; int count; public: void put(item x){ // 当队列中的数据以达到最大值 // 则将本进程放进等待队列中阻塞 if(count>=N)cwait(notfull); buffer[in]=x; in=(in+1)%N; count++; // 唤醒队列其中一个进程可以进行消费了 csignal(notempty); } void get(item x){ // 当队列没有数据时 // 将进程放进等待数据的队列中阻塞 if(count<=0)cwait(notempty); x=buffer[out]; out=(out+1)%N; count--; // 当数据被消费了 // 通知被阻塞的生产者可以继续生产了 csignal(notfull); } // init { in=0;out=0;count=0; }}PC;
在利用管程解决生产者-消费者时,其中的生产者消费者可描述为:
void producer(){ item x; while(true){ produce an item in nextp; PC.put(x); }}void consumer(){ item x; while(true){ PC.get(x); consume the item in nextc; }}void main(){ cobegin; producer(); consumer(); coend;}
可以用一个信号量表示一只筷子,五个信号量构成一个数组
semaphore chopstick[5]={1,1,1,1,1}
do{ // 拿左手边的筷子,如果没有则阻塞 wait(chopstick[i]); // 拿右手边的筷子,如果没有则阻塞 wait(chopstick[(i+1)%5]; // 拿到筷子了,吃饭。。 // 释放左手边的筷子 signal(chopstick[i]); // 释放右手边的筷子 signal(chopstick[(i+1)%5];}
上述解法可保证不会有两个相邻的哲学家同时进餐,但却有可能引起死锁。假如五位哲学家同时极饿而各自拿起左边的筷子时,都将因为无筷子可拿而无限期地等待。对于这样的死锁问题,可采取一下几种解决方法:
从上述可以知道,第二个解决方案
- 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐
semaphore chopstick chopstick[5]={1,1,1,1,1}do { ... // think ... Sswait(chopstick[(i+1)%5],chopstick[i]); // eat ... Ssignal(chopstick[(i+1)%5],chopstick[i]);} while(true)
也就是对某个文件进行read,write操作,read操作可以多个进程同时执行,但是write只能允许一个进程执行并且此时不允许read操作以及write操作,否则回引起混乱
为实现reader与writer进程间在读或写时的互斥信号量Wmutex
。另外,再设置一个整形变量Readcount
表示正在读的进程数目,由于只要有一个reader进程在读,就不允许writer进程去写。因此,仅当readcount=0,reader进程才需要执行wait(wmutex)操作。当reader执行了readcount减1的操作后其值为0时,才需要执行signal(Wmutex),以便让writer进程写操作。因为readcount是一个临界值,所以需要设置一个互斥锁rmutex
注意⚠️:这里注意两个信号量,
rmutex
表示对readcount
临界资源的互斥锁,而wmutex
表示对被读写的资源的一个互斥锁
// 两个信号量semaphore rmutex=1,wmutex=1;// 记录获得读锁的进程数int readcount=0;void reader(){ do{ // 阻塞等待readcount的互斥锁 wait(rmutex); // 当readcount为0时,代表没有进程在进行读操作 // 那么有可能此时该资源正在被写 // 所以需要等待该资源的锁 if(readcount==0) wait(wmutex); // 获得资源的锁后,说明写操作已经结束了 // 可以进行一些操作了 // 对readcount++操作,表示此时多了一个进程进行读操作 readcount++; // 此时可以将rmutex锁释放了 // 因为同一时间可以有多个进程进行读操作 // 若没有释放rmutex锁的话 // 别的进程则无法获取到该锁 // 进而退化成,读操作也是互斥了的 signal(rmutex); // ..读操作 // ..读完 // 读完之后获取readcount锁 // 因为需要对readcount进行减1的操作 wait(rmutex); readcount--; // 如果当没有进程在读时,那么可以释放该资源的互斥锁了 // 因为只要有1个进程在读,那么就无法进行写操作 // 所以当readcount为0时,则可以释放该锁 // 便可以唤醒writer进程,进行写操作 if(readcount==0) signal(wmutex); // 对readcount--完后,就可以释放该锁了 signal(rmutex); }while(true);}// writer进程void writer(){ do{ // 等待该资源的互斥锁 wait(wmutex); // .. 写操作 // 释放 signal(wmutex); }while(true);}
这里与上述的问题不同,它增加了一个限制,最多只允许RN个读者同时读,为此又引入一个信号量L,并赋予其初值RN,通过执行wait(L,1,1)
操作来控制读者的数目,每当有一个读者进入时,要先执行wait(L,1,1)
操作,使L的值减1。当有RN个读者进入读后,L便减为0,当第RN+1个读者想要进入读时,则会阻塞。
RN: 表示限制的读进程数
mux: 表示对资源的写锁
int RN;semaphore L=RN,mx=1;void reader(){ do{ // 获取读锁 // 读锁资源总共L个,一次最少获取资源1个,本次获取1个资源 Swait(L,1,1); // 这里表示所有进程都可以获取锁 // mx-- Swait(mx,1,0); // 读操作... // 释放资源 Ssignal(L,1); }while(true);}void writer(){ do{ // 等待读锁,获取全部的读资源锁 Swait(mx,1,1,L,RN,0); // 写操作... // 写完释放写锁 Ssignal(mx,1); }while(true);}
]]>
Swait(S,1,0)
当S>=1时,允许多个进程进入某特定区,当S变为0后,则阻止任何进程进入特定区
为了提高资源利用率和系统吞吐量,通常采用多道程序技术,将多个程序同时装进内存,并使之并发运行,此时作为资源分配和独立运行的基本单位都是进程。
为了使参与并发执行的每个程序(含数据)都能独立地运行,操作系统中必须为止配置一个专门的数据结构,称为进程控制块(Process Control Block, PCB)。系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。
一般情况下,我们把进程实体就简称为进程,例如,所谓创建进程,实质上是创建进程实体中的PCB;而撤销进程,实质上是撤销进程的PCB。
较为典型的进程定义:
进程的特征
由于多个进程在并发时共享系统资源,致使它们在运行过程中呈现间断性的运行规律,所以进程在其生命周期内可能具有多种状态。一般而言,每一个进程至少应处以下三种基本状态之一
为了满足进程控制块对数据及操作的完整性要求以及增强管理的灵活性,通常在系统中又为进程引入两种常见的状态:创建状态和终止状态。
进程五种基本状态及转换
除了三种基本状态外,还引入了一个对进程重要的操作——挂起操作。当该操作作用于某个进程时,该进程将被挂起,意味着此时进程处于静止状态。如果进程正在执行,它将暂停执行。若原本处于就绪状态,则该进程此时暂不接受调度。与挂起操作对应的操作时激活操作。
在上述两个原语操作的作用下,可能发生以下几种状态转换
OS管理资源、进程等数据结构一般分为以下四类:内存表、设备表、文件表和用于进程管理的进程表,通常进程表又被称为进程控制块PCB。
为了便于系统描述和管理进程的运行,在OS的核心为每个进程专门定义了一个数据结构——进程控制块PCB(Process Control Block)。PCB作为进程实体的一部分,记录了操作系统所需的,用于描述进程的当前情况以及管理进程运行的全部信息,是操作系统中最重要的记录型数据结构。
PCB具体作用的阐述:
进程控制块PCB的组织方式
进程控制一般是由内核中的原语来实现的。
引起进程阻塞和唤醒的时间
进程的阻塞过程
正在执行的进程,如果发生了上述某时间,进程便通过调用阻塞原语block将自己阻塞。可见阻塞是进程自身的一种主动行为。
进入阻塞的过程
调用block –> 将PCB状态改为阻塞 –> 将PCB插入阻塞队列
进程唤醒过程
当被阻塞进程所期待的事情发生时,比如它所启动的I/O操作已完成,或其所期待的数据已经到达,则由有关进程(不如提供数据的进程)调用唤醒原语wakeup
,将等待该事件的进程唤醒。
被唤醒的过程
将被阻塞的进程从等待该事件的阻塞队列中移出 –> 将PCB状态改为就绪 –> 将该PCB插入到就绪队列中
挂起
OS利用挂起原语suspend
将指定进程或处于阻塞状态的进程挂起。
激活
利用原语active
,将指定进程激活。激活原语先将进程从外存调入内存,检查该进程的现行状态,若是静止就绪,将之改为活动就绪。
单处理机系统中的进程同步机制——硬件同步机制、信号量机制、管程机制等。
每个进程中访问临界资源的那段代码称为临界区。若能保证诸进程互斥地进入自己地临界区,便可实现诸进程对临界资源地互斥访问。
为此,每个进程进入临界区之前,应先对欲访问的临界资源进行检查,看它是否正被访问。
在临界区前面增加一段用于进行检查的代码称为进入区(entry section),相应的,在临界区后面也要加上一段称为退出区(exit section)的代码。
一个访问临界资源的循环过程描述如下:
while(TRUE){ 进入区 临界区 退出区 剩余区}
所有同步机制应遵循下述四条准则
boolean TS(boolean *lock){ Boolean old; old = *lock; *lock = TRUE; return old;}
这条指令的执行过程是不可分割的,即是一条原语。其中lock有两种状态,当为FALSE时表示该资源空闲,当为TRUE时,表示该资源正在被使用利用TS指令实现互斥的循环进程结构可描述如下
do { // ... while TS(&lock); // 进入区 critical section; // 临界区 lock := FALSE; // 退出区 remainder section; // 剩余区}while(TRUE)
对换指令
void swap(boolean *a, boolean *b){ boolean temp; temp = *a; *a = *b; *b = temp;}
用对换指令可以简单有效地实现互斥,方法是为每个临界资源设置一个全局的布尔变量lock,其初值为false,在每个进程中再利用一个局部布尔变量key。利用swap指令实现进程互斥的循环进程可描述如下:
do{ key=TRUE; do{ swap(&lock,&key); }while(key!=FALSE); // 临界区操作; lock=FALSE; // ...} while(TRUE);
整形S仅能通过两个标准的原子操作wait(S)和signal(S)来访问
wait(S){ while(S<=0); S--;}signal(S){ S++;}
在整型信号量机制中的wait操作,只要是信号量S<=0,就会不断测试,因此该机制并未遵循“让权等待”的准则,处于“忙等”状态。
记录型信号量机制采取了“让权等待”的策略,会出现多个进程等待访问一个临界资源的情况,为此信号量机制除了用于代表数目的整型变量value外,还应增加一个进程链表指针list,用于链接上述的所有等待进程
typedef struct{ int value; struct process_controller_block *list;}semaphore;
相应的,wait(S)和signal(S)操作可描述如下:
wait(semaphore *S){ S->value--; if(S->value<0) block(S->list);}signal(semaphore *S){ S->value++; if(S->value<=0) wakeup(S->list);}
S->value的初值表示系统中某类资源的数目,对于每次wait操作意味着进程请求一个单位的该类资源,使系统中可供分配的该类资源减少一个,因此S->value–;当S->value<0时说明该类资源已分配晚比。
因此进程应调用block原语进行自我阻塞,放弃处理机,并进入S->list中。
signal操作表示释放一个单位的资源,将S->value++,并waitup第一个list中阻塞的进程。
对若干个临界资源的分配采取原子操作方式:要么把它请求的资源全部分配到进程,要么一个也不分配。
为此,在wait操作中增加一个“AND”条件,称为AND同步,或称为同时wait操作,S(Simultaneous wait)定义如下
Swait(S1,S2,...,Sn){ while(TRUE){ if(Si>=1&&...&&Sn>=1){ for(i=1;i<=n;i++) Si--; break; }else{ // 进入等待队列 } }}Ssignal(S1,S2,...,Sn){ while(TRUE){ for(i=1;i<=n;i++){ Si++; // 将所有在等待队列的Si的进程移除 } }}
对AND信号量机制的扩充,对进所申请的所有资源以及每类资源不同的资源需求量,再一次P、V原语操作中完成申请或释放。进程对信号量Si的测试值不再是1,而是该资源的分配下限指ti,即要求Si>=ti,否则不予分配。进程对该资源的需求值为di,表示资源占用量,进行Si:=Si-di操作。由此形成一般化的“信号量集”机制,对应的Swait、Ssignal格式为:
Si: 表示资源总量
ti: 分配资源下限值
di: 分配多少
Swait(S1,t1,d1,...,Sn,tn,dn);Ssignal(S1,t1,...,Sn,tn);
一般“信号量集”还有下面几种特殊情况
定义
代表共享资源的数据结构以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成了一个操作系统的资源管理模块,称之为管程。
管程被请求和释放资源的进程所调用。
管程由四部分组成
管程的语法描述:
// 管程名Monitor monitor_name { // 共享变量说明 share variable declarations; // 条件变量说明 cond declarations; // 能被进程调用的过程 public: // 对数据结构操作的过程 void P1(...){...} void P2(...){...} // .... // 管程主体 { // 初始化代码 initialization code; }}
管程包含了面向对象的思想,将表征共享资源的数据结构及其对数据结构操作的一组过程,包括同步机制,都集中并封装在一个对象内部,隐藏了实现袭击。
每次只能由一个进程请求管程的过程,从而实现了进程互斥。
应考虑的一种情况:当一个进程调用了管程,在管程中时被阻塞或挂起,直到阻塞或挂起的原因解除,而在此期间,如果该进程不释放管程,则其它进程无法进入管程,被迫长时间等待。
为此引入condition,管程中对每个条件变量都须予以说明,形式为: condition x,y;对条件变量的操作也仅为wait和signal,提供了两个操作,x.wait 和 x.signal
redis通过multi
,exec
,watch
等命令来实现事务功能。
例如
127.0.0.1:6379> MULTIOK127.0.0.1:6379> set "name" "practical common lisp"QUEUED127.0.0.1:6379> get "name"QUEUED127.0.0.1:6379> set "author" "peter seibel"QUEUED127.0.0.1:6379> get "author"QUEUED127.0.0.1:6379> exec1) OK2) "practical common lisp"3) OK4) "peter seibel"
像是sql里的,
start transaction
,commit
一个事务从开始到结束通常会经历以下三个阶段
multi
命令标志着事务的开始
如果客户端发送的命令为exec
,discard
,watch
,multi
命令其中一个,那么服务器立即执行这个命令,相反,发送的命令是上述4个以外的命令, 则不会立即执行这个命令,而会将这个命令放入一个事务队列里面,然后向客户端返回queue回复。
watch命令是一个乐观锁,他可以在exec命令执行之前,监视任意数量的数据库键,并在exec命令执行时,检查被监视的键是否至少有一个被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复
]]>Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障庄毅功能
连接各个节点的工作可以使用cluster meet
命令来完成
cluster meet <ip> <port>
该命令可以让node节点与ip和port指定的节点进行握手,当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中
一个节点就是一个运行在集群模式下的redis服务器,redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式
clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点当前的配置纪元、节点的ip地址和端口号等等
struct clusterNode { // 创建节点的时间 mstime_t ctime; // 节点的名字,由40个十六进制字符组成 char name[REDIS_CLUSTER_NAMELEN]; // 节点标识 // 使用各种不同的标识值记录节点的角色(比如主节点或者从节点) // 以及节点目前所处的状态 int flags; // 节点当前的纪元,用于实现故障转移 uint64_t configEpoch; // 节点的ip char ip[REDIS_IP_STR_LEN]; // 保存连接节点所需的有关信息 clusterLink *link;}
clusterLink保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区:
typedef struct clusterLink { // 连接的创建时间 mstime_t ctime; // TCP套接字描述符 int fd; // 输出缓冲区,保存着等待发送其他节点的消息 sds sndbuf; // 输入缓冲区,保存着从其他节点接收到的消息 sds rcvbuf; // 与这个连接相关联的节点,如果没有的话就为null struct clusterNode *node;} clusterLink;
每个节点都保存着一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元,诸如此类:
typedef struct clusterState { // 指向当前节点的指针 clusterNode *myself; // 集群当前的配置纪元,用户实现故障转移 uint64_t currentEpoch; // 集群当前的状态:是在线还是下线 int state; // 集群中至少处理着一个槽的节点的数量 int size; // 集群节点名单(包括myself节点) // 字典的键为节点的名字,字典的值为节点对应的clusterNode结构 dict *nodes; // ...} clusterState;
Sentinel哨兵是redis高可用的解决方案:由一个或多个sentinel实例组成的sentinel系统,可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求
启动一个Sentinel可以使用命令
$ redis-sentinel /path/to/your/sentinel.conf
或者
$ redis-server /path/to/your/sentinel.conf --sentinel
当一个sentinel启动时,它需要执行以下步骤:
初始化服务器
sentinel本质上是一个运行在特殊模式下的redis服务器,所以启动sentinel的第一步就是初始化一个普通的redis服务器,不过sentinel不实用数据库,所以初始化不会载入rdb文件或者aof文件
sentinel默认会每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器当前的信息
当sentinel发现主服务器有新的从服务器出现时,sentinel除了会为这个新的从服务器创建相应的实例结构外,sentinel还会创建连接到从服务器的命令连接和订阅连接
默认情况下,sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下格式命令:
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
命令向__sentinel__:hello
频道发送了一条信息
为了解决旧版复制功能在处理断线重复制的低效问题,使用PSYNC命令代替SYNC命令来执行复制时的同步操作
PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式;
部分重同步功能由以下三个部分构成:
复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB。
当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入对到复制积压缓冲区里面
当从服务器重新上线连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作
]]>根据需要调整复制缓冲区的大小,为了安全起见,可以将复制积压缓冲区的大小设为 2 * second * write_size_per_second,这样可以保证绝大部分断线情况都能用部分重同步来处理
redis过期删除策略由惰性删除跟定期删除配合组成
在规定时间内,分多次遍历服务器中的各个数据库,并从数据库的expires
字典中随机检查一部分键的过期时间,并删除其中的键。
在执行SAVE
或者BGSAVE
命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
这里会区分主从服务器的
当服务器以AOF持久化模式运行时,如果数据库中某个键已经过期,但它还没被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响
当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式记录该键被删除
在执行AOF重写过程中,会对键检查,已过期的键不会被保存到重写后的AOF文件中
从服务器:
DEL KEY
的命令,则会将对应的key删除主服务器:
DEL KEY
的命令RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中。RDB所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态
因为AOF文件的更新频率会比RDB文件更新频率更高,所以如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。只有AOF功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化时通过保存Redis服务器所执行的写命令来记录数据库状态的。
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤
因为AOF持久化时通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,体积越来越大,如果不加以控制的话,体积过大的AOF很可能对Redis服务器、甚至整个宿主机造成影响,并且AOF文件的题体积越来越大,使用AOF文件来进行数据还原所需的时间就越多。
为了解决AOF文件体积膨胀的问题,redis提供AOF文件重写(rewrite)功能。通过该功能redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新的AOF通常比旧的AOF文件体积小的多
AOF文件重写并不需要对现有的AOF文件进行任何读取、分析、写入操作,这个功能时通过读取数据库当前的数据库状态来实现的。
在子进程工作的目的:
为了避免在后台重写AOF的期间,有新的数据写入,则造成丢失
为了解决这种数据不一致问题,redis设置了一个AOF重写缓冲区,这个缓冲区在分局武器创建子进程之后开始使用,当redis服务器执行一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区
redis基于reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler):
I//O多路复用程序会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。
redis的时间事件分为以下两类:
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器
]]>Redis没有直接使用c语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示
除了用来保存数据库中的字符串值之外,SDS还被用作缓冲区(buffer): AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是又SDS实现的。
struct sdshdr { int len; int free; char buf[];}
C字符串获取长度是需要逐个字符遍历的,时间复杂度为O(n)
,而SDS的一个字段len用来记录长度,获取字符串长度时间复杂度为O(1)
C字符串会根据空字符\0
判断字符串是不是已经读完,可能导致某些特殊格式的二进制数据被错误的获取或写入。而sds是根据len这个属性判断的。
typedef struct listNode{ struct listNode *prev; struct listNode *next; void *value;}listNode;
用过list来持有链表,主要为了方便操作
typedef struct list { listNode *head; listNode *tail; unsigned long len; // 节点值复制函数 void *(*dup) (void *ptr); // 节点复制函数 void (*free) (void *ptr); // 节点值对比函数 int (*match)(void *ptr,void *key);}list;
Redis字典所用的哈希表又dict.h/dictht
结构定义
typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 // 记录的是哈希表的大小 unsigned long size; // 哈希表大小掩码,用于计算索引值 // 该值总是等于size-1 // 这个属性和哈希值一起决定一个键 // 应该被放到table的哪个索引上 unsigned long sizemask; // 该哈希表已有的节点数量 // 记录的是记录数,即键值对的数量 unsigned long used;}dictht;
哈希表节点
typedef struct dictEntry { void *key; union{ void *val; uint64_tu64; int64_ts64; } v; // 指向下个哈希表节点,形成链表 struct dictEntry *next;} dictEntry;
扩展和收缩哈希表的工作可以通过rehash操作完成,redis对字典的哈希表执行rehash步骤:
当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
BGSAVE
或者BGREWRITEAOF
命令,并且哈希表的负载因子大于等于1BGSAVE
或者BGREWRITEAOF
命令,并且哈希表的负载因子大于等于5其中,哈希表的负载因子可以通过以下公式计算
load_factor = ht[0].used / ht[0].size
当哈希表的负载因子小于0.1时,程序会自动对哈希表执行收缩操作
渐进式rehash的详细步骤
在渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,字典的delete,find,update操作会在两个哈希表上进行。例如要查找一个键的话,程序会现在ht[0]里面进行查找,没找到再去ht[1]里面找。另外,新添加的字典的键值对一律被保存到ht[1]里面,保证了ht[0]的键值对数量只减不增
跳跃表(skiplist)是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中的元素成员是比较长的字符串时,redis就会使用跳跃表作为有序集合键的底层实现
去看看跳跃表
根据备份方法的不同可以将备份分为:
按照备份后文件的内容,备份又可以分为
逻辑备份:
指备份出的文件内容是可读的,一般是文本文件。内容一般是一条条SQL语句。一般适用于数据库的升级、迁移等工作,缺点是恢复所需要的时间比较长
裸文件备份:
指数据库的物理文件,既可以是在数据库运行中复制,也可以是在数据库停止运行时直接的数据文件复制,这类备份的恢复时间往往较逻辑备份短很多
按照备份数据库的内容来分,备份又可以分为:
对于mysqldump备份工作来说,可以通过添加–single-transaction选项获得Innodb的一致性备份。此时的备份是在一个执行时间很长的事务中完成的。
只需要备份Mysql的frm文件,共享表空间文件,独立表空间文件(*.ibd),重做日志文件。另外最好可以定期备份mysql的配置文件my.cnf
mysqldump
mysql -uroot -p <test_backup.sql
也可以
source /home/mysql/test_backup.sql
二进制日志非常关键,用户可以通过它完成point-in-time的恢复工作,mysql的replication同样需要二进制日志。在默认情况下并不启用二进制日志,要使用二进制日志首先必须启用它。在配置文件中进行设置
推荐的配置:
[mysqld]log-bin = mysql-binsync_binlog = 1innodb_support_xa = 1
ibbackup是innodb官方提供的热备工具,是innodb备份的首选方式,不过是收费。但是!XtraBackup是免费了,实现了ibbackup所有的功能,并且扩展支持了真正的增量备份功能
]]>innodb事务完全符合ACID特性
从事务的理论角度来说,可以把事务分为以下几种类型
原子性,一致性,持久性通过数据库的redo log和undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。
锁的类型,InnoDB存储引擎实现了如下两种标准的行级锁:
如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没还有改变行r的数据,称这种情况为锁兼容(Lock Compatible),若有其他的事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁——这种情况称为锁不兼容。
- | X | s |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
需要注意的是,S和X锁都是行锁,兼容是指同一记录(row)锁的兼容性情况
意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁。 如下图所示
总结来说的话,就是对最细粒度的对象进行上锁的话,需要对粗粒度的对象上IX锁,也就是意向锁,但是会存在兼容性的问题
InnoDB在支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁
由于InnoDB支持的是行级别的锁,因此意向锁其实不会阻塞全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如下
我理解的应该这么说 (列)想要获得表(行)上的一个(列)锁,e.g. IS想要获得表的IS锁,所以是兼容的
- | IS | IX | S | X |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
一致性非锁定读是指InnoBD存储引擎通过行多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB会去读取行地一个快照数据。
快照数据时指该行地之前版本地数据,该实现是通过undo段来完成地。而undo用来在事务中回滚数据,因此此快照数据本身是没有额外地开销。此外,读取快照数据是不需要上锁地,因为没有事务需要对历史地数据进行修改操作。在默认设置下,这是默认的读取方式,但在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。
多版本并发控制
一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术,由此带来的并发控制,称之为多版本并发控制。
下面有个例子,用于说明在READ COMMITED 与 READ REPEATABLE不同的事务隔离级别下,读取的方式区别
如下图,COMMITED READ级别下,总是读取最新的版本,即会话A最开始读取到id为1,在会话B更新了id为3,然后commit后,A仍然会读取最新的版本,所以select id为1就不存在了,因为最新的版本id是3。
而在READ REPEATABLE下,始终会读取最初始的版本,所以读取到的id仍然为1
需要特别注意的是,对于READ COMMITED 的事务隔离级别而言,从数据库理论的角度看,其违反了事务ACID中的I的特性,即隔离性。
在默认配置下,READ COMMITED 跟 REPEATABLE READ这两种事务隔离级别下都是采用非锁定读的。但在某些情况下,用户需要显式地对数据库读取操作进行加锁保证数据逻辑地一致性。
InnoDB对于select语句支持两种一致性锁定读(locking read)操作
SELECT ... FOR UPDATE
对读取地行记录加一个X锁,其他事务不能对已锁定地行加上任何锁SELECT ... LOCK IN SHARE MODE
对读取地行记录加一个S锁,其他事务可以向被锁定地行加S锁,但是如果加X锁,则会被阻塞对于上述连个SELECT锁定语句时,务必加上BEGIN,START TRANSACTION或者SET AUTOCOMMIT=0
innodb_autoinc_lock_mode
,总共有3个有效值可供设定,0、1、2
对于一个外键列,如果没有显示地对这个列加索引,InnoDB会自定对其加一个索引,因为这样可以避免表锁。(我:可能因为全表扫描不会采用一致性非锁定读?而会采用表锁?只在读的情况下?)
对于外键值的插入或更新,首先需要查询父表中的记录,即SELECT父表。但是对于父表的SELECT操作,不是使用一致性非锁定读的方式,因为这样会发生数据不一致的问题,因此这是使用的时SELECT ... LOCK IN SHARE MODE
方式,即主动对父表加一个S锁。如果这时父表上已经这样加了X锁,子表上的操作会被阻塞。
两个会话都没有进行COMMIT或ROLLBACK操作,而会话B被阻塞的原因是:
id为3的父表在A中已经加了一个X锁(因为是DELETE操作?),然和人B中又需要对id为3的行获取一个S锁,此时INSERT操作会被阻塞。
如果此时采用的是一致性非锁定读,这是Session B就会读到父表有id为3的记录,可以插入数据库。但是如果会话A对事务提交了,父表中就不存在id为3的记录。数据在父、子表就会存在不一致的情况。
Next-Key Lock
如果一个索引有10,11,13,20四个值,那么该索引可能被next-key locking的区间为:
采用Next-Key lock的锁定技术称为 Next-Key Locking。其设计的目的是为了解决幻读(Phantom Problem),利用这种锁定技术,锁定的不是单个值,而是一个方位,是谓词锁(predict lock)的一种改进。
若事务T1已经通过next-key lock锁定了如下范围(10,11]、(11,13]
当插入新的记录12时,则锁定的范围会变成:(10,11]、(11,12]、(12,13]
然后当查询的索引含有唯一属性时,innodb会对next-key lock进行优化,将其降级为Record lock,即仅锁住索引本身,而不是范围。
举个栗子,创建了如下表(z)
a(主键) | b(辅助索引) |
---|---|
1 | 1 |
3 | 1 |
5 | 3 |
7 | 6 |
10 | 8 |
当在会话A执行下面的SQL语句:
SELECT * FROM z WHERE b = 3 FOR UPDATE
此时sql通过索引b进行查询,因为时非唯一的辅助索引,所以采用了传统的next-key locking技术,并且由于有两个索引,其需要分别进行锁定。对于聚集索引来说,其仅对列a等于5的索引加上record lock。所以对于辅助索引,其加上了Next-Key Lock,锁定的范围是(1,3)。 需要特别注意的是,InnoDB还会对辅助索引的下一个键值加上gap lock,即还有一个辅助索引范围为(3,6)的锁 因此,若在新会话B中允许下面的SQL语句,都会被阻塞
SELECT * FROM z WHERE a = 5 LOCK IN SHARE MODE;INSERT INTO z SELECT 4,2INSERT INTO z SELECT 6,5
分析:
第一个sql语句不能执行,因为A中的sql已经对聚集索引列中a=5的值加上了X锁,因此会被阻塞。
第二句sql语句,主键插入4没有问题,但是插入的辅助索引值为2在锁定范围(1,3)之间,因此会被阻塞
第三个sql语句,主键插入6没有问题,没有被锁定,索引5插入不在(1,3)中,但是在(3,6)中,所以依然会被锁定。
但是如果执行以下sql,是不会被阻塞的
INSERT INTO z SELECT 8,6INSERT INTO z SELECT 2,0INSERT INTO z SELECT 6,7
从上面例子可以看到,Gap Lock为了阻止多个事务将记录插入到同一范围内,而这回导致幻读的产生。
例如上面的例子中,会话A中已经锁定b=3的记录。若此时没有gap lock锁定(3,6),那么用户可以插入索引b列为3的记录,这会导致会话A中的用户再次执行同样查询时会返回不同的记录,即幻读(Phantom Problem)。
在innodb存储引擎中,对于insert操作,会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。
对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询,故innodb依然使用next-key lock进行锁定
在默认的事务隔离级别下,即REPEATABLE READ下,innodb采用next-key locking机制来避免幻读问题。
Phantom Problem是指同一事务下,连续执行2次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能返回之前不存在的行
举个栗子,创建一个表t,CREATE TABLE t (a INT PRIMARY KEY);
a |
---|
1 |
2 |
5 |
若此时事务T1执行如下语句SELECT * FROM t WHERE a>2 FOR UPDATE;
注意,此时事务T1并没有进行提交操作,上述应该返回5这个结果。若此时另一个事务T2插入了4这个值(假设数据库允许这个操作)那么事务T1再次执行上述SQL,会得出结果4、5,这与第一次得到的结果不同,违反了事务的隔离性。
InnoDB采用了Next-Key Locking的算法避免了幻读。对于上述sql语句,其锁住的不是5这单个值,而是对(2,+∞)这个范围加了X锁,因此对于这个范围的插入操作时不被允许的,从而避免幻读。
背景介绍:
在开发过程中,很多时候,我们有很多的需求都需要“若不存在则插入”的需求。
然后目前做法是如下面的代码,但是这里会有个并发问题,因为这不是一个原子操作,在并发情况下可能会出现多个name相同的问题,当然这个可以在数据库里面做限制,name设置称unique
user := dao.getUserByName(name)if user != nil { return "该用户名已存在"}dao.insertUser(&User{Name:name})
所以不设置unique的时候在想,有没有这样一种方法,但是并没有
insert into user(name) values(name) where name != 'name'
然后现在知道了多一个用法,当然,需要开启一个事务
select * from user where name = 'name' lock in share mode;
用法与上面第一种一致,但是在执行的时候,只有一个事务会成功,别的都会抛出死锁的错误
dao.insertUser(&User{Name:name})
通过锁定机制可以实现事务的隔离性要求,使得事务可以并发地工作。锁提高了并发,却会带来潜在地问题。不过因为事务隔离性地要求,锁只会带来三种问题,防止这三种情况地发生,那将不会产生异常。
脏数据是指未提交地数据,如果读到脏数据,即一个事务可以读到另外一个事务中为提交地数据,显然违反了数据库地隔离性。
READ UNCOMMITTED(未提交读)
脏读隔离看似毫无用处,但在一些比较特殊地情况下还是可以将事务地隔离级别设置为READ UNCOMMITTED。例如replication环境中地slave节点,并且该slave上的查询并不需要特别精确的返回值
READ COMMITTED(提交读)
同一个事务内两次读取数据不一样的情况。一般来说,不可重复读的问题可以接受,因为其读到的是已经提交的数据,本身不会带来很大的问题。
在innodb中,通过使用Next-Key Lock算法来避免不可重复读。在MySQL文档中,将不可重复的也定义成幻读。在Next-Key Lock算法下,对于索引的扫描,不仅锁住扫描到的索引,而且还锁住这些索引覆盖到的范围(gap)。因此这个范围内的插入都是不允许的,这样就避免了另外的事务在这个范围内插入数据导致的不可重复度的问题。
丢失更新是另一个锁导致的问题,就是一个事务的更新操作被另一个事务的更新操作覆盖了,从而导致数据的不一致。
避免更新丢失的做法就是将操作并行化,也就是获取一个排它锁,所以在一个事务中可以这么使用
SELECT * FROM XX WHERE id=? FOR UPDATE
在innodb中,参数innodb_lock_wait_timeout
用来控制等待时间(默认是50秒),innodb_rollback_on_timeout
(静态的,不可在启动时进行修改)来设定是否在等待超时时间对进行中的事务进行回滚(默认是OFF,代表不回滚),innodb_lock_wait_timeout
是动态的,可以在MySQL数据库运行时进行调整
SET @@innodb_lock_wait_timeout=60;
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象
除了超时机制 当前数据库普遍采用wait-for graph(等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测机制。innodb也采用这种方式。wait-for graph要求数据库保存以下两种信息:
通过上述链表可以构造出一张图,而在这个图中若存在回路,则代表存在死锁,因此资源间相互发生等待。在wait-for graph中,事务为图中的节点,而在图中,事务T1指向T2边的定义为:
看下面一个例子
看到Transaction Wait Lists中共有4个事务,故在wait-for graph中应有4个节点
因为t2对row1占用了x锁,t1对row2占用了s锁,t1需要等待t2中row1的资源,因此在wait-for graph中有条边从节点t1指向了t2.
而又因为t2需要等待t1、t4所占用了row2资源,所以存在t2到t1、t4的边。
同样,t3需要等待t1,t4,t2所占用的tow2资源,所以有一条t3到t1,t4,t2的边。
因此最终的wait-for graph如下图所示
通过上述例子,可以发现wait-for graph是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说innodb选择回滚undo量最小的事务
wait-for graph的死锁检测通常采用深度优先算法实现
锁升级是指锁的粒度降低,e.g 行锁升级成页锁,页锁升级成表锁
]]>innodb根据页进行加锁,并采用位图的方式。
innodb常见的索引
B+树索引并不能找到一个给定键值的具体行。B+树索引能找到的只是被查找数据行所在的页。然后数据库通过页读入到内存,再在内存中进行查找,最后得到要查找的数据。
晚点去学习下B+树
B+索引在数据库中又一个特点是高扇出性,因此在数据库中,B+树的高度一般在2~4层。
数据库中B+树索引可以分为聚集索引和辅助索引,但其内部都是B+树,即高度平衡的,叶子节点存放着所有的数据。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的信息
innodb存储引擎表是索引组织表,即表中数据按主键顺序存放。而聚集索引(clustered index)就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子节点成为数据页。聚集索引的这个特性决定了索引组织表中数据也是索引的一部分。同B+树数据结构一样,每个数据页都通过一个双向链表来进行链接
叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签(bookmark)
书签
该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB是索引组织表,因此InnoDB的辅助索引的书签就是相应行数据的聚集索引键。
没看懂
如何查看索引是否是高选择性?
可以通过show index
结果中的列Cardinality
来观察。Cardinality值非常关键,表示索引中不重复记录的预估值。值得注意的是该值是一个预估值并不是准确的值。实际应用中Cardinality/n_rows_in_table
应尽可能地接近1.
MySQL5.6开始支持Multi-Range Read(MRR)优化,MRR优化的目的为了减少磁盘的随机访问,并将随机访问转化为较为顺序的数据访问,这对IO-bound类型的SQL查询语句可带来性能极大的提升。MRR优化可适用于range,ref,eq_ref类型的查询。
MRR优化有以下几个好处
对于InnoDB和MyISAM的范围查询和JOIN查询操作,MRR工作方式如下:
此外,若InnoDB或MyISAM缓冲池不是足够大,不能放下一张表中的所有数据,此时频繁的离散读操作还会导致缓存中的页被替换出缓冲池,然后又不断地读入缓冲池。若是按照主键顺序进行访问,则可以将此重复行为降为最低
ICP同样是MySQL5.6开始支持的一种根据索引进行查询的优化方式。
之前的MySQL版本不支持ICP的话,当进行索引查询时,首先根据索引来查找记录,然后再根据WHERE
条件来过滤记录。
在支持ICP之后,WHERE
操作放在了存储引擎层,在取出索引的同时根据WHERE
条件过滤。在某些查询下,可以大大减少上层SQL层对记录的索取,从而提高数据库的整体性能。
对于缓冲池的哈希表来说,在缓冲池中的Page都有一个chain
指针,他指向相同哈希函数值的页。
对于除法散列,m的取值为略大于2倍的缓冲池页的质数。例如:当前参数
innodb_buffer_pool_size
的大小为10M,则公有640个16kb的页。对于缓冲池内存的哈希表来说,需要分配640*2=1280个槽,但是由于128不是质数,需要取比1280略大的质数,应该是1399,所以启动时会分配1399个槽的哈希表。
那么innodb的缓冲池对于其中的页是怎么进行查找的?
innodb的表空间都有一个space_id
,用户要查找的应该是某个表空间的某个连续16kb的页,即偏移量offset。innodb将space_id
左移20位,然后加上这个space_id
和offset,即关键字K=space_id<<20 + space_id + offset
,然后通过除法散列到各个槽中去。