两万字的性能优化指南!39个策略提升接口性能!

张开发
2026/5/9 11:04:32 15 分钟阅读
两万字的性能优化指南!39个策略提升接口性能!
39个技巧从8个方面全面梳理后端性能优化的套路。为了更好评估后端接口性能我们需要对不同行为的耗时进行比较。从上图可以看出一个CPU周期少于1纳秒而一次从北京到上海的跨地域访问可能需要约30毫秒。如果让你进行后端接口的优化你是首选优化代码行数还是首选避免跨地域访问呢在评估接口性能时我们需要首先找出最耗时的部分并优化它这样优化效果才会立竿见影。上图提供了一个很好的参考。需要注意的是上图中没有显示机房内网络的耗时。一次机房内网络的延迟Ping通常在1毫秒以内相比跨地域网络延迟要少很多。插播一个问题怎么计算跨地域耗时呢我们已知光在真空中传播折射率为 1其光速约为 c30 万公里/秒当光在其他介质里来面传播其介质折射自率为 n,光在其中的速度就降为 vc/n光纤的材料是二氧化硅其折射率 n 为 1.44 左右计算延迟的时候可以近似认为 1.5我们通过计算可以得出光纤中的光传输速度近似为 vc/1.5 20 万公里/秒。以北京和深圳为例直线距离 1920 公里接近 2000 公里传输介质如果使用光纤光缆那么延迟时间 tL/v 0.2 万公里/20 万公里/秒10ms 也就是说从北京到深圳拉一根 2000 公里的光缆单纯的距离延迟就要 10ms 实际上是没有这么长的光缆的中间是需要通过基站来进行中继并且当光功率损耗到一定值以后需要通过转换器加强功率以后继续传输这个中转也是要消耗时间的。另外数据包在网络中长距离传输的时候是会经过多次的封包和拆包这个也会消耗时间。综合考虑各种情况以后以北京到深圳为例总的公网延迟大约在 40ms 左右北京到上海的公网延迟大约在 30ms如果数据出国的话延迟会更大比如中国到美国延迟一般在 150ms ~ 200ms 左右因为要经过太平洋的海底光缆过去的。对于机房内的访问Redis缓存的访问耗时通常在1-5毫秒之间而数据库的主键索引访问耗时在5-15毫秒之间。当然这两者最大的区别不仅仅在于耗时而更重要的是它们在承受高并发访问方面的能力。Redis单机可以承受10万并发往往瓶颈在网络带宽和CPU而MySQL要考虑主从读写分离和分库分表才能稳定支持5千并发以上的访问。下面我们重点分析 39种策略 如何优化后端接口性能1. 优化前端接口1.1 核心数据和非核心数据拆分为多个接口我曾经对用户会员主页接口进行了优化该接口返回的数据非常庞大。由于各个模块的数据都在同一个接口中只要其中一部分数据的查询耗时较长整体性能就会下降导致接口的失败率增加前端无法展示核心数据。这主要是因为核心数据和非核心数据没有进行隔离耗时数据和非耗时数据没有分开。对于庞大的接口我们需要先梳理每个模块中数据的获取逻辑和性能情况明确前端必须展示和重点关注的核心数据并确保这些数据能够快速、稳定地响应给前端。而非核心的数据和性能较差的数据则可以拆分到另外的接口中即使这些接口的失败率较高对用户影响也不大。这种优化方式除了能保证快速返回核心数据也能提高稳定性。如果非核心数据故障可以单独降级不会影响核心数据展示大大提高了稳定性。1.2 前端并行调用多个接口后端提供给前端的接口应保证能够独立调用避免出现需要先调用A接口再调用B接口的情况。如果接口设计不合理前端需要的总耗时将是A接口耗时与B接口耗时之和。相反如果接口能够独立调用总耗时将取决于A接口和B接口中耗时较长的那个。显然后者的性能更优。在A接口与B接口都依赖相同的公共数据的情况下会导致重复查询。为了优化总耗时重复查询是无法避免的因此应着重优化公共数据的性能。在代码设计层面应封装每个模块的取值逻辑避免A接口与B接口出现重复代码或拷贝代码的情况。1.3 使用MD5加密防篡改数据减少重复校验在提单接口中需要校验用户对应商品的可见性、是否符合优惠活动规则以及是否可用对应的优惠券等内容。由于用户可能篡改报文来伪造提单请求后端必须进行校验。然而由于提单链路本身耗时较长多次校验以上数据将大大增加接口的耗时。那么是否可以不进行以上内容的校验呢是可以的。在用户提单页面商品数据、优惠活动数据以及优惠券等数据都是预览接口校验过的。后端可以生成一个预览Token并将预览结果存在缓存中前端在提单接口中指定预览Token。后端将校验提单数据和预览数据是否一致如果不一致则说明用户伪造了请求。为了避免预览数据占用过多的缓存空间可以设置一个过期时间例如预览数据在15分钟内不进行下单操作则会自动失效。另外还可以对关键数据进行MD5加密处理加密后的数据只有64位数据量大大减少。后端在提单接口中对关键数据进行MD5加密并与缓存中的MD5值进行比对如果不一致则说明用户伪造了提单数据。更详细请参考# 如何防止提单数据被篡改1.4 同步写接口改为异步写接口在写接口耗时较高的情况下可以采取将接口拆分为两步来优化性能。首先第一步是接收请求并创建一个异步任务然后将任务交给后端进行处理。第二步是前端轮训异步任务的执行结果以获取最终结果。通过将同步接口异步化可以避免后端线程资源被长时间占用并且可以避免浏览器和服务器的socket连接被长时间占用从而提高系统的并发能力和稳定性。此外还可以在前端接口设置更长的轮训时间以有效提高接口的成功率降低同步接口超时失败的概率提升系统的性能和用户体验。1.5 页面静态化在电商领域商品详情页和活动详情页通常会有非常高的流量特别是在秒杀场景或大促场景下流量会更高。同时商品详情页通常包含大量的信息例如商品介绍、商品参数等导致每次访问商品详情都需要访问后端接口给后端接口带来很大的压力。为了解决这个问题可以考虑将商品详情页中不会变动的部分如商品介绍、头图、商品参数等静态化到html文件中前端浏览器直接访问这些静态文件而无需访问后端接口。这样做可以极大地减轻商品详情接口的查询压力。然而对于未上架的商品详情页、后台管理等页面仍然需要查询商品详情接口来获取最新的信息。页面静态化需要先使用模版工具例如Thymeleaf等将商品详情数据渲染到Html文件然后使用运维工具rsync将html文件同步到各个nginx机器。前端就可以访问对应的商品详情页。当商品上下架状态变化时将对应Html文件重新覆盖或置为失效。1.6 不变资源访问CDNCDN内容分发网络是一种分布式网络架构它将网站的静态内容缓存在全球各地的服务器上使用户能够从最近的服务器获取所需内容从而加速用户访问。这样用户不需要从原始服务器请求内容可以减少因网络延迟导致的等待时间提高用户的访问速度和体验。通过注入静态Html文件到CDN可以避免每次用户的请求都访问原始服务器。相反这些文件会被缓存在CDN的服务器上因此用户可以直接从离他们最近的服务器获取内容。这种方式可以大大减少因网络延迟导致的潜在用户流失因为用户能够更快地获取所需的信息。此外CDN的使用还可以提高系统在高并发场景下的稳定性。在高并发情况下原始服务器可能无法承受大量的请求流量并可能导致系统崩溃或响应变慢。但是通过将静态Html文件注入到CDN让CDN来处理部分请求分担了原始服务器的负载从而提高了整个系统的稳定性。通过将商品详情、活动详情等静态Html文件注入到CDN可以加速用户访问速度减少用户因网络延迟而流失的可能性并提高系统在高并发场景下的稳定性。2. 调用链路优化调用链路优化重点减少RPC的调用、减少跨地域调用。2.1 如果跨地域调用不可避免那么尝试减少它的次数刚才我提到了北京到上海的跨地域调用需要耗费大约30毫秒的时间这个耗时是相当高的所以我们应该特别关注调用链路上是否存在跨地域调用的情况。这些跨地域调用包括Rpc调用、Http调用、数据库调用、缓存调用以及MQ调用等等。在整理调用链路的时候我们还应该标注出跨地域调用的次数例如跨地域调用数据库可能会出现多次在链路上我们需要明确标记。我们可以考虑通过降低调用次数来提高性能因此在设计优化方案时我们应该特别关注如何减少跨地域调用的次数。举个例子在某种情况下假设上游服务在上海而我们的服务在北京和上海都有部署但是数据库和缓存的主节点都在北京这时候就无法避免跨地域调用。那么我们该如何进行优化呢考虑到我们的服务会更频繁地访问数据库和缓存如果让我们上海节点的服务去访问北京的数据库和缓存那么跨地域调用的次数就会非常多。因此我们应该让上游服务去访问我们在北京的节点这样只会有1次跨地域调用而我们的服务在访问数据库和缓存时就无需进行跨地域调用。2.2 单元化架构不同的用户路由到不同的集群单元如果主数据库位于北京那么南方的用户每次写请求就只能通过跨地域访问来完成吗实际上并非如此。数据库的主库不仅可以存在于一个地域而是可以在多个地域上部署主数据库。将每个用户归属于最近的地域该用户的请求都会被路由到所在地域的数据库。这样的部署不仅提升了系统性能还提高了系统的容灾等级即使单个机房发生故障也不会影响全网的用户。这个思想类似于CDN内容分发网络它能够将用户请求路由到最近的节点。事实上由于用户的存储数据已经在该地域的数据库中用户的请求极少需要切换到其他地域。为了实现这一点我们需要一个用户路由服务来提供用户所在地域的查询并且能够提供高并发的访问。除了数据库之外其他的存储中间件如MQ、Redis等以及Rpc框架都需要具备单元化架构能力。当我们无法避免跨地域调用时我们可以选择整体上跨地域调用次数最少的方案来进行优化。2.3 微服务拆分过细会导致Rpc调用较多微服务拆分过细会导致更多的RPC调用一次简单的请求可能就涉及四五个服务当访问量非常高时多出来的三五次Rpc调用会导致接口耗时增加很多。每个服务都需要处理网络IO序列化反序列化服务的GC 也会导致耗时增加这样算下来一个大服务的性能往往优于5个微服务。当然服务过于臃肿会降低开发维护效率也不利于技术升级。微服务过多也有问题例如增加整体链路耗时、基础架构升级工作量变大、单个需求代码变更的服务更多等弊端。需要你权衡开发效率、线上性能、领域划分等多方面因素。总之应该极力避免微服务过多的情况。怎么评估微服务过多呢我的个人经验是团队内平均一个人两个服务以上就是微服务过多了。例如三个人的团队6个服务5个人的团队10个服务。2.4 去掉中间商减少Rpc调用当整个系统的调用链路中涉及到过多的Rpc调用时可以通过去除中间服务的方式减少Rpc调用。例如从A服务到E服务的调用链路包含了4次Rpc调用A-B-C-D-E而我们可以评估中间的B、C、D三个服务的功能是否冗余是否只是作为转发服务而没有太多的业务逻辑如果是的话我们可以考虑让A服务直接调用E服务从而避免中间的Rpc调用减少系统的负担。总的来说无论是调用链路过长或是微服务过多都可能导致过多的Rpc请求因此可以尝试去除中间的服务来优化系统性能。2.5 提供Client工具方法处理而非Rpc调用如果中间服务有业务逻辑不能直接移除可以考虑使用基于Java Client工具方法的服务提供方式而非Rpc方式。举例来说如果存在一个调用链路为A-B-C其中B服务有自己的业务逻辑。此时B服务可以考虑提供一个Java Client jar包给A服务使用。B服务所依赖的数据可以由A服务提供这样就减少1次 A 服务到B 服务的Rpc调用。这样做有一个好处当A、B都共同依赖相同的数据A服务查询一遍就可以提供给自己和B服务Client使用。如果基于Rpc方式A、B都需要查询一遍。微服务过多也不好啊通过改变服务提供方式尽量减少Rpc调用次数和开销从而优化整个系统的性能。例如社交关注关系服务。在这个服务中需要查询用户之间的关注关系。为了提高服务性能关注服务内部使用缓存来存储关注关系。为了降低高并发场景下的调用延迟和机器负载关注服务提供了一个Java Client Jar查询关注关系放弃了上游调用rpc接口的方式。这样做的好处是可以减少一次Rpc调用避免了下游服务因GC 停顿而导致的耗时。2.6 单条调用改为批量调用无论是查询还是写入都可以使用批量调用来代替单条调用。比如在查询用户订单的详情时应该批量查询多个订单而不是通过循环逐个查询订单详情。批量调用虽然会比单条调用稍微耗时多一些但是循环调用的耗时却是单条调用的N倍所以批量查询耗时要低很多。在接口设计和代码流程中我们应该尽量避免使用for循环进行单条查询或单条写入操作。正如此文所提到的批量插入数据库的性能可能是单条插入的3-5倍。# 10亿数据如何插入Mysql10连问你想到了几个2.7 并行调用在调用多个接口时可以选择串行调用或并行调用的两种方式。串行调用是指依次调用每个接口一个接口完成后才能调用下一个接口而并行调用是指同时调用多个接口。可以看出并行调用的耗时更低因为串行调用的耗时是多个接口耗时的总和而并行调用的耗时是耗时最高的接口耗时。为了灵活实现多个接口的调用顺序和依赖关系可以使用Java中的CompletableFuture类。CompletableFuture可以将多个接口的调用任务编排成一个有序的执行流程可以实现最大程度的并发查询或并发修改。例如可以并行调用两个接口然后等待两个接口全部成功后再对查询结果进行汇总处理。这样可以提高查询或修改的效率。CompletableFutureVoid first CompletableFuture.runAsync(()-{ System.out.println(do something first); Thread.sleep(200); }); CompletableFutureVoid second CompletableFuture.runAsync(() - { System.out.println(do something second); Thread.sleep(300); }); CompletableFutureVoid allOfFuture CompletableFuture.allOf(first, second).whenComplete((m,k)-{ System.out.println(all finish do something); }); allOfFuture.get();//汇总处理结果CompletaleFuture还支持自定义线程池支持同步调用、异步调用支持anyOf任一成功则返回等多种编排策略。由于不是本文重点不再一一说明2.8 提前过滤减少无效调用在某些活动匹配的业务场景里相当多的请求实际上是不满足条件的如果能尽早的过滤掉这些请求就能避免很多无效查询。例如用户匹配某个活动时会有非常多的过滤条件如果该活动的特点是仅少量用户可参加那么可首先使用人群先过滤掉大部分不符合条件的用户。2.9 拆分接口前面提到如果Http接口功能过于庞大核心数据和非核心数据杂糅在一起耗时高和耗时低的数据耦合在一起。为了优化请求的耗时可以通过拆分接口将核心数据和非核心数据分别处理从而提高接口的性能。而在Rpc接口方面也可以使用类似的思路进行优化。当上游需要调用多个Rpc接口时可以并行地调用这些接口。优先返回核心数据如果处理非核心数据或者耗时高的数据超时则直接降级只返回核心数据。这种方式可以提高接口的响应速度和效率减少不必要的等待时间。3. 选择合适的存储系统无论是查询接口还是写入接口都需要访问数据源访问存储系统。读高写低读低写高读写双高等不同场景需要选择不同的存储系统。3.1 MySQL 换 Redis当系统查询压力增加时可以把MySQL数据异构到Redis缓存中。3.1.1 选择合适的缓存结构Redis包含了一些常见的数据结构包括字符串String、列表List、有序集合SortSet、哈希Hash和基数估计HyperLogLog、GEOHash等。在不同的应用场景下我们可以根据需求选择合适的数据结构来存储数据。举例来说如果我们需要存储用户的关注列表可以选择使用哈希结构Hash。对于需要对商品或文章的浏览量进行去重的情况可以考虑使用基数估计结构HyperLogLog。而对于用户的浏览记录可以选择列表List等结构来存储。如果想实现附近的人功能可以使用Redis GEOHash结构。Redis提供了丰富的API来操作这些数据结构我们可以根据实际需要选择适合的数据结构和相关API来简化代码实现提高开发效率。关于缓存结构选择可以参考这篇文章。# 10WTPS高并发场景【我的浏览记录】系统设计3.1.2 选择合适的缓存策略缓存策略指的是何时更新缓存和何时将缓存标记为过期或清理缓存。主要有两种策略。策略1是当数据更新时更新缓存并且在缓存Miss即缓存中没有所需数据时从数据源加载数据到缓存中。策略2是将缓存设置为常驻缓存即缓存永远不过期。当数据更新时会即时更新缓存中的数据。这种策略通常会占用大量内存空间因此一般只适用于数据量较小的情况下使用。另外定时任务会定期将数据库中的数据更新到缓存中以兜底缓存数据的一致性。总的来说选择何种缓存策略取决于具体的应用需求和数据规模。如果数据量较大一般会选择策略1而如果数据量较小且要求缓存数据的实时性可以考虑策略2。关于缓存使用可以参考我的踩坑记录#点击这里了解 第一次使用缓存翻车了3.2 Redis 换 本地缓存Redis相比传统数据库更快且具有更强的抗并发能力。然而与本地缓存相比Redis缓存仍然较慢。前面提到的Redis访问速度大约在3-5毫秒之间而使用本地缓存几乎可以忽略不计。如果频繁访问Redis获取大量数据将会导致大量的序列化和反序列化操作这会显著增加young gc频率也会增加CPU负载。本地缓存的性能更强当使用Redis仍然存在性能瓶颈时可以考虑使用本地缓存。可以设置多级缓存机制首先访问本地缓存如果本地缓存中没有数据则访问Redis分布式缓存如果仍然不存在则访问数据库。通过使用多级缓存策略来实现更高效的性能。本地缓存可以使用Guava Cahce 。参考本地缓存框架Guava Cache也可以使用性能更强的Caffeine。点击这里了解Redis由于单线程架构在热点缓存应对上稍显不足。使用本地缓存可以极大的解决缓存热点问题。例如以下代码创建了Caffeine缓存最大长度1W写入后30分钟过期同时指定自动回源取值策略。public LoadingCacheString, User createUserCache() { return Caffeine.newBuilder() .initialCapacity(1000) .maximumSize(10000L) .expireAfterWrite(30L, TimeUnit.MINUTES) //.concurrencyLevel(8) .recordStats() .build(key - userDao.getUser(key)); }3.3 Redis 换 Memcached当存在热点key和大key时Redis集群的负载会变得不均衡从而降低整个集群的性能。这是因为Redis是单线程执行的系统当处理热点key和大key时会对整个集群的性能产生影响。相比之下Memcached缓存是多线程执行的它可以更好地处理热点key和大key的问题因此可以更好地应对上述性能问题。如果遇到这些问题可以考虑使用Memcached进行替代。另外还可以通过使用本地缓存并结合Redis来处理热点key和热点大key的情况。这样可以减轻Redis集群的负担并提升系统的性能。3.4 MySQL 换 ElasticSearch在后台管理页面中通常需要对列表页进行多条件检索。MySQL 无法满足多条件检索的需求原因有两点。第一点是拼接条件检索的查询SQL非常复杂且需要进行定制化难以进行维护和管理。第二点是条件检索的查询场景非常灵活很难设计合适的索引来提高查询性能并且难以保证查询能够命中索引。相比之下ElasticSearch是一种天然适合于条件检索场景的解决方案。无论数据量的大小对于列表页查询和检索等场景推荐首选ElasticSearch。可以将多个表的数据异构到ElasticSearch中建立宽表并在数据更新时同步更新索引。在进行检索时可以直接从ElasticSearch中获取数据无需再查询数据库提高了检索性能。3.5 MySQL 换 HBaseMySQL并不适合大数据量存储若不对数据进行归档数据库会一直膨胀从而降低查询和写入的性能。针对大数据量的读写需求可以考虑以下方法来存储订单数据。首先将最近1年的订单数据存储在MySQL数据库中。这样可以保证较高的数据库查询性能因为MySQL对于相对较小的数据集来说是非常高效的。其次将1年以上的历史订单数据进行归档并将这些数据异构转储到HBase中。HBase是一种分布式的NoSQL数据库可以存储海量数据并提供快速的读取能力。在订单查询接口上可以区分近期数据和历史数据使得上游系统能够根据自身的需求调用适当的订单接口来查询订单详情。在将历史订单数据存储到HBase时可以设置合理的RowKey。RowKey是HBase中数据的唯一标识在查询过程中可以通过RowKey来快速找到目标数据。通过合理地设置RowKey可以进一步提高HBase的查询性能。通过将订单数据分别存储在MySQL和HBase中并根据需求进行区分查询可以满足大数据量场景的读写需求。MySQL用于存储近期数据以保证查询性能而HBase用于存储归档的历史数据并通过合理设置的RowKey来提高查询性能。4.代码层次优化4.1 同步转异步将写请求从同步转为异步可以显著提升接口的性能。以发送短信接口为例该接口需要调用运营商接口并在公网上进行调用因此耗时较高。如果业务方选择完全同步发送短信就需要处理失败、超时、重试等与稳定性有关的问题且耗时也会非常高。因此我们需要采用同步加异步的处理方式。公司的短信平台应该采用Rpc接口发送短信。在收到请求后首先进行校验包括校验业务方短信模板的合法性以及短信参数是否合法。待校验完成后我们可以将短信发送任务存入数据库并通过消息队列进行异步处理。而对业务方提供的Rpc接口的语义也发生了变化我们成功接收了发送短信的请求稍后将以异步的方式进行发送。至于发送短信失败、重试、超时等与稳定性和可靠性有关的问题将由短信平台保证。而业务方只需确保成功调用短信平台的Rpc接口即可4.2 减少日志打印在高并发的查询场景下打印日志可能导致接口性能下降的问题。我曾经不认为这会是一个问题直到我的同事犯了这个错误。有同事在排查问题时顺手打印了日志并且带上线。第二天高峰期发现接口的 tp99 耗时大幅增加同时 CPU 负载和垃圾回收频率也明显增加磁盘负载也增加很多。日志删除后系统回归正常。特别是在日志中包含了大数组或大对象时更要谨慎避免打印这些日志。4.3 使用白名单打印日志不打日志无法有效排查问题。怎么办呢为了有效地排查问题建议引入白名单机制。具体做法是在打印日志之前先判断用户是否在白名单中如果不在则不打印日志如果在则打印日志。通过将公司内的产品、开发和测试人员等相关同事加入到白名单中有利于及时发现线上问题。当用户提出投诉时也可以将相关用户添加到白名单并要求他们重新操作以复现问题。这种方法既满足了问题排查的需求又避免了给线上环境增加压力。在测试环境中可以完全开放日志打印功能4.4 避免一次性查询过多数据在进行查询操作时应尽量将单次调用改为批量查询或分页查询。不论是批量查询还是分页查询都应注意避免一次性查询过多数据比如每次加载10000条记录。因为过大的网络报文会降低查询性能并且Java虚拟机JVM倾向于在老年代申请大对象。当访问量过高时频繁申请大对象会增加Full GC垃圾回收的频率从而降低服务的性能。建议最好支持动态配置批量查询的数量。当接口的性能较差时可以通过动态配置批量查询的数量来优化接口的性能根据实际情况灵活地调整每次查询的数量。4.5 避免深度分页深度分页指的是对一个大数据集进行分页查询时每次只查询一页的数据但是要获取到指定页数的数据就需要依次查询前面的页数这样查询的范围就会越来越大导致查询效率变低。在进行深度分页时MySQL和ElasticSearch会先加载大量的数据然后根据分页要求返回少量的数据。这种处理方式导致深度分页的效率非常低同时也给MySQL和ElasticSearch带来较高的内存压力和CPU负载。因此我们应该尽可能地避免使用深度分页的方式。为了避免深度分页可以采用每次查询时指定最小id或最大id的方法。具体来说当进行分页查询时可以记录上一次查询结果中的最小id或最大id根据排序方式来决定。在进行下一次查询时指定查询结果中的最小id或最大id作为起始条件从而缩短查询范围。这样每次只获取前N条数据可以提高查询效率。关于分页可以参考 我的文章# 四选一如何选择适合你的分页方案4.6 只访问需要用到的数据为了查询数据库和下游接口所需的字段我们可以采取一些方法。例如商品数据的字段非常多如果每次调用都返回全部字段将导致数据量过大。因此上游可以指定使用的字段从而有效降低接口的数据量提升接口的性能。这种方式不仅可以减少网络IO的耗时而且还可以减少Rpc序列化和反序列化的耗时因为接口的数据量较少。对于访问量极大的接口来说处理这些多余的字段将会增加CPU的负载并增加Young GC的次数。因此不要把所有的字段都返回给上游应该按需定制。4.7 预热低流量接口对于访问量较低的接口来说通常首次接口的响应时间较长。原因是JVM需要加载类、Spring Aop首次动态代理以及新建连接等。这使得首次接口请求时间明显比后续请求耗时长。然而在流量较低的接口中这种影响会更大。用户可能尝试多次请求但依然经常出现超时严重影响了用户体验。每次服务发布完成后接口超时失败率都会大量上升那么如何解决接口预热的问题呢可以考虑在服务启动时自行调用一次接口。如果是写接口还可以尝试更新特定的一条数据。另外可以在服务启动时手动加载对应的类以减少首次调用的耗时。不同的接口预热方式有所不同建议使用阿里开源的诊断工具arthas通过监控首次请求时方法调用堆栈的耗时来进行接口的预热。arthas使用文档 https://arthas.aliyun.com/doc/trace.html使用arthas trace命令可以查看 某个方法执行的耗时情况。trace com.xxxx.ClassA function15. 数据库优化5.1 读写分离增加MySQL数据库的从节点来实现负载均衡减轻主节点的查询压力让主节点专注于处理写请求保证读写操作的高性能。除此之外当需要跨地域进行数据库的查询时由于较高网络延迟等问题接口性能可能变得很差。在数据实时性不太敏感的情况下可以通过在多个地域增加从节点来提高这些地域的接口性能。举个例子如果数据库主节点在北京可以在广州、上海等地区设置从节点在数据实时性要求较低的查询场景可有效提高南方地区的接口性能。5.2 索引优化5.2.1查询更新务必命中索引查询和更新SQL必须命中索引。查询SQL如果没命中索引在访问量较大时会出现大量慢查询严重时会导致整个MySQL集群雪崩影响到其他表、其他数据库。所以一定要严格审查SQL是否命中索引。可以使用explain命令查看索引使用情况。在SQL更新场景MySQL会在索引上加锁如果没有命中索引会对全表加锁全表的更新操作都会被阻塞住。所以更新SQL更要确保命中索引。因此为了避免这种情况的发生需要严格审查SQL是否命中索引。可以使用explain命令来查看SQL的执行计划从而判断是否有使用索引。这样可以及早发现潜在的问题并及时采取措施进行优化和调整。除此之外最好索引字段能够完全覆盖查询需要的字段。MySQL索引分主键索引和普通索引。普通索引命中后往往需要再查询主键索引获取记录的全部字段。如果索引字段完全包含查询的字段即索引覆盖查询就无需再回查主键索引可以有效提高查询性能。更详细请参考本篇文章 # 深入理解mysql 索引特性5.2.2 常见索引失效的场景查询表达式索引项上有函数.例如date(created_at) XXXX等.字符处理等。mysql将无法使用相应索引一次查询简单查询子查询不算只能使用一个索引 不等于无法使用索引未遵循最左前缀匹配导致索引失效类型转换导致索引失效例如字符串类型指定为数字类型等。like模糊匹配以通配符开头导致索引失效索引字段使用is not null导致失效查询条件存在 OR且无法命中索引。5.2.3 提高索引利用率当索引数量过多时索引的数据量就会增加这可能导致数据库无法将所有的索引数据加载到内存中从而使得查询索引时需要从磁盘读取数据进而大大降低索引查询的性能。举例来说我们组有张表700万条数据共4个索引索引数据量就达到2.8GB。在一个数据库中通常有多张表在进行分库分表时可能会存在100张表。100张表就会产生280GB的索引数据这么庞大的数据量无法全部放入内存查询索引时会大大降低缓存命中率进而降低查询和写入操作的性能。简而言之避免创建过多的索引。可以选择最通用的查询字段作为联合索引最左前缀让索引覆盖更多的查询场景。5.3 事务和锁优化为了提高接口并发量需要避免大事务。当需要更新多条数据时避免一次性更新过多的数据。因为updatedelete语句会对索引加锁如果更新的记录数过多会锁住太多的数据由于执行时间较长会严重限制数据库的并发量。间隙锁是MySQL在执行更新时为了保证数据一致性而添加的锁定机制。虽然更新的记录数量很少但MySQL可能会锁定比更新数量更大的范围。因此需要注意查询语句中的where条件是否包含了较大的范围这样可能会锁定不应该被锁定的记录。如果有批量更新的情况需要降低批量更新的数量缩小更新的范围。其次在事务内可能有多条SQL例如扣减库存和新增库存扣减流水有两条SQL。因为两个SQl在同一个事务内所以可以保证原子性。但是需要考虑两个SQL谁先执行谁后执行建议先增加流水再增扣库存。扣减库存的更新操作耗时较长且使用了行锁而新增流水的速度较快且并行执行如果先扣减库存再增加流水会增加行锁的持有时间降低了扣减库存的并发度同时会阻塞其他扣减库存的事务。相反如果先新增流水再扣减库存库存表行记录被锁定的时间较短有利于提高库存扣减的并发度。此外也可以考虑异步新增库存流水当异步新增库存流水冲突时订单可能已提单成功所以要发起订单退款消息回滚整个提单流程。这样缩小了事务最大程度提高了库存扣减的并发度。5.4 分库分表降低单表规模MySQL单库单表的性能瓶颈很容易达到。当数据量增加到一定程度时查询和写入操作可能会变得缓慢。这是因为MySQL的B树索引结构在单表行数超过2000万时会达到4层同时索引的数据规模也会变得非常庞大。如果无法将所有索引数据都放入内存缓存中那么查询索引时就需要进行磁盘查询。这会导致查询性能下降。参考# 10亿数据如何插入Mysql10连问你想到了几个为了克服这个问题系统设计在最初阶段就应该预测数据量并设置适合的分库分表策略。通过将数据分散存储在多个库和表中可以有效提高数据库的读写性能。此外分库分表也可以突破单表的容量限制。分库分表工具推荐使用Sharding-JDBC5.5 冗余数据提高查询性能使用分库分表后索引的使用受到限制。例如在关注服务中需要满足两个查询需求1. 查询用户的关注列表2. 查询用户的粉丝列表。关注关系表包含两个字段即关注者的fromUserId和被关注者的toUserId。对于查询1我们可以指定fromUserId A即可查询用户A的关注列表。对于查询2我们可以指定toUserId B即可查询用户B的粉丝列表。在单库单表的情况下我们可以设计fromUserId和toUserId这两个字段作为索引。然而当进行分库分表后我们面临选择哪个字段作为分表键的困扰。无论我们选择使用fromUserId还是toUserId作为分表键都会导致另一个查询场景变得难以实现。解决这个问题的思路是存储结构不仅要方便写入还要方便查询。既然查询不方便我们可以冗余一份数据以便于查询。我们可以设计两张表即关注列表表Follows和粉丝列表表Fans。其中Follows表使用fromUserId作为分表键用于查询用户的关注列表Fans表使用toUserId作为分表键用于查询用户的粉丝列表。通过冗余更多的数据我们可以提高查询性能这是常见的优化方案。除了引入新的表外还可以在表中冗余其他表的字段以减少关联查询的次数。关注关系设计 请参考 #解密亿级流量【社交关注关系】系统设计5.6 归档历史数据降低单表规模MySQL并不适合存储大数据量如果不对数据进行归档数据库会持续膨胀从而降低查询和写入的性能。为了满足大数据量的读写需求需要定期对数据库进行归档。在进行数据库设计时需要事先考虑到对数据归档的需求为了提高归档效率可以使用ctime创建时间进行归档例如归档一年前的数据。可以通过以下SQL语句不断执行来归档过期数据delete from order where ctime ${minCtime} order by ctime limit 100;需要注意的是执行delete操作时ctime字段应该有索引否则将会锁住整个表另外在将数据库数据归档之前如果有必要一定要将数据同步到Hive中这样以后如果需要进行统计查询可以使用Hive中的数据。如果归档的数据还需要在线查询可以将过期数据同步到HBase中这样数据库可以提供近期数据的查询而HBase可以提供历史数据的查询。可参考上述MySQL转HBase的内容。5.7 使用更强的物理机 CPU/内存/SSD硬盘MySQL的性能取决于内存大小、CPU核数和SSD硬盘读写性能。为了适配更强的宿主机可以进行以下MySQL优化配置innodb_buffer_pool_size缓冲池是数据和索引缓存的地方。默认大小为128M。这个值越大越好决于CPU的架构这能保证你在大多数的读取操作时使用的是内存而不是硬盘。典型的值是5-6GB(8GB内存)20-25GB(32GB内存)100-120GB(128GB内存)。max_connections数据库最大连接数。可以适当调大数据库链接innodb_flush_log_at_trx_commit控制MySQL刷新数据到磁盘的策略。默认1即每次事务提交都会刷新数据到磁盘安全性最高不会丢失数据。当配置为0、2 会每隔1s刷新数据到磁盘 在系统宕机、mysql crash时可能丢失1s的数据。innodb_thread_concurrencyinnodb_thread_concurrency默认是0则表示没有并发线程数限制所有请求都会直接请求线程执行。当并发用户线程数量小于64建议设置innodb_thread_concurrency0在大多数情况下最佳的值是小于并接近虚拟CPU的个数innodb_read_io_threads设置InnoDB存储引擎的读取线程数。默认值是4表示使用4个线程来读取数据。可以根据服务器的CPU核心数来调整这个值。例如调整到16甚至32。innodb_io_capacityinnodb_io_capacityInnoDB可用的总I/O容量。该参数应该设置为系统每秒可以执行的I/O操作数。该值取决于系统配置。当设置innodb_io_capacity时主线程会根据设置的值来估算后台任务可用的I/O带宽innodb_io_capacity_max: 如果刷新操作过于落后InnoDB可以超过innodb_io_capacity的限制进行刷新但是不能超过本参数的值默认情况下MySQL 分别配置了200 和2000的默认值。当磁盘为SSD时可以考虑设置innodb_io_capacity 2000innodb_io_capacity_max4000。6. 压缩数据6.1 压缩数据库和缓存数据压缩文本数据可以有效地减少该数据所需的存储空间从而提高数据库和缓存的空间利用率。然而压缩和解压缩的过程会增加CPU的负载因此需要仔细考虑是否有必要进行数据压缩。此外还需要评估压缩后数据的效果即压缩对数据的影响如何。例如下面这一段文字我们使用GZIP进行压缩假设上游服务在上海而我们的服务在北京和上海都有部署但是数据库和缓存的主节点都在北京这时候就无法避免跨地域调用。那么我们该如何进行优化呢考虑到我们的服务会更频繁地访问数据库和缓存如果让我们上海节点的服务去访问北京的数据库和缓存那么跨地域调用的次数就会非常多。因此我们应该让上游服务去访问我们在北京的节点这样只会有1次跨地域调用而我们的服务在访问数据库和缓存时就无需进行跨地域调用。该段文字使用UTF-8编码共570位byte。使用GZIP 压缩后变为328位Byte。压缩效果还是很明显的。压缩代码如下//压缩 public static byte[] compress(String str, String encoding) { if (str null || str.length() 0) { return null; } byte[] values null; ByteArrayOutputStream out new ByteArrayOutputStream(); GZIPOutputStream gzip; try { gzip new GZIPOutputStream(out); gzip.write(str.getBytes(encoding)); gzip.close(); values out.toByteArray(); out.close(); } catch (IOException e) { log.error(gzip compress error., e); throw new RuntimeException(压缩失败, e); } return values; } // 解压缩 public static String uncompressToString(byte[] bytes, String encoding) { if (bytes null || bytes.length 0) { return null; } ByteArrayOutputStream out new ByteArrayOutputStream(); ByteArrayInputStream in new ByteArrayInputStream(bytes); try { GZIPInputStream ungzip new GZIPInputStream(in); byte[] buffer new byte[256]; int n; while ((n ungzip.read(buffer)) 0) { out.write(buffer, 0, n); } String value out.toString(encoding); out.close(); return value; } catch (IOException e) { log.error(gzip uncompress to string error., e); throw new RuntimeException(解压缩失败, e); } }值得一提的是使用GZIP压缩算法的cpu负载和耗时都是比较高的。使用压缩非但不能起到降低接口耗时的效果可能导致接口耗时增加要谨慎使用。除此之外还有其他压缩算法在压缩时间和压缩率上有所权衡。可以选择适合的自己的压缩算法。7. 系统优化7.1 优化GC无论是Young GC还是Full GC在进行垃圾回收时都会暂停所有的业务线程。因此需要关注垃圾回收的频率以确保对业务的影响尽可能小。插播提问为什么young gc也需要stop the world 阿里面试官问我的把我问懵逼了。一般情况下通过调整堆大小和新生代大小可以解决大部分垃圾回收问题。其中新生代是用于存放新创建的对象的区域。对于Young GC的频率增加的情况一般是系统的请求量大量增长导致。但如果young gc增长非常多就需要考虑是否需要增加新生代的大小。因为如果新生代过小很容易被打满。这导致本可以被Young GC掉的对象被晋升Promotion到老年代过早地进入老年代。这样一来不仅Young GC频繁触发Full GC也会频繁触发。gc场景非常多建议参考美团的技术文章详细概括了9种CMS GC问题。# Java中9种常见的CMS GC问题分析与解决7.2 提升服务器硬件如果cpu负载较高 可以考虑提高每个实例cpu数量提高实例个数。同时关注网络IO负载如果机器流量较大网卡带宽可能成为瓶颈。高峰期和低峰期如果机器负载相差较大可以考虑设置弹性伸缩策略高峰期之前自动扩容低峰期自动缩容最大程度提高资源利用率。8. 交互优化8.1 调整交互顺序我曾经负责过B端商品数据创建当时产品提到创建完虚拟商品后要立即跳转到商品列表页。当时我们使用ElasticSearch 实现后台管理页面的商品查询但是ElasticSearch 在新增记录时默认是每 1 秒钟构建1次索引所以如果创建完商品立即跳转到商品列表页是无法查到刚创建的商品的。于是和产品沟通商品创建完成跳转到商品详情页是否可以沟通后产品也认可这个交互。于是我无需调整ElasticSearch 构建索引的时机。后来了解到 ElasticSearch 提供了API。新增记录后可立即构建索引就不存在1秒的延迟了。但是这样操作索引文件会非常多影响索引查询性能不过后台管理对性能要求不高也能接收。通过和产品沟通交互和业务逻辑有时候能解决很棘手的技术问题。有困难不要闷头自己扛哦~8.2 限制用户行为在社交类产品中用户关注功能。如果不限制用户可以关注的人数可能会出现恶意用户大量关注其他用户的情况导致系统设计变得复杂。为了判断用户A是否关注用户B可以查看A的关注列表中是否包含B而不是检查B的粉丝列表中是否包含A。这是因为粉丝列表的数量可能非常庞大可能达到上千万。而正常用户的关注列表通常不会很多一般只有几百到几千人。为了提高关注关系的查询性能可将关注列表数据导入到Redis Hash结构中。系统通过限制用户的最大关注上限避免出现Redis大key的情况也避免大key过期时的性能问题保证集群的整体性能的稳定。避免恶意用户攻击系统。可以看这篇文章 详细了解关注系统设计。# 解密亿级流量【社交关注关系】系统设计最后分享下五阳的浏览器AI插件没想到火了累计下载 1000同时提问豆包 DeepSeek GPT Gemini等10大 AI 平台还支持查询必应 百度 谷歌三大搜索引擎极大提升资料检索和学习效率领取链接

更多文章