最牛网站建设是谁,263企业邮箱登录官网,wordpress pc6,盘锦市住房和城乡建设厅网站我负责的系统到2021年初完成了功能上的建设#xff0c;开始进入到推广阶段。随着推广的逐步深入#xff0c;收到了很多好评的同时也收到了很多对性能的吐槽。刚刚收到吐槽的时候#xff0c;我们的心情是这样的#xff1a;
当越来越多对性能的吐槽反馈到我们这里的时候开始进入到推广阶段。随着推广的逐步深入收到了很多好评的同时也收到了很多对性能的吐槽。刚刚收到吐槽的时候我们的心情是这样的
当越来越多对性能的吐槽反馈到我们这里的时候我们意识到接口性能的问题的优先级必须提高了。然后我们就跟踪了1周的接口性能监控这个时候我们的心情是这样的 有20多个慢接口5个接口响应时间超过5s1个超过10s其余的都在2s以上稳定性不足99.8%。作为一个优秀的后端程序员这个数据肯定是不能忍的我们马上就进入了漫长的接口优化之路。本文就是对我们漫长工作历程的一个总结。
正文开始 哪些问题会引起接口性能问题
这个问题的答案非常多需要根据自己的业务场景具体分析。这里做一个不完全的总结
数据库慢查询 深度分页问题未加索引索引失效join过多子查询过多in中的值太多单纯的数据量过大业务逻辑复杂 循环调用顺序调用线程池设计不合理锁设计不合理机器问题fullGC机器重启线程打满
问题解决
1、慢查询基于mysql
1.1 深度分页
所谓的深度分页问题涉及到mysql分页的原理。通常情况下mysql的分页是这样写的
select name,code from student limit 100,20含义当然就是从student表里查100到120这20条数据mysql会把前120条数据都查出来抛弃前100条返回20条。当分页所以深度不大的时候当然没问题随着分页的深入sql可能会变成这样
select name,code from student limit 1000000,20这个时候mysql会查出来1000020条数据抛弃1000000条如此大的数据量速度一定快不起来。那如何解决呢一般情况下最好的方式是增加一个条件
select name,code from student where id1000000 limit 20这样mysql会走主键索引直接连接到1000000处然后查出来20条数据。但是这个方式需要接口的调用方配合改造把上次查询出来的最大id以参数的方式传给接口提供方会有沟通成本调用方老子不改。
1.2 未加索引
这个是最容易解决的问题我们可以通过
show create table xxxx表名查看某张表的索引。具体加索引的语句网上太多了不再赘述。不过顺便提一嘴加索引之前需要考虑一下这个索引是不是有必要加如果加索引的字段区分度非常低那即使加了索引也不会生效。另外加索引的alter操作可能引起锁表执行sql的时候一定要在低峰期血泪史
1.3 索引失效
这个是慢查询最不好分析的情况虽然mysql提供了explain来评估某个sql的查询性能其中就有使用的索引。但是为啥索引会失效呢mysql却不会告诉咱需要咱自己分析。大体上可能引起索引失效的原因有这几个可能不完全 需要特别提出的是关于字段区分性很差的情况在加索引的时候就应该进行评估。如果区分性很差这个索引根本就没必要加。区分性很差是什么意思呢举几个例子比如
某个字段只可能有3个值那这个字段的索引区分度就很低。再比如某个字段大量为空只有少量有值再比如某个字段值非常集中90%都是1剩下10%可能是2,3,4....
进一步的那如果不符合上面所有的索引失效的情况但是mysql还是不使用对应的索引是为啥呢这个跟mysql的sql优化有关mysql会在sql优化的时候自己选择合适的索引很可能是mysql自己的选择算法算出来使用这个索引不会提升性能所以就放弃了。这种情况可以使用force index 关键字强制使用索引建议修改前先实验一下是不是真的会提升查询效率
select name,code from student force index(XXXXXX) where name 天才 其中xxxx是索引名。
现在我也找了很多测试的朋友做了一个分享技术的交流群共享了很多我们收集的技术文档和视频教程。
如果你不想再体验自学时找不到资源没人解答问题坚持几天便放弃的感受
可以加入我们一起交流。而且还有很多在自动化性能安全测试开发等等方面有一定建树的技术大牛
分享他们的经验还会分享很多直播讲座和技术沙龙
可以免费学习划重点开源的
qq群号310357728【暗号csdn999】 1.4 join过多 or 子查询过多
我把join过多 和子查询过多放在一起说了。一般来说不建议使用子查询可以把子查询改成join来优化。同时join关联的表也不宜过多一般来说2-3张表还是合适的。具体关联几张表比较安全是需要具体问题具体分析的如果各个表的数据量都很少几百条几千条那么关联的表的可以适当多一些反之则需要少一些。
另外需要提到的是在大多数情况下join是在内存里做的如果匹配的量比较小或者join_buffer设置的比较大速度也不会很慢。但是当join的数据量比较大的时候mysql会采用在硬盘上创建临时表的方式进行多张表的关联匹配这种显然效率就极低本来磁盘的IO就不快还要关联。
一般遇到这种情况的时候就建议从代码层面进行拆分在业务层先查询一张表的数据然后以关联字段作为条件查询关联表形成map然后在业务层进行数据的拼装。一般来说索引建立正确的话会比join快很多毕竟内存里拼接数据要比网络传输和硬盘IO快得多。
1.5 in的元素过多
这种问题如果只看代码的话不太容易排查最好结合监控和数据库日志一起分析。如果一个查询有inin的条件加了合适的索引这个时候的sql还是比较慢就可以高度怀疑是in的元素过多。一旦排查出来是这个问题解决起来也比较容易不过是把元素分个组每组查一次。想再快的话可以再引入多线程。
进一步的如果in的元素量大到一定程度还是快不起来这种最好还是有个限制
select id from student where id in (1,2,3 ...... 1000) limit 200当然了最好是在代码层面做个限制
if (ids.size() 200) {throw new Exception(单次查询数据量不能超过200);
}1.6 单纯的数据量过大
这种问题单纯代码的修修补补一般就解决不了了需要变动整个的数据存储架构。或者是对底层mysql分表或分库分表或者就是直接变更底层数据库把mysql转换成专门为处理大数据设计的数据库。这种工作是个系统工程需要严密的调研、方案设计、方案评审、性能评估、开发、测试、联调同时需要设计严密的数据迁移方案、回滚方案、降级措施、故障处理预案。除了以上团队内部的工作还可能有跨系统沟通的工作毕竟做了重大变更下游系统的调用接口的方式有可能会需要变化。
出于篇幅的考虑这个不再展开了笔者有幸完整参与了一次亿级别数据量的数据库分表工作对整个过程的复杂性深有体会后续有机会也会分享出来。
2、业务逻辑复杂
2.1 循环调用
这种情况一般都循环调用同一段代码每次循环的逻辑一致前后不关联。比如说我们要初始化一个列表预置12个月的数据给前端
ListModel list new ArrayList();
for(int i 0 ; i 12 ; i ) {Model model calOneMonthData(i); // 计算某个月的数据逻辑比较复杂难以批量计算效率也无法很高list.add(model);
}这种显然每个月的数据计算相互都是独立的我们完全可以采用多线程方式进行
// 建立一个线程池注意要放在外面不要每次执行代码就建立一个具体线程池的使用就不展开了
public static ExecutorService commonThreadPool new ThreadPoolExecutor(5, 5, 300L,TimeUnit.SECONDS, new LinkedBlockingQueue(10), commonThreadFactory, new ThreadPoolExecutor.DiscardPolicy());// 开始多线程调用
ListFutureModel futures new ArrayList();
for(int i 0 ; i 12 ; i ) {FutureModel future commonThreadPool.submit(() - calOneMonthData(i););futures.add(future);
}// 获取结果
ListModel list new ArrayList();
try {for (int i 0 ; i futures.size() ; i ) {list.add(futures.get(i).get());}
} catch (Exception e) {LOGGER.error(出现错误, e);
}2.2 顺序调用
如果不是类似上面循环调用而是一次次的顺序调用而且调用之间没有结果上的依赖那么也可以用多线程的方式进行例如 代码上看
A a doA();
B b doB();C c doC(a, b);D d doD(c);
E e doE(c);return doResult(d, e);那么可用CompletableFuture解决
CompletableFutureA futureA CompletableFuture.supplyAsync(() - doA());
CompletableFutureB futureB CompletableFuture.supplyAsync(() - doB());
CompletableFuture.allOf(futureA,futureB) // 等a b 两个任务都执行完成C c doC(futureA.join(), futureB.join());CompletableFutureD futureD CompletableFuture.supplyAsync(() - doD(c));
CompletableFutureE futureE CompletableFuture.supplyAsync(() - doE(c));
CompletableFuture.allOf(futureD,futureE) // 等d e两个任务都执行完成return doResult(futureD.join(),futureE.join());
这样A B 两个逻辑可以并行执行D E两个逻辑可以并行执行最大执行时间取决于哪个逻辑更慢。
3、线程池设计不合理
有的时候即使我们使用了线程池让任务并行处理接口的执行效率仍然不够快这种情况可能是怎么回事呢
这种情况首先应该怀疑是不是线程池设计的不合理。我觉得这里有必要回顾一下线程池的三个重要参数核心线程数、最大线程数、等待队列。这三个参数是怎么打配合的呢当线程池创建的时候如果不预热线程池则线程池中线程为0。当有任务提交到线程池则开始创建核心线程。 当核心线程全部被占满如果再有任务到达则让任务进入等待队列开始等待。 如果队列也被占满则开始创建非核心线程运行。 如果线程总数达到最大线程数还是有任务到达则开始根据线程池抛弃规则开始抛弃。 那么这个运行原理与接口运行时间有什么关系呢
核心线程设置过小核心线程设置过小则没有达到并行的效果线程池公用别的业务的任务执行时间太长占用了核心线程另一个业务的任务到达就直接进入了等待队列任务太多以至于占满了线程池大量任务在队列中等待
在排查的时候只要找到了问题出现的原因那么解决方式也就清楚了无非就是调整线程池参数按照业务拆分线程池等等。
4、锁设计不合理
锁设计不合理一般有两种锁类型使用不合理 or 锁过粗。
锁类型使用不合理的典型场景就是读写锁。也就是说读是可以共享的但是读的时候不能对共享变量写而在写的时候读写都不能进行。在可以加读写锁的时候如果我们加成了互斥锁那么在读远远多于写的场景下效率会极大降低。
锁过粗则是另一种常见的锁设计不合理的情况如果我们把锁包裹的范围过大则加锁时间会过长例如
public synchronized void doSome() {File f calData();uploadToS3(f);sendSuccessMessage();
}这块逻辑一共处理了三部分计算、上传结果、发送消息。显然上传结果和发送消息是完全可以不加锁的因为这个跟共享变量根本不沾边。因此完全可以改成
public void doSome() {File f null;synchronized(this) {f calData();}uploadToS3(f);sendSuccessMessage();
}5、机器问题fullGC机器重启线程打满
造成这个问题的原因非常多笔者就遇到了定时任务过大引起fullGC代码存在线程泄露引起RSS内存占用过高进而引起机器重启等待诸多原因。需要结合各种监控和具体场景具体分析进而进行大事务拆分、重新规划线程池等等工作
6、万金油解决方式
万金油这个形容词是从我们单位某位老师那里学来的但是笔者觉得非常贴切。这些万金油解决方式往往能解决大部分的接口缓慢的问题而且也往往是我们解决接口效率问题的最终解决方案。当我们实在是没有办法排查出问题或者实在是没有优化空间的时候可以尝试这种万金油的方式。
6.1 缓存
缓存是一种空间换取时间的解决方案是在高性能存储介质上例如内存、SSD硬盘等存储一份数据备份。当有请求打到服务器的时候优先从缓存中读取数据。如果读取不到则再从硬盘或通过网络获取数据。由于内存或SSD相比硬盘或网络IO的效率高很多则接口响应速度会变快非常多。缓存适合于应用在数据读远远大于数据写且数据变化不频繁的场景中。从技术选型上看有这些
简单的mapguava等本地缓存工具包缓存中间件redis、tair或memcached
当然memcached现在用的很少了因为相比于redis他不占优势。tair则是阿里开发的一个分布式缓存中间件他的优势是理论上可以在不停服的情况下动态扩展存储容量适用于大数据量缓存存储。相比于单机redis缓存当然有优势而他与可扩展Redis集群的对比则需要进一步调研。
进一步的当前缓存的模型一般都是key-value模型。如何设计key以提高缓存的命中率是个大学问好的key设计和坏的key设计所提升的性能差别非常大。而且key设计是没有一定之规的需要结合具体的业务场景去分析。各个大公司分享出来的相关文章缓存设计基本上是最大篇幅。
6.2 回调 or 反查
这种方式往往是业务上的解决方式在订单或者付款系统中应用的比较多。举个例子当我们付款的时候需要调用一个专门的付款系统接口该系统经过一系列验证、存储工作后还要调用银行接口以执行付款。由于付款这个动作要求十分严谨银行侧接口执行可能比较缓慢进而拖累整个付款接口性能。这个时候我们就可以采用fast success的方式当必要的校验和存储完成后立即返回success同时告诉调用方一个中间态“付款中”。而后调用银行接口当获得支付结果后再调用上游系统的回调接口返回付款的最终结果“成果”or“失败”。这样就可以异步执行付款过程提升付款接口效率。当然为了防止多业务方接入的时候回调接口不统一可以把结果抛进kafka让调用方监听自己的结果。 END今天的分享就到此结束了~点赞关注不迷路