RxLoaron's Blog

Stay Hungry,Stay Foolish

⼀、引⾔
随着业务的发展,业务复杂度变⾼、⽤⼾量增加、并发量变⼤、数据量增加、技术复杂度变⾼、系统
调⽤链路变⻓等⼀系列变化会凸显出来,最终导致系统的性能急剧下降,⼯作中就需要持续去优化系
统,以应对业务的发展需要。本⽂从接⼝性能优化的⻆度,去分析接⼝性能优化需要关注的指标,以
及常⽤的性能优化⼿段,希望给⼤家⽇常⼯作带来帮助和启发。
⼆、接⼝性能的关键指标
关键指标
QPS(Queries Per Second)每秒查询
每秒查询数率,系统每秒能够处理的查询请求次数,即⼀台服务器每秒能够相应的查询次数,是对⼀
个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
有两种计算公式:
QPS = req/sec = 请求数/秒
QPS = 总请求数 / ( 进程总数 * 请求时间 )
TPS(Transactions Per Second)每秒事务
每秒事务数,即每秒系统能够处理的事务次数。
TPS 的过程包括:客⼾端请求服务端、服务端内部处理、服务端返回客⼾端。客⼾机在发送请求时开
始计时,收到服务器响应后结束计时,以此来计算使⽤的时间和完成的事务个数。
TPS与QPS区别
⼀个事务是指⼀个客⼾机向服务器发送请求然后服务器做出反应的过程。⽽在这个TPS中,为了处理第
⼀次请求可能会引发后续多次对服务端的访问才能完成这次⼯作,每次访问都算⼀个QPS。所以,⼀
个TPS可能包含多个QPS
对于⼀个⻚⾯的⼀次访问,形成⼀个Tps;但⼀次⻚⾯请求,可能产⽣多次对服务器的请求,服务器对
这些请求,就可计⼊“Qps”之中。如:访问⼀个⻚⾯会请求服务器3次,⼀次放,产⽣⼀个“T”,
产⽣3个“Q”
RT(Response-time)响应时间
执⾏⼀个请求从开始到最后收到响应数据所花费的总体时间,即从客⼾端发起请求到收到服务器响应
结果的时间。它的数值⼤⼩直接反应了系统的快慢。
⽤⼾平均请求等待时间(Time per requests)
计算公式:⽤⼾平均请求等待时间 = 总时间 / (总请求数 / 并发⽤⼾数)
服务器平均请求等待时间(Time per requests: across all concurrent requests)
计算公式:服务器平均等待时间 = 总时间 / 总请求数 = ⽤⼾平均请求等待时间 / 并发⽤⼾数
QPS和RT的关系?
1.对于⼤部分web系统,响应时间⼀般由CPU执⾏时间,线程等待时间(IO等待,sleep, wait)时间组
成。QPS和RT成反⽐关系
2.在实际的测试环境中,QPS和RT并不是⾮常直接的反⽐关系
并发数(The number of concurrent connections)
并发请求数/连接数,是指系统同时能处理的请求数量,这个也是反应了系统的负载能⼒。
计算公式:并发量 = QPS * 平均响应时间,并发请求数 = 并发⽤⼾数 * 单个⽤⼾平均请求数
并发⽤⼾数(The number of concurrent users, Concurrent Level)
指的是某个时刻同时在线的⽤⼾数。⼀个⽤⼾可能同时会产⽣多个会话,也即多个请求连接数。
RPS(Requests Per Second )吞吐量
吞吐量是指单位时间内系统能处理的请求数量,单位是 reqs/s,体现系统处理请求的能⼒。
吞吐率是基于并发⽤⼾数的。这句话代表了两个含义:
a、吞吐率和并发⽤⼾数相关;
b、不同的并发⽤⼾数下,吞吐率⼀般是不同的
某个并发⽤⼾数下单位时间内能处理的最⼤的请求数,称之为最⼤吞吐率。
系统的吞吐量(承压能⼒)与request对CPU的消耗、外部接⼝、IO等等紧密关联。单个request 对
CPU消耗越⾼,外部系统接⼝、IO速度越慢,系统吞吐能⼒越低,反之越⾼。系统吞吐量⼏个重要参
数:QPS(TPS)、并发数、响应时间。
计算公式:吞吐率 =并发请求数/总请求处理时⻓
⽐如:在并发⽤⼾数为1000时,⼀共有5000个请求,请求了5分钟,那么每秒钟,服务器可以处理
5000/560 = 16个请求呢。这就是服务器的吞吐率。
PV(Page View)⻚⾯访问量
⻚⾯被浏览的次数,每次⽤⼾访问或者刷新⻚⾯都会被计算在内。
⽤⼾对同⼀⻚⾯的多次刷新,访问量累计。与 PV 相关的还有 RV,即重复访问者数量(repeat
visitors)。
计算公式:
⽇PV=QPS
606024 //即QPS乘以⼀天的秒数
峰值QPS=(⽇PV80%)/(60602420%) //通⽤公式每天80%的访问集中在20%的时间⾥,这20%时
间叫做峰值时间
UV(Unique Visitor)独⽴访客访问数
统计 1 天内访问某站点的⽤⼾数(以 cookie 为依据),⼀台电脑终端为⼀个访客。
可以理解成访问某⽹站的电脑的数量。⽹站判断来访电脑的⾝份是通过来访电脑的 cookies 实现的。
如果更换了 IP 后但不清除 cookies,再访问相同⽹站,该⽹站的统计中 UV 数是不变的。如果⽤⼾不
保存 cookies 访问、清除了 cookies 或者更换设备访问,计数会加 1。00:00-24:00 内相同的客⼾端多
次访问只计为 1 个访客。
IP(Internet Protocol)独⽴ IP 数
指 1 天内多少个独⽴的 IP 浏览了⻚⾯,即统计不同的 IP 浏览⽤⼾数量。
同⼀ IP 不管访问了⼏个⻚⾯,独⽴ IP 数均为 1;不同的 IP 浏览⻚⾯,计数会加 1。IP 是基于⽤⼾⼴域
⽹ IP 地址来区分不同的访问者的,所以,多个⽤⼾(多个局域⽹ IP)在同⼀个路由器(同⼀个⼴域⽹
IP)内上⽹,可能被记录为⼀个独⽴ IP 访问者。如果⽤⼾不断更换 IP,则有可能被多次统计。
实际举例
我们通过⼀个实例来把上⾯⼏个概念串起来理解。如果每天 80% 的访问集中在 20% 的时间⾥,这
20% 时间就叫做峰值时间。
公式:(总PV数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数(QPS)
机器:峰值时间每秒QPS / 单台机器的QPS = 需要的机器
1、每天300w PV 的在单台机器上,这台机器需要多少QPS?
( 3000000 * 0.8 ) / (86400 * 0.2 ) = 139 (QPS)
2、如果⼀台机器的QPS是58,需要⼏台机器来⽀持?
139 / 58 = 3
3、服务器计算
服务器数量 = ceil( 每天总PV / 单台服务器每天总PV )
最佳线程数
1、单线程QPS公式:QPS=1000ms/RT
对同⼀个系统⽽⾔,⽀持的线程数越多,QPS越⾼。假设⼀个RT是80ms, 则可以很容易的计算出QPS,
QPS = 1000/80 = 12.5
多线程场景,如果把服务端的线程数提升到2,那么整个系统的QPS则为 2(1000/80) = 25, 可⻅
QPS随着线程的增加⽽线性增⻓,那QPS上不去就加线程呗,听起来很有道理,公司也说的通,但是
往往现实并⾮如此。
2、最佳线程数量
刚好消耗完服务器的瓶颈资源的临界线程数,公式如下
最佳线程数量=((线程等待时间+线程cpu时间)/线程cpu时间)
cpu数量
特性:
• 在达到最佳线程数的时候,线程数量继续递增,则QPS不变,⽽响应时间变⻓,持续递增线程数
量,则QPS开始下降。
• 每个系统都有其最佳线程数量,但是不同状态下,最佳线程数量是会变化的。
• 瓶颈资源可以是CPU, 可以是内存,可以是锁资源,IO资源:超过最佳线程数-导致资源的竞争,超
过最佳线程数-响应时间递增。
常⽤压测⼯具
1、Apache JMeter
JMeter作为⼀款⼴为流传的开源压测产品,最初被设计⽤于Web应⽤测试,如今JMeter可以⽤于测试
静态和动态资源,例如静态⽂件、Java ⼩服务程序、CGI 脚本、Java 对象、数据库、FTP服务器等
等,还能对服务器、⽹络或对象模拟巨⼤的负载,通过不同压⼒类别测试它们的强度和分析整体性
能。另外,JMeter能够对应⽤程序做功能测试和回归测试,通过创建带有断⾔的脚本来验证你的程序
返回了你期望的结果。为了最⼤限度的灵活性,JMeter允许使⽤正则表达式创建断⾔。
JMeter的特点:
• 包括对HTTP、FTP服务器、数据库进⾏压⼒测试和性能测试;
• 完全的可移植性;完全 Swing和轻量组件⽀持包;
• 完全多线程;
• 缓存和离线分析/回放测试结果;
• 可链接的取样器;
• 具有提供动态输⼊到测试的功能;
• ⽀持脚本编程的取样器等。在设计阶段,
• JMeter能够充当HTTP PROXY(代理)来记录浏览器的HTTP请求,也可以记录Apache等
WebServer的log⽂件来重现HTTP流量,并在测试运⾏时以此为依据设置重复次数和并发度(线程
数)来进⾏压测。
参考⽂章:《云智慧压测实战分享之JMeter⼯具使⽤初探》
https://segmentfault.com/a/1190000007922515)
官⽹链接:http://jmeter.apache.org/
2、LoadRunner
LoadRunner是⼀种预测系统⾏为和性能的负载测试⼯具,通过模拟实际⽤⼾的操作⾏为进⾏实时性能
监测,来帮助测试⼈员更快的查找和发现问题。LoadRunner适⽤于各种体系架构,能⽀持⼴泛的协议
和技术,为测试提供特殊的解决⽅案。企业通过LoadRunner能最⼤限度地缩短测试时间,优化性能并
加速应⽤系统的发布周期。
LoadRunner提供了3⼤主要功能模块:VirtualUser Generator(⽤于录制性能测试脚本),
LoadRunner Controller(⽤于创建、运⾏和监控场景),LoadRunner Analysis(⽤于分析性能测试
结果)既可以作为独⽴的⼯具完成各⾃的功能,⼜可以作为LoadRunner的⼀部分彼此衔接,与其他模
块共同完成软件性能的整体测试。
详⻅:《性能测试⼊⻔⸺LoadRunner使⽤初探》
http://www.admin5.com/article/20161114/695706.shtml)
LoadRunner官⽹:https://saas.hpe.com/zh-cn/software/loadrunner
三、接⼝性能优化的常⽤⼿段

  1. 批量思想:批量操作数据库
    优化前:
    1 //for循环单笔⼊库
    2 for(TransDetail detail:transDetailList){
    3 insert(detail);
    4 }
    优化后:
    1 batchInsert(transDetailList);
  2. 异步思想:耗时操作,考虑放到异步执⾏
    耗时操作,考虑⽤异步处理 ,这样可以降低接⼝耗时。
    假设⼀个转账接⼝,匹配联⾏号,是同步执⾏的,但是它的操作耗时有点⻓ ,优化前的流程:
    为了降低接⼝耗时,更快返回,你可以把匹配联⾏号 移到异步处理 ,优化后:
    • 除了转账这个例⼦,⽇常⼯作中还有很多这种例⼦。⽐如:⽤⼾注册成功后,短信邮件通知,也是
    可以异步处理的 ~
    • ⾄于异步的实现⽅式,你可以⽤线程池,也可以⽤消息队列实现 。
  3. 空间换时间思想:恰当使⽤缓存
    在适当的业务场景,恰当地使⽤缓存,是可以⼤⼤提⾼接⼝性能的。缓存其实就是⼀种空间换时间的
    思想 ,就是你把要查的数据,提前放好到缓存⾥⾯,需要时,直接查缓存,⽽避免去查数据库或者计
    算的过程 。
    这⾥的缓存包括: Redis 缓存, JVM 本地缓存, memcached ,或者 Map 等等。我举个我⼯作
    中,⼀次使⽤缓存优化的设计吧,⽐较简单,但是思路很有借鉴的意义。
    那是⼀次转账接⼝的优化,⽼代码 ,每次转账,都会根据客⼾账号,查询数据库,计算匹配联⾏
    号。
    因为每次都查数据库,都计算匹配,⽐较耗时 ,所以使⽤缓存 ,优化后流程如下:
  4. 预取思想:提前初始化到缓存
    预取思想很容易理解,就是提前把要计算查询的数据,初始化到缓存 。如果你在未来某个时间需要⽤
    到某个经过复杂计算的数据,才实时去计算的话,可能耗时⽐较⼤ 。这时候,我们可以采取预取思
    想,提前把将来可能需要的数据计算好,放到缓存中 ,等需要的时候,去缓存取就⾏。这将⼤幅度提
    ⾼接⼝性能。
    我记得以前在第⼀个公司做视频直播的时候,看到我们的直播列表就是⽤到这种优化⽅案 。就是启动
    个任务,提前把直播⽤⼾、积分等相关信息,初始化到缓存 。
  5. 池化思想:预分配与循环使⽤
    ⼤家应该都记得,我们为什么需要使⽤线程池 ?
    线程池可以帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。
    如果你每次需要⽤到线程,都去创建,就会有增加⼀定的耗时,⽽线程池可以重复利⽤线程,避免不
    必要的耗时。 池化技术不仅仅指线程池,很多场景都有池化思想的体现,它的本质就是预分配与循环
    使⽤ 。
    ⽐如 TCP 三次握⼿,⼤家都很熟悉吧,它为了减少性能损耗,引⼊了 Keep-Alive⻓连接 ,避免频
    繁的创建和销毁连接。当然,类似的例⼦还有很多,如数据库连接池、 HttpClient 连接池。
    我们写代码的过程中,学会池化思想 ,最直接相关的就是使⽤线程池⽽不是去 new ⼀个线程。
  6. 事件回调思想:拒绝阻塞等待
    如果你调⽤⼀个系统 B 的接⼝,但是它处理业务逻辑,耗时需要 10s 甚⾄更多。然后你是⼀直阻塞
    等待,直到系统B的下游接⼝返回 ,再继续你的下⼀步操作吗?这样显然不合理 。
    我们参考IO多路复⽤模型 。即我们不⽤阻塞等待系统 B 的接⼝,⽽是先去做别的操作。等系统 B 的
    接⼝处理完,通过事件回调 通知,我们接⼝收到通知再进⾏对应的业务操作即可。
  7. 远程调⽤由串⾏改为并⾏
    假设我们设计⼀个APP⾸⻚的接⼝,它需要查⽤⼾信息、需要查banner信息、需要查弹窗信息等等。
    如果是串⾏⼀个⼀个查,⽐如查⽤⼾信息 200ms ,查banner信息 100ms 、查弹窗信息 50ms ,那
    ⼀共就耗时 350ms 了,如果还查其他信息,那耗时就更⼤了。
    其实我们可以改为并⾏调⽤,即查⽤⼾信息、查banner信息、查弹窗信息,可以同时并⾏发起 。
    最后接⼝耗时将⼤⼤降低 。
  8. 锁粒度避免过粗
    在⾼并发场景,为了防⽌超卖等情况 ,我们经常需要加锁来保护共享资源 。但是,如果加锁的粒度过
    粗,是很影响接⼝性能的。
    什么是加锁粒度呢?
    其实就是就是你要锁住的范围是多⼤。⽐如你在家上卫⽣间,你只要锁住卫⽣间就可以了吧 ,不需
    要将整个家都锁起来不让家⼈进⻔吧,卫⽣间就是你的加锁粒度。
    不管你是 synchronized 加锁还是 redis 分布式锁,只需要在共享临界资源加锁即可,不涉及共
    享资源的,就不必要加锁。这就好像你上卫⽣间,不⽤把整个家都锁住,锁住卫⽣间⻔就可以了。
    ⽐如,在业务代码中,有⼀个 ArrayList 因为涉及到多线程操作,所以需要加锁操作,假设刚好⼜
    有⼀段⽐较耗时的操作(代码中的 slowNotShare ⽅法)不涉及线程安全问题。反例加锁,就是⼀
    锅端,全锁住 :
    1 //不涉及共享资源的慢⽅法
    2 private void slowNotShare() {
    3 try {
    4 TimeUnit.MILLISECONDS.sleep(100);
    5 } catch (InterruptedException e) {
    6 }
    7 }
    8
    9 //错误的加锁⽅法
    10 public int wrong() {
    11 long beginTime = System.currentTimeMillis();
    12 IntStream.rangeClosed(1, 10000).parallel().forEach(i -> {
    13 //加锁粒度太粗了,slowNotShare其实不涉及共享资源
    14 synchronized (this) {
    15 slowNotShare();
    16 data.add(i);
    17 }
    18 });
    19 log.info(“cosume time:{}”, System.currentTimeMillis() - beginTime);
    20 return data.size();
    21 }
    正例:
    1 public int right() {
    2 long beginTime = System.currentTimeMillis();
    3 IntStream.rangeClosed(1, 10000).parallel().forEach(i -> {
    4 slowNotShare();//可以不加锁
    5 //只对List这部分加锁
    6 synchronized (data) {
    7 data.add(i);
    8 }
    9 });
    10 log.info(“cosume time:{}”, System.currentTimeMillis() - beginTime);
    11 return data.size();
    12 }
  9. 切换存储⽅式:⽂件中转暂存数据
    如果数据太⼤,落地数据库实在是慢的话,就可以考虑先⽤⽂件的⽅式暂存 。先保存⽂件,再异步下
    载⽂件,慢慢保存到数据库 。
    这⾥可能会有点抽象,给⼤家分享⼀个,我之前的⼀个真实的优化案例 吧。
    之前开发了⼀个转账接⼝。如果是并发开启,10个并发度,每个批次 1000 笔转账明细数据,数据
    库插⼊会特别耗时,⼤概6秒左右 ;这个跟我们公司的数据库同步机制有关,并发情况下,因为优先
    保证同步,所以并⾏的插⼊变成串⾏啦,就很耗时。
    优化前 , 1000 笔明细转账数据,先落地 DB 数据库,返回处理中给⽤⼾,再异步转账。如图:
    记得当时压测的时候,⾼并发情况,这 1000 笔明细⼊库,耗时都⽐较⼤。所以我转换了⼀下思路,
    把批量的明细转账记录保存的⽂件服务器,然后记录⼀笔转账总记录到数据库即可 。接着异步再把明
    细下载下来,进⾏转账和明细⼊库。最后优化后,性能提升了⼗⼏倍 。
    优化后 ,流程图如下:
    如果你的接⼝耗时瓶颈就在数据库插⼊操作这⾥ ,⽤来批量操作等,还是效果还不理想,就可以考虑
    ⽤⽂件或者 MQ 等暂存。有时候批量数据放到⽂件,会⽐插⼊数据库效率更⾼。
  10. 索引
    提到接⼝优化,很多⼩伙伴都会想到添加索引 。没错,添加索引是成本最⼩的优化 ,⽽且⼀般优化效
    果都很不错。
    索引优化这块的话,⼀般从这⼏个维度去思考:
    • 你的SQL加索引了没?
    • 你的索引是否真的⽣效?
    • 你的索引建⽴是否合理?
    10.1 SQL没加索引
    我们开发的时候,容易疏忽⽽忘记给SQL添加索引。所以我们在写完 SQL 的时候,就顺⼿查看⼀下
    explain 执⾏计划。
    1 explain select * from user_info where userId like ‘%123’;
    你也可以通过命令 show create table ,整张表的索引情况。
    1 show create table user_info;
    如果某个表忘记添加某个索引,可以通过 alter table add index 命令添加索引
    1 alter table user_info add index idx_name (name);
    ⼀般就是: SQL 的 where 条件的字段,或者是 order by 、group by 后⾯的字段需需要添加
    索引。
    10.2 索引不⽣效
    有时候,即使你添加了索引,但是索引会失效的。索引失效的常⻅原因 :
    10.3 索引设计不合理
    我们的索引不是越多越好,需要合理设计。⽐如:
    • 删除冗余和重复索引。
    • 索引⼀般不能超过 5 个
    • 索引不适合建在有⼤量重复数据的字段上、如性别字段
    • 适当使⽤覆盖索引
    • 如果需要使⽤ force index 强制⾛某个索引,那就需要思考你的索引设计是否真的合理了
  11. 优化SQL
    除了索引优化,其实SQL还有很多其他有优化的空间。⽐如这些:
    12.避免⼤事务问题
    为了保证数据库数据的⼀致性,在涉及到多个数据库修改 操作时,我们经常需要⽤到事务。⽽使⽤
    spring 声明式事务,⼜⾮常简单,只需要⽤⼀个注解就⾏ @Transactional ,如下⾯的例⼦:
    1 @Transactional
    2 public int createUser(User user){
    3 //保存⽤⼾信息
    4 userDao.save(user);
    5 passCertDao.updateFlag(user.getPassId());
    6 return user.getUserId();
    7 }
    这块代码主要逻辑就是创建个⽤⼾,然后更新⼀个通⾏证 pass 的标记。如果现在新增⼀个需求,创
    建完⽤⼾,调⽤远程接⼝发送⼀个 email 消息通知,很多⼩伙伴会这么写:
    1 @Transactional
    2 public int createUser(User user){
    3 //保存⽤⼾信息
    4 userDao.save(user);
    5 passCertDao.updateFlag(user.getPassId());
    6 sendEmailRpc(user.getEmail());
    7 return user.getUserId();
    8 }
    这样实现可能会有坑,事务中嵌套 RPC 远程调⽤,即事务嵌套了⼀些⾮ DB 操作。如果这些⾮ DB 操
    作耗时⽐较⼤的话,可能会出现⼤事务问题 。
    所谓⼤事务问题就是,就是运⾏时间⻓的事务 。由于事务⼀致不提交,就会导致数据库连接被占
    ⽤,即并发场景下,数据库连接池被占满,影响到别的请求访问数据库,影响别的接⼝性能 。
    ⼤事务引发的问题主要有:接⼝超时、死锁、主从延迟 等等。因此,为了优化接⼝,我们要规避⼤事
    务问题。我们可以通过这些⽅案来规避⼤事务:
    • RPC远程调⽤不要放到事务⾥⾯
    • ⼀些查询相关的操作,尽量放到事务之外
    • 事务中避免处理太多数据
  12. 深分⻚问题
    在以前公司分析过⼏个接⼝耗时⻓的问题,最终结论都是因为深分⻚问题 。
    深分⻚问题,为什么会慢?我们看下这个SQL
    1 select id,name,balance from account where create_time> ‘2020-09-19’ limit 100000
    limit 100000,10 意味着会扫描 100010 ⾏,丢弃掉前 100000 ⾏,最后返回 10 ⾏。即使
    create_time ,也会回表很多次。
    我们可以通过标签记录法和延迟关联法 来优化深分⻚问题。
    13.1 标签记录法
    就是标记⼀下上次查询到哪⼀条了,下次再来查的时候,从该条开始往下扫描。就好像看书⼀样,上
    次看到哪⾥了,你就折叠⼀下或者夹个书签,下次来看的时候,直接就翻到啦。
    假设上⼀次记录到 100000 ,则SQL可以修改为:
    1 select id,name,balance FROM account where id > 100000 limit 10;
    这样的话,后⾯⽆论翻多少⻚,性能都会不错的,因为命中了 id 主键索引。但是这种⽅式有局限
    性:需要⼀种类似连续⾃增的字段。
    13.2 延迟关联法
    延迟关联法,就是把条件转移到主键索引树,然后减少回表。优化后的SQL如下:
    1 select acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT
    优化思路就是 ,先通过 idx_create_time ⼆级索引树查询到满⾜条件的主键ID,再与原表通过主
    键ID内连接,这样后⾯直接⾛了主键索引了,同时也减少了回表。
  13. 优化程序结构
    优化程序逻辑、程序代码,是可以节省耗时的。⽐如,你的程序创建多不必要的对象、或者程序逻辑
    混乱,多次重复查数据库、⼜或者你的实现逻辑算法不是最⾼效的 ,等等。
    我举个简单的例⼦:复杂的逻辑条件,有时候调整⼀下顺序,就能让你的程序更加⾼效。
    假设业务需求是这样:如果⽤⼾是会员,第⼀次登陆时,需要发⼀条感谢短信。如果没有经过思考,
    代码直接这样写了
    1 if(isUserVip && isFirstLogin){
    2 sendSmsMsg();
    3 }
    假设有 5 个请求过来, isUserVip 判断通过的有 3 个请求, isFirstLogin 通过的只有 1 个请
    求。那么以上代码, isUserVip 执⾏的次数为 5 次, isFirstLogin 执⾏的次数也是 3 次,如
    下:
    如果调整⼀下 isUserVip 和 isFirstLogin 的顺序:
    1 if(isFirstLogin && isUserVip ){
    2 sendMsg();
    3 }
    isFirstLogin 执⾏的次数是 5 次, isUserVip 执⾏的次数是 1 次:
    酱紫程序是不是变得更⾼效了呢?
  14. 压缩传输内容
    压缩传输内容,传输报⽂变得更⼩,因此传输会更快啦。 10M 带宽,传输 10k 的报⽂,⼀般⽐传输
    1M 的会快呀。
    打个⽐喻,⼀匹千⾥⻢,它驮着100⽄的货跑得快,还是驮着10⽄的货物跑得快呢?
    再举个视频⽹站的例⼦:
    如果不对视频做任何压缩编码,因为带宽⼜是有限的。巨⼤的数据量在⽹络传输的耗时会⽐编码压缩
    后,慢好多倍
  15. 海量数据处理,考虑NoSQL
    之前看过⼏个慢 SQL ,都是跟深分⻚问题有关的。发现⽤来标签记录法和延迟关联法,效果不是很明
    显 ,原因是要统计和模糊搜索,并且统计的数据是真的⼤。最后跟组⻓对⻬⽅案,就把数据同步到
    Elasticsearch ,然后这些模糊搜索需求,都⾛ Elasticsearch 去查询了。
    我想表达的就是,如果数据量过⼤,⼀定要⽤关系型数据库存储的话,就可以分库分表。但是有时
    候,我们也可以使⽤ NoSQL,如Elasticsearch、Hbase 等。
  16. 线程池设计要合理
    我们使⽤线程池,就是让任务并⾏处理,更⾼效地完成任务 。但是有时候,如果线程池设计不合理,
    接⼝执⾏效率则不太理想。
    ⼀般我们需要关注线程池的这⼏个参数:核⼼线程、最⼤线程数量、阻塞队列 。
    • 如果核⼼线程过⼩,则达不到很好的并⾏效果。
    • 如果阻塞队列不合理,不仅仅是阻塞的问题,甚⾄可能会 OOM
    • 如果线程池不区分业务隔离,有可能核⼼业务被边缘业务拖垮 。
    18.机器问题 (fullGC、线程打满、太多IO资源没关闭等等)
    有时候,我们的接⼝慢,就是机器处理问题。主要有 fullGC 、线程打满、太多IO资源没关闭等等。
    • 之前排查过⼀个 fullGC 问题:运营⼩姐姐导出 60多万 的 excel 的时候,说卡死 了,接着我
    们就收到监控告警。后⾯排查得出,我们⽼代码是 Apache POI ⽣成的 excel ,导出 excel
    数据量很⼤时,当时JVM内存吃紧会直接 Full GC 了。
    • 如果线程打满了,也会导致接⼝都在等待了。所以。如果是⾼并发场景,我们需要接⼊限流,把多
    余的请求拒绝掉 。
    • 如果IO资源没关闭,也会导致耗时增加 。这个⼤家可以看下,平时你的电脑⼀直打开很多很多⽂
    件,是不是会觉得很卡。

1. 项目性能优化

性能问题分析理论:3S 定理
 性能指标:RT、TPS、并发数…
 压测监控平台:Docker、InfluxDB、Grafana、Prometheus 和 node_exporter 环境搭建
 梯度压测:分析接口性能瓶颈
 性能瓶颈剖析
 分布式压测:构建百万次请求的压力
 服务容器优化:Tomcat、IO 模型、Undertow 等调优
 数据库调优:为什么要数据库调优,什么影响数据库性能
 OpenResty 调优
 多级缓存调优
 JVM 调优:为什么 JVM 调优,什么时候调优,调优调什么?

2. JVM虚拟机

  1. 一个学好数学的最重要的方法是:不断训练自己的思维方式。
  2. 升级思维认知:从个案到整体规律,从个别定理到完整的知识体系,从具体到抽象,从完全的确定性,到把握不确定性。
  3. 数学与自然科学的区别:
    1. 推理逻辑与测量的区别。
    2. 用逻辑证明与用事实证实的区别。
    3. 数学结论的绝对性与科学结论的相对性的区别。

数学定理不接受例外情况,数学定理的大致确立过程:从几个特例,到发现很多例证,提出猜想,证明猜想,成立定理,定理会有推论,在此基础上,有新的定理和应用。

  1. 数学思维最重要的原则就是:从逻辑出发思考问题。基于数学知识,用逻辑发现问题、预见问题。
  2. 数学的边界是一个硬边界,数学的边界有时候不一定是我们解决问题的边界,除了数学的方法,还可以寻找其他的方法。很多工程上的问题,可以通过使用近似的数值解,解决实际的问题。
  3. 数学和其他知识体系有密切的联系,掌握数学上的基本原则,有助于对其他知识的理解和应用。
  4. 有准则总比没有好。
  5. 少数几个数得到的所谓“规律”,和采用大量数据后得到的规律,是两回事;数学大部分时候研究的不是一个孤立的数,而是揭示一些规律和趋势;数列,关心的不仅是积累的趋势,还有积累的速度、和积累的结果。
  6. 数学就是工具,个别具体问题的解题技巧,其作用有限。学习数学最需要做的就是,将问题由自然语言翻译成数学语言,然后用数学的工具解决问题。学会把问题抽象成模型,才能解决更多更难的新问题。
  7. 学习数学应注重学习的是概念,及概念之间的联系,把现实问题转化为数学问题。不要花太多时间在那些无法举一反三的解题技巧上。
  8. 从形象到抽象,现实不存在,但是建立在不存在的基础之上的工具,却能解决实际的问题。
  9. 无穷大不是一个静态的、具体的大数字,而是一个动态的,不断扩大的变化趋势。不能以有限的认知,去理解无限的事物。
  10. 无穷小不是一个数,和无穷大一样,是一个概念、一个趋势。它弥补了关于数字在连续性方面的一个定义的缺失。
  11. 无穷大、无穷小不是具体的数字,但是它们也能比较大小,比的不是具体数字,而是变化的趋势。变化趋势快的,叫高阶,变化趋势慢的,叫低阶。
  12. 函数的4个共性:函数里面都有变量;都有一种对应关系;对应关系必须是确定的;函数所对应的关系,可以通过数学的方法,或者其他方法算出来。任何一个变量只能对应一个函数值,一个变量对应多个函数值,这种关系不是函数;有了函数,很容易看出两个变量之间是怎样互相影响的;函数让我们从对具体事物、具体数字的关注变成对趋势的关注,而且可以准确度量变化趋势带来的差异;函数可以帮助我们通过学习几个个例,掌握解决一系列问题的方法。我们的思维方式要从常数思维到变量思维,到函数思维。函数为同一类问题提供具有普遍性的答案。
  13. 函数中的因果关系:1-数学中的因果关系和生活中的可能不完全相同;2-当一个函数的变化由两个、或更多个变量决定时,单个变量和函数之间的因果关系,并不是函数值变化的必然原因。
  14. 导数的本质是对变化快慢的准确量化度量。
  15. 导数在数学上更本质的意义是,在于它是对于连续性的一种测度,光滑、连续的导数曲线,可以成为判断未来走势的依据。
  16. 有关不确定性的规律,只有在大量随机性实验时才显现出来,当试验的次数不足,它则显现出偶然性和随意性。
    伯努利分布;方差;标准差;
  17. 泊松分布、高斯分布

数据库驱动、数据库连接池、MySQL 的连接池

一个不变的原则:网络连接必须让线程来处理

SQL 接口:负责处理接收到的 SQL 语句

查询解析器:让 MySQL 能看懂 SQL 语句

查询优化器:选择最优的查询路径

调用存储引擎接口,真正执行 SQL 语句

执行器:根据执行计划调用存储引擎的接口

InnoDB 的重要内存结构:缓冲池(Buffer Pool)

对 Buffer Pool 进行调整
[server]
innodb_buffer_pool_size

  • 数据页:MySQL 中抽象出来的数据单位
    磁盘文件中有很多的数据页,每一页数据里很多行数据
  • 磁盘上的数据页和 Buffer Pool 中的缓存页是如何对应起来的?
    默认情况下,一个缓存页的大小和磁盘上的一个数据页的大小是一一对应,都是 16KB。
  • 缓存页对应的描述信息
    描述信息本身也是一块数据,在 Buffer Pool 中,每个缓存页的描述数据放在最前面,然后各个缓存页放在后面。描述信息包括:这个数据页所属的表空间、数据页的编号、这个缓存页在 Buffer Pool 中的地址等等其他信息。
  • 怎么知道哪些缓存页是空闲的?(free 链表)
    free 链表,是一个双向链表数据结构,free 链表里,每个节点就是一个空闲的缓存
    页的描述数据块的地址,也就是,只要一个缓存页是空闲的,那么他的描述数据块就会被放入这个 free 链表中。
  • 怎么知道数据页有没有被缓存?
    数据页缓存哈希表的结构。
  • 哪些缓存页是脏页?
    flush 链表,这个 flush 链表本质也是通过缓存页的描述数据块中的两个指
    针,让被修改过的缓存页的描述数据块,组成一个双向链表。
  • 淘汰缓存数据
    引入 LRU 链表来判断哪些缓存页是不常用的。
    使用简单的 LRU 链表的机制,其实是漏洞百出的,因为很可能预读机制,或者全表扫描的机制,都会一下子把大量未来可能不怎么访问的数据页加载到缓存页里去,然后 LRU 链表的前面全部是这些未来可能不怎么会被访问的缓存页!
  • 基于冷热数据分离的思想设计 LRU 链表
    参数 innodb_old_blocks_pct
  • 触发 MySQL 的预读机制
    参数 innodb_read_ahead_threshold、innodb_random_read_ahead

多线程并发访问一个 Buffer Pool,必然是要加锁的,然后让一个线程先完成一系列的操作,比如说加载数据页到缓存页,更新 free 链表,更新 lru 链表,然后释放锁,接着下一个线程再执行一系列的操作。

  • 配置多个 Buffer Pool 优化并发能力
    参数 innodb_buffer_pool_size、innodb_buffer_pool_instances
  • 基于 chunk 机制支持运行期间动态调整 buffer pool 大小

undo 日志文件:如何让你更新的数据可以回滚

Redo Log Buffer:万一系统宕机,如何避免数据丢失

redo 日志,记录对数据做的修改

交事务的时候将 redo 日志写入磁盘中

三种 redo 日志刷盘策略优缺点

MySQL binlog

redo log,偏向物理性质的重做日志
binlog 归档日志,记录的是偏向于逻辑性的日志
binlog 不是 InnoDB 存储引擎特有的日志文件,是属于 mysql server 自己的日志文件。


执行器是非常核心的组件,负责跟存储引擎配合完成一个 SQL 语句在磁盘与内存层面的全部数据更新操作。
上图更新语句的执行,拆分为了两个阶段,1、2、3、4 几个步骤,本质是执行这个更新语句的时候干的事。
5 和 6 两个步骤,是从提交事务开始的,属于提交事务的阶段。
5、6、7 三个步骤,必须是三个步骤都执行完毕,才算是提交了事务。

在 redo 日志中写入 commit 标记的意义
保持 redo log 日志与 binlog 日志一致

binlog 日志的刷盘策略

sync_binlog 参数

基于 binlog 和 redo log 完成事务的提交

后台 IO 线程随机将内存更新后的脏数据刷回磁盘

 

QPS.TPS、IOPS、吞吐量、latency

数据库压测工具:sysbench
检测机器(Linux)性能 top 命令
对数据库监控 Prometheus+Grafana

 

0%