副标题: 一级缓存、二级缓存、查询缓存大揭秘!让你的数据库查询快如闪电!⚡
🎬 开场白:为什么需要缓存?
嘿,朋友!👋 想象一下这个场景:
没有缓存的世界:
用户:"查询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. 如果书架没有 → 去图书馆(数据库)
特点:
- 快速定位
- 配合二级缓存使用
- 书架变化时索引更新
📊 三级缓存对比表
| 对比项 | 一级缓存 | 二级缓存 | 查询缓存 |
|---|---|---|---|
| 级别 | Session | SessionFactory | SessionFactory |
| 生命周期 | 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("缓存命中率过低,请检查缓存配置!");
}
}
}
🎉 总结
核心要点
-
三级缓存:
- 一级缓存:Session级别,自动开启
- 二级缓存:SessionFactory级别,需配置
- 查询缓存:缓存查询结果,依赖二级缓存
-
缓存策略:
- READ_ONLY:只读数据
- READ_WRITE:读写数据(常用)
- NONSTRICT_READ_WRITE:允许短暂不一致
- TRANSACTIONAL:事务型
-
配置要点:
- 启用二级缓存
- 选择缓存提供商
- 在实体上添加@Cache注解
- 配置缓存区域
-
注意事项:
- 避免直接执行SQL
- 懒加载集合也要缓存
- 监控缓存命中率
- 防止缓存雪崩
📚 参考资料
- Hibernate官方文档:Caching
- EhCache官方文档
- 《Java Persistence with Hibernate》
- 《高性能MySQL》
🎮 课后练习
练习1:缓存命中率测试
编写测试,对比有无缓存的性能差异。
练习2:自定义缓存区域
为不同的实体配置不同的缓存策略和过期时间。
练习3:Redis二级缓存
将EhCache替换为Redis,实现分布式二级缓存。
💬 最后的话
缓存是提升性能的利器,但也是双刃剑!⚔️
记住:过早优化是万恶之源,合理使用缓存才是王道!
使用缓存前问自己三个问题:
- 这个数据真的需要缓存吗?
- 缓存失效会有什么影响?
- 缓存命中率能达到多少?
只有想清楚这些,才能发挥缓存的最大价值!🚀✨
作者心声:我曾经把所有表都加了二级缓存,结果内存爆了😂。后来才明白:不是所有数据都适合缓存,热点数据才是重点!
如果觉得有用,点赞收藏走一波!👍⭐
文档版本:v1.0
最后更新:2025-10-23
难度等级:⭐⭐⭐⭐(高级)