最近在登录项目后台的时候,发现当我输入账号和密码后,竟然就卡在了 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;}}