架构师训练营第 9 周学习总结
9.1 数据库的基本原理
PrepareStatement 预编译
PrepareStatement 会预先提交带占位符的SQL 到数据库进行预处理,提前生成执行计划,当给定占位符参数,真正执行SQL 的时候,执行引擎可以直接执行,效率更好一点。
PrepareStatement 可以防止SQL 注入攻击。
数据库架构
连接器 - 数据库连接器会为每个连接请求分配一块专用的内存空间用于会话上下文管理。建立连接对数据库而言相对比较重,需要花费一定的时间,因此应用程序启动的时候,通常会初始化建立一些数据库连接放在连接池里,这样当处理外部请求执行SQL 操作的时候,就不需要花费时间建立连接了。
语法分析器 - 抽象语法树
语义分析与优化器 - 语义分析与优化器就是要将各种复杂嵌套的SQL 进行语义等价转化,得到有限几种关系代数计算结构,并利用索引等信息进一步进行优化。
执行计划
B+树
B+树是多层的检索树,数据记录在叶子节点上。
聚簇索引
聚簇索引:聚簇索引的数据库记录和索引存储在一起。
MySQL 数据库的主键就是聚簇索引,主键ID 和所在的记录行存储在一个B+树中。
非聚簇索引
非聚簇索引在叶子节点记录的就不是数据行记录,而是聚簇索引,也就是主键。
通过非聚簇索引找到主键索引,再通过主键索引找到行记录的过程也被称作回表。
添加必要的索引优化SQL查询性能
在几百万行的数据库中查找一个条记录,如果没有索引,就需要全表扫描,检索所有的行记录,才能找到需要的记录。
合理使用索引
不要盲目添加索引,尤其在生产环境中
添加索引的alter操作会消耗较长的时间(分钟级)
Alter操作期间,所有数据库的增删改操作全部阻塞,对应用而言,因为连接不能释放,事实上,查询也被阻塞。
删除不用的索引,避免不必要的增删开销。索引对查询的性能好很多,但是索引越多,插入一条记录的时候,更新的B+树也越多,性能会变差。
使用更小的数据类型创建索引
数据库事务
事务特性ACID
原子性(Atomicity): 事务要么全部完成,要么全部取消。如果事务崩溃,状态回到事务之前(事务回滚)。
隔离性(Isolation): 如果2个事务T1 和T2 同时运行,事务T1 和T2 最终的结果是相同的,不管T1和T2谁先结束,隔离性主要依靠锁实现。
持久性(Durability): 一旦事务提交,不管发生什么(崩溃或者出错),数据要保存在数据库中。
一致性(Consistency): 只有合法的数据(依照关系约束和函数约束)才能写入数据库。
数据库事务日志
进行事务操作时,事务日志文件会记录更新前的数据记录,然后再更新数据库中的记录,如果全部记录都更新成功,那么事务正常结束,如果过程中某条记录更新失败,那么整个事务全部回滚,已经更新的记录根据事务日志中记录的数据进行恢复,这样全部数据都恢复到事务提交前的状态,仍然保持数据一致性。
9.2 JVM虚拟机架构原理
JVM 组成架构
类加载器
运行期数据区
方法区 - 记录.class的字节码指令
堆 - 记录创建出来的对象或者数组
Java栈 - 每个线程有自己的一个栈
程序计数寄存器 - 每个线程有自己的程序计数寄存器,记录当前执行到字节码的哪一行指令里去了。
执行引擎
Java字节码文件
Java 所有的指令有200 个左右,一个字节( 8 位)可以存储256 种不同的指令信息,一个这样的字节称为字节码( Bytecode )。在代码的执行过程中, JVM 将字节码解释执行,屏蔽对底层操作系统的依赖, JVM 也可以将字节码编译执行,如果是热点代码,会通过JIT 动态地编译为机器码,提高执行效率。
字节码执行流程
Java 字节码文件编译过程
类加载器的双亲委托模型
自定义类加载器
堆& 栈
堆:每个JVM 实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享。
堆栈:JVM 为每个新创建的线程都分配一个堆栈。也就是说,对于一个Java 程序来说,它的运行就是通过对堆栈的操作来完成的。
Java 中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的引用而已。
方法区& 程序计数器
方法区主要存放从磁盘加载进来的类字节码,而在程序运行过程中创建的类实例则存放在堆里。程序运行的时候,实际上是以线程为单位运行的,当JVM 进入启动类的main方法的时候,就会为应用程序创建一个主线程,main 方法里的代码就会被这个主线程执行,每个线程有自己的Java 栈,栈里存放着方法运行期的局部变量。而当前线程执行到哪一行字节码指令,这个信息则被存放在程序计数寄存器。
Java(线程)栈
所有在方法内定义的基本类型变量,都会被每个运行这个方法的线程放入自己的栈中,线程的栈彼此隔离,所以这些变量一定是线程安全的。
线程工作内存& volatile
Java 内存模型规定在多线程情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存变量副本来进行。
一个共享变量(类的成员变量、类的静态成员变量)被volatile 修饰之后,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
9.3 JVM垃圾回收性能分析
JVM 的垃圾回收
JVM 垃圾回收就是将JVM 堆中的已经不再被使用的对象清理掉,释放宝贵的内存资源。
JVM 通过一种可达性分析算法进行垃圾对象的识别
垃圾对象清理主要有三种方法:
清理
压缩
复制
JVM 分代垃圾回收:新生代和老年代
JVM 垃圾回收器算法
串行回收器 - stop the world,单核CPU
并行回收器 - stop the world, 多核CPU。所有的用户线程都被终止
并发垃圾回收器CMS
初始化标记
并发标记
重标记
并发清理
G1 垃圾回收器
拆分成小块,不同区域。
-XX:MaxGCPauseMillis
Java启动参数
标准参数,所有的JVM 实现都必须实现这些参数的功能,而且向后兼容
运行模式-server,-client
类加载路径-cp,-classpath
运行调试–verbose
系统变量–D
非标准参数, 默认jvm实现这些参数,但不保证所有JVM 实现都实现,且不保证向后兼容
-Xms 初始堆大小
-Xmx 最大堆大小
-Xmn 新生代大小
-Xss 线程堆栈大小
非Stable参数, 此类参数各个jvm实现会有所不同,将来可能会随时取消
-XX:-UseConcMarkSweepGC 启用CMS 垃圾回收
JVM 性能诊断工具
基本工具:JPS ,JSTAT,JMAP,JSTACK
JPS 用来查看host 上运行的所有Java 进程的pid(jvmid),一般情况下使用这个工具的目的只是为了找出运行的JVM 进程ID,即lvmid,然后可以进一步使用其它的工具来监控和分析JVM
JSTAT( “Java Virtual Machine statistics monitoring tool” )是JDK 自带的一个轻量级小工具。主要对Java 应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size 和垃圾回收状况的监控。
JMAP 是一个可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。
jstack 可以查看JVM 内的线程堆栈信息
集成工具: JConsole,JVisualVM
9.4 Java代码优化技巧及原理
合理并谨慎使用多线程
使用场景(I/O 阻塞,多CPU 并发)
资源争用与同步问题
java.util.concurrent
启动线程数= [任务执行时间/ (任务执行时间- IO 等待时间)] * CPU 内核数
竞态条件与临界区
在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。
在临界区中使用适当的同步就可以避免竞态条件。
Java 线程安全
允许被多个线程安全执行的代码称作线程安全的代码。
方法局部变量
局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。
方法局部的对象引用
如果在某个方法中创建的对象不会逃逸出该方法,那么它就是线程安全的。
对象成员变量
对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。
ThreadLocal
Java 内存泄漏
Java 内存泄漏是由于开发人员的错误引起的。
如果程序保留对永远不再使用的对象的引用,这些对象将会占用并耗尽内存。
长生命周期对象
静态容器
缓存
合理使用线程池和对象池
复用线程或对象资源,避免在程序的生命期中创建和删除大量对象
池管理算法(记录哪些对象是空闲的,哪些对象正在使用)
对象内容清除(ThreadLocal 的清空)
使用合适的JDK 容器类(顺序表,链表,Hash)
LinkList 和ArrayList 的区别及适用场景
HashMap 的算法实现及应用场景
使用concurrent 包,ConcurrentHashMap 和HashMap 的线程安全特性有什么不同?
缩短对象生命周期,加速垃圾回收
减少对象驻留内存的时间
在使用时创建对象,用完释放
创建对象的步骤(静态代码段-静态成员变量-父类构造函数-子类构造函数)
使用I/O buffer 及NIO
延迟写与提前读策略
异步无阻塞IO 通信
优先使用组合代替继承
减少对象耦合
避免太深的继承层次带来的对象创建性能损失
合理使用单例模式
无状态对象
线程安全
9.5 系统性能优化案例:秒杀系统(上)
高并发下的风险
网络带宽耗尽
服务器Load 飙高,停止响应。
数据库瘫痪
高并发下的事故
事故:网站运营推广页面弹出1兆大图片导致带宽耗尽
增加审核机制:运营推广增加的图片流量不能超过现有流量的30%
合作媒体推广:迅雷,暴风影音浮出广告,导致ZZ 集群Crash
技术挑战
瞬间高并发
8000 并发:预估秒杀在线人数可达8000 人。
风险:带宽耗尽。
服务器:崩溃,可以理解成自己给自己准备的D.D.O.S 攻击。
秒杀器
第一种:秒杀前不断刷新秒杀页面,直到秒杀开始,抢着下单。
第二种:跳过秒杀页面,直接进入下单页面,下单。
9.6 系统性能优化案例:秒杀系统(下)
部署秒杀服务器集群,构建一套新的秒杀系统。新的秒杀系统和原系统完全隔离。
秒杀系统:服务器和网络准备
服务器准备(隔离出一部分服务器)
style 服务器(Lighttpd集群):5台
图片服务器(Nginx集群) :5台
静态服务器(Apache集群) :10台
交易服务器(JBoss动态集群):10台
带宽准备
图片出口带宽上限:2.5G (出口带宽支持10G,但图片服务器集群的处理能力:图片服务集群最大并发处理能力X 网站平均图片大小= 2.5G)
CDN 准备:Chinacache 沟通;借用CCCC CDN
秒杀系统:架构目标
图片网络带宽:1.0G
新增图片带宽:必须控制在1.0G 左右
每件商品秒杀页面的图片总大小不得超过:1000000/(1000*8) = 125K/每商品
网站并发:
单件商品并发:1000 【来自运营的预估】
总并发: 8(件商品)X 1000(人/商品)= 8000
秒杀系统:组成
简单系统:
三个页面组成:秒杀商品列表,秒杀商品介绍,下单
秒杀系统:设计原则
静态化
采用JS 自动更新技术将动态页面转化为静态页面
并发控制,防秒杀器
设置阀门,只放最前面的一部分人进入秒杀系统
简化流程
砍掉不重要的分支流程,如下单页面的所有数据库查询
以下单成功作为秒杀成功标志。支付流程只要在1 天内完成即可。
前端优化
采用YSLOW 原则提升页面响应速度
秒杀系统:静态化
秒杀商品list 和Detail 是静态HTML 页面
秒杀商品列表/秒杀商品介绍页面,如何判断秒杀开始否。
答案: valid-offer.js
三道阀门的设计
阀门:基于TT 的计数器
秒杀器的预防
秒杀Detail 页面
URL:随机
秒杀前2秒放出,脚本生成,秒杀前。
1000次访问上限控制【每件商品只能放入1000人浏览】。
下单页面:
订单ID,随机。
不能直接跳过秒杀Detail 页面进入。
每个秒杀商品,带预先生成的随机Token 作URL 参数。
如果秒杀过,直接跳到秒杀结束页面。
100 次访问上限控制【每件商品只能放入100人下单】。
Web Server 调优
Apache调优
Boss调优
秒杀静态页面优化
图片合并
8 张图片合并成1 张,CSS 偏移展示。
减少HTTP 请求数,减少请求等待数。
减少发送Cookies 的量。
HTML 内容压缩
图片压缩:图片Bytes < 长X宽/2250
HTML Header Cache-Control 设置
CSS,JS 精简
CSS,JS 精简到极致,部分直接写在页面中,减少HTTP 请求次数。
下单页面优化
数据库操作:全部砍掉
原下单页面要访问8 次数据库,全部砍掉。
秒杀流程精简
砍掉填写或选择收货地址,放在秒杀成功后填写。
砍掉调用是否开通支付接口,秒杀首页文案提示必须开通。
采用内存缓存
秒杀Offer 数据,支付相关信息,缓存。
交易系统性能优化
二跳页面的优化
XXXX.com 其他页面
前端优化: Yslow 规则调优
减少HTTP 请求,合并JS,CSS,图片,充分利用浏览器缓存。
图片压缩
避免发送Cookies
交易系统优化
普通订单管理列表和XXXX 秒批订单管理列表分离
禁止用模糊查询功能
应急预案
域名分离,独立域名,不影响XXXX原有业务。
机动服务器
拆东墙补西墙战略
壁虎断尾策略
万能出错页面:秒杀活动已经结束
评论