- 分页
对于返回数组的 API 响应以及在表格、列表、选项等中使用大量数据,查询语句必须使用分页,不得获取全部数据。
Java Spring Boot
使用 JPA 和 Hibernate
代码语言:java复制List<Post> posts = entityManager.createQuery(
"select p "
"from Post p "
"left join fetch p.comments "
"order by p.createdOn", Post.class)
.setFirstResult(10)
.setMaxResults(10)
.getResultList();
然而对于 Java Spring Boot,如果你有数千条记录并且使用了 join fetch,使用上述方式会出现如下警告:
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
特别需要注意 Hibernate N 1 问题
:( 从一开始为什么不直接说呢!直到性能问题出现才提到
为什么会有 HHH000104 警告,以及它对查询速度和响应有何影响,答案肯定是有的。
实际上 Hibernate 会将其编译成本地 SQL 查询,如下所示:
代码语言:java复制SELECT p.id AS id1_0_0_
c.id AS id1_1_1_,
p.created_on AS created_2_0_0_,
p.title AS title3_0_0_,
c.post_id AS post_id4_1_1_,
c.review AS review3_1_1_,
c.post_id AS post_id4_1_0__,
c.id AS id1_1_0__
FROM post p
LEFT OUTER JOIN post_comment c ON p.id=c.post_id
ORDER BY p.created_on
查询中没有任何与 limit、offset 或 row_number 相关的关键字,...
Hibernate 查询会获取所有数据,然后再进行实体/模型/DTO的反序列化,导致查询数据库时间很长
有两种解决这个问题的方法:
- 方法1:使用两个查询语句
仅选择 post.id 以获取满足条件的 id 列表。
代码语言:java复制List<Long> postIds = entityManager.createQuery("""
select p.id
from Post p
where p.title like :titlePattern
order by p.createdOn
""", Long.class)
.setParameter(
"titlePattern",
"High-Performance Java Persistence %"
)
.setMaxResults(5)
.getResultList();
获取到 post id 列表后,再查询包含在该列表中的 post。
代码语言:java复制List<Post> posts = entityManager.createQuery("""
select distinct p
from Post p
left join fetch p.comments
where p.id in (:postIds)
order by p.createdOn
""", Post.class)
.setParameter("postIds", postIds)
.setHint(
QueryHints.HINT_PASS_DISTINCT_THROUGH,
false
)
.getResultList();
这种方式需要注意一下 postIds 数组的限制,因为它是有限制的。
- 方法2:如果使用 Oracle Database,可以使用 DENSE_RANK 我还没有应用过这种方法,所以想要应用并了解更多细节,请参考此处。
除了 N 1 和分页,对于 Java Spring Boot Hibernate JPA,还有很多与性能相关的问题,比如 spring.jpa.open-in-view、Hikari:Connection is not available、request timeout after 30000ms、EntityGraph 等。如果你有兴趣,可以评论讨论哦 :D
- 异步日志记录
后端记录日志以监视错误、信息、调试是理所当然的,但如果日志不是异步记录,也会影响性能,这个问题在考虑性能时经常被忽略。
对于大型系统来说,请求量很大,如果日志不是异步记录,而是花费时间或者空间来解决逻辑和返回响应,则会降低 API 的延迟。
Java Spring Boot
如果使用 logback.xml,可以进行类似如下的配置:
代码语言:xml复制<configuration>
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
${LOG_PATTERN}
</pattern>
</encoder>
</appender>
<appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>
./logs/application.log
</file>
<encoder>
<pattern>
${LOG_PATTERN}
</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
./logs/archive/application-%d{yyyy-MM-dd}-%i.log.zip
</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>100</maxHistory>
</rollingPolicy>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="ROLLING_FILE" />
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="CONSOLE" />
</appender>
<root level="INFO">
<appender-ref ref="ROLLING_FILE" />
<appender-ref ref="CONSOLE" />
</root>
</configuration>
- 缓存
这项技术极大地提升了加载速度,并且有多种应用方式,比如 Redis,或者如果使用 Java Spring Boot,该框架也已经提供了
支持。
基本的缓存机制是根据键将数据存储在内存中,并且有一个过期时间。
通常第一次调用时不会很快,因为缓存还不存在,所以会直接查询数据库,之后的调用才会变快。
因此,我经常编写调度程序/定时任务/定时器,每天清晨系统将会预先获取和缓存用于大量数据查询的 API,比如列表、图表、统计等。
- 负载压缩
简而言之,这将在客户端的反序列化和响应时优化数据量。
一些方法包括:
- gzip 响应
- 对于每个 API 使用 DTO 技术而不是使用实体或模型中的完整列
- 对字段使用简短的命名(不建议这种方式,因为返回的字段难以理解含义)
对于 Java Spring Boot,你可以在这里了解更多。
我正在参与2023腾讯技术创作特训营第四期有奖征文,快来和我瓜分大奖!