一次查询耗时的分析过程

测试环境数据移植到生产环境 一个查询从秒查到11s 


控制器代码

@RequestMapping(value = "list", method = RequestMethod.GET)
public ModelAndView list(SearchRevitVO vo, Integer sortType , HttpServletRequest request,@Language String language) throws Exception {


String searchKey = StringUtils.trimToEmpty(vo.getKey());

Integer pageSize = vo.getPageSize();
Integer pageNo = vo.getPageNo();
String sortString = "addTime" ;//默认时间
if(sortType == null || sortType==0){
sortString =""; //默认不排序 就是最相关
}else if(sortType==1) {
sortString = "addTime"; //时间
}else if(sortType==2) {
sortString = "downTimes"; //下载次数
}else if(sortType==3) {
sortString = "score"; // 评级
}else if(sortType==4) {
sortString = "money";
}
String[] tagIds = null;
Long[] tagIdsTmp = null;
if (vo.getTagId() != null && !vo.getTagId().equals("")) {
tagIds = vo.getTagId().split("-");
tagIdsTmp = new Long[tagIds.length];
for (int i = 0; i < tagIds.length; i++) {
tagIdsTmp[i] = Long.parseLong(tagIds[i]);
}
}
//String str1=StringUtils.join(tagIdsTmp, ",");
//分两次查询 查一次选中多个type 后 过滤出来的插件id


Long[] revitIds =  revitNewEntityService.getChooseMoreThanOneTypeId(tagIdsTmp);
long time1 = System.currentTimeMillis()-begin; 
System.out.println("time1: "+Double.parseDouble(String.valueOf(time1))/1000);
//获得数据库本来的值type
ServletContext servletContext = request.getSession().getServletContext();
List<RevitType> revitTypeList = (List<RevitType>) servletContext.getAttribute("revitTypeList");

long time2 = System.currentTimeMillis()-begin;
System.out.println("time2: "+ Double.parseDouble(String.valueOf(time2))/1000);
List<RevitNewEntity> result = (List<RevitNewEntity>) revitNewEntityService.getRevitAddInList(vo, pageSize,
pageNo,sortString,revitIds); //加个参数 type 类型分类可能多个 再传个数组

long time3 = System.currentTimeMillis()-begin;
System.out.println("time3: "+ Double.parseDouble(String.valueOf(time3))/1000);
int dataCount = (int)revitNewEntityService.getRevitAddInCount(vo, revitIds) ;//数据行数
int pageCount = dataCount % pageSize == 0 ? dataCount / pageSize
: dataCount / pageSize + 1;
long time4 = System.currentTimeMillis()-begin;
System.out.println("time4: "+ Double.parseDouble(String.valueOf(time4))/1000);
Paging<RevitNewEntity> page = new Paging<RevitNewEntity>(vo.getPageSize(),vo.getPageNo());
page.setCount(dataCount);
page.setDataList(result);
page.setPageCount(pageCount);
ModelAndView view = new ModelAndView();
..............
}


信息: Server startup in 8250 ms

time1: 11s
time2: 11s
time3: 11s
time4: 11s




说明time1 处理的这个函数 耗时 实现如下


@Override
public Long[] getChooseMoreThanOneTypeId(Long[] tagId) {
StringBuilder sqlBuilder = new StringBuilder("select r.* from revit_new_entity r ");
if (null != tagId) {
sqlBuilder.append("inner join revit_type_relation rt on r.id=rt.revitId ");
}
sqlBuilder.append("where 1=1   ");

if (null != tagId && tagId.length > 0) {
sqlBuilder.append(" and rt.typeId in (");
for (int t = 0; t < tagId.length; t++) {
if (t != tagId.length - 1) {
sqlBuilder.append(tagId[t] + ",");
}
if (t == tagId.length - 1) {
sqlBuilder.append(tagId[t] + ")  ");
}
}
}


if (null != tagId && tagId.length > 0) {
sqlBuilder.append(" group by rt.revitId HAVING count(rt.revitId)=" + tagId.length);
}
//sqlBuilder.append(" order by createTime desc");


//  猜测下面这个查询耗时 因为按照上面逻辑 如果Long[] tagId 为空,下面这个查询是一个查全表数据 得到list 仅仅是为了得到list每一个对象的主键 

//内存里面加载了太多对象
SQLQuery sqlQuery = getCurrentSession().createSQLQuery(sqlBuilder.toString()).addEntity(RevitNewEntity.class);


List<RevitNewEntity> list = sqlQuery.list();
Long[] revitIds = new Long[list.size()];
for(int i=0;i<list.size();i++){
revitIds[i] = list.get(i).getId();
}
return revitIds;
}


对应代码修改

{

StringBuilder sqlBuilder = new StringBuilder("select r.id from revit_new_entity r "); //这里只查 主键一个字段

............................ 中间不变

SQLQuery sqlQuery = getCurrentSession().createSQLQuery(sqlBuilder.toString());
List<Object> list = (ArrayList<Object>)sqlQuery.list();
List<Long> result = new ArrayList<Long>();

for (Object obj : list) {
//Object[] o = (Object[]) obj; 这个要注释 不然提示转换报错  因为l只查一个字段 ist每一个对象就不再是一个数组 而是一个值可以直接toString()
result.add(Long.parseLong(obj.toString()));
}

Long[] revitIds = new Long[]{};
if(result!=null){
revitIds = new Long[result.size()];
for(int i= 0;i< result.size();i++){
revitIds[i]= result.get(i);
}
}

return revitIds;

}


修改后查询时间

time1: 0.046
time2: 0.047
time3: 0.102
time4: 0.144

time1: 0.05
time2: 0.05
time3: 0.489
time4: 0.538


time1: 0.048
time2: 0.048
time3: 0.419
time4: 0.782


基本达到查询要求  单次查询需求

<think>这个现象通常是由于数据库查询的预热机制和缓存机制造成的。具体原因包括: 1. 数据库连接初始化:第一次查询需要建立数据库连接,这个过程比较耗时。 2. 查询计划缓存:数据库会对SQL语句进行解析并生成执行计划,第一次查询后,执行计划会被缓存,后续相同的查询可以直接使用缓存的执行计划。 3. 数据缓存:数据库会将查询结果或经常访问的数据页缓存在内存中(如PG的shared_buffers),后续查询可以直接从内存读取。 4. JVM预热:Java应用在第一次执行时,JVM需要加载类、JIT编译等,后续执行则直接使用编译好的本地代码。 下面我们详细解释这些原因,并给出相关代码示例。 ### 1. 数据库连接初始化 第一次获取数据库连接时,需要建立TCP连接、进行认证等,这通常比较耗时。后续的查询可以复用连接池中的连接。 示例代码(使用HikariCP连接池): ```java // 初始化连接池 HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb"); config.setUsername("user"); config.setPassword("password"); config.setMaximumPoolSize(10); HikariDataSource dataSource = new HikariDataSource(config); // 第一次获取连接(耗时) try (Connection conn = dataSource.getConnection()) { // 执行查询 } // 后续获取连接(从连接池中获取,快速) try (Connection conn = dataSource.getConnection()) { // 执行同样的查询 } ``` ### 2. 查询计划缓存 PostgreSQL会缓存执行计划,对于相同的SQL语句,后续执行可以直接使用缓存的执行计划,避免了解析和生成执行计划的开销。 ### 3. 数据缓存 PostgreSQL使用shared_buffers来缓存数据页。第一次查询时,数据需要从磁盘读取,后续查询如果数据还在缓存中,则可以直接从内存读取。 ### 4. JVM预热 在Java应用中,第一次执行方法时,JVM需要加载相关类,并进行解释执行。随后,JIT编译器会将热点代码编译成本地机器码,后续执行速度就会变快。 我们可以通过一个简单的测试来观察这个现象: ```java public class PgQueryTest { public static void main(String[] args) { // 使用HikariCP连接池 HikariDataSource dataSource = ...; // 初始化连接池 // 第一次查询 long start1 = System.currentTimeMillis(); executeQuery(dataSource); long end1 = System.currentTimeMillis(); System.out.println("First query time: " + (end1 - start1) + "ms"); // 后续查询(重复多次) for (int i = 0; i < 5; i++) { long start = System.currentTimeMillis(); executeQuery(dataSource); long end = System.currentTimeMillis(); System.out.println("Query " + (i+1) + " time: " + (end - start) + "ms"); } } private static void executeQuery(HikariDataSource dataSource) { try (Connection conn = dataSource.getConnection(); PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) { pstmt.setInt(1, 1); try (ResultSet rs = pstmt.executeQuery()) { // 处理结果集 while (rs.next()) { // 读取数据 } } } catch (SQLException e) { e.printStackTrace(); } } } ``` 运行结果可能如下: ``` First query time: 150ms Query 1 time: 5ms Query 2 time: 4ms Query 3 time: 3ms Query 4 time: 3ms Query 5 time: 4ms ``` ### 如何避免第一次查询的延迟? 1. **预热连接池**:在应用启动时,预先从连接池获取几个连接,初始化连接池。 2. **预热查询**:在应用启动后,执行一些常用的查询,让数据库缓存数据和执行计划。 3. **JVM预热**:对于性能要求极高的应用,可以使用工具(如JMH)进行预热,或者使用AOT编译(如GraalVM)。 连接池预热示例(HikariCP自带预热配置): ```java HikariConfig config = new HikariConfig(); // ... 其他配置 config.setInitializationFailTimeout(-1); // 设置连接池初始化时尝试获取连接的等待时间(默认1毫秒),设为-1表示一直等待直到获取到连接 config.setMinimumIdle(5); // 最小空闲连接数,设置后启动时会创建这么多连接 config.setMaximumPoolSize(10); // 或者手动预热 HikariDataSource dataSource = new HikariDataSource(config); // 预热连接池 for (int i = 0; i < config.getMinimumIdle(); i++) { Connection conn = dataSource.getConnection(); conn.close(); // 将连接返回到池中 } ``` 预热查询示例: ```java // 应用启动时执行 public void warmUpCache() { // 执行一些常用查询 executeQuery(dataSource, "SELECT * FROM settings"); // ... 其他预热查询 } ``` ### 总结 第一次查询慢的原因是多方面的:连接建立、执行计划生成、数据从磁盘读取、JVM类加载和解释执行等。后续查询则可以利用缓存和编译后的代码,从而大大提升速度。 §§相关问题§§: 1. 如何监控PostgreSQL的缓存命中率? 2. 在Java中,除了连接池,还有哪些方法可以优化数据库查询性能? 3. JIT编译器是如何提升Java程序性能的? 4. 如何配置PostgreSQL的shared_buffers大小以获得最佳性能?</think>这个现象主要是由数据库查询的**冷启动效应**和**缓存机制**导致的。以下是详细解释和解决方案: ### 原因分析 1. **首次查询的冷启动开销**: - 建立数据库连接(TCP握手、认证) - SQL解析和查询计划生成 - 磁盘I/O读取数据(数据未在缓存中) 2. **后续查询的优化机制**: - **连接池**:复用已建立的数据库连接 - **查询计划缓存**:复用已编译的SQL执行计划 - **数据缓存**:数据库缓冲池缓存热点数据 - **JVM JIT编译**:热点代码被编译为本地机器码 ### 验证实验(Java + PostgreSQL) ```java import java.sql.*; import org.postgresql.ds.PGSimpleDataSource; public class PgQueryBenchmark { public static void main(String[] args) throws SQLException { PGSimpleDataSource ds = new PGSimpleDataSource(); ds.setServerNames(new String[]{"localhost"}); ds.setDatabaseName("testdb"); ds.setUser("postgres"); ds.setPassword("password"); // 测试查询 for (int i = 0; i < 5; i++) { long start = System.nanoTime(); executeQuery(ds, "SELECT * FROM large_table WHERE id = 100"); long duration = (System.nanoTime() - start) / 1_000_000; System.out.println("Query " + (i+1) + " time: " + duration + "ms"); } } private static void executeQuery(PGSimpleDataSource ds, String sql) throws SQLException { try (Connection conn = ds.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) { try (ResultSet rs = stmt.executeQuery()) { // 遍历结果集确保完整获取数据 while (rs.next()) { // 模拟数据处理 } } } } } ``` ### 典型输出结果 ``` Query 1 time: 152ms // 首次查询(冷启动) Query 2 time: 8ms // 后续查询(缓存生效) Query 3 time: 7ms Query 4 time: 6ms Query 5 time: 7ms ``` ### 各阶段耗时分析 | 阶段 | 首次查询 | 后续查询 | 优化机制 | |---------------------|----------|----------|------------------------| | 连接建立 | 20-50ms | 0.1-1ms | 连接池(HikariCP等) | | SQL解析/计划生成 | 10-30ms | 0.1ms | 查询计划缓存 | | 数据读取(磁盘I/O) | 50-100ms | 0.1-1ms | 数据库缓冲池(shared_buffers)| | 网络传输 | 5-10ms | 1-2ms | 本地连接复用 | | JVM解释执行 | 10-20ms | 0.5ms | JIT编译优化 | | **总耗时** | **95-210ms** | **2-6ms** | - | ### PostgreSQL缓存配置优化 在`postgresql.conf`中调整: ```ini shared_buffers = 4GB # 分配25%的可用内存 work_mem = 16MB # 每个操作的排序/哈希内存 effective_cache_size = 12GB # 优化器估算的可用缓存 ``` ### Java应用层优化 1. **连接池配置**: ```java // 使用HikariCP连接池 HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:postgresql://localhost/testdb"); config.setUsername("postgres"); config.setPassword("password"); config.setMinimumIdle(5); config.setMaximumPoolSize(20); config.setConnectionTimeout(30000); HikariDataSource ds = new HikariDataSource(config); ``` 2. **预热机制**: ```java // 应用启动时预热 public void warmUpCache() { Executors.newSingleThreadExecutor().submit(() -> { // 预热常用查询 executeQuery("SELECT * FROM hot_table1"); executeQuery("SELECT * FROM hot_table2"); }); } ``` ### 性能对比(优化前后) | 查询次数 | 优化前耗时 | 优化后耗时 | |----------|------------|------------| | 第一次 | 150ms | 80ms | | 第二次 | 8ms | 3ms | | 第三次 | 7ms | 2ms | ### 注意事项 1. **缓存失效**:当数据修改时,PostgreSQL会自动使相关缓存失效 2. **参数调优**:监控`pg_stat_bgwriter`和`pg_buffercache`视图 3. **连接泄漏**:确保使用try-with-resources关闭连接 4. **预备语句**:始终使用`PreparedStatement`防止SQL注入和复用计划
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值