DEV Community

Tuan
Tuan

Posted on

4 cách để nâng cao tốc độ load APIs, và áp dụng vào Java Spring Boot

1. Pagination

Đối với các apis response trả về là một mảng, và số lượng dữ liệu lớn được dùng trong các table, list, option,... thì câu lệnh query phải sử dụng phân trang, không được lấy hết tất cả.

  • Java Spring Boot

Với JPA và Hibernate

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();
Enter fullscreen mode Exit fullscreen mode

Với Java Spring Boot đó chưa phải là tất cả, nếu bạn có hàng ngàn record và khi sử dụng join fetch thì khi dùng cú pháp phía trên hệ thống sẽ warning như sau:
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

Và đặc biệt phải chú ý đến vấn đề Hibernate N+1 problem

:( ngay từ đầu sao không nói vậy đi!!! Đến khi xảy ra vấn đề hiệu năng mới nói

Tại sao lại có cảnh báo về HHH000104 và nó có ảnh hưởng đến tốc độ query cũng như response, câu trả lời chắc chắn là có.
Thực tế Hibernate sẽ biên dịch thành native sql query như sau:

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.created_on AS created_2_1_1_,
       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
Enter fullscreen mode Exit fullscreen mode

Không hề có từ khoá nào liên quan đến limit, offset hoặc row_number,...
Hibernate query lấy hết data với các điều kiện, sau đó mới tiến hành Deserialization vào các entity/model/dto, dẫn tới việc query xuống database rất lâu

Có 2 cách giải quyết được vấn đề này:
- Cách 1: dùng 2 câu query

Chỉ select post.id để lấy list id thoả mãn điều kiện cần.

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();
Enter fullscreen mode Exit fullscreen mode

Có được list post id thì tiến hành query post có id nằm trong list đó

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();
Enter fullscreen mode Exit fullscreen mode

Cách này thì nên chú ý về giới hạn mảng postIds trong câu lệnh in nhá, vì nó có giới hạn đấy.

- Cách 2: Nếu sử dụng Oralce Database có thể sử dụng DENSE_RANK
Cách này mình chưa áp dụng, nên các bạn muốn áp dụng và xem chi tiết thì tham khảo tại đây nhá.

Ngoài N+1, pagination thì đối với Java Spring Boot Hibernate JPA, có rất nhiều vấn đề liên quan đến hiệu năng nữa như là spring.jpa.open-in-view, Hikari: Connection is not available, request timeout after 30000ms, EntityGraph,... Nếu các bạn quan tâm thì có thể comment nhé :D

2. Async Logging

Việc backend tiến hành ghi logs để monitor lỗi, thông tin, debug là điều tất nhiên, tuy nhiên nếu log không được ghi bất đồng bộ thì cũng ảnh hưởng đến hiệu năng đấy, và vấn đề này thường được bỏ qua khi xem xét hiệu năng.

Vì đối với 1 hệ thống lớn thì số lượng request nhiều, để logs được ghi bất đồng bộ và thay vào đó dành thời gian hay chỗ để giải quyết các logic và response trả về sẽ giảm tải độ trễ của APIs

  • Java Spring Boot

Nếu sử dụng logback.xml thì có thể config tương tự như sau

<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>
Enter fullscreen mode Exit fullscreen mode

3. Caching

Kỹ thuật này cực kỳ cải thiện tốc độ load và có nhiều phương thức để áp dụng như Redis, hoặc nếu dùng Java Spring Boot thì framework này cũng đã hỗ trợ sẵn

https://www.baeldung.com/spring-cache-tutorial

Cơ chế cơ bản của cache là tạo và lưu trữ data vào bộ nhớ trong (in-memory) dựa vào key và có 1 thời gian hết hạn.

Thông thường ở lần gọi đầu tiên sẽ không được nhanh vì cache chưa có nên sẽ query trực tiếp vào database, ở các lần tiếp theo mới nhanh được.

Vì thế mình hay viết các scheduler/cron-job/cron-tab để đầu giờ sáng hằng ngày hệ thống sẽ fetch và tạo cache trước cho các api query dữ liệu lớn hay được dùng như list, chart, thống kê,...

4. Payload compress

Nôm na hiểu là sẽ tối ưu dung lượng lúc Deserialization và lúc response cho client.

Và sẽ có một vài cách:

  • gzip response
  • sử dụng kỹ thuật DTO cho từng APIs thay vì dùng full column trong entity hoặc model
  • sử dụng tên ngắn cho các fields (cách này không khuyến khích, vì các fields trả về khó hiểu nghĩa)

Với Java Spring Boot, các bạn có thể tham khảo tại đây.

Cảm ơn các bạn đã theo dõi và đọc bài viết.

Resources:

Top comments (0)