导读:本期聚焦于星河创作的《SQLite实战项目:如何将SQLite与Ehcache整合实现高效二级缓存?》,敬请观看详情。缓存穿透到磁盘时该选什么存储方案?把嵌入式数据库SQLite作为Ehcache的二级缓存存储层,是Java应用中兼顾性能与持久化的常见做法。本文围绕SQLite与Ehcache的整合实战展开,先讲清一级缓存、二级缓存的职责边界,再分析为什么选SQLite而不是纯磁盘文件,随后给出完整的Maven依赖配置、ehcache.xml磁盘存储配置、DiskStore路径与SQLite持久化单元对接方式,并提供Java代码示例演示缓存的写入、读取与回收过程,最后总结整合中的常见坑点,包括路径权限、缓存大小限制与序列化问题,帮助你快速搭建一套稳定可靠的持久化缓存体系。

在Java应用的缓存体系中,Ehcache凭借其进程内缓存的高性能表现被广泛使用。但当内存不足以容纳所有缓存数据时,Ehcache会将数据溢写到磁盘,这个溢写机制的性能与可靠性,很大程度上取决于底层的磁盘存储方案。SQLite作为一款零配置、单文件、支持事务的嵌入式数据库,正好可以承担这个角色。本文将通过一个完整的实战项目,演示如何将SQLite与Ehcache整合,构建一套内存加磁盘的两级缓存体系。

SQLite实战项目:如何将SQLite与Ehcache整合实现高效二级缓存?

一、为什么二级缓存要选SQLite

先理解一下Ehcache的层次结构。Ehcache默认情况下数据全部存放在堆内存中,这被称为堆内缓存(Heap-based)。当配置了PersistenceConfiguration之后,Ehcache支持将数据溢写到堆外内存或者磁盘,形成事实上的两级缓存:热数据留在内存,冷数据落到磁盘。默认的磁盘存储是Ehcache自己实现的一套文件格式,写入和读取都由内部的DiskStore组件负责。

那为什么还要引入SQLite?主要有三点考虑。第一,默认的DiskStore文件在进程崩溃时可能损坏,虽然可以通过diskPersistent配置做持久化,但容错能力有限,而SQLite本身支持ACID事务,异常断电后数据库文件的完整性更有保障。第二,SQLite的数据文件是跨平台可读的标准格式,运维人员可以直接用命令行工具或图形化客户端打开排查问题,而Ehcache的原生磁盘文件是私有格式,出了问题只能靠日志猜。第三,如果你的项目本身已经在用SQLite做本地存储,把缓存数据也放进同一个体系,可以复用备份、迁移等基础设施。

需要说明的是,Ehcache官方并没有内置SQLite存储策略,整合方式通常有两种思路:一种是利用Ehcache的自定义CacheExtensionCacheLoaderWriter SPI,把SQLite作为缓存的后端存储;另一种是在业务层做双层封装,Ehcache作为一级缓存,SQLite作为二级缓存,读写时先查内存再查库。下面我们采用第二种思路,因为它实现简单、可控性强,更适合中小型项目。

二、项目搭建与依赖配置

创建一个Maven项目,引入Ehcache和SQLite的JDBC驱动。Ehcache建议使用2.10.x版本,它的API相对直观,社区资料也更丰富。SQLite驱动选择sqlite-jdbc,这是一个纯Java实现的JDBC驱动,无需安装任何本地依赖。

<dependencies>
    <!-- Ehcache 核心依赖 -->
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
        <version>2.10.9.2</version>
    </dependency>
    <!-- SQLite JDBC 驱动 -->
    <dependency>
        <groupId>org.xerial</groupId>
        <artifactId>sqlite-jdbc</artifactId>
        <version>3.44.1.0</version>
    </dependency>
</dependencies>

接下来在资源目录下创建ehcache.xml,配置一级缓存的容量上限和溢写策略。maxEntriesLocalHeap限制堆内元素数量,超过后按LRU策略淘汰,被淘汰的数据我们将在代码层写入SQLite,而不是让它直接丢失,这就是二级缓存的核心逻辑。

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">

    <diskStore path="java.io.tmpdir/ehcache-sqlite-demo"/>

    <cache name="userCache"
           maxEntriesLocalHeap="1000"
           eternal="false"
           timeToIdleSeconds="300"
           timeToLiveSeconds="600"
           memoryStoreEvictionPolicy="LRU"
           overflowToDisk="false">
    </cache>
</ehcache>

注意这里把overflowToDisk设为了false,因为我们不使用Ehcache自带的磁盘溢写,磁盘层完全由SQLite接管。这样职责边界非常清晰:Ehcache只管内存,SQLite只管磁盘,中间的同步由我们封装的组件负责。

三、封装两级缓存的读写逻辑

核心思路是写一个TwoLevelCache工具类,内部持有Ehcache的CacheManager和一个SQLite连接。读取时先查Ehcache,命中直接返回;未命中则查SQLite,查到后回填到Ehcache再返回。写入时同时写Ehcache和SQLite,其中SQLite写入采用INSERT OR REPLACE语法保证幂等。

先看SQLite侧的建表和初始化代码。缓存表设计了三个字段:cache_key存键,cache_value存序列化后的对象,expire_at存过期时间戳。用BLOB字段存序列化字节,可以缓存任意可序列化的Java对象。

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import java.io.*;
import java.sql.*;
import java.util.Arrays;

public class TwoLevelCache {

    private final Cache cache;
    private final Connection conn;

    public TwoLevelCache() throws Exception {
        // 初始化 Ehcache
        CacheManager manager = CacheManager.create(
                getClass().getResourceAsStream("/ehcache.xml"));
        this.cache = manager.getCache("userCache");

        // 初始化 SQLite,缓存文件放在用户目录下
        File dbFile = new File(System.getProperty("user.home"),
                "demo_cache.db");
        this.conn = DriverManager.getConnection(
                "jdbc:sqlite:" + dbFile.getAbsolutePath());

        Statement st = conn.createStatement();
        st.execute("CREATE TABLE IF NOT EXISTS cache_store (" +
                "cache_key TEXT PRIMARY KEY," +
                "cache_value BLOB," +
                "expire_at INTEGER)");
        // 为过期时间建索引,方便定期清理
        st.execute("CREATE INDEX IF NOT EXISTS idx_expire " +
                "ON cache_store(expire_at)");
        st.close();
    }

    /** 写入两级缓存 */
    public void put(String key, Serializable value, int ttlSeconds)
            throws Exception {
        // 第一级:写入 Ehcache 堆内缓存
        cache.put(new Element(key, value, ttlSeconds, ttlSeconds));

        // 第二级:序列化后写入 SQLite
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
            oos.writeObject(value);
        }
        long expireAt = System.currentTimeMillis()
                + ttlSeconds * 1000L;
        try (PreparedStatement ps = conn.prepareStatement(
                "INSERT OR REPLACE INTO cache_store "
                + "(cache_key, cache_value, expire_at) "
                + "VALUES (?, ?, ?)")) {
            ps.setString(1, key);
            ps.setBytes(2, bos.toByteArray());
            ps.setLong(3, expireAt);
            ps.executeUpdate();
        }
    }

    /** 读取:先查内存,未命中再查 SQLite 并回填 */
    public Object get(String key) throws Exception {
        // 一级命中
        Element e = cache.get(key);
        if (e != null) {
            return e.getObjectValue();
        }
        // 二级查询
        try (PreparedStatement ps = conn.prepareStatement(
                "SELECT cache_value FROM cache_store "
                + "WHERE cache_key = ? AND expire_at > ?")) {
            ps.setString(1, key);
            ps.setLong(2, System.currentTimeMillis());
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    byte[] data = rs.getBytes(1);
                    try (ObjectInputStream ois =
                            new ObjectInputStream(
                                new ByteArrayInputStream(data))) {
                        Object value = ois.readObject();
                        // 回填一级缓存
                        cache.put(new Element(key, value));
                        return value;
                    }
                }
            }
        }
        return null;
    }

    public void close() throws Exception {
        cache.getCacheManager().shutdown();
        conn.close();
    }
}

这段代码有几个细节值得展开。第一,SQLite的INSERT OR REPLACE语义是主键冲突时整行替换,天然适合缓存的覆盖写场景。第二,过期判断没有依赖定时任务,而是在查询时用expire_at > 当前时间做过滤,这是一种惰性过期策略,简单可靠;如果缓存量大,可以再配合一个低频的清理线程删除已过期行。第三,回填一级缓存时没有指定TTL,实际项目中建议根据业务设置合理的空闲时间,避免回填数据长期驻留内存。

四、性能测试与常见坑点

写一个简单的测试类,对比纯内存、两级缓存、直接查SQLite三种读取路径的耗时。在机械硬盘环境下,典型数据大致是:堆内读取在微秒级,回填后的二级缓存读取也是微秒级(因为命中了一级),未命中走SQLite的读取在0.5到2毫秒之间,这与SQLite单条索引查询的常规表现一致。可以看出,两级缓存的价值在于热点数据完全无磁盘IO,只有冷数据才付出一次磁盘查询的代价,而冷数据被再次访问后会自动升级为热数据。

public class CacheTest {
    public static void main(String[] args) throws Exception {
        TwoLevelCache tlc = new TwoLevelCache();
        tlc.put("user:1001", "张三的资料对象", 600);

        // 第一次读取:走 SQLite 二级缓存
        long t1 = System.nanoTime();
        Object v1 = tlc.get("user:1001");
        long t2 = System.nanoTime();

        // 第二次读取:命中 Ehcache 一级缓存
        Object v2 = tlc.get("user:1001");
        long t3 = System.nanoTime();

        System.out.println("二级读取耗时: " + (t2 - t1) / 1000 + " 微秒");
        System.out.println("一级读取耗时: " + (t3 - t2) / 1000 + " 微秒");
        System.out.println("两次结果一致: " + v1.equals(v2));
        tlc.close();
    }
}

整合过程中有几个容易踩的坑需要提醒。一是并发问题:SQLite的写操作在同一个连接上是串行的,多线程场景下建议对Connection加锁,或者采用单一写线程加队列的模型;如果并发写压力大,可以考虑开启WAL模式,即执行PRAGMA journal_mode=WAL;,读写可以并行进行。二是序列化兼容性:缓存对象一旦改了包名或字段结构,旧的反序列化会抛InvalidClassException,建议给实体类显式声明serialVersionUID,并在版本升级时评估是否需要清空缓存。三是文件路径权限:Windows下如果数据库文件放在C:\Program Files等受保护目录,写入会直接失败,Linux下则要注意运行用户的目录写权限,生产环境建议将数据库文件放在独立的、有备份策略的数据盘路径下。

最后总结一下这套方案的最佳使用场景:本地单机应用、桌面软件、数据量在百万级以内的服务端热点缓存,都可以放心使用。如果需要多节点共享缓存,SQLite就不合适了,应该转向Redis这样的集中式缓存方案。通过本文的实战演示,你应该已经掌握了Ehcache与SQLite整合的完整流程,可以在此基础上扩展统计、监控和定期清理等能力,构建出适合自己业务的持久化缓存体系。

SQLiteEhcache二级缓存修改时间:2026-09-06 21:50:49

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260906/51818.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。