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


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

    图片

    加载了10S才进后台

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

    图片

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

    图片

    初始化接口花了7S

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

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

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

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

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

    什么是连接池?

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

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

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

    1. public class TEST {
    2.     // JDBC 驱动名 及数据库 URL
    3.     static String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    4.     static String DB_URL = "jdbc:mysql://localhost:3306/webcourse";
    5.     // 数据库的用户名与密码,需要根据自己的设置
    6.     static String USER = "root";
    7.     static String PASS = "121314";
    8.     public static void main(String[] args) throws Exception {
    9.         Connection conn = null;
    10.         Statement stmt = null;
    11.         try {
    12.             // 注册 JDBC 驱动
    13.             // 把Driver类装载进jvm
    14.             Class.forName("com.mysql.jdbc.Driver");
    15.             // 打开链接
    16.             System.out.println("连接数据库...");
    17.             conn = (Connection) DriverManager.getConnection(DB_URL, USER, PASS);
    18.             // 执行查询
    19.             System.out.println(" 实例化Statement对...");
    20.             stmt = (Statement) conn.createStatement();
    21.             String sql = "SELECT * FROM bbs";
    22.             ResultSet rs = stmt.executeQuery(sql);
    23.             while (rs.next()) {
    24.                 // 通过字段检索
    25.                 int id = rs.getInt("id");
    26.                 String name = rs.getString("name");
    27.                 String content = rs.getString("content");
    28.                 // 输出数据
    29.                 System.out.print("ID: " + id);
    30.                 System.out.print(",姓名: " + name);
    31.                 System.out.print(",内容: " + content);
    32.                 System.out.print("\n");
    33.             }
    34.             // 完成后关闭
    35.             rs.close();
    36.             stmt.close();
    37.             conn.close();
    38.         } catch (SQLException se) {
    39.             // 处理 JDBC 错误
    40.             se.printStackTrace();
    41.         } catch (Exception e) {
    42.             // 处理 Class.forName 错误
    43.             e.printStackTrace();
    44.         } finally {
    45.             // 关闭资源
    46.             if (stmt != null)
    47.                 stmt.close();
    48.             if (conn != null)
    49.                 conn.close();
    50.         }
    51.         System.out.println("测试结束");
    52.     }
    53. }

    因此,在实际的开发过程中,是会考虑在数据库操作前,先提前创建并维护一批的数据库连接对象,当我们需要使用的时候,就从这批对象中获取一个连接,用完之后再返还,通过这一系列的操作,从而避免了不必要的时间开销,从而提高了运行效率,这种技术在 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 的,即从日志表中查看有多少用户访问了蘑菇

    1. SELECT COUNT(ip)
    2. FROM (
    3.     SELECT ip
    4.     FROM t_web_visit
    5.     WHERE create_time >= "2022-08-08 00:00:00"
    6.         AND create_time <= "2022-08-08 23:59:59"
    7.     GROUP BY ip
    8. ) 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 文件中加入

    1. <!-- 引入druid数据源 -->
    2. <dependency>
    3.     <groupId>com.alibaba</groupId>
    4.     <artifactId>druid</artifactId>
    5.     <version>1.1.8</version>
    6. </dependency>

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

    1. #spring
    2. spring:
    3.   # DATABASE CONFIG
    4.   datasource:
    5.     username: root
    6.     password: root
    7.     url: jdbc:mysql://localhost:3306/mogu_blog_business?useUnicode=true&allowMultiQueries=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&useSSL=false&serverTimezone=Asia/Shanghai
    8.     driver-class-name: com.mysql.cj.jdbc.Driver
    9.     type: com.alibaba.druid.pool.DruidDataSource
    10.     # 初始化大小,最小,最大
    11.     initialSize: 20
    12.     minIdle: 5
    13.     maxActive: 200
    14.     #连接等待超时时间
    15.     maxWait: 60000
    16.     #配置隔多久进行一次检测(检测可以关闭的空闲连接)
    17.     timeBetweenEvictionRunsMillis: 60000
    18.     #配置连接在池中的最小生存时间
    19.     minEvictableIdleTimeMillis: 300000
    20.     validationQuery: SELECT 1 FROM DUAL
    21.     dbcp:
    22.       remove-abandoned: true
    23.       #泄露的连接可以被删除的超时时间(秒),该值应设置为应用程序查询可能执行的最长时间
    24.       remove-abandoned-timeout: 180
    25.     testWhileIdle: true
    26.     testOnBorrow: false
    27.     testOnReturn: false
    28.     poolPreparedStatements: true
    29.     #配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    30.     filters: stat,wall,log4j
    31.     maxPoolPreparedStatementPerConnectionSize: 20
    32.     useGlobalDataSourceStat: true
    33.     connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

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

    1. @Slf4j
    2. @Configuration
    3. public class DruidConfig {
    4.     @Value("${spring.datasource.url}")
    5.     private String dbUrl;
    6.     @Value("${spring.datasource.username}")
    7.     private String username;
    8.     @Value("${spring.datasource.password}")
    9.     private String password;
    10.     @Value("${spring.datasource.driver-class-name}")
    11.     private String driverClassName;
    12.     @Value("${spring.datasource.initialSize}")
    13.     private int initialSize;
    14.     @Value("${spring.datasource.minIdle}")
    15.     private int minIdle;
    16.     @Value("${spring.datasource.maxActive}")
    17.     private int maxActive;
    18.     @Value("${spring.datasource.maxWait}")
    19.     private int maxWait;
    20.     @Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
    21.     private int timeBetweenEvictionRunsMillis;
    22.     @Value("${spring.datasource.minEvictableIdleTimeMillis}")
    23.     private int minEvictableIdleTimeMillis;
    24.     @Value("${spring.datasource.validationQuery}")
    25.     private String validationQuery;
    26.     @Value("${spring.datasource.testWhileIdle}")
    27.     private boolean testWhileIdle;
    28.     @Value("${spring.datasource.testOnBorrow}")
    29.     private boolean testOnBorrow;
    30.     @Value("${spring.datasource.testOnReturn}")
    31.     private boolean testOnReturn;
    32.     @Value("${spring.datasource.poolPreparedStatements}")
    33.     private boolean poolPreparedStatements;
    34.     @Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")
    35.     private int maxPoolPreparedStatementPerConnectionSize;
    36.     @Value("${spring.datasource.filters}")
    37.     private String filters;
    38.     @Value("{spring.datasource.connectionProperties}")
    39.     private String connectionProperties;
    40.     /**
    41.      * 声明其为Bean实例
    42.      * 在同样的DataSource中,首先使用被标注的DataSource
    43.      *
    44.      * @return
    45.      */
    46.     @Bean
    47.     @Primary
    48.     public DataSource dataSource() {
    49.         DruidDataSource datasource = new DruidDataSource();
    50.         datasource.setUrl(this.dbUrl);
    51.         datasource.setUsername(username);
    52.         datasource.setPassword(password);
    53.         datasource.setDriverClassName(driverClassName);
    54.         // configuration
    55.         datasource.setInitialSize(initialSize);
    56.         datasource.setMinIdle(minIdle);
    57.         datasource.setMaxActive(maxActive);
    58.         datasource.setMaxWait(maxWait);
    59.         datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
    60.         datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
    61.         datasource.setValidationQuery(validationQuery);
    62.         datasource.setTestWhileIdle(testWhileIdle);
    63.         datasource.setTestOnBorrow(testOnBorrow);
    64.         datasource.setTestOnReturn(testOnReturn);
    65.         datasource.setPoolPreparedStatements(poolPreparedStatements);
    66.         datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
    67.         try {
    68.             /**
    69.              * 加入过滤
    70.              */
    71.             List filterList = new ArrayList<>();
    72.             filterList.add(wallFilter());
    73.             datasource.setProxyFilters(filterList);
    74.             datasource.setFilters(filters);
    75.         } catch (SQLException e) {
    76.             log.error("druid configuration initialization filter");
    77.         }
    78.         datasource.setConnectionProperties(connectionProperties);
    79.         return datasource;
    80.     }
    81.     /**
    82.      * 配置一个管理后台的Servlet
    83.      */
    84.     @Bean
    85.     public ServletRegistrationBean statViewServlet() {
    86.         ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
    87.         Map initParams = new HashMap<>(Constants.NUM_TWO);
    88.         initParams.put("loginUsername""admin");
    89.         initParams.put("loginPassword"" ");
    90.         //默认就是允许所有访问
    91.         initParams.put("allow""");
    92.         bean.setInitParameters(initParams);
    93.         return bean;
    94.     }
    95.     /**
    96.      * 配置一个web监控的filter
    97.      *
    98.      * @return
    99.      */
    100.     @Bean
    101.     public FilterRegistrationBean webStatFilter() {
    102.         FilterRegistrationBean bean = new FilterRegistrationBean();
    103.         bean.setFilter(new WebStatFilter());
    104.         Map initParams = new HashMap<>(Constants.NUM_ONE);
    105.         initParams.put("exclusions""*.vue,*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*");
    106.         bean.setInitParameters(initParams);
    107.         bean.setUrlPatterns(Arrays.asList("/*"));
    108.         return bean;
    109.     }
    110.     @Bean
    111.     public WallFilter wallFilter() {
    112.         WallFilter wallFilter = new WallFilter();
    113.         WallConfig config = new WallConfig();
    114.         //允许一次执行多条语句
    115.         config.setMultiStatementAllow(true);
    116.         //允许非基本语句的其他语句
    117.         config.setNoneBaseStatementAllow(true);
    118.         wallFilter.setConfig(config);
    119.         return wallFilter;
    120.     }
    121. }
  • 相关阅读:
    DDOS防护如何建设?
    手把手教你如何在报表中查询数据
    app备案
    Spring AOP浅谈
    【C++】基于Easyx的UI库
    【C++ Primer Plus学习记录】复合类型总结
    SAP:增强中用commit和wait up会导致操作异常
    伊朗黑客对以色列科技行业发起恶意软件攻击
    SpringBoot 整合Mybatis
    SaaSpace:4种最佳硬盘恢复软件工具
  • 原文地址:https://blog.csdn.net/LINgZone2/article/details/134474215