怎样学习做网站的编程,营销型网站及原因有哪些方面,足彩网站怎样做推广,广告推广媒体一、索引优化
接口性能优化时#xff0c;大家第一个想到的通常是#xff1a;优化索引。
确实#xff0c;优化索引的成本是最小的。
你可以通过查看线上日志或监控报告#xff0c;发现某个接口使用的某条SQL语句耗时较长。
此时#xff0c;你可能会有以下疑问#xff…一、索引优化
接口性能优化时大家第一个想到的通常是优化索引。
确实优化索引的成本是最小的。
你可以通过查看线上日志或监控报告发现某个接口使用的某条SQL语句耗时较长。
此时你可能会有以下疑问 这条SQL语句是否已经加了索引 加的索引是否生效了 MySQL是否选择了错误的索引 1.1 没加索引
在SQL语句中忘记为WHERE条件的关键字段或ORDER BY后的排序字段加索引是项目中常见的问题。
在项目初期由于表中的数据量较小加不加索引对SQL查询性能影响不大。
然而随着业务的发展表中的数据量不断增加这时就必须加索引了。
可以通过以下命令查看/添加索引
show index from table_name
CREATE INDEX index_name ON table_name (column_name);这种方式能够显著提高查询性能尤其是在数据量庞大的情况下。
1.2 索引没生效
通过上述命令我们已经确认索引是存在的但它是否生效呢
此时你可能会有这样的疑问。 那么如何查看索引是否生效呢 答案是可以使用 EXPLAIN 命令查看 MySQL 的执行计划它会显示索引的使用情况。
例如
EXPLAIN SELECT * FROM order WHERE code002;结果 这个命令将显示查询的执行计划包括使用了哪些索引。
如果索引生效你会在输出结果中看到相关的信息。
通过这几列可以判断索引使用情况执行计划包含列的含义如下图所示 说实话SQL语句没有使用索引除去没有建索引的情况外最大的可能性是索引失效了。
以下是索引失效的常见原因 了解这些原因可以帮助你在查询优化时避免索引失效的问题确保数据库查询性能保持最佳。
1.3 选错索引
此外你是否遇到过这样一种情况明明是同一条SQL语句只是入参不同。
有时候使用的是索引A有时候却使用索引B 没错有时候MySQL会选错索引。 必要时可以使用 FORCE INDEX 来强制查询SQL使用某个索引。
例如
SELECT * FROM order FORCE INDEX (index_name) WHERE code002;至于为什么MySQL会选错索引原因可能有以下几点 了解这些原因可以帮助你更好地理解和控制MySQL的索引选择行为确保查询性能的稳定性。 二、SQL优化
如果优化了索引之后效果不明显接下来可以尝试优化一下SQL语句因为相对于修改Java代码来说改造SQL语句的成本要小得多。
以下是SQL优化的15个小技巧 三、远程调用
多时候我们需要在一个接口中调用其他服务的接口。
例如有这样的业务场景
在用户信息查询接口中需要返回以下信息用户名称、性别、等级、头像、积分和成长值。
其中用户名称、性别、等级和头像存储在用户服务中积分存储在积分服务中成长值存储在成长值服务中。为了将这些数据统一返回我们需要提供一个额外的对外接口服务。
因此用户信息查询接口需要调用用户查询接口、积分查询接口和成长值查询接口然后将数据汇总并统一返回。
调用过程如下图所示 调用远程接口总耗时 530ms 200ms 150ms 180ms
显然这种串行调用远程接口性能是非常不好的调用远程接口总的耗时为所有的远程接口耗时之和。
3.1 串行改并行
上面说到既然串行调用多个远程接口性能很差为什么不改成并行呢
如下图所示 调用远程接口的总耗时为200ms这等于耗时最长的那次远程接口调用时间。
在Java 8之前可以通过实现Callable接口来获取线程的返回结果。
在Java 8之后可以通过CompletableFuture类来实现这一功能。
以下是一个使用CompletableFuture的示例
public class RemoteServiceExample {public static void main(String[] args) throws ExecutionException, InterruptedException {// 调用用户服务接口CompletableFutureString userFuture CompletableFuture.supplyAsync(() - {// 模拟远程调用simulateDelay(200);return User Info;});// 调用积分服务接口CompletableFutureString pointsFuture CompletableFuture.supplyAsync(() - {// 模拟远程调用simulateDelay(150);return Points Info;});// 调用成长值服务接口CompletableFutureString growthFuture CompletableFuture.supplyAsync(() - {// 模拟远程调用simulateDelay(100);return Growth Info;});// 汇总结果CompletableFutureVoid allOf CompletableFuture.allOf(userFuture, pointsFuture, growthFuture);// 等待所有异步操作完成allOf.join();// 获取结果String userInfo userFuture.get();String pointsInfo pointsFuture.get();String growthInfo growthFuture.get();}
}
3.2 数据异构
为了提升接口性能尤其在高并发场景下可以考虑数据冗余将用户信息、积分和成长值的数据统一存储在一个地方比如Redis。
这样通过用户ID可以直接从Redis中查询所需的数据从而避免远程接口调用 但需要注意的是如果使用了数据异构方案就可能会出现数据一致性问题。
用户信息、积分和成长值有更新的话大部分情况下会先更新到数据库然后同步到redis。
但这种跨库的操作可能会导致两边数据不一致的情况产生。 四、重复调用
在我们的日常工作代码中重复调用非常常见但如果没有控制好会严重影响接口的性能。
让我们一起来看看这个问题。
4.1 循环查数据库 有时候我们需要从指定的用户集合中查询出哪些用户已经存在于数据库中。
一种实现方式如下
public ListUser findExistingUsers(ListString userIds) {ListUser existingUsers new ArrayList();for (String userId : userIds) {User user userRepository.findById(userId);if (user ! null) {existingUsers.add(user);}}return existingUsers;
}
上述代码会对每个用户ID执行一次数据库查询这在用户集合较大时会导致性能问题。 我们可以通过批量查询来优化性能减少数据库的查询次数。
public ListUser findExistingUsers(ListString userIds) {// 批量查询数据库ListUser users userRepository.findByIds(userIds);return users;
} 这里有个需要注意的地方是id集合的大小要做限制最好一次不要请求太多的数据。要根据实际情况而定建议控制每次请求的记录条数在500以内。 五、异步处理
在进行接口性能优化时有时候需要重新梳理业务逻辑检查是否存在设计不合理的地方。
假设有一个用户请求接口需要执行以下操作 业务操作 发送站内通知 记录操作日志 为了实现方便通常会将这些逻辑放在接口中同步执行但这会对接口性能造成一定影响。 这个接口表面上看起来没有问题但如果你仔细梳理一下业务逻辑会发现只有业务操作才是核心逻辑其他的功能都是非核心逻辑。
在这里有个原则就是 核心逻辑可以同步执行同步写库。非核心逻辑可以异步执行异步写库。 上面这个例子中发站内通知和用户操作日志功能对实时性要求不高即使晚点写库用户无非是晚点收到站内通知或者运营晚点看到用户操作日志对业务影响不大所以完全可以异步处理。
异步处理方案 异步处理通常有两种主要方式多线程和消息队列MQ 5.1 线程池异步优化
使用线程池改造之后接口逻辑如下 5.2 MQ异步
使用线程池有个小问题就是如果服务器重启了或者是需要被执行的功能出现异常了无法重试会丢数据。
为了避免使用线程池处理异步任务时出现数据丢失的问题可以考虑使用更加健壮和可靠的异步处理方案如消息队列MQ。消息队列不仅可以异步处理任务还能够保证消息的持久化和可靠性支持重试机制。
使用mq改造之后接口逻辑如下 六、避免大事务
很多小伙伴在使用Spring框架开发项目时为了方便喜欢使用Transactional注解提供事务功能。
没错使用Transactional注解这种声明式事务的方式提供事务功能确实能少写很多代码提升开发效率。
但也容易造成大事务引发性能的问题。 为了避免大事务引发的问题可以考虑以下优化建议 少用Transactional注解 将查询(select)方法放到事务外 事务中避免远程调用 事务中避免一次性处理太多数据 有些功能可以非事务执行 有些功能可以异步处理 七、锁粒度
在一些业务场景中为了避免多个线程并发修改同一共享数据而引发数据异常通常我们会使用加锁的方式来解决这个问题。
然而如果锁的设计不当导致锁的粒度过粗也会对接口性能产生显著的负面影响。
7.1 synchronized
在Java中我们可以使用synchronized关键字来为代码加锁。
通常有两种写法在方法上加锁和在代码块上加锁。
1. 方法上加锁
public synchronized void doSave(String fileUrl) {mkdir();uploadFile(fileUrl);sendMessage(fileUrl);
}在方法上加锁的目的是为了防止并发情况下创建相同的目录避免第二次创建失败而影响业务功能。
但这种直接在方法上加锁的方式锁的粒度较粗。
因为doSave方法中的文件上传和消息发送并不需要加锁只有创建目录的方法需要加锁。
我们知道文件上传操作非常耗时如果将整个方法加锁那么需要等到整个方法执行完之后才能释放锁。
显然这会导致该方法的性能下降得不偿失。
2. 代码块上加锁我们可以将加锁改在代码块上从而缩小锁的粒度 如下
public void doSave(String path, String fileUrl) {synchronized(this) {if (!exists(path)) {mkdir(path);}}uploadFile(fileUrl);sendMessage(fileUrl);
}这样改造后锁的粒度变小了只有并发创建目录时才加锁。
创建目录是一个非常快的操作即使加锁对接口性能的影响也不大。
最重要的是其他的文件上传和消息发送功能仍然可以并发执行。
多节点环境中的问题 在单机版服务中这种做法没有问题。但在生产环境中为了保证服务的稳定性同一个服务通常会部署在多个节点上。如果某个节点挂掉其他节点的服务仍然可用。
多节点部署避免了某个节点挂掉导致服务不可用的情况同时也能分摊整个系统的流量避免系统压力过大。
但这种部署方式也带来了新的问题synchronized只能保证一个节点加锁有效。 7.2 Redis分布式锁
在分布式系统中由于Redis分布式锁的实现相对简单且高效因此它在许多实际业务场景中被广泛采用。
使用Redis分布式锁的伪代码如下
public boolean doSave(String path, String fileUrl) {try {String result jedis.set(lockKey, requestId, NX, PX, expireTime);if (OK.equals(result)) {if (!exists(path)) {mkdir(path);uploadFile(fileUrl);sendMessage(fileUrl);}return true;}} finally {unlock(lockKey, requestId);}return false;
}与之前使用synchronized关键字加锁时一样这里的锁的范围也太大了换句话说锁的粒度太粗。这会导致整个方法的执行效率很低。
实际上只有在创建目录时才需要加分布式锁其余代码不需要加锁。
于是我们需要优化代码
public void doSave(String path, String fileUrl) {if (tryLock()) {try {if (!exists(path)) {mkdir(path);}} finally {unlock(lockKey, requestId);}}uploadFile(fileUrl);sendMessage(fileUrl);
}private boolean tryLock() {String result jedis.set(lockKey, requestId, NX, PX, expireTime);return OK.equals(result);
}private void unlock(String lockKey, String requestId) {// 解锁逻辑
}上面的代码将加锁的范围缩小了只有在创建目录时才加锁。这样的简单优化后接口性能可以得到显著提升。
7.3 数据库锁
MySQL数据库中的三种锁 表锁 优点加锁快不会出现死锁。 缺点锁定粒度大锁冲突的概率高并发度最低。 行锁 优点锁定粒度最小锁冲突的概率低并发度最高。 缺点加锁慢会出现死锁。 间隙锁 优点锁定粒度介于表锁和行锁之间。 缺点开销和加锁时间介于表锁和行锁之间并发度一般也会出现死锁。
锁与并发度
并发度越高接口性能越好。因此数据库锁的优化方向是 优先使用行锁 其次使用间隙锁 最后使用表锁 八、分页处理
有时候需要调用某个接口来批量查询数据例如通过用户ID批量查询用户信息然后为这些用户赠送积分。
但是如果一次性查询的用户数量太多例如一次查询2000个用户的数据传入2000个用户的ID进行远程调用时用户查询接口经常会出现超时的情况。
调用代码如下
ListUser users remoteCallUser(ids);众所周知调用接口从数据库获取数据需要经过网络传输。如果数据量过大无论是数据获取速度还是网络传输速度都会受到带宽限制从而导致耗时较长。
优化使用分页处理。
将一次性获取所有数据的请求改为分多次获取每次只获取一部分用户的数据最后进行合并和汇总。
其实处理这个问题可以分为两种场景同步调用和异步调用。
8.1 同步调用
如果在job中需要获取2000个用户的信息它要求只要能正确获取到数据即可对获取数据的总耗时要求不高。
但对每一次远程接口调用的耗时有要求不能大于500ms否则会有邮件预警。
这时我们可以同步分页调用批量查询用户信息接口。
具体示例代码如下
ListListLong allIds Lists.partition(ids, 200);for (ListLong batchIds : allIds) {ListUser users remoteCallUser(batchIds);
}代码中我使用了Google Guava工具中的Lists.partition方法用它来做分页简直太好用了不然要写一大堆分页的代码。 8.2 异步调用 如果是在某个接口中需要获取2000个用户的信息需要考虑的因素更多。
除了远程调用接口的耗时还需要考虑该接口本身的总耗时也不能超过500ms。
这时使用上面的同步分页请求远程接口的方法肯定是行不通的。
那么只能使用异步调用了。
代码如下
ListListLong allIds Lists.partition(ids, 200);final ListUser result Lists.newArrayList();
allIds.stream().forEach(batchIds - {CompletableFuture.supplyAsync(() - {result.addAll(remoteCallUser(batchIds));return Boolean.TRUE;}, executor);
});
使用CompletableFuture类通过多个线程异步调用远程接口最后汇总结果统一返回。 九、加缓存
通常情况下我们最常用的缓存是Redis和Memcached。
但对于Java应用来说绝大多数情况下使用的是Redis所以接下来我们以Redis为例。
在关系型数据库例如MySQL中菜单通常有上下级关系。某个四级分类是某个三级分类的子分类三级分类是某个二级分类的子分类而二级分类又是某个一级分类的子分类。
这种存储结构决定了想一次性查出整个分类树并非易事。这需要使用程序递归查询而如果分类很多这个递归操作会非常耗时。
因此如果每次都直接从数据库中查询分类树的数据会是一个非常耗时的操作。
这时我们可以使用缓存。在大多数情况下接口直接从缓存中获取数据。操作Redis可以使用成熟的框架比如Jedis和Redisson等。 使用Jedis的伪代码如下
String json jedis.get(key);
if (StringUtils.isNotEmpty(json)) {CategoryTree categoryTree JsonUtil.toObject(json);return categoryTree;
}
return queryCategoryTreeFromDb();十、分库分表
有时候接口性能受限的并不是其他方面而是数据库。
当系统发展到一定阶段用户并发量增加会有大量的数据库请求这不仅需要占用大量的数据库连接还会带来磁盘IO的性能瓶颈问题。
此外随着用户数量的不断增加产生的数据量也越来越大一张表可能无法存储所有数据。由于数据量太大即使SQL语句使用了索引查询数据时也会非常耗时。
那么这种情况下该怎么办呢
答案是需要进行分库分表。
如下图所示 图中将用户库拆分成了三个库每个库都包含了三张用户表。
如果有用户请求过来先根据用户ID路由到其中一个用户库然后再定位到某张表。
路由的算法有很多 根据ID取模 例如ID7有3张表则7%31模为1路由到用户表1。 给ID指定一个区间范围 例如ID的值是0-10万则数据存在用户表0ID的值是10-20万则数据存在用户表1。 一致性Hash算法。 分库分表主要有两个方向垂直和水平。
1. 垂直分库分表 垂直分库分表即业务方向更简单将不同的业务数据存储在不同的库或表中。
例如将用户数据和订单数据存储在不同的库中。
2. 水平分库分表 水平分库分表即数据方向上分库和分表的作用有区别不能混为一谈。
分库 目的解决数据库连接资源不足问题和磁盘IO的性能瓶颈问题。
分表 目的解决单表数据量太大SQL语句查询数据时即使走了索引也非常耗时的问题。此外还可以解决消耗CPU资源的问题。
分库分表 目的综合解决数据库连接资源不足、磁盘IO性能瓶颈、数据检索耗时和CPU资源消耗等问题。
业务场景中的应用 只分库 用户并发量大但需要保存的数据量很少。 只分表 用户并发量不大但需要保存的数据量很多。 分库分表 用户并发量大并且需要保存的数据量也很多。 十一、监控功能
优化接口性能问题除了上面提到的这些常用方法之外还需要配合使用一些辅助功能因为它们真的可以帮我们提升查找问题的效率。
11.1 开启慢查询日志
通常情况下为了定位SQL的性能瓶颈我们需要开启MySQL的慢查询日志。把超过指定时间的SQL语句单独记录下来方便以后分析和定位问题。
开启慢查询日志需要重点关注三个参数 slow_query_log慢查询开关 slow_query_log_file慢查询日志存放的路径 long_query_time超过多少秒才会记录日志
通过MySQL的SET命令可以设置
SET GLOBAL slow_query_log ON;
SET GLOBAL slow_query_log_file /usr/local/mysql/data/slow.log;
SET GLOBAL long_query_time 2;设置完之后如果某条SQL的执行时间超过了2秒会被自动记录到slow.log文件中。
当然也可以直接修改配置文件my.cnf
[mysqld]
slow_query_log ON
slow_query_log_file /usr/local/mysql/data/slow.log
long_query_time 2
但这种方式需要重启MySQL服务。
很多公司每天早上都会发一封慢查询日志的邮件开发人员根据这些信息优化SQL。
11.2 加监控
为了在出现SQL问题时能够及时发现我们需要对系统做监控。
目前业界使用比较多的开源监控系统是Prometheus
它提供了监控和预警的功能。
架构图如下 我们可以用它监控如下信息 接口响应时间 调用第三方服务耗时 慢查询sql耗时 cpu使用情况 内存使用情况 磁盘使用情况 数据库使用情况 等等。。。
它的界面大概长这样子 可以看到MySQL的当前QPS、活跃线程数、连接数、缓存池的大小等信息。
如果发现连接池占用的数据量太多肯定会对接口性能造成影响。
这时可能是由于代码中开启了连接却忘记关闭或者并发量太大导致的需要进一步排查和系统优化
11.3链路跟踪
有时候一个接口涉及的逻辑非常复杂例如查询数据库、查询Redis、远程调用接口、发送MQ消息以及执行业务代码等等。
这种情况下接口的一次请求会涉及到非常长的调用链路。如果逐一排查这些问题会耗费大量时间此时我们已经无法用传统的方法来定位问题。
有没有办法解决这个问题呢
答案是使用分布式链路跟踪系统SkyWalking
SkyWalking的架构图如下 在SkyWalking中可以通过traceId全局唯一的ID来串联一个接口请求的完整链路。你可以看到整个接口的耗时、调用的远程服务的耗时、访问数据库或者Redis的耗时等功能非常强大。
之前没有这个功能时为了定位线上接口性能问题我们需要在代码中加日志手动打印出链路中各个环节的耗时情况然后再逐一排查。这种方法不仅费时费力而且容易遗漏细节。
如果你用过SkyWalking来排查接口性能问题你会不自觉地爱上它的功能。如果你想了解更多功能可以访问SkyWalking的官网www.skywalking.apache.org