一次显著的接口性能优化,从10s优化到0.9s

最近在登录项目后台的时候,发现当我输入账号和密码后,竟然就卡在了 Loading 页面。。

图片

加载了10S才进后台

等了足足 10S 才进去后台!

图片

通过 F12,打开 Network 网络请求一看,竟然是因为有两个接口返回的数据太慢了,最慢的一个接口竟然耗时 7 秒!

图片

初始化接口花了7S

通过查看接口的实现代码可以看到,init 接口其实是做仪表盘的数据展示的,需要从 博客表评论表用户表 以及 用户访问表 中查询数据进行展示。

@ApiOperation(value = "首页初始化数据", notes = "首页初始化数据", response = String.class)
@RequestMapping(value = "/init", method = RequestMethod.GET)
public String init() {Map<String, Object> map = new HashMap<>(Constants.NUM_FOUR);map.put(SysConf.BLOG_COUNT, blogService.getBlogCount(EStatus.ENABLE));CommentVO commentVO = new CommentVO();map.put(SysConf.COMMENT_COUNT, commentService.getCommentCount(commentVO));map.put(SysConf.USER_COUNT, userService.getUserCount(EStatus.ENABLE));map.put(SysConf.VISIT_COUNT, webVisitService.getWebVisitCount());return ResultUtil.result(SysConf.SUCCESS, map);
}

如果要一步步分析是哪里比较慢的话,最简单的方法,就是查看每个方法的具体实现,然后对源码进行分析找出具体的问题。

今天,我们就从另外一个角度来解决这个慢查询问题~

如果有认真看过蘑菇博客的系统架构图的小伙伴,应该在上方看到有数据库 SQL 性能监控 Druid 连接池。。

什么是连接池?

连接池的诞生是因为数据库连接是一种资源,而用户在使用该资源的时候必须先创建,但是创建的过程存在比较大的时间和空间花销。

如果每次在执行数据库操作的时候,都需要先创建连接,使用,关闭连接的话,这个过程必然是效率比较低下的。

对于刚刚学习 MySQL 操作的同学来说,下面的这些模板代码简直是初学者的噩梦,原来陌溪在学 JDBC 连接数据库的时候,这些模板代码都默写了好几遍~

public class TEST {// JDBC 驱动名 及数据库 URLstatic String JDBC_DRIVER = "com.mysql.jdbc.Driver";static String DB_URL = "jdbc:mysql://localhost:3306/webcourse";// 数据库的用户名与密码,需要根据自己的设置static String USER = "root";static String PASS = "121314";public static void main(String[] args) throws Exception {Connection conn = null;Statement stmt = null;try {// 注册 JDBC 驱动// 把Driver类装载进jvmClass.forName("com.mysql.jdbc.Driver");// 打开链接System.out.println("连接数据库...");conn = (Connection) DriverManager.getConnection(DB_URL, USER, PASS);// 执行查询System.out.println(" 实例化Statement对...");stmt = (Statement) conn.createStatement();String sql = "SELECT * FROM bbs";ResultSet rs = stmt.executeQuery(sql);while (rs.next()) {// 通过字段检索int id = rs.getInt("id");String name = rs.getString("name");String content = rs.getString("content");// 输出数据System.out.print("ID: " + id);System.out.print(",姓名: " + name);System.out.print(",内容: " + content);System.out.print("\n");}// 完成后关闭rs.close();stmt.close();conn.close();} catch (SQLException se) {// 处理 JDBC 错误se.printStackTrace();} catch (Exception e) {// 处理 Class.forName 错误e.printStackTrace();} finally {// 关闭资源if (stmt != null)stmt.close();if (conn != null)conn.close();}System.out.println("测试结束");}
}

因此,在实际的开发过程中,是会考虑在数据库操作前,先提前创建并维护一批的数据库连接对象,当我们需要使用的时候,就从这批对象中获取一个连接,用完之后再返还,通过这一系列的操作,从而避免了不必要的时间开销,从而提高了运行效率,这种技术在 JDBC 中被称为连接池技术(Connection Pool

图片

连接池

Druid 连接池

Druid 连接池是阿里巴巴开源的数据库连接池项目。Druid连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防 SQL 注入,被誉为:Java 语言中最好的数据库连接池。

Github:https://github.com/alibaba/druid

目前比较常见的连接池技术包含

  • C3P0

  • BDCP

  • Tomcat-JDBC

  • Druid

通过下图的的竞品对比,Druid 连接池在性能、监控、诊断、安全、扩展性这些方面远远超出竞品。

图片

连接池对比

如何使用Druid监控

Druid 连接池最初就是为监控系统采集 JDBC 运行信息而生的,Druid 连接池内置了一个监控页面,提供了非常完备的监控信息,可以快速诊断系统的瓶颈。

好了,经过上述的一顿铺垫,相信小伙伴们对 Druid 连接池已经有一定的了解了,下面我们就通过 Druid 的监控,来看看蘑菇存在的 SQL 性能问题

通过在后端端口加上 /druid/index.html 即可打开 Druid 的内置的监控页面

http://localhost:8601/druid/index.html

此时输入账号和密码:admin 123456 (密码是可以配置的,后面在集成的时候可以看到)

这个时候,会进入到 Druid Monitor 的主页,这里能够查看到对应版本、驱动,以及 Java 版本

图片

Druid监控首页

切换到数据源的 Tab 页面,能够看到我们配置的数据库连接池的各个参数

图片

数据库连接池参数

下面,我们切换到 SQL 监控,是可以看到目前运行的所有 SQL 的执行情况,按时间排序即可看到,最慢的 SQL 执行时间到达了 8S

图片

SQL监控页面

我们点击左侧的 SQL 语句,即可看到完整的 SQL 详情页,这里面记录了几个关键的信息,包括:慢SQL语句、执行耗时、发生时间、SQL 参数

图片

慢SQL详情

其实这个 SQL 对应的就是 init 中,用来查询用户 UV 的,即从日志表中查看有多少用户访问了蘑菇

SELECT COUNT(ip)
FROM (SELECT ipFROM t_web_visitWHERE create_time >= "2022-08-08 00:00:00"AND create_time <= "2022-08-08 23:59:59"GROUP BY ip
) tmp

我们把 SQL 复制到 SQLyog 执行,好家伙,这次执行花了 10 S

图片

复制SQL到SQLyog执行

上面 SQL 脚本的思路,其实是就是查询出单天内不同的 ip,但是用到了 group by 进行分组去重,最后统计 ip 的次数

我们可以针对上面的 SQL 进行优化,使用 SQL 内置的 DISTINCT() 函数进行去重

SELECT COUNT(DISTINCT(ip)) FROM t_web_visit WHERE create_time >= "2022-08-08 00:00:00" AND create_time <= "2022-08-08 23:59:59";

优化后的 SQL,可以看到执行时间已经从原来的 10 S -> 0.57 S

图片

优化后的SQL执行

我们通过 explain 再次查看该 SQL 的索引执行情况

EXPLAIN SELECT COUNT(DISTINCT(ip)) FROM t_web_visit WHERE create_time >= "2022-08-03 00:00:00" AND create_time <= "2022-08-03 23:59:59";

通过输出结果可以看到,该语句没有走索引,进行了全表的扫描,同时查询了 658559 行数据

图片

explain查看索引使用情况

我们分析这次查询参数,主要是使用了 create_time 进行范围查询,可以接着对查询进行优化,给 create_time 新增索引

ALTER TABLE t_web_visit ADD INDEX _create_time( `create_time` );

再次执行第一条 SQL 语句,可以看到查询时间有了大幅度的提升,直接从原来的 10S -> 0.18S

图片

添加索引后的第一条SQL执行

在看第二条 SQL,时间也有了很大的提升,从 0.57 S -> 0.046 S

图片

添加索引后的第二条SQL执行

最后通过 explain 分析 SQL,可以看到,优化后的 SQL ,使用了 create_time 索引,只需要查询 871 条数据

图片

查看索引使用情况

优化后的 SQL 后,我们再打开蘑菇后台页面,可以看到页面从原来的 10S 变成了 0.9S~

图片

优化后,首页打开时间

下面,让我们一起来看看如何给自己的网站,集成 Druid 连接池,用来检测网站 SQL 性能吧~

SpringBoot如何集成Druid?

首先,需要添加依赖,在 pom.xml 文件中加入

<!-- 引入druid数据源 -->
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.8</version>
</dependency>

然后在 application.yml 中,添加配置

#spring
spring:# DATABASE CONFIGdatasource:username: rootpassword: rooturl: jdbc:mysql://localhost:3306/mogu_blog_business?useUnicode=true&allowMultiQueries=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&useSSL=false&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Drivertype: com.alibaba.druid.pool.DruidDataSource# 初始化大小,最小,最大initialSize: 20minIdle: 5maxActive: 200#连接等待超时时间maxWait: 60000#配置隔多久进行一次检测(检测可以关闭的空闲连接)timeBetweenEvictionRunsMillis: 60000#配置连接在池中的最小生存时间minEvictableIdleTimeMillis: 300000validationQuery: SELECT 1 FROM DUALdbcp:remove-abandoned: true#泄露的连接可以被删除的超时时间(秒),该值应设置为应用程序查询可能执行的最长时间remove-abandoned-timeout: 180testWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: true#配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙filters: stat,wall,log4jmaxPoolPreparedStatementPerConnectionSize: 20useGlobalDataSourceStat: trueconnectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

在创建配置 DruidConfig.java,创建 DataSource 数据源,同时配置监控页面的登录账号和密码

@Slf4j
@Configuration
public class DruidConfig {@Value("${spring.datasource.url}")private String dbUrl;@Value("${spring.datasource.username}")private String username;@Value("${spring.datasource.password}")private String password;@Value("${spring.datasource.driver-class-name}")private String driverClassName;@Value("${spring.datasource.initialSize}")private int initialSize;@Value("${spring.datasource.minIdle}")private int minIdle;@Value("${spring.datasource.maxActive}")private int maxActive;@Value("${spring.datasource.maxWait}")private int maxWait;@Value("${spring.datasource.timeBetweenEvictionRunsMillis}")private int timeBetweenEvictionRunsMillis;@Value("${spring.datasource.minEvictableIdleTimeMillis}")private int minEvictableIdleTimeMillis;@Value("${spring.datasource.validationQuery}")private String validationQuery;@Value("${spring.datasource.testWhileIdle}")private boolean testWhileIdle;@Value("${spring.datasource.testOnBorrow}")private boolean testOnBorrow;@Value("${spring.datasource.testOnReturn}")private boolean testOnReturn;@Value("${spring.datasource.poolPreparedStatements}")private boolean poolPreparedStatements;@Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")private int maxPoolPreparedStatementPerConnectionSize;@Value("${spring.datasource.filters}")private String filters;@Value("{spring.datasource.connectionProperties}")private String connectionProperties;/*** 声明其为Bean实例* 在同样的DataSource中,首先使用被标注的DataSource** @return*/@Bean@Primarypublic DataSource dataSource() {DruidDataSource datasource = new DruidDataSource();datasource.setUrl(this.dbUrl);datasource.setUsername(username);datasource.setPassword(password);datasource.setDriverClassName(driverClassName);// configurationdatasource.setInitialSize(initialSize);datasource.setMinIdle(minIdle);datasource.setMaxActive(maxActive);datasource.setMaxWait(maxWait);datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);datasource.setValidationQuery(validationQuery);datasource.setTestWhileIdle(testWhileIdle);datasource.setTestOnBorrow(testOnBorrow);datasource.setTestOnReturn(testOnReturn);datasource.setPoolPreparedStatements(poolPreparedStatements);datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);try {/*** 加入过滤*/List<Filter> filterList = new ArrayList<>();filterList.add(wallFilter());datasource.setProxyFilters(filterList);datasource.setFilters(filters);} catch (SQLException e) {log.error("druid configuration initialization filter");}datasource.setConnectionProperties(connectionProperties);return datasource;}/*** 配置一个管理后台的Servlet*/@Beanpublic ServletRegistrationBean statViewServlet() {ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");Map<String, String> initParams = new HashMap<>(Constants.NUM_TWO);initParams.put("loginUsername", "admin");initParams.put("loginPassword", " ");//默认就是允许所有访问initParams.put("allow", "");bean.setInitParameters(initParams);return bean;}/*** 配置一个web监控的filter** @return*/@Beanpublic FilterRegistrationBean webStatFilter() {FilterRegistrationBean bean = new FilterRegistrationBean();bean.setFilter(new WebStatFilter());Map<String, String> initParams = new HashMap<>(Constants.NUM_ONE);initParams.put("exclusions", "*.vue,*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*");bean.setInitParameters(initParams);bean.setUrlPatterns(Arrays.asList("/*"));return bean;}@Beanpublic WallFilter wallFilter() {WallFilter wallFilter = new WallFilter();WallConfig config = new WallConfig();//允许一次执行多条语句config.setMultiStatementAllow(true);//允许非基本语句的其他语句config.setNoneBaseStatementAllow(true);wallFilter.setConfig(config);return wallFilter;}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/189780.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

二、程序员指南:数据平面开发套件

MEMPOOL库 内存池是固定大小对象的分配器。在DPDK中&#xff0c;它由名称标识&#xff0c;并使用环形结构来存储空闲对象。它提供一些其他可选服务&#xff0c;例如每个核心的对象缓存和一个对齐辅助工具&#xff0c;以确保对象填充以将它们均匀分布在所有DRAM或DDR3通道上。 …

SSH协议简介与使用

Secure Shell(SSH) 是由 IETF(The Internet Engineering Task Force) 制定的建立在应用层基础上的安全网络协议。它是专为远程登录会话(甚至可以用Windows远程登录Linux服务器进行文件互传)和其他网络服务提供安全性的协议&#xff0c;可有效弥补网络中的漏洞。通过SSH&#xf…

【淘宝API】商品详情+搜索商品列表接口

淘宝商品详情API接口可以使用淘宝开放平台提供的SDK或API来获取。这些接口可以用于获取商品的详细信息&#xff0c;如标题、价格、描述、图片等。 以下是使用淘宝开放平台API获取商品详情的步骤&#xff1a; 注册淘宝开放平台账号&#xff0c;并创建应用&#xff0c;获取应用…

2023年中职“网络安全“—Linux系统渗透提权③

2023年中职"网络安全"—Linux系统渗透提权③ Linux系统渗透提权任务环境说明&#xff1a;1. 使用渗透机对服务器信息收集&#xff0c;并将服务器中SSH服务端口号作为flag提交&#xff1b;2. 使用渗透机对服务器信息收集&#xff0c;并将服务器中主机名称作为flag提交…

02-1解析xpath

我是在edge浏览器中安装的xpath&#xff0c;需要安装的朋友可以参考下面这篇博客最新版edge浏览器中安装xpath插件 一、xpathd的使用 安装lxml pip install lxml ‐i https://pypi.douban.com/simple导入lxml.etree from lxml import etreeetree.parse() 解析本地文件 htm…

代码随想录算法训练营第五十八天丨 动态规划part18

739. 每日温度 思路 首先想到的当然是暴力解法&#xff0c;两层for循环&#xff0c;把至少需要等待的天数就搜出来了。时间复杂度是O(n^2) 那么接下来在来看看使用单调栈的解法。 什么时候用单调栈呢&#xff1f; 通常是一维数组&#xff0c;要寻找任一个元素的右边或者左边…

Humanoid Robotics Whole Body Control(WBC)全身控制

系列文章目录 文章目录 系列文章目录前言一、ROS —— 什么是全身控制&#xff1f;二、IEEE - RAS三、维也纳工业大学 —— 自动化与控制研究所&#xff08;ACIN&#xff09;四、IIt&#xff08;意大利技术研究院&#xff09; 前言 谷歌的几种解释 一、ROS —— 什么是全身控制…

[开源]基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案

原文&#xff1a;[开源]基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案 一飞开源&#xff0c;介绍创意、新奇、有趣、实用的开源应用、系统、软件、硬件及技术&#xff0c;一个探索、发现、分享、使用与互动交流的开源技术社区平台。致力于打造活力开源社区&#xff0…

图数据库Neo4J 中文分词查询及全文检索(建立全文索引)

Neo4j的全文索引是基于Lucene实现的&#xff0c;但是Lucene默认情况下只提供了基于英文的分词器&#xff0c;下篇文章我们在讨论中文分词器&#xff08;IK&#xff09;的引用&#xff0c;本篇默认基于英文分词来做。我们前边文章就举例说明过&#xff0c;比如我要搜索苹果公司&…

Python学习笔记--自定义元类

四、自定义元类 到现在&#xff0c;我们已经知道元类是什么鬼东西了。 那么&#xff0c;从始至终我们还不知道元类到底有啥用。 只是了解了一下元类。 在了解它有啥用的时候&#xff0c;我们先来了解下怎么自定义元类。 因为只有了解了怎么自定义才能更好的理解它的作用。…

简单线性回归函数

简单线性回归函数 定义术语理解简单线性回归例子 定义 线性回归&#xff1a;利用线性回归方程中最小平方函数对一个或多个自变量和因变量之间关系进行建模的一个回归分析。该建模的目标为找到各个系数的最佳值让预测误差最小 简单线性回归&#xff1a;只有一个自变量的线性回…

6.2 List和Set接口

1. List接口 List接口继承自Collection接口&#xff0c;List接口实例中允许存储重复的元素&#xff0c;所有的元素以线性方式进行存储。在程序中可以通过索引访问List接口实例中存储的元素。另外&#xff0c;List接口实例中存储的元素是有序的&#xff0c;即元素的存入顺序和取…