Week 09 学习总结
本周主要介绍了Java虚拟机JVM的原理、Java代码的优化、秒杀系统设计、宅米架构设计以及搜索引擎相关知识。
1 JVM虚拟机原理
1.1 JVM组成架构
Java是一种跨平台的语言,JVM屏蔽了底层系统的不同,为Java字节码文件构造了一个统一的运行环境。
JVM的组成:
Java运行环境:
1.2 Java字节码
Java如何实现在不同操作系统、不同硬件平台上,都可以不用修改代码就能顺畅地执
行?
计算机领域的任何问题都可以通过增加个中间层(虚拟层)来解决,Java所有的指令有200个左右,一个字节(8位)可以存储256种不同的指令信息,一个这样的字节称为字节码(Bytecode )。
在代码的执行过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖,JVM也可以将字节码编译执行,如果是热点代码,会通过JIT动态地编译为机器码,提高执行效率。
字节码执行流程:
Java字节码编译过程:
1.3 类加载器
字节码是通过类加载器加载的。
同一个JVM的不同的类加载器加载的类是不一样的。
类加载器的双亲委托模型:
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,需要上级类加载器确认,只有当上级类加载器没有加载过这个类,也允许加载的时候, 才让当前类加载器加载这个未知类。
1.4 堆和栈
堆:
每个JVM实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放
在这个堆中,并由应用所有的线程共享。
注意:对象是在堆中分配的,但对象的引用是在栈中分配的。
堆栈:
JVM为每个新创建的线程都分配一个堆栈。也就是说,对于一个Java程序来说,
它的运行就是通过对堆栈的操作来完成的。
Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对
象,而在堆栈中分配的内存只是一个指向这个堆对象的引用而已。
1.5 方法区和程序计数器
方法区主要存放从磁盘加载进来的类字节码,而在程序运行过程中创建的类实例则存放
在堆里。
程序运行的时候,实际上是以线程为单位运行的,当JVM进入启动类的main方法的时候,就会为应用程序创建一个主线程,main方法里的代码就会被这个主线程执行,每个线程有自己的Java栈,栈里存放着方法运行期的局部变量。而当前线程执行
到哪一行字节码指令,这个信息则被存放在程序计数寄存器。
1.6 volitail关键字
Java内存模型规定在多线程情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存变量副本来进行。
被volitail修饰的实例变量或者类变量具备如下两层语义:a.保证了不同线程之间对共享变量操作时的可见性,也就是说当一个线程修改volitail修饰的变量,另外一个线程会立即看到最新的值。b.禁止对指令进行重排序操作。
注意:
volitail并不是锁,volitail只是保证可见性而不能保证线程的安全性。
1.7 JVM的垃圾回收
JVM垃圾回收就是将JVM堆中的已经不再被使用的对象清理掉,释放宝贵的内存资源。
在 JVM 进行垃圾回收之前,首先就是判断哪些对象是垃圾,也就是说,要判断哪些对象是可以被销毁的,其占有的空间是可以被回收的。
根据 JVM 的架构划分,我们知道, 在 Java 世界中,几乎所有的对象实例都在堆中存放,所以垃圾回收也主要是针对堆来进行的。
在 JVM 的眼中,垃圾就是指那些在堆中存在的,已经“死亡”的对象。而对于“死亡”的定义,我们可以简单的将其理解为“不可能再被任何途径使用的对象”。那怎样才能确定一个对象是存活还是死亡呢?这就涉及到了垃圾判断算法,其主要包括引用计数法和可达性分析法。
-引用计数法:
在这种算法中,假设堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并且初始化赋值后,该对象的计数器的值就设置为 1,每当有一个地方引用它时,计数器的值就加 1,例如将对象 b 赋值给对象 a,那么 b 被引用,则将 b 引用对象的计数器累加 1。
反之,当引用失效时,例如一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,则之前被引用的对象的计数器的值就减 1。而那些引用计数为 0 的对象,就可以称之为垃圾,可以被收集。
特别地,当一个对象被当做垃圾收集时,它引用的任何对象的计数器的值都减 1。
· 优点:引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
· 缺点:需要额外的空间来存储计数器,难以检测出对象之间的循环引用。
-可达性分析法:
可达性分析法也被称之为根搜索法,可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。
更准确的说,一个对象只有满足下述两个条件之一,就会被判断为可达的:
· 对象是属于根集中的对象
· 对象被一个可达的对象引用
在这里,我们引出了一个专有名词,即根集,其是指正在执行的 Java 程序可以访问的引用变量(注意,不是对象)的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。在 JVM 中,会将以下对象标记为根集中的对象,具体包括:
· 虚拟机栈(栈帧中的本地变量表)中引用的对象
· 方法区中的常量引用的对象
· 方法区中的类静态属性引用的对象
· 本地方法栈中 JNI(Native 方法)的引用对象
· 活跃线程(已启动且未停止的 Java 线程)
根集中的对象称之为GC Roots,也就是根对象。
· 优点:可以解决循环引用的问题,不需要占用额外的空间。
· 缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用。
可达性分析法的基本思路是:
将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为不可达对象。
在可达性分析法中,对象有两种状态,那么是可达的、要么是不可达的,在判断一个对象的可达性的时候,就需要对对象进行标记,所有被标记过的对象都是被使用的对象,而那些没有被标记的对象就是可回收的垃圾对象了。
进行完标记以后,JVM就会对垃圾对象占用的内存进行回收,回收主要有三种方法:
清理:
将垃圾对象占据的内存清理掉,其实JVM并不会真的将这些垃圾内存进行清理,而是将这些垃圾对象占用的内存空间标记为空闲,记录在-个空闲列表里,当应用程序需要创建新对象的时候,就从空闲列表中找一-段空闲内存分配给这个新对象。
压缩:
从堆空间的头部开始,将存活的对象拷贝放在一段连续的内存空间中,那么其余的空
间就是连续的空闲空间。
复制:
将堆空间分成两部分,只在其中一-部分创建对象,当这个部分空间用完的时候,将标
记过的可用对象复制到另一个空间中。
JVM分代垃圾回收:
分代收集(Generational Collector)算法的将堆内存划分为新生代、老年代和永久代。
新生代又被进一步划分为 Eden 和 Survivor 区,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)组成。
所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
分代收集,是基于这样一个事实:
不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收,以便提高回收效率。
在分代收集算法中,对象的存储具有以下特点:
1. 对象优先在 Eden 区分配。
2. 大对象直接进入老年代。
3. 长期存活的对象将进入老年代,默认为 15 。
GC 的分类:
-新生代 GC(Minor GC / Scavenge GC):
发生在新生代的垃圾收集动作。因为 Java 对象大多都具有朝生夕灭的特性,因此 Minor GC 非常频繁(不一定等 Eden 区满了才触发),一般回收速度也比较快。
在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
-老年代 GC(Major GC / Full GC):
发生在老年代的垃圾回收动作。Major GC 经常会伴随至少一次 Minor GC。由于老年代中的对象生命周期比较长,因此 Major GC 并不频繁,一般都是等待老年代满了后才进行 Full GC,而且其速度一般会比 Minor GC 慢10倍以上。
另外,如果分配了 Direct Memory,在老年代中进行 Full GC 时,会顺便清理掉 Direct Memory 中的废弃对象。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”算法或“标记-整理”算法来进行回收。
新生代采用空闲指针的方式来控制 GC 触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发 GC。
当连续分配对象时,对象会逐渐从 Eden 到 Survivor,最后到老年代。
垃圾回收器:
具体JVM进行垃圾回收是靠垃圾回收器来实现。
垃圾回收(GC)线程与应用线程保持相对独立,当系统需要执行垃圾回收任务时,先停止工作线程,然后命令 GC 线程工作。
垃圾回收器的分类:
-Serial 收集器
串行收集器采用单线程方式进行收集,且在 GC 线程工作时,系统不允许应用线程打扰。此时,应用程序进入暂停状态,即 Stop-the-world。Stop-the-world 暂停时间的长短,是衡量一款收集器性能高低的重要指标。Serial 是针对新生代的垃圾回收器,采用“复制”算法。
-ParNew 收集器
并行收集器充分利用了多处理器的优势,采用多个 GC 线程并行收集。可想而知,多条 GC 线程执行显然比只使用一条 GC 线程执行的效率更高。一般来说,与串行收集器相比,在多处理器环境下工作的并行收集器能够极大地缩短 Stop-the-world 时间。ParNew 是针对新生代的垃圾回收器,采用“复制”算法,可以看成是 Serial 的多线程版本
-Parallel Scavenge 收集器
Parallel Scavenge 是针对新生代的垃圾回收器,采用“复制”算法,和 ParNew 类似,但更注重吞吐率。在 ParNew 的基础上演化而来的 Parallel Scanvenge 收集器被誉为“吞吐量优先”收集器。
吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。如虚拟机总运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是99%。
-Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,单线程收集器,采用“标记-整理”算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。
-Parallel Old 收集器
Parallel Old 是 Parallel Scanvenge 收集器的老年代版本,多线程收集器,采用“标记-整理”算法。
-CMS收集器
CMS(Concurrent Mark Swee)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器仅作用于老年代的收集,采用“标记-清除”算法。
它的运作过程分为 4 个步骤:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
CMS 收集器优点:并发收集,低停顿。
CMS 收集器缺点:
CMS 收集器对 CPU 资源非常敏感;
CMS 收集器无法处理浮动垃圾;
CMS 收集器是基于“标记-清除”算法,该算法的缺点都有。
G1 收集器
G1(Garbage First)重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成,即 G1 提供了接近实时的收集特性。
G1 具备如下特点:
并行与并发:
G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-the-world 停顿的时间,部分其他收集器原来需要停顿 Java 线程执行的 GC 操作,G1 收集器仍然可以通过并发的方式让 Java 程序继续运行。
分代收集:
打破了原有的分代模型,将堆划分为一个个区域。
空间整合:
与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
可预测的停顿:
这是 G1 相对于 CMS 的一个优势,降低停顿时间是 G1 和 CMS 共同的关注点。
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。在堆的结构设计时,G1 打破了以往将收集范围固定在新生代或老年代的模式,G1 将堆分成许多相同大小的区域单元,每个单元称为 Region,Region 是一块地址连续的内存空间。
G1 收集的运作过程大致如下:
初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这阶段需要停顿线程,但耗时很短。
并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行。
筛选回收(Live Data Counting and Evacuation):首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
G1 的 GC 模式可以分为两种,分别为:
Young GC:在分配一般对象(非巨型对象)时,当所有 Eden 区域使用达到最大阀值并且无法申请足够内存时,会触发一次 YoungGC。每次 Young GC 会回收所有 Eden 以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。
Mixed GC:当越来越多的对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并不是一个 Old GC,除了回收整个新生代,还会回收一部分的老年代,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些 Old 区域进行收集,从而可以对垃圾回收的耗时时间进行控制。G1 没有 Full GC概念,需要 Full GC 时,调用 Serial Old GC 进行全堆扫描。
参考资料:
1.8 JVM性能诊断工具
基本工具:JPS , JSTAT,JMAP
集成工具:JConsole,JVisualVM
2 Java代码优化
2.1 合理并谨慎使用多线程
-多线程使用场景(I/O阻塞,多 CPU 并发)
-多线程要考虑资源争用与同步问题。
-java.util.concurrent包提供了很多有用的类,方便我们进行并发程序的开发。
-系统启用线程数可参考以下公式:
启动线程数 = [任务执行时间 / (任务执行时间 - IO等待时间)] * CPU内核数
说明:
最佳启动线程数和CPU内核数量成正比,和IO阻塞时间成反比。
如果任务都是CPU计算型任务,那么线程数最多不超过CPU内核数,因为启动再多线程,CPU 也来不及调度;
相反如果是任务需要等待磁盘操作,网络响应,那么多启动线程有助于提高任务并发度,提高系统吞吐能力,改善系统性能。
-竞态条件与临界区
在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。
在临界区中使用适当的同步就可以避免竞态条件。
2.2 Java线程安全
允许被多个线程安全执行的代码称作线程安全的代码。
局部变量:
存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,
基础类型的局部变量是线程安全的。
局部的对象引用:
如果在某个方法中创建的对象不会逃逸出该方法,那么它就是线程安全的。
对象成员:
存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是
线程安全的。
2.3 ThreadLocal
ThreadLocal并不是一个Thread,而是Thread的局部变量。意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal实现主要涉及Thread,ThreadLocal,ThreadLocalMap这三个类。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作,这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。
-ThreadLocal类中提供的方法:
1.public T get() { }
2.public void set(T value) { }
3.public void remove() { }
4.protected T initialValue(){ }
基本用法:
创建一个ThreadLocal变量:
private ThreadLocal myThreadLocal = new ThreadLocal();
存储此对象的值:
myThreadLocal.set("A thread local value");
读取一个ThreadLocal对象的值:
String threadLocalValue = (String) myThreadLocal.get();
-ThreadLocal和Synchronized区别:
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同点如下:
Synchronized是通过线程等待,牺牲时间来解决访问冲突。
ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
-ThreadLocal使用场景:
最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理;
在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。
当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
-ThreadLocal原理:
(1)每个Thread维护着一个ThreadLocalMap的引用
(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。
(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中
(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。
(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
-ThreadLocal的内存泄露:
这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。
1、Thread中有一个map,就是ThreadLocalMap
2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
2.4 Java内存泄露
Java内存泄漏是由于开发人员的错误引起的。
-如果程序保留对永远不再使用的对象的引用,这些对象将会占用并耗尽内存:
·长生命周期对象
·静态容器
·缓存
-合理使用线程池和对象池:
·复用线程或对象资源,避免在程序的生命期中创建和删除大量对象
·池管理算法(记录哪些对象是空闲的,哪些对象正在使用)
·对象内容清除(ThreadLocal的清空)
-缩短对象生命周期,加速垃圾回收:
·减少对象驻留内存的时间
·在使用时创建对象,用完释放
·创建对象的步骤(静态代码段–静态变量-父类构造函数–子类构造函数)
3 秒杀系统设计
秒杀是电子商务网站常见的一种营销手段:将少量商品(通常只有一件)以极低的 价格,在特定的时间点开始出售。
3.1 秒杀系统需要面对的技术挑战
1.对现有网站业务造成冲击:
秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特
点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致
整个网站瘫痪。
2.高并发下的应用、数据库负载:
用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果
按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服
务器造成极大的负载压力。
3.突然增加的网络及服务器带宽:
假设商品页面大小 200K(主要是商品图片大小),那么需要的网络和服务器带宽是
2G(200K×10,000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。
4.直接下单:
秒杀的游戏规则是到了秒杀时间才能开始对商品下单购买,在此时间点之前,只能
浏览商品信息,不能下单。而下单页面也是一个普通的 URL,如果得到这个 URL,不用
等到秒杀开始就可以下单了。
3.2 秒杀系统的应对策略
1.秒杀系统独立部署
为了避免因为秒杀活动的高并发访问而拖垮整个网站,使整个网站不必面对蜂拥而 来的用户访问,可将秒杀系统独立部署;
如果需要,还可以使用独立的域名,使其与网站完全隔离,即使秒杀系统崩溃了,也不会对网站造成任何影响。
2.秒杀商品页面静态化
重新设计秒杀商品页面,不使用网站原来的商品详情页面,秒杀商品页面内容静态化:将商 品描述、商品参数、成交记录和用户评价全部写入一个静态页面,用户请求不需要经过 应用服务器的业务逻辑处理,也不需要访问数据库。
所以秒杀商品服务不需要部署动态的Web服务器和数据库服务器。
3.租借秒杀活动网络带宽
因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器 的压力,需要将秒杀商品页面缓存在CDN,同样需要和 CDN 服务商临时租借新增的出口带宽。
4.动态生成随机下单页面
URL 为了避免用户直接访问下单页面 URL,需要将该 URL 动态化,即使秒杀系统的开发 者也无法在秒杀开始前访问下单页面的 URL。
办法是在下单页面 URL 加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到。
3.3 秒杀系统的核心设计
核心解决方案的关键词:静态化、限流。
1. 实现秒杀相关页面的静态化及独立部署:
可将秒杀业务独立于原系统进行部署;
采用JS自动更新技术将动态页面转化为静态页面;
2. 秒杀购买按钮的点亮控制:
商品页面中的购买按钮只有在秒杀活动开始的时候才变亮,在此之前及秒杀商品卖出后,该按钮都是灰色的,不可以点击:
可使用 JavaScript 脚本控制,在秒杀商品静态页面中加入一个 JavaScript文件引用,该 JavaScript 文件中加入秒杀是否开始的标志和下单页面 URL 的随机数参数,当秒杀开始的时候生成一个新的 JavaScript 文件并被用户浏览器加载,控制秒杀商品页面 的展示。
这个 JavaScript 文件使用随机版本号,并且不被浏览器、CDN 和反向代理服务器缓存:
这个 JavaScript 文件非常小,即使每次浏览器刷新都访问 JavaScript 文件服务器也不 会对服务器集群和网络带宽造成太大压力。
3. 通过阀门限制进入秒杀系统的人数:
为了减轻下单页面服务器的负载压力,可设置阀门,只允许少数用户能进入秒杀页面、下单页面及支付系统,其他用户直接进入秒杀结束页面:
3.4 秒杀系统的应急预案示例
-域名分离,独立域名,不影响XXXX原有业务
>Style 集群:style.XXXX.china.XXXX.com
>图片服务器集群:img.XXXX.china.XXXX.com
>静态页面集群:page.XXXX.china.XXXX.com
>出问题直接把XXXX相关域名卡掉,所有请求跳到万能出错页面。
-机动服务器10台,备用。
-拆东墙补西墙战略:
>5天时间来不及采购服务器,因此SA待命,随时准备将非核心应用集群的冗余服务器下线,加入到秒杀集群。
-壁虎断尾策略:
>所有办法均失效的情况下,例如流量耗尽。
>非核心应用集群统统停止服务,如资讯,论坛,博客等社区系统。
>保住首页,Offer Detail,旺铺页面等核心应用的可用性。
-万能出错页面:秒杀活动已经结束
>任何出错都302跳转到此页面
>位于另外集群
4 搜索引擎
4.1 互联网搜索引擎整体架构
4.2 爬虫系统架构
4.3 爬虫禁爬协议
在网站首页根路径下添加Robot.txt文件,里面可以指定哪些路径下的文件是允许爬虫搜索的。
4.4 文档矩阵与倒排索引
搜索引擎之所以可以很快进行搜索核心在于使用了倒排索引。
-倒排索引示例:
-带词频的倒排索引示例:
-带词频和位置的倒排索引示例:
4.5 Lucene搜索引擎架构
-Lucene的倒排索引:
-Lucene 索引文件准实时更新:
索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,性能也很差。
Lucene 中引入了段的概念,将一个索引文件拆分为多个子文件,每个子文件叫做段,每个段都是一个独立的可被搜索的数据集,索引的修改针对段进行操作。
新增:
当有新的数据需要创建索引时,原来的段不变,选择新建一个段来存储新增的数据。
删除:
当需要删除数据时,在索引文件新增一个.del的文件,用来专门存储被删除的数据ID。当查询时,被删除的数据还是可以被查到的,只是在进行文档链表合并时,才把已经删除的数据过滤掉。被删除的数据在进行段合并时才会被真正被移除。
更新:
更新的操作其实就是删除和新增的组合,先在.del 文件中记录旧数据,再在新段中添加一条更新后的数据。
为了控制索引里段的数量,我们必须定期进行段合并操作。
注意:
Lucene不支持分布式!对于大数据量的分布式搜索可用ElasticSearch。
4.6 ElasticSearch架构
ElasticSearch特点:
·索引分片,实现分布式
·索引备份,实现高可用
·API更简单、更高级
评论