Hibernate缓存的三层秘密花园 🏰💾

43 阅读12分钟

副标题: 一级缓存、二级缓存、查询缓存大揭秘!让你的数据库查询快如闪电!⚡


🎬 开场白:为什么需要缓存?

嘿,朋友!👋 想象一下这个场景:

没有缓存的世界:
用户:"查询ID=1的用户"
应用:跑去数据库 → SELECT * FROM user WHERE id=1
用户:"再查一次ID=1的用户"
应用:又跑去数据库 → SELECT * FROM user WHERE id=1
用户:"还是查ID=1"
应用:再跑去数据库 → SELECT * FROM user WHERE id=1
数据库:😫 "求你了,别问了!"

有缓存的世界:
用户:"查询ID=1的用户"
应用:跑去数据库 → SELECT * FROM user WHERE id=1 → 存到缓存
用户:"再查一次ID=1的用户"
应用:直接从缓存拿 → 秒回!✨
用户:"还是查ID=1"
应用:继续从缓存拿 → 又秒回!✨
数据库:😎 "终于可以休息了!"

这就是缓存的魔力!

Hibernate/JPA提供了三层缓存:

  • 🥇 一级缓存:Session级别,默认开启
  • 🥈 二级缓存:SessionFactory级别,需要配置
  • 🥉 查询缓存:查询结果集缓存

今天,我们把这三层秘密全部揭开!🚀


🏗️ 缓存体系架构

┌────────────────────────────────────────────┐
│              应用层(查询请求)              │
└────────────────┬───────────────────────────┘
                 │
                 ▼
┌────────────────────────────────────────────┐
│   一级缓存(Session/EntityManager级别)    │
│   - 自动开启,无需配置                      │
│   - 生命周期:同Session                     │
│   - 作用域:单个Session                     │
│   - 隔离性:Session间不共享                 │
└────────────────┬───────────────────────────┘
                 │ 未命中
                 ▼
┌────────────────────────────────────────────┐
│   二级缓存(SessionFactory级别)           │
│   - 需要手动配置                            │
│   - 生命周期:同SessionFactory              │
│   - 作用域:所有Session共享                 │
│   - 隔离性:支持并发访问                    │
└────────────────┬───────────────────────────┘
                 │ 未命中
                 ▼
┌────────────────────────────────────────────┐
│   查询缓存(Query Cache)                   │
│   - 需要手动配置                            │
│   - 缓存查询结果集的ID列表                  │
│   - 配合二级缓存使用                        │
└────────────────┬───────────────────────────┘
                 │ 未命中
                 ▼
┌────────────────────────────────────────────┐
│              数据库                         │
└────────────────────────────────────────────┘

🥇 一级缓存(Session Cache)

什么是一级缓存?

一级缓存是Hibernate的默认缓存,也叫Session缓存持久化上下文(Persistence Context)。

核心特点

特性说明
级别Session级别(JPA中是EntityManager)
生命周期与Session同生共死
作用域仅当前Session可用
配置无需配置,自动开启
关闭无法关闭(强制特性)
存储内容实体对象

工作原理

Session session = sessionFactory.openSession();

// 第1次查询:走数据库
User user1 = session.get(User.class, 1L);
System.out.println("第1次查询");
// SQL: SELECT * FROM user WHERE id = 1

// 第2次查询:走一级缓存(不发SQL)
User user2 = session.get(User.class, 1L);
System.out.println("第2次查询");
// 没有SQL!从缓存拿!

// 验证是同一个对象
System.out.println(user1 == user2);  // true!

session.close();  // 一级缓存被清空

一级缓存的生命周期

Session创建 → 一级缓存初始化
    ↓
查询实体 → 存入一级缓存
    ↓
再次查询同一实体 → 直接从缓存返回
    ↓
修改实体 → 缓存中的对象被修改(脏数据标记)
    ↓
flush() → 将脏数据同步到数据库
    ↓
clear() → 清空一级缓存
    ↓
Session关闭 → 一级缓存销毁

实战案例一:一级缓存的验证

@Test
public void testFirstLevelCache() {
    Session session = sessionFactory.openSession();
    Transaction tx = session.beginTransaction();
    
    System.out.println("=== 第1次查询 ===");
    User user1 = session.get(User.class, 1L);
    System.out.println("User1: " + user1.getName());
    
    System.out.println("\n=== 第2次查询(同一Session)===");
    User user2 = session.get(User.class, 1L);
    System.out.println("User2: " + user2.getName());
    
    System.out.println("\n=== 验证是否同一对象 ===");
    System.out.println("user1 == user2: " + (user1 == user2));
    
    tx.commit();
    session.close();
}

/* 输出:
=== 第1次查询 ===
Hibernate: select user0_.id, user0_.name, user0_.age from user user0_ where user0_.id=?
User1: 张三

=== 第2次查询(同一Session)===
User2: 张三
(注意:没有SQL!)

=== 验证是否同一对象 ===
user1 == user2: true
*/

实战案例二:不同Session不共享缓存

@Test
public void testDifferentSessions() {
    // Session 1
    Session session1 = sessionFactory.openSession();
    System.out.println("=== Session 1 查询 ===");
    User user1 = session1.get(User.class, 1L);
    System.out.println("User1: " + user1.getName());
    session1.close();
    
    // Session 2
    Session session2 = sessionFactory.openSession();
    System.out.println("\n=== Session 2 查询 ===");
    User user2 = session2.get(User.class, 1L);
    System.out.println("User2: " + user2.getName());
    session2.close();
    
    System.out.println("\n=== 验证是否同一对象 ===");
    System.out.println("user1 == user2: " + (user1 == user2));
}

/* 输出:
=== Session 1 查询 ===
Hibernate: SELECT ... FROM user WHERE id=?
User1: 张三

=== Session 2 查询 ===
Hibernate: SELECT ... FROM user WHERE id=?  (又发了SQL!)
User2: 张三

=== 验证是否同一对象 ===
user1 == user2: false(不是同一对象!)
*/

一级缓存的操作方法

Session session = sessionFactory.openSession();

// 1. 清空整个一级缓存
session.clear();

// 2. 从缓存中移除特定实体
session.evict(user);

// 3. 刷新实体(从数据库重新加载)
session.refresh(user);

// 4. 手动同步缓存到数据库
session.flush();

// 5. 判断实体是否在缓存中
boolean contains = session.contains(user);

🥈 二级缓存(Second Level Cache)

什么是二级缓存?

二级缓存是SessionFactory级别的缓存,所有Session共享,需要手动配置。

核心特点

特性说明
级别SessionFactory级别
生命周期应用程序生命周期
作用域所有Session共享
配置需要手动配置
关闭可以关闭
存储内容实体数据(分散存储)
并发支持并发访问

二级缓存提供商

Hibernate支持多种二级缓存实现:

缓存提供商特点Maven依赖
EhCache轻量级,常用hibernate-ehcache
Redis分布式,高性能hibernate-redis
Infinispan分布式,JBoss推荐hibernate-infinispan
Hazelcast分布式,易用hibernate-hazelcast

配置二级缓存(EhCache示例)

Step 1:添加依赖

<!-- Hibernate核心 -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.6.15.Final</version>
</dependency>

<!-- EhCache二级缓存 -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.6.15.Final</version>
</dependency>

Step 2:配置hibernate.cfg.xml

<hibernate-configuration>
    <session-factory>
        <!-- 其他配置... -->
        
        <!-- 启用二级缓存 -->
        <property name="hibernate.cache.use_second_level_cache">true</property>
        
        <!-- 指定缓存提供商 -->
        <property name="hibernate.cache.region.factory_class">
            org.hibernate.cache.ehcache.EhCacheRegionFactory
        </property>
        
        <!-- EhCache配置文件路径 -->
        <property name="hibernate.cache.provider_configuration_file_resource_path">
            ehcache.xml
        </property>
        
        <!-- 是否在日志中显示缓存统计 -->
        <property name="hibernate.generate_statistics">true</property>
    </session-factory>
</hibernate-configuration>

Step 3:配置ehcache.xml

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd">
    
    <!-- 默认缓存配置 -->
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="false"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120"/>
    
    <!-- User实体缓存配置 -->
    <cache name="com.example.entity.User"
        maxElementsInMemory="1000"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="false"/>
    
    <!-- 集合缓存配置 -->
    <cache name="com.example.entity.User.orders"
        maxElementsInMemory="1000"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="false"/>
</ehcache>

Step 4:在实体上启用缓存

@Entity
@Table(name = "user")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)  // 启用二级缓存!
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private Integer age;
    
    @OneToMany(mappedBy = "user")
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)  // 集合也要缓存
    private List<Order> orders;
    
    // Getter和Setter...
}

缓存策略(CacheConcurrencyStrategy)

策略说明适用场景
READ_ONLY只读,不可修改静态数据(字典表)
READ_WRITE读写,最常用可读可写的数据
NONSTRICT_READ_WRITE非严格读写对一致性要求不高
TRANSACTIONAL事务型需要事务支持

实战案例三:二级缓存验证

@Test
public void testSecondLevelCache() {
    // Session 1
    Session session1 = sessionFactory.openSession();
    System.out.println("=== Session 1 查询 ===");
    User user1 = session1.get(User.class, 1L);
    System.out.println("User1: " + user1.getName());
    session1.close();  // Session关闭,一级缓存清空
    
    // Session 2(新Session)
    Session session2 = sessionFactory.openSession();
    System.out.println("\n=== Session 2 查询 ===");
    User user2 = session2.get(User.class, 1L);
    System.out.println("User2: " + user2.getName());
    session2.close();
    
    // 查看缓存统计
    Statistics stats = sessionFactory.getStatistics();
    System.out.println("\n=== 缓存统计 ===");
    System.out.println("二级缓存命中次数: " + stats.getSecondLevelCacheHitCount());
    System.out.println("二级缓存未命中次数: " + stats.getSecondLevelCacheMissCount());
}

/* 输出:
=== Session 1 查询 ===
Hibernate: SELECT ... FROM user WHERE id=?
User1: 张三
(数据被放入二级缓存)

=== Session 2 查询 ===
User2: 张三
(没有SQL!从二级缓存拿!)

=== 缓存统计 ===
二级缓存命中次数: 1
二级缓存未命中次数: 0
*/

Spring Boot配置二级缓存

# application.yml
spring:
  jpa:
    properties:
      hibernate:
        # 启用二级缓存
        cache.use_second_level_cache: true
        # 启用查询缓存
        cache.use_query_cache: true
        # 缓存提供商
        cache.region.factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory
        # 显示统计信息
        generate_statistics: true

🥉 查询缓存(Query Cache)

什么是查询缓存?

查询缓存缓存的是查询结果集的ID列表,而不是完整对象。

核心特点

特性说明
缓存内容查询结果的ID列表
依赖必须启用二级缓存
配置需要手动配置
失效表数据变化时失效
适用相同查询条件的场景

工作原理

1. 执行查询:SELECT * FROM user WHERE age > 18

2. 查询缓存存储:
   Key: [HQL, 参数]
   Value: [1, 2, 3, 5, 8]  (ID列表)

3. 根据ID列表查二级缓存:
   ID=1 → User(id=1, name="张三", age=25)
   ID=2 → User(id=2, name="李四", age=30)
   ...

4. 返回结果

配置查询缓存

<!-- hibernate.cfg.xml -->
<property name="hibernate.cache.use_query_cache">true</property>

使用查询缓存

@Test
public void testQueryCache() {
    Session session1 = sessionFactory.openSession();
    
    // 第1次查询
    System.out.println("=== 第1次查询 ===");
    Query<User> query1 = session1.createQuery(
        "FROM User WHERE age > :age", User.class
    );
    query1.setParameter("age", 18);
    query1.setCacheable(true);  // 启用查询缓存!
    List<User> users1 = query1.list();
    System.out.println("查询结果: " + users1.size() + "条");
    
    session1.close();
    
    // 第2次查询(新Session)
    Session session2 = sessionFactory.openSession();
    System.out.println("\n=== 第2次查询 ===");
    Query<User> query2 = session2.createQuery(
        "FROM User WHERE age > :age", User.class
    );
    query2.setParameter("age", 18);
    query2.setCacheable(true);  // 启用查询缓存!
    List<User> users2 = query2.list();
    System.out.println("查询结果: " + users2.size() + "条");
    
    session2.close();
    
    // 查看统计
    Statistics stats = sessionFactory.getStatistics();
    System.out.println("\n=== 查询缓存统计 ===");
    System.out.println("查询缓存命中次数: " + stats.getQueryCacheHitCount());
    System.out.println("查询缓存未命中次数: " + stats.getQueryCacheMissCount());
}

/* 输出:
=== 第1次查询 ===
Hibernate: SELECT ... FROM user WHERE age > ?
查询结果: 5条

=== 第2次查询 ===
查询结果: 5条
(没有SQL!从查询缓存拿!)

=== 查询缓存统计 ===
查询缓存命中次数: 1
查询缓存未命中次数: 0
*/

🎨 生活化比喻:图书馆借书系统

一级缓存 = 你的书包 🎒

你去图书馆借书:
1. 第1次:从图书馆借《Java编程》→ 放进书包
2. 想再看:直接从书包拿 → 不用跑图书馆
3. 回家:书包清空 → 缓存销毁

特点:
- 只有你能用(Session级别)
- 回家就没了(生命周期短)

二级缓存 = 班级书架 📚

班级有个公共书架:
1. 张三借《Java编程》→ 看完放书架
2. 李四也要看《Java编程》→ 直接从书架拿
3. 不用跑图书馆了!

特点:
- 全班共享(SessionFactory级别)
- 学期末才清空(生命周期长)
- 并发安全(多人可同时借)

查询缓存 = 借书索引卡 🗂️

索引卡记录:
"计算机类书籍" → [Java编程, Python入门, 算法导论]

查询流程:
1. 看索引卡 → 找到书的位置
2. 去班级书架拿书 → 二级缓存
3. 如果书架没有 → 去图书馆(数据库)

特点:
- 快速定位
- 配合二级缓存使用
- 书架变化时索引更新

📊 三级缓存对比表

对比项一级缓存二级缓存查询缓存
级别SessionSessionFactorySessionFactory
生命周期Session应用程序应用程序
作用域单个Session所有Session所有Session
配置自动开启手动配置手动配置
关闭不可关闭可关闭可关闭
存储内容实体对象实体数据ID列表
并发不涉及支持支持
适用场景单次事务内跨Session查询重复查询
失效条件Session关闭手动清除/更新表数据变化

💻 完整实战案例

实体类

@Entity
@Table(name = "user")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    private Integer age;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private List<Order> orders = new ArrayList<>();
    
    // Getter和Setter...
}

@Entity
@Table(name = "orders")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Order {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String orderNo;
    
    private BigDecimal amount;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    
    // Getter和Setter...
}

测试类

@SpringBootTest
public class CacheTest {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Autowired
    private EntityManagerFactory entityManagerFactory;
    
    /**
     * 测试一级缓存
     */
    @Test
    @Transactional
    public void testFirstLevelCache() {
        System.out.println("=== 第1次查询 ===");
        User user1 = entityManager.find(User.class, 1L);
        System.out.println("User: " + user1.getName());
        
        System.out.println("\n=== 第2次查询 ===");
        User user2 = entityManager.find(User.class, 1L);
        System.out.println("User: " + user2.getName());
        
        System.out.println("\n=== 是否同一对象 ===");
        System.out.println("user1 == user2: " + (user1 == user2));
    }
    
    /**
     * 测试二级缓存
     */
    @Test
    public void testSecondLevelCache() {
        // 第1次查询
        EntityManager em1 = entityManagerFactory.createEntityManager();
        System.out.println("=== 第1次查询(em1)===");
        User user1 = em1.find(User.class, 1L);
        System.out.println("User: " + user1.getName());
        em1.close();
        
        // 第2次查询(新EntityManager)
        EntityManager em2 = entityManagerFactory.createEntityManager();
        System.out.println("\n=== 第2次查询(em2)===");
        User user2 = em2.find(User.class, 1L);
        System.out.println("User: " + user2.getName());
        em2.close();
        
        // 查看统计
        printCacheStatistics();
    }
    
    /**
     * 测试查询缓存
     */
    @Test
    public void testQueryCache() {
        // 第1次查询
        EntityManager em1 = entityManagerFactory.createEntityManager();
        System.out.println("=== 第1次查询 ===");
        List<User> users1 = em1.createQuery(
            "SELECT u FROM User u WHERE u.age > :age", User.class
        )
        .setParameter("age", 18)
        .setHint("org.hibernate.cacheable", true)
        .getResultList();
        System.out.println("结果数: " + users1.size());
        em1.close();
        
        // 第2次查询
        EntityManager em2 = entityManagerFactory.createEntityManager();
        System.out.println("\n=== 第2次查询 ===");
        List<User> users2 = em2.createQuery(
            "SELECT u FROM User u WHERE u.age > :age", User.class
        )
        .setParameter("age", 18)
        .setHint("org.hibernate.cacheable", true)
        .getResultList();
        System.out.println("结果数: " + users2.size());
        em2.close();
        
        // 查看统计
        printCacheStatistics();
    }
    
    /**
     * 打印缓存统计
     */
    private void printCacheStatistics() {
        Statistics stats = entityManagerFactory.unwrap(SessionFactory.class)
            .getStatistics();
        
        System.out.println("\n========== 缓存统计 ==========");
        System.out.println("二级缓存命中: " + stats.getSecondLevelCacheHitCount());
        System.out.println("二级缓存未命中: " + stats.getSecondLevelCacheMissCount());
        System.out.println("查询缓存命中: " + stats.getQueryCacheHitCount());
        System.out.println("查询缓存未命中: " + stats.getQueryCacheMissCount());
        System.out.println("=====================================");
    }
}

⚠️ 常见坑点与避坑指南

坑点1:二级缓存失效

// ❌ 错误:直接执行SQL绕过缓存
entityManager.createNativeQuery(
    "UPDATE user SET age = 30 WHERE id = 1"
).executeUpdate();
// 数据库更新了,但缓存没更新!数据不一致!

// ✅ 正确:使用JPA API更新
User user = entityManager.find(User.class, 1L);
user.setAge(30);
entityManager.merge(user);
// 缓存会自动更新!

坑点2:懒加载与缓存

// ❌ 错误:懒加载集合未缓存
@OneToMany(mappedBy = "user")
private List<Order> orders;  // 没有@Cache注解

// 每次访问orders都会查数据库!

// ✅ 正确:给集合也加缓存
@OneToMany(mappedBy = "user")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private List<Order> orders;

坑点3:查询缓存未命中

// ❌ 错误:参数不同,缓存未命中
query1.setParameter("age", 18);  // 缓存Key: [HQL, age=18]
query2.setParameter("age", 20);  // 缓存Key: [HQL, age=20] 未命中!

// ✅ 注意:查询缓存对参数敏感
// 只有完全相同的查询才能命中缓存

坑点4:缓存雪崩

// ❌ 错误:所有缓存同时失效
<cache ... timeToLiveSeconds="3600"/>  // 1小时后全部失效

// 可能导致大量请求同时打到数据库!

// ✅ 正确:设置随机过期时间
int randomTTL = 3600 + new Random().nextInt(600);  // 3600-4200秒

🎯 最佳实践

1. 选择合适的缓存策略

// 静态数据(字典表)
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class Dictionary { ... }

// 读多写少
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User { ... }

// 读写频繁,允许短暂不一致
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class ViewCount { ... }

2. 合理设置缓存区域

<!-- 热点数据:大容量,长时间 -->
<cache name="com.example.entity.User"
    maxElementsInMemory="10000"
    timeToLiveSeconds="3600"/>

<!-- 冷数据:小容量,短时间 -->
<cache name="com.example.entity.Log"
    maxElementsInMemory="100"
    timeToLiveSeconds="300"/>

3. 监控缓存效率

@Component
public class CacheMonitor {
    
    @Autowired
    private EntityManagerFactory emf;
    
    @Scheduled(fixedRate = 60000)  // 每分钟
    public void monitorCache() {
        Statistics stats = emf.unwrap(SessionFactory.class)
            .getStatistics();
        
        long hit = stats.getSecondLevelCacheHitCount();
        long miss = stats.getSecondLevelCacheMissCount();
        double hitRate = (double) hit / (hit + miss) * 100;
        
        log.info("二级缓存命中率: {}%", String.format("%.2f", hitRate));
        
        // 命中率低于70%需要优化
        if (hitRate < 70) {
            log.warn("缓存命中率过低,请检查缓存配置!");
        }
    }
}

🎉 总结

核心要点

  1. 三级缓存:

    • 一级缓存:Session级别,自动开启
    • 二级缓存:SessionFactory级别,需配置
    • 查询缓存:缓存查询结果,依赖二级缓存
  2. 缓存策略:

    • READ_ONLY:只读数据
    • READ_WRITE:读写数据(常用)
    • NONSTRICT_READ_WRITE:允许短暂不一致
    • TRANSACTIONAL:事务型
  3. 配置要点:

    • 启用二级缓存
    • 选择缓存提供商
    • 在实体上添加@Cache注解
    • 配置缓存区域
  4. 注意事项:

    • 避免直接执行SQL
    • 懒加载集合也要缓存
    • 监控缓存命中率
    • 防止缓存雪崩

📚 参考资料

  • Hibernate官方文档:Caching
  • EhCache官方文档
  • 《Java Persistence with Hibernate》
  • 《高性能MySQL》

🎮 课后练习

练习1:缓存命中率测试

编写测试,对比有无缓存的性能差异。

练习2:自定义缓存区域

为不同的实体配置不同的缓存策略和过期时间。

练习3:Redis二级缓存

将EhCache替换为Redis,实现分布式二级缓存。


💬 最后的话

缓存是提升性能的利器,但也是双刃剑!⚔️

记住:过早优化是万恶之源,合理使用缓存才是王道!

使用缓存前问自己三个问题:

  1. 这个数据真的需要缓存吗?
  2. 缓存失效会有什么影响?
  3. 缓存命中率能达到多少?

只有想清楚这些,才能发挥缓存的最大价值!🚀✨


作者心声:我曾经把所有表都加了二级缓存,结果内存爆了😂。后来才明白:不是所有数据都适合缓存,热点数据才是重点!

如果觉得有用,点赞收藏走一波!👍⭐


文档版本:v1.0
最后更新:2025-10-23
难度等级:⭐⭐⭐⭐(高级)