Java性能微调之数据库性能

  大部分Java系统性能问题基本上是由于错误的数据库访问方式引起的,带来了大量额外日志和内存消耗,这些都会对JVM的垃圾回收造成冲击影响,本文主要针对这种错误的数据库访问方式进行分析和诊断。

Java性能诊断工具使用Java自带的Java Mission Control或JProfiler等工具,许多框架如Hibernate或Spring访问数据的方式都可以通过日志输出得到诊断。

首先我们需要确认需要提升性能的部位,通常会有以下几个方面:

  • 不够效率的数据库使用: 错误查询设计,;业务逻辑主要集中在SQL语句中,很少使用Java实现的业务逻辑;数据访问框架的不正确配置方式。
  • 坏的数据表结构设计:数据表的关系太多;太慢的存储视图;没有或错误的索引;过时的数据表统计。
  • 不适当的数据库配置: 内存, 磁盘, 表空间, 连接池配置等

为了追查这些热点部位,可以对照下面的数据库问题模式checklist进行逐步排查:

  • 太长的SQL语句:  一次性执行很多(> 500)不同的SQL语句
  • N+1 查询问题:  多次(>20)执行同样的查询语句:
  • 单条SQL语句很慢:: 执行某条SQL语句占据整个过程的80%以上响应时间
  • 数据驱动问题Data-Driven Issue:同样的请求因为不同输入参数执行不同的SQL语句。
  • 数据库负载太重:数据库响应时间占据整个请求响应时间的 60%以上。
  • Unprepared 语句:没有使用prepare执行SQL语句。
  • 资源枯竭:数据库连接时间超过语句执行时间。
  • 没有效率的连接池访问:连接池连接频繁获得,调用 getConnection次数大于执行正常SQL语句的50%以上。
  • 数据库服务器超负载: 来自大量太多的请求导致数据库服务器超载,而运行Java的应用服务器比如Tomcat等的负载很低。整个系统的负载都集中到了数据库服务器上。

下面我们通过案例来对上述checklist进行详细描述。

首先,我们对一个Web应用进行响应时间统计,比如一个典型的Web应用是通过三个环节:Nginx/Apache + Tomcat + Database。我们通过性能测试工具可以大概获得三个部分的分别响应时间是:

Nginx: 3.18ms 0.01%

Tomcat: 20.28s 33.49%

Database:40.27 66.51%

从以上数据可以看出,数据库的响应时间占据整个请求响应时间的66.51%,超过一半时间以上,这说明数据库超负载运行了。

通过跟踪数据库访问方式,也就是SQL语句执行情况,会发现同一个SQL因为不同参数执行很多次,也就是N+1性能问题,比如可能我们的Java代码有一个循环语句:

foreach (catIDs:catID) {
Cat cat= find_cat(catID);
// ...
}

其中find_cat是一个JDBC查询语句:

SELECT * FROM hat WHERE catID = 

这样这个循环执行就变成如下SQL语句执行:

SELECT * FROM hat WHERE catID = 1
SELECT * FROM hat WHERE catID = 2
SELECT * FROM hat WHERE catID = 3
SELECT * FROM hat WHERE catID = 4
SELECT * FROM hat WHERE catID = 5
...

这种因为不同的catID参数值不同,但是SQL语句相同的情况执行N多次,不如一次性使用批查询更加快捷:

SELECT * FROM hat WHERE catID IN (1, 2, 3, 4, 5, ...)

这就是典型的数据库N+1性能问题。除了使用SQL批查询,也可以使用缓存减少每个对象从SQL语句构造消耗的时间,或者使用O/R映射框架如Hibernate的懒加载。

这里需要比较一下使用SQL的inner join查询好还是使用缓存机制好呢?使用join查询虽然能够快速获得性能提升,但是可扩展性很差,join涉及的库表必须放在一个数据库服务器中,将来如果访问量负载更大,就无法分库分表了,丧失了扩展性Scalable,NoSQL数据库与关系数据库的主要区别就在于NoSQL消灭了join。

下面再谈谈Perpare语句:Hibernate框架缺省都是使用prepare,但是我们自己的SQL语句有可能没有使用,一条SQL语句是需要被数据库引擎分析的,然后创建数据访问计划,这个计划是存储在数据库的缓存中,这样,下次同样SQL语句,虽然参数不同,但是SQL语句不再需要重新分析,这样能够提高性能。

我们再看看连接池配置大小的误配:通常默认的连接池大小是每个池10或20个连接,在没有尖峰大量访问情况下,一般这个参数不需要配置优化,但是在真正运行时刻可能会造成瓶颈。连接池情况可以通过JMX测量发现,每个应用服务器如tomcat都会有后台管理,显示其当前的各种运行数据,我们通过观察数据库连接池Active活跃数据量是否达到最大值来进行判断。

总之,Java性能调试有两个方向:一个是在微调思路上做细做深,但是这对于有大量代码的关键业务运行场合几乎是很难实现,没有一个探测仪器不会对生产现场的稳定性不产生影响,看病X光扫描还会影响健康呢,但是没有X光又不能确诊。第二个方向就是重构,从新的思路在架构上重新梳理,这些方式引入后,会从根本上改变之前架构上的性能隐患和Bug。新思路:读写分离,读操作加入缓存机制,写操作引入并发机制,并在整个架构上引入分区分库等横向扩展性。

 

Java性能专题

性能优化的首要法则

Tomcat实战中的微调

什么是数据库ACID

业界最大谎言:大部分关系数据库并不真的支持ACID

Java持久锁总结

数据库系统并发控制原理

PostgreSQL、Oracle/MySQL和SQL Server的MVCC实现原理方式

最终一致性其实比MVCC简单

线性化与串行化比较

ACID和CAP的详尽比较