Hibernate Caching - Bài 2: Second Level Cache

Chào các bạn! Chúng ta lại quay trở lại trong series hướng dẫn về Hibernate Caching. Trước khi đọc những gì mình viết dưới đây, hãy dành chút thời gian để nhớ lại những gì mình đã đề cập đến trong bài Hibernate Caching - Bài 1: Second Level Cache nhé.

2. Second Level Cache (L2)

2.1. Giới thiệu

Như mình đã đề cập trong bài trước, phạm vi ảnh hưởng của First Level Cache(L1) là nội session. Nghĩa là khi bạn gọi đối tượng X trong sessionA, thì nó sẽ chỉ tìm đối tượng X trong sessionA và trong trường hợp sessionA không có, nó sẽ thực thi câu lệnh truy vấn tới DB.

Khác với L1, phạm vi lưu trữ đối tượng được mở rộng hơn trong L2, sang mức SessionFactory thay vì session như L1. Bạn có thể hình dung rằng, thay vì các session có một vùng nhớ cache riêng lẻ (ở L1), thì tất cả chúng (những session được tạo ra bởi cùng 1 session factory) sẽ có chung một "ngôi nhà" to đùng và các đối tượng được cache lại sẽ nằm ở đó và khi gọi cũng từ đó mà được lấy ra. Các đối tượng này khi được cache lại "ngôi nhà" này cũng sẽ có những phòng mà 1, 1 vài hoặc 1 nhóm sẽ dùng chung mà khái niệm trong L2 gọi là region (bạn có thể hiểu như đó là tên phòng nơi chúng được lưu trữ) và mặc định nó là Full Class Name của đối tượng đó (ví dụ com.nhs3108.hibernate.User). Khái niệm này khá quá trọng mà bạn sẽ cần phải nhớ để hiểu ở phần sau.

LƯU Ý:: L2 sẽ chỉ hoạt động trong những trường hợp sau:

  • Khi bạn lấy đối tượng bằng ID (tuy nhiên, nếu bạn sử dụng SQL/HQL thì cũng không được cho dù bạn có dùng lệnh where id=?)
  • Khi các liên kết ngoại của bạn là lazy-loaded (hoặc eager-loaded với selects thay vì joins)

2.2: Enable Second Level Cache

Để bật Second Level Cache trong Hibernate, bạn chỉ cần cấu hình 2 thuộc tính

  • hibernate.cache.use_second_level_cache : để báo với Hibernate rằng chúng ta có(true) dùng L2 hay không(false)
  • hibernate.cache.region.factory_class: để chỉ định tên lớp Region Factory

như sau

<properties>
    ...
    <property name="hibernate.cache.use_second_level_cache" value="true"/>
    <property name="hibernate.cache.region.factory_class"
      value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
    ...
</properties>

2.3: Cache Concurrency Strategy (CCS)

(Mình dịch nôm na nó như là Cơ chế/Chiến lược truy cập đồng thời) Dựa vào từng use-case, ta có thể tùy ý chọn các kiểu CCS sao cho phù hợp

  • READ_ONLY: Chỉ được sử dụng cho các thực thể được đọc thường xuyên nhưng không bao giờ bị thay đổi. Mình hay gọi đó là master-data. Ví dụ trong DB của bạn có 1 bảng Category(category_id, category_name) và trong list các chức năng của bạn không có cái nào làm thay đổi dữ liệu của bất cứ record nào trong bảng category cả. Đó là 1 ví dụ nho nhỏ thôi, còn đâu là tùy thuộc vào dữ liệu trong hệ thống mà ta sử dụng nữa.
  • NONSTRICT_READ_WRITE: Cơ chế này không đảm bảo tính nhất quán giữa bộ nhớ cache và cơ sở dữ liệu. Sử dụng chiến lược này nếu dữ liệu hầu như không thay đổi và trong trường hợp rất hiếm còn lại, thì sự không nhất quán đó cũng không phải là vấn đề.
  • READ_WRITE: Cơ chế này đảm bảo tính nhất quán dữ liệu cao bằng việc sử dụng 'soft lock`. Khi một thực thể đã được cache bị update, một 'soft lock' được lưu lại trong cache cho entity và nó sẽ được giải phóng (release) khi transaction được commit. Tất cả các transaction nếu truy cập vào các đối tượng đang bị softblock sẽ được lấy trực tiếp từ cơ sở dữ liệu. Bạn hình dung là, nếu 1 TRANSACTION A đang muốn update đối tượng X, thì nó sẽ "khóa" đối tượng X lại và khi các TRANSACTION B, C muốn sử dụng đối tượng X, nó sẽ phải lấy trực tiếp từ DB thay vì từ cache cho đến khi nào TRANSACTION A mở khóa cho đối tượng X (như mình vừa nói là sau khi transaction commit)
  • TRANSACTIONAL: Cơ chế này đảm bảo các transaction làm việc biệt lập hoàn toàn. Cái này chắc fai nhờ các bạn tìm hiểu thêm. Mấy thằng CacheProvider mình liệt kê ngay dứoi đây cũng ko hỗ trợ nó 😄 Nên tạm thời mình cững chưa có dịp tìm hiểu qua =))

2.4. Region Factory , Cache Provider

Về cơ bản, nó làm việc giống như một chiếc cầu nối giữa Hibernate và Cache Provider.

Trong code sample của mình, mình có sử dụng EhCache trong vai trò của 1 Cache Provider. EhCache được sử dụng rộng rãi nhất trong số các Cache Provider của Hibernate. Tất nhiên bản có thể chọn bất cứ Cache Provider nào khác nhưng hãy cân nhắc bởi mỗi Cache Provider có thể sẽ không thích hợp với 1 hoặc 1 số Cache Concurrency Strategy (CCS)

Provider\Strategy Read-only Nonstrictread-write Read-write Transactional
EHCache Yes Yes Yes X
OSCache Yes Yes Yes X
SwarmCache Yes Yes X X
JBoss Cache Yes X X X

2.5. Khai báo 1 Entity có thể Cache

Để taọ ra một thực thể đủ điều kiện cho L2 caching, chúng ta khai báo annotation @org.hibernate.annotations.Cache và chỉ định 1 Cache Concurrency Strategy

Mình ví dụ, trong sample của mình có Entity User như sau:

package entity;


import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.persistence.*;

@Entity
@Table(name = "\"user\"")
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
public class User implements java.io.Serializable {
    private static final long serialVersionUID = -3885948600652210064L;

    /**
     *
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "user_id", unique = true, nullable = false)
    private String userId;

    @Column(name = "user_name", length = 64)
    private String userName;

    @Column(name = "email", length = 256)
    private String email;

    @Column(name = "infos")
    private String infos;
    
    // Các getter, setter
    // .v.v.
}

2.6. Cache Management

Nếu không cấu hình chính sách hết hạn và gỡ bỏ, bộ nhớ cache có thể phát triển vô hạn và cuối cùng tiêu thụ hết bộ nhớ hiện có. Trong hầu hết các trường hợp, Hibernate trao nhiệm vụ đó cho Cache Provider. Ví dụ chúng ta khai báo cấu hình EhCache để hạn chế số lượng instance của User với max = 6969 như sau:

ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
    <diskStore path="java.io.tmpdir" />

    <defaultCache maxElementsInMemory="1" eternal="false"
                  timeToIdleSeconds="10000" timeToLiveSeconds="60000" overflowToDisk="false" />
    <cache name="entity.User" maxElementsInMemory="6969" />
</ehcache>

2.7. Collection Cache

Collection mặc định sẽ không được cache. Chúng ta cần khai báo tuờng minh nếu chúng ta cần cache nó lại. Mình ví dụ

package entity;


import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.persistence.*;

import entity.Store;

@Entity
@Table(name = "\"user\"")
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
public class User implements java.io.Serializable {
    private static final long serialVersionUID = -3885948600652210064L;

    /**
     *
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "user_id", unique = true, nullable = false)
    private String userId;

    @Column(name = "user_name", length = 64)
    private String userName;

    @Column(name = "email", length = 256)
    private String email;

    @Column(name = "infos")
    private String infos;

    @Cacheable
    @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @OneToMany
    private Collection<Store> stores;
    // Constructors, getters, setters

Phần này có thể mình sẽ phải viết 1 bài riêng. Tạm thời thì chưa nên các tham khảo ở các nguồn khác giúp mình nhé

2.8. Ví dụ cụ thể

Mình có 1 ví dụ như sau:

Đầu tiên mình phải báo với Hibernate rằng User của mình có thể được sử dụng với L2 như sau

package entity;


import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.persistence.*;

@Entity
@Table(name = "\"user\"")
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
public class User implements java.io.Serializable {
    private static final long serialVersionUID = -3885948600652210064L;

    /**
     *
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "user_id", unique = true, nullable = false)
    private String userId;

    @Column(name = "user_name", length = 64)
    private String userName;

    @Column(name = "email", length = 256)
    private String email;

    @Column(name = "infos")
    private String infos;
    
    // Các getter, setter
    // .v.v.
}
import entity.User;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

/**
 * Created by nhs3108 on 13/09/2017.
 */
public class App {
    public static void main(String args[]) {
        SessionFactory sessionFactory = new Configuration()
                .configure("hibernate.cfg.xml").buildSessionFactory();
        Session curSession1 = sessionFactory.openSession();
        Session curSession2 = sessionFactory.openSession();
        curSession1.beginTransaction();
        curSession2.beginTransaction();

        User user1 = curSession1.load(User.class, "FPLy4QY2");
        System.out.println(user1.getUserName());

        User user2 = curSession2.load(User.class, "FPLy4QY2");
        System.out.println(user2.getUserName());

        curSession1.disconnect();
        curSession2.disconnect();
        curSession1.close();
        curSession2.close();
        sessionFactory.close();
    }
}

Khi đó, output của mình nhận được như sau

Hibernate: 
    select
        user0_.user_id as user_id1_0_0_,
        user0_.email as email2_0_0_,
        user0_.infos as infos3_0_0_,
        user0_.user_name as user_nam4_0_0_ 
    from
        "user" user0_ 
    where
        user0_.user_id=?
nhs3108
nhs3108

Như các bạn cũng thấy, đối tượng User(id=FPLy4QY2) được thực hiện truy vấn load từ DB 1 lần cho dù chúng ta có đang gọi nó từ 2 session khác nhau.

Sẽ thế nào nếu chúng ta xóa bỏ nó giống như cách mà chúng ta xóa trong L1 trước khi load User(id=FPLy4QY2) từ session2 nhỉ. Thử nhé! Ta đổi lại code như sau:

User user = curSession1.load(User.class, "FPLy4QY2");
System.out.println(user.getUserName());

curSession1.evict(user);
curSession1.clear();
curSession2.evict(user);
curSession2.clear();

user = curSession2.load(User.class, "FPLy4QY2");
System.out.println(user.getUserName());

Và console mình nhận được như sau

Hibernate: 
    select
        user0_.user_id as user_id1_0_0_,
        user0_.email as email2_0_0_,
        user0_.infos as infos3_0_0_,
        user0_.user_name as user_nam4_0_0_ 
    from
        "user" user0_ 
    where
        user0_.user_id=?
nhs3108
nhs3108

Ồ! Ngạc nhiên đúng ko? Vẫn như ở phần trên, điều đó có nghĩa là việc xóa bỏ các đối tượng cache thông qua đối tượng session sẽ không hữu dụng cho dù chúng ta có xóa thông qua tất cả các session được tạo bởi SessionFactory sinh ra nó.

Vậy, để xóa đối tượng user khỏi Cache trong L2, ta phải làm gì nhỉ? Mình đã tìm hiểu nhưng hiện tại cũng chưa thể đưa ra câu trả lời, rất mong các bạn có thể tự tìm tòi để đưa ra đáp án cho bản thân mình và nếu có thể hãy comment ở phần bình luận bên dưới để mình có thể cập nhật lại bài viết cho hoàn thiện hơn. Mình có thử

sessionFactory.getCache().evictEntity(User.class, user);

Tuy nhiên nó không giúp mình xóa đối tượng user khỏi L2 Cache bởi log nhận được vẫn giống như 2 phần trước.

Nhưng nếu bạn muốn xóa bỏ toàn bộ các đối tượng User hoặc toàn bộ cách đối tượng đã được cache thì lại có cách đấy.

User user = curSession1.load(User.class, "FPLy4QY2");
System.out.println(user.getUserName());

sessionFactory.getCache().evictEntityRegion(User.class);
// sessionFactory.getCache().evictAllRegions(); nếu muốn xóa bỏ toàn bộ mọi đối tượng của mọi lớp

user = curSession2.load(User.class, "FPLy4QY2");
System.out.println(user.getUserName());

và kết quả mình nhận được đây

Hibernate: 
    select
        user0_.user_id as user_id1_0_0_,
        user0_.email as email2_0_0_,
        user0_.infos as infos3_0_0_,
        user0_.user_name as user_nam4_0_0_ 
    from
        "user" user0_ 
    where
        user0_.user_id=?
nhs3108
Hibernate: 
    select
        user0_.user_id as user_id1_0_0_,
        user0_.email as email2_0_0_,
        user0_.infos as infos3_0_0_,
        user0_.user_name as user_nam4_0_0_ 
    from
        "user" user0_ 
    where
        user0_.user_id=?

Bạn có nhớ khái niệm region mà mình đã nhắc ở phần 2.1 ko? Nếu nhớ thì bạn có thể hiểu được ra 2 hàm trên có ý nghĩa gì rồi đúng k?

Ok. Tạm thế thôi. Trong Interface org.hibernate.Cache còn nhiều hàm evict, các bạn tự khám phá giúp mình. Một số hàm được sử dụng ở bài sau (Hibernate Caching - Bài 3: Query Cache).


package org.hibernate;

public interface Cache {
    boolean containsEntity(Class var1, Serializable var2);

    boolean containsEntity(String var1, Serializable var2);

    void evictEntity(Class var1, Serializable var2);

    void evictEntity(String var1, Serializable var2);

    void evictEntityRegion(Class var1);

    void evictEntityRegion(String var1);

    void evictEntityRegions();

    void evictNaturalIdRegion(Class var1);

    void evictNaturalIdRegion(String var1);

    void evictNaturalIdRegions();

    boolean containsCollection(String var1, Serializable var2);

    void evictCollection(String var1, Serializable var2);

    void evictCollectionRegion(String var1);

    void evictCollectionRegions();

    boolean containsQuery(String var1);

    void evictDefaultQueryRegion();

    void evictQueryRegion(String var1);

    void evictQueryRegions();

    void evictAllRegions();
}

Cảm ơn các bạn đã quan tâm theo dõi bài viết của mình. Bài viết trên đây của mình chỉ là tìm hiểu ở mức CƠ BẢN của cá nhân mình. Thực tế mình cũng chưa làm việc nhiều với nó. Vì thế mong sẽ nhận được sự đóng góp của các bạn. Hẹn gặp lại các bạn trong bài sau Hibernate Caching - Bài 3: Query Cache