MyBatis三级缓存实战:高级缓存策略的实现与应用

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: MyBatis三级缓存实战:高级缓存策略的实现与应用

基于前面的内容,我们已经非常熟悉了 MyBatis 的一级缓存和二级缓存的设计,一级缓存是将数据存储在了 SqlSession 的 BaseExecutor 中,仅对同一个 SqlSession 有效,二级缓存是基于一级缓存的基础之上,使用 CachingExecutor 来装饰一级缓存,而 CachingExecutor 是在 MyBatis 初始化阶段就完成了创建,全局有效。

不过我们通过源码可以看到,归根结底 MyBatis 是通过 PerpetualCache 来存储的缓存数据,而 PerpetualCache 的底层仅仅是使用的 HashMap 来存储,设计的还是非常的简陋,包括缓存的清理都是非常的粗暴(更新就清空),而且一级、二级缓存都是存储在本机内存的,如果是分布式的集群部署就会存在数据不一致的情况,所以我们有必要再引入一种更为高级的缓存。

三级缓存

三级缓存也称为定制缓存,它可以跨应用共享缓存数据。不过 MyBatis 自身是没有提供三级缓存的方案,所以通常都是引入第三方作为缓存方案。三级缓存也可以再次细分一下:

  1. JVM 缓存:EhCache、OsCache、JBossCache…
  2. 中间件缓存:Redis、MemCache…

本文我们选取两个有代表性的方案进行集成测试,分别是 EhCache 和 Redis。

在我看来,MyBatis 的 ”三级缓存“ 这个词似乎并不恰当,因为其目的是为了替换 MyBatis 的二级缓存。并不是说实实在在就有三个层级。

整合 EhCache

Ehcache 是一个流行的开源 Java 缓存框架,被广泛应用于各种 Java 项目中。在 MyBatis 中,Ehcache 通常被用作 MyBatis 的二级缓存的实现之一,用于在多个会话之间共享缓存数据,提高数据访问性能。

  1. 作用范围:跨会话缓存,Ehcache 可以在多个 SqlSession 之间共享缓存数据,有效减少数据库访问次数,提高系统性能和响应速度。
  2. 缓存实现
  • 插件化实现:MyBatis 支持插件化的缓存实现,可以通过配置文件来选择使用 Ehcache 作为二级缓存的实现。
  • 基于内存:Ehcache 是一种基于内存的缓存框架,可以快速访问缓存数据,适用于需要快速读取数据的场景。
  1. 配置方式:
  • XML 配置:可以通过 MyBatis 的 XML 配置文件来配置 Ehcache,包括缓存的属性、大小、过期时间等。
  • 注解配置:也可以通过注解的方式来配置 Ehcache,例如使用 @CacheNamespace 注解来配置缓存策略。
  1. 特性:
  • 高性能:Ehcache 是一个高性能的缓存框架,能够快速地读取和写入缓存数据。
  • 可扩展性:Ehcache 支持水平扩展,可以根据应用程序的需求来扩展缓存集群,以满足不同规模的应用场景。
  • 数据一致性:Ehcache 提供了丰富的配置选项,可以通过配置来保证缓存数据的一致性和可靠性。
  1. 集成方式:
  • Maven 集成:可以通过 Maven 或 Gradle 等构建工具来集成 Ehcache 到 MyBatis 项目中。
  • 配置文件:需要在 MyBatis 的配置文件中进行相关配置,指定使用 Ehcache 作为二级缓存的实现。
  1. 注意事项:
  • 缓存清理策略:需要根据业务需求和系统性能来选择合适的缓存清理策略,以保证缓存数据的及时更新和一致性。
  • 内存管理:由于 Ehcache 是基于内存的缓存框架,需要注意内存的管理和监控,避免内存溢出或性能下降的问题。

添加依赖

<!--引入 EhCache-->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.1.0</version>
</dependency>
<!--引入 EhCache 所需要的日志依赖-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.8.0-beta4</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.8.0-beta4</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

配置文件

在 resources 目录下创建 ehcache.xml,并添加以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <!--
       diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
       user.home – 用户主目录
       user.dir  – 用户当前工作目录
     -->
    <diskStore path="cache"/>
    <!--
       defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
     -->
    <!--
      name:缓存名称。
      maxElementsInMemory:缓存最大数目
      maxElementsOnDisk:硬盘最大缓存个数。
      eternal:对象是否永久有效,一但设置了,timeout将不起作用。
      overflowToDisk:是否保存到磁盘,当系统当机时
      timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
      timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
      diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
      diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
      diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
      memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
      clearOnFlush:内存数量最大时是否清除。
      memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
      FIFO,first in first out,这个是大家最熟的,先进先出。
      LFU,Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
      LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
   -->
    <defaultCache
            eternal="false"
            maxElementsInMemory="10000"
            overflowToDisk="true"
            diskPersistent="true"
            timeToIdleSeconds="1800"
            timeToLiveSeconds="259200"
            memoryStoreEvictionPolicy="LRU"/>
</ehcache>

Mapper XML 配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 配置 namespace -->
<mapper namespace="world.xuewei.mybatis.dao.Account1Dao">
    <!-- 指定缓存类型 -->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    <!-- useCache="true" 可以省略 -->
    <select id="getAll" resultType="Account">
        select * from account
    </select>
</mapper>

cache 标签也是支持配置若干个 property 标签的,这样就可以省略上面的 ehcache.xml

<cache type="org.mybatis.caches.ehcache.EhcacheCache">
 <property name="eternal" value="false"/>
 <property name="maxElementsInMemory" value="10000"/>
 <property name="overflowToDisk" value="true"/>
 <property name="diskPersistent" value="true"/>
 <property name="timeToIdleSeconds" value="1800"/>
 <property name="timeToLiveSeconds" value="259200"/>
 <property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>

但是推荐还是使用单独的配置文件进行统一管理,这样可以避免冗余。

测试程序

public class Cache3Test {
    private SqlSession sqlSession;
    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sessionFactory.openSession();
    }
    /**
     * 验证二级缓存,多个 SqlSession 有效
     */
    @Test
    public void testGetAll() {
        Account1Dao accountDao1 = sqlSession.getMapper(Account1Dao.class);
        Account1Dao accountDao2 = sqlSession.getMapper(Account1Dao.class);
        System.out.println("==== 第一次执行,查询出数据并缓存 ====");
        accountDao1.getAll().forEach(System.out::println);
        sqlSession.commit();
        System.out.println("==== 第二次执行,查询出缓存数据 ====");
        accountDao2.getAll().forEach(System.out::println);
        sqlSession.commit();
    }
}

实现效果

整合 Redis

使用 Redis 作为 MyBatis 的二级缓存,即将查询结果缓存到 Redis 中,从而避免了频繁地访问数据库。通过 Redis 的高性能和可扩展性,可以有效地提高系统的性能和可伸缩性。

使用 Redis 作为 MyBatis 的二级缓存有以下几个好处:

  1. 性能提升: Redis 是一个高性能的内存数据库,具有快速的读写速度和低延迟的特性。将查询结果缓存到 Redis 中可以大大加快数据的访问速度,减少了对数据库的频繁访问,从而提升了系统的整体性能。
  2. 减轻数据库压力: 将查询结果缓存到 Redis 中可以减轻数据库的压力,特别是在高并发的场景下。通过减少数据库的访问次数,可以降低数据库的负载,提高数据库的性能和稳定性。
  3. 分布式支持: Redis 支持分布式部署,可以搭建多个 Redis 节点来构建一个高可用的缓存集群。这样可以保证缓存的高可用性和可扩展性,同时还能够通过分片和复制等技术来提高缓存的吞吐量和容量。
  4. 数据持久化: Redis 支持数据持久化,可以将缓存数据持久化到磁盘中,以防止数据丢失。这样即使发生系统故障或者重启,缓存数据也不会丢失,保证了系统的数据一致性和可靠性。
  5. 灵活性和扩展性: Redis 提供了丰富的数据结构和功能,支持字符串、哈希、列表、集合、有序集合等数据类型,可以满足不同场景下的缓存需求。同时,通过 Redis 的丰富的配置选项和扩展机制,可以灵活地定制和扩展缓存功能,满足不同项目的需求。

手写实现

添加依赖
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.2.3</version>
</dependency>
<dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.12.0</version>
</dependency>
Jedis 工具类
/**
 * @author 薛伟
 */
public class JedisUtil {
    private static final JedisPool jedisPool;
    static {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxIdle(5);
        config.setMaxTotal(50);
        config.setMaxWait(Duration.ofMinutes(5));
        // 这里最好是将配置写在配置文件中
        jedisPool = new JedisPool(config, "*.*.*.*", 6379);
    }
    /**
     * 获取客户端
     */
    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
    /**
     * 关闭客户端
     */
    public static void close(Jedis jedis) {
        jedis.close();
    }
}
自定义 RedisCache
/**
 * 自定义开发 Redis 三级缓存
 * 存储数据的时候将 Key 和 Value 都转为 byte[],提升效率
 * 需要缓存的 Key 和 Value 都实现序列化接口
 *
 * @author 薛伟
 */
@Slf4j
public class RedisCache implements Cache {
    /**
     * 缓存的唯一标识
     */
    private final String id;
    /**
     * Redis 客户端
     */
    private Jedis jedis;
    public RedisCache(String id) {
        this.id = id;
    }
    @Override
    public String getId() {
        return id;
    }
    @Override
    public void putObject(Object key, Object value) {
        log.info("设置 Redis 缓存:key {}", key);
        byte[] keyBytes = SerializationUtils.serialize((Serializable) key);
        byte[] valueBytes = SerializationUtils.serialize((Serializable) value);
        jedis = JedisUtil.getJedis();
        jedis.set(keyBytes, valueBytes);
        JedisUtil.close(jedis);
    }
    @Override
    public Object getObject(Object key) {
        byte[] keyBytes = SerializationUtils.serialize((Serializable) key);
        jedis = JedisUtil.getJedis();
        byte[] bytes = jedis.get(keyBytes);
        if (bytes == null) {
            return null;
        }
        Object deserialize = SerializationUtils.deserialize(bytes);
        JedisUtil.close(jedis);
        log.info("获取到 Redis 缓存:key {}", key);
        return deserialize;
    }
    @Override
    public Object removeObject(Object key) {
        Object object = getObject(key);
        if (object == null) {
            return null;
        }
        byte[] keyBytes = SerializationUtils.serialize((Serializable) key);
        jedis = JedisUtil.getJedis();
        jedis.del(keyBytes);
        JedisUtil.close(jedis);
        log.info("删除到 Redis 缓存:key {}", key);
        return null;
    }
    @Override
    public void clear() {
        jedis = JedisUtil.getJedis();
        jedis.flushDB();
        JedisUtil.close(jedis);
    }
    @Override
    public int getSize() {
        jedis = JedisUtil.getJedis();
        int size = (int) jedis.dbSize();
        JedisUtil.close(jedis);
        return size;
    }
    @Override
    public ReadWriteLock getReadWriteLock() {
        return null;
    }
}
Mapper XML 配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 配置 namespace -->
<mapper namespace="world.xuewei.mybatis.dao.Account1Dao">
    <!-- 需要明确指定此标签,表示开启二级缓存 -->
    <cache type="world.xuewei.cache.RedisCache"/>
    <!-- useCache="true" 可以省略 -->
    <select id="getAll" resultType="Account">
        select * from account
    </select>
</mapper>
测试程序
public class Cache3Test {
    private SqlSession sqlSession;
    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sessionFactory.openSession();
    }
    /**
     * 验证二级缓存,多个 SqlSession 有效
     */
    @Test
    public void testGetAll() {
        Account1Dao accountDao1 = sqlSession.getMapper(Account1Dao.class);
        Account1Dao accountDao2 = sqlSession.getMapper(Account1Dao.class);
        System.out.println("==== 第一次执行,查询出数据并缓存 ====");
        accountDao1.getAll().forEach(System.out::println);
        sqlSession.commit();
        System.out.println("==== 第二次执行,查询出缓存数据 ====");
        accountDao2.getAll().forEach(System.out::println);
        sqlSession.commit();
    }
}
实现效果

整合现有方案

添加依赖
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>
配置文件

在 resources 目录下创建 redis 配置文件:

host=127.0.0.1
port=6379
Mapper XML 配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 配置 namespace -->
<mapper namespace="world.xuewei.mybatis.dao.Account1Dao">
    <!-- 需要明确指定此标签,表示开启二级缓存 -->
    <cache type="org.mybatis.caches.redis.RedisCache"/>
    <!-- useCache="true" 可以省略 -->
    <select id="getAll" resultType="Account">
        select * from account
    </select>
</mapper>

实现的效果的测试程序都是一样的,我这里就不重复描述了。



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
存储 缓存 芯片
让星星⭐月亮告诉你,当我们在说CPU一级缓存二级缓存三级缓存的时候,我们到底在说什么?
本文介绍了CPU缓存的基本概念和作用,以及不同级别的缓存(L1、L2、L3)的特点和工作原理。CPU缓存是CPU内部的存储器,用于存储RAM中的数据和指令副本,以提高数据访问速度,减少CPU与RAM之间的速度差异。L1缓存位于处理器内部,速度最快;L2缓存容量更大,但速度稍慢;L3缓存容量最大,由所有CPU内核共享。文章还对比了DRAM和SRAM两种内存类型,解释了它们在计算机系统中的应用。
111 1
|
3月前
|
缓存 Java 数据库连接
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
文章介绍了MyBatis的缓存机制,包括一级缓存和二级缓存的配置和使用,以及如何整合第三方缓存EHCache。详细解释了一级缓存的生命周期、二级缓存的开启条件和配置属性,以及如何通过ehcache.xml配置文件和logback.xml日志配置文件来实现EHCache的整合。
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
|
22天前
|
缓存 Java 数据库连接
MyBatis缓存机制
MyBatis提供两级缓存机制:一级缓存(Local Cache)默认开启,作用范围为SqlSession,重复查询时直接从缓存读取;二级缓存(Second Level Cache)需手动开启,作用于Mapper级别,支持跨SqlSession共享数据,减少数据库访问,提升性能。
27 1
|
25天前
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
41 4
|
1月前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
43 5
|
1月前
|
缓存 NoSQL 数据库
运用云数据库 Tair 构建缓存为应用提速,完成任务得苹果音响、充电套装等好礼!
本活动将带大家了解云数据库 Tair(兼容 Redis),通过体验构建缓存以提速应用,完成任务,即可领取罗马仕安卓充电套装,限量1000个,先到先得。邀请好友共同参与活动,还可赢取苹果 HomePod mini、小米蓝牙耳机等精美好礼!
|
1月前
|
SQL 缓存 Java
MyBatis如何关闭一级缓存(分注解和xml两种方式)
MyBatis如何关闭一级缓存(分注解和xml两种方式)
82 5
|
2月前
|
存储 缓存 数据库
缓存技术有哪些应用场景呢
【10月更文挑战第19天】缓存技术有哪些应用场景呢
|
2月前
|
缓存 Java 数据库连接
使用MyBatis缓存的简单案例
MyBatis 是一种流行的持久层框架,支持自定义 SQL 执行、映射及复杂查询。本文介绍了如何在 Spring Boot 项目中集成 MyBatis 并实现一级和二级缓存,以提高查询性能,减少数据库访问。通过具体的电商系统案例,详细讲解了项目搭建、缓存配置、实体类创建、Mapper 编写、Service 层实现及缓存测试等步骤。