MyBatis 缓存教程:工作原理以及具体使用方法 —— 一级缓存(Local Cache)、二级缓存(Second Level Cache)、查询缓存(QueryCache)、更新缓存等

作者:禅与计算机程序设计艺术

1.简介

Mybatis是Apache的一个开源项目,最早于2007年由mybatis团队开发,经过多年的不断迭代优化已成为Java世界中使用最广泛的ORM框架之一。它内部集成了相当多的缓存机制,包括一级缓存(Local Cache)、二级缓存(Second Level Cache)、查询缓存(QueryCache)、更新缓存(UpdateCache),极大的提升了系统的性能和效率。

在使用Mybatis进行数据持久化查询时, MyBatis 会从配置好的缓存(如Redis等)中读取缓存数据。如果没有命中缓存,则 MyBatis 将根据对应的 SQL 查询语句生成CacheKey并查找数据库是否存在该数据,如果存在则将其存入缓存供下次查询使用;如果不存在则执行 SQL 语句进行数据库查询,并把查询结果存入缓存供下次查询使用。MyBatis 的缓存机制分为以下几种类型:

  1. 一级缓存(Local Cache):对相同的数据在一次会话中可直接获取,减少了数据库的访问次数。由于每次都会先去缓存中查找,所以整体上具有较高的命中率。同时也提高了应用程序的性能。

  2. 二级缓存(Second Level Cache):也是本地缓存,但是缓存存放在分布式缓存中,也就是说不同的服务器可以共享同一个缓存。它可以有效地避免缓存穿透问题,对于缓存中的热点数据能够提供更快的响应速度。在分布式环境下,基于本地缓存又增加了额外的复杂性。但它的优势在于适用于读多写少的应用场景,因为这种情况下缓存的命中率更高。

  3. 查询缓存(QueryCache): 只针对 select 操作,结果被缓存起来,以便下次查询时可以复用,减少数据库查询操作。只要查询条件相同,就会命中缓存,从而使得查询效率得到提升。而对于增删改操作,Mybatis 默认不会刷新缓存。可以在 xml 文件中开启或关闭此功能。

  4. 更新缓存(UpdateCache): 除了查询缓存之外,Mybatis 还提供了一种更加细粒度的缓存机制——更新缓存(UpdateCache)。它只缓存涉及到 update 或 delete 操作的数据,并且缓存的周期只有在调用 commit() 方法之后才会提交到数据库。对于那些需要频繁修改数据的场景,此功能尤为有效。
    上述四种缓存机制都可以作为配置项开启或关闭,也可以通过注解的方式指定某些 SQL 需要加入哪种缓存机制。另外,Mybatis 对缓存的过期时间也有设置,不过通常默认情况下都是按照最近最少使用(LRU,Least Recently Used)策略来清理缓存,缓存越长时间没有被访问到,就越容易被清除掉。
    本文将详细介绍Mybatis各个缓存机制的工作原理以及具体使用方法。

2.基本概念与术语介绍

(1)什么是缓存?

缓存就是临时的存储空间,用来保存最近访问的数据或者计算结果。比如,我们打开浏览器时,浏览网页都是先加载本地缓存,如果本地缓存没有相关数据,则再向服务器发送请求。访问过的数据都会保存在本地缓存里,下次访问时就可以直接从本地缓存中获得,不用重新向服务器请求。缓存是提高系统性能的一项重要手段,缓存能够降低服务器负载,提高服务响应速度。缓存可以减少网络流量、磁盘I/O和数据库的压力。缓存就是内存里的数据,它包含多个缓存块,每个缓存块包含很多缓存项。每个缓存项都是一组键值对,其中键是唯一标识符,值是被缓存的数据。

缓存的特点:

  1. 命中率高:缓存的命中率决定着缓存的效率,如果缓存命中率高,那么命中率低的数据就可以不必经过计算,而直接从缓存中取出,这样可以节省大量的时间。
  2. 空间换时间:缓存虽然比原数据更快一些,但是空间是有限的,因此,缓存应该选择大小合适的缓存数据。一般来说,缓存应该足够大,才能获得良好的性能。
  3. 数据一致性:缓存是临时的存储空间,随时可能丢失,因此,为了保证缓存数据的一致性,需要配合数据源,确保数据同步。

(2)什么是缓存回收机制?

当缓存数据过期或缓存容量达到上限时,需要对缓存中的数据进行清理,清理掉过期或不需要的数据,并释放占用的缓存空间。缓存回收机制有两种:

  1. 定时回收:定期扫描所有缓存数据,删除过期或需要回收的数据。
  2. 空间回收:当缓存中的数据已经超过一定数量时,触发缓存回收,自动删除不必要的数据,释放空间。例如,淘宝网站首页的商品推荐数据会在一段时间内保持不变,这些静态数据可以缓存在缓存中,每次访问时只需从缓存中获取,避免重复查询,提升用户体验。

(3)缓存的分类

目前,缓存主要分为三类:

  1. 数据缓存:主要指页面、组件等数据的缓存,根据数据的访问热度缓存起来,提升访问速度。例如,搜索引擎的结果缓存,热门文章的缓存等。
  2. 对象缓存:主要指业务对象缓存,提升业务处理速度。例如,Spring 中的缓存框架,Hibernate 的实体缓存,Memcached,Redis等。
  3. 框架缓存:主要指框架本身的缓存,包括 Spring MVC 的视图层缓存,Struts2 的 Action 层缓存,Talend Open Studio 中组件的缓存等。

(4)缓存使用的目的?

通过缓存可以解决以下几个问题:

  1. 提升系统运行效率:缓存能够提升系统运行效率,尤其是在访问热点数据时。例如,搜索引擎的结果缓存,热门文章的缓存等,能够提升系统的查询性能。
  2. 减少网络流量:缓存能够减少客户端与服务器之间的通信,降低系统的整体负载。
  3. 降低数据库负载:缓存能够降低数据库服务器的负载,减少对数据库的查询压力。
  4. 减少系统故障:缓存能够减少系统的故障率。

(5)缓存和数据库的数据一致性

缓存是临时的存储空间,随时可能丢失,因此,为了保证缓存数据的一致性,需要配合数据源,确保数据同步。

  1. Redis 集群模式下:使用 Redis 集群模式可以实现数据分布式缓存,即同一份缓存数据可能会分布在不同节点上,但最终结果一致。但同时也带来了一些挑战:
  • 大量数据迁移:随着集群规模扩大,会涉及大量数据的迁移,耗费时间长且风险高。
  • 主备切换:集群内存在主备模式,在主节点出现意外故障时,需要切换到备节点。
  1. 使用消息队列:Redis 作为缓存服务,发布订阅模式可实现数据一致性,但是引入消息队列消费延�uiton和重复消费问题,影响系统吞吐量。

(6)什么是序列化和反序列化?

在缓存中存放的是对象,需要序列化和反序列化。序列化和反序列化是指把对象转换为字节序列和从字节序列恢复对象的过程。比如,将 Java 对象转换为字节序列后存储在缓存中,需要反序列化后才能被 JVM 读取,否则无法被使用。序列化和反序列化的目的是为了保证缓存中存储的数据的完整性和正确性。

(7)什么是缓存一致性协议?

在分布式缓存中,常用的一致性协议有两类:

  1. 分布式锁(Distributed Lock):不同的机器上的进程需要互斥地访问某个资源时,可以使用分布式锁。
  2. 共识算法(Consensus Algorithm):不同的机器上的进程需要达成共识时,可以使用共识算法。

(8)什么是缓存雪崩?

缓存雪崩是指缓存服务重启或宕机导致大量缓存数据不可用,影响系统的可用性。原因主要有两个:

  1. 大量缓存集中失效:缓存数据只能缓存在一台机器上,一旦这个机器挂掉,缓存也就失效。当大量缓存失效时,所有的请求都落到数据库上,造成瘫痪。
  2. 缓存服务过多:缓存服务是单点部署还是集群部署,都会对系统的可用性产生影响。单点部署,一旦该节点宕机,整个系统都会瘫痪。

(9)缓存回收机制的演进

随着业务的发展,系统的缓存数量逐渐增多,单个缓存的容量也会增大。缓存过期时间的设定不得不减小,否则缓存将出现失效,发生雪崩效应。因此,缓存回收机制的演变如下:

  1. 清除策略:最初采用定时回收策略,每隔一定的时间扫描缓存数据并删除过期或需要回收的数据。缺点是扫描过多缓存数据时,系统会消耗大量 CPU 资源。
  2. 可靠通知机制:为了保证缓存数据及时失效,引入可靠通知机制,通知各个节点更新自己的缓存数据。同时,引入复制缓存机制,保证数据的一致性。缺点是引入复杂度,增加网络开销。
  3. LRU 策略:经过实践证明,LRU 策略效率最高。缓存回收机制修改为每隔一定的时间检查缓存中数据量,如果超过了一定的阈值,则删除最旧的缓存数据。

(10)什么是缓存击穿?

缓存击穿是指缓存服务宕机,导致大量请求失败。原因是当缓存失效时,有请求抵达数据库,但数据库无法满足这些请求,导致大量请求超时。
解决方案:

  1. 设置较短的过期时间:缓存数据在失效前尽量短暂,可以防止缓存击穿。
  2. 双层缓存:对于缓存失效的 key,首先查看其是否在另一个缓存中存在,如果存在,则返回;否则,查询数据库并更新缓存。

(11)什么是缓存穿透?

缓存穿透是指缓存服务正常,但查询数据库时,却无匹配的记录,这种情况称之为缓存穿透。
原因:

  1. 有些 key 是数据库中不存在的,导致缓存服务一直无法命中,一直查询数据库。
  2. 黑客攻击,爬虫发起大量缓存穿透请求。
    解决方案:
  3. 参数校验:对于参数为空、非法等特殊场景,直接拒绝掉。
  4. 设置冷启动:对于缓存中不存在的数据,先查询数据库,并设置冷启动策略,使缓存服务快速响应。
  5. IP 限制:对于特定 IP 发起的请求,限制其访问频率。

3.Mybatis缓存机制

3.1 一级缓存(Local cache)

3.1.1 什么是一级缓存

顾名思义,一级缓存就是应用程序本地缓存,属于本地缓存范畴。

3.1.2 一级缓存的作用
  1. 减少数据库的访问次数:缓存可以减少与数据库的交互次数,提升系统性能。
  2. 可以提升应用性能:对相同的数据可以直接返回缓存中的数据,提升应用性能。

3.1.2.1 一级缓存的使用方式

  1. 配置文件中启用一级缓存:

2.mapper接口方法添加@CacheNamespace注解,标注其使用一级缓存:@CacheNamespace(flushInterval=60000)
2. 在xml文件中配置元素,定义一级缓存的区域,并定义一级缓存的生命周期:

<cache type="org.mybatis.caches.ehcache.EhcacheCache" >
<property name="cacheManager" ref="cacheManager" />
<property name="timeToLiveSeconds" value="600" />
<property name="clearOnFlush" value="false" />
<property name="eternal" value="false" />
<property name="maxEntriesLocalHeap" value="10000" />
<property name="maxEntriesLocalDisk" value="-1" />
</cache>

此处通过Ehcache实现一级缓存,Ehcache是一个开源的Java内存缓存,支持缓存的持久化和集群化,可以配置内存、磁盘缓存策略。
使用Ehcache可以通过最大缓存数量、超时时间、持久化策略、清除策略等配置缓存,达到缓存效果最佳的目的。
Ehcache需要配置spring缓存管理器(CacheManager):

<!-- 创建CacheManager -->
<bean id="cacheManager" class="net.sf.ehcache.CacheManager">
<constructor-arg value="${configLocation}"/> <!-- 指定配置文件路径 -->
<constructor-arg><value>true</value></constructor-arg> <!-- 是否显示状态日志 -->
</bean>

<!-- 在配置文件中指定Ehcache配置文件位置-->
<context:property-placeholder location="classpath:/config/application.properties"/>
<bean id="configLocation" class="java.lang.String" scope="singleton">
<constructor-arg value="${app.home}/ehcache/${spring.profiles.active}.xml"></constructor-arg>
</bean>

在上面示例中,我们通过spring cache管理器配置了Ehcache的配置文件路径,并创建了CacheManager实例,通过xml文件配置了缓存区域和相关属性,如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ehcache PUBLIC "-//EHCACHE//DTD EHCACHE 2.10.0//EN" "http://www.ehcache.org/dtd/ehcache-2.10.0.dtd">
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
 profile="local">

<diskStore path="/data/cache" /> <!-- 磁盘缓存路径 -->

<!-- 内存缓存配置 -->
<cache alias="my_cache">
<keyType>java.lang.String</keyType>
<valueType>java.util.List&lt;MyBean&gt;</valueType>
<heap size="100m" unit="entries" /> <!-- 缓存最大值 -->
<expiryPolicy type="tti" value="600" /> <!-- 超时时间600秒 -->
</cache>

<!-- 磁盘缓存配置 -->
<cache alias="my_big_cache">
<keyType>java.lang.String</keyType>
<valueType>java.util.Map&lt;String, Integer&gt;</valueType>
<disk persistent="true" diskExpiryThreadIntervalSeconds="300" maxBytesLocalDisk="10g" />
<memoryUsage unit="bytes" maxBytesLocalHeap="100m" /> <!-- 缓存最大值 -->
<expiryPolicy type="timeToIdle" durationSeconds="300" />
</cache>

</ehcache>

这里,我们通过配置缓存别名、key类型、value类型、超时时间、缓存最大值等属性,实现了缓存的配置。

通过这种方式,我们可以在Mybatis中使用Ehcache来实现一级缓存,达到提升系统性能、降低数据库访问次数、提升应用性能的效果。

注意事项:
  1. 不要把数据库查询的结果直接返回给客户端,建议不要把查询结果缓存到一级缓存,因为一级缓存是以对象形式存储的,对象占用空间比较大。
  2. 在开发阶段,建议禁用一级缓存,使用二级缓存测试系统性能,然后调整缓存策略;在生产环境中,部署完毕后,使用缓存,确保系统的稳定性和性能。

3.2 二级缓存(Second level cache)

3.2.1 为什么要有二级缓存?

在实际的分布式系统中,缓存往往是作为第一道防线,在数据库层面防止缓存雪崩和击穿等问题,提升系统的响应速度。
  但是,如果将缓存放置在前端,前端仍然需要承受缓存服务的网络传输和硬件资源消耗。因此,我们需要设计一种二级缓存,将缓存分布式缓存到其他节点上,让缓存服务承担更多的任务,增强系统的弹性。
  总结一下,二级缓存是为了解决“缓存雪崩”和“缓存击穿”,通过将缓存分布式缓存到其他节点,来增强系统的弹性,提升系统的性能。

3.2.2 二级缓存的工作流程

二级缓存的工作流程如下:

  1. 先从一级缓存中获取数据,若命中则直接返回数据;
  2. 如果未命中,则到二级缓存中获取数据,若命中则直接返回数据;
  3. 如果未命中,则到其他节点的缓存中获取数据,若命中则直接返回数据;
  4. 如果未命中,则到数据库中查询数据,并将查询到的结果缓存到本地一级缓存和二级缓存;
  5. 返回查询结果。

二级缓存可以有效降低缓存命中率、提升缓存命中效率,并且支持热点数据的缓存共享,提升系统整体性能。如果您想了解详情,欢迎关注公众号:「架构先锋」获取更多信息。

3.3 查询缓存(QueryCache)

查询缓存可以理解为 SQL 查询语句的本地缓存,以减少数据库查询次数。仅对 SELECT 语句生效,对 Insert、Delete、Update 语句不生效。

查询缓存的工作原理

  1. 根据查询的 ID 生成一个 key,与查询结果绑定。
  2. 检查缓存中是否有相同的 key,如果有则直接返回查询结果。
  3. 如果没有,则执行 SQL 查询,并将结果与 key 绑定,返回查询结果。
  4. 将查询结果缓存到缓存区。

查询缓存的使用方法

  1. 配置文件中启用查询缓存:
  2. mapper.xml 文件中添加 queryCache 属性。
<select id="getUserById" parameterType="int" useCache="true" resultType="User">
select * from user where id = #{id}
</select>
  1. 在 xml 文件中配置 cache-ref 属性引用缓存配置。
<cache-ref namespace="com.aaa.bbb.UserDaoImpl" />

其中,namespace 对应 mapper.xml 中 cache-namespace 属性的值,多个 cache-ref 时通过逗号分隔。

注意事项:
  1. 在分布式缓存中,不要使用查询缓存。因为缓存数据在分布式环境下存在一致性问题。
  2. 避免缓存查询条件参数,缓存时间应尽量短。
  3. 每次查询之前,先删除缓存数据,防止脏数据导致错误。

4. Mybatis Cache 代码实例

4.1 使用 XML 配置Ehcache缓存插件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
 ...
<settings>
 ...
<setting name="cacheEnabled" value="true"/>
</settings>

<!-- 使用Ehcache实现缓存 -->
<typeAliases>
<package name="com.taobao.mybatis.model"/>
</typeAliases>

<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<property name="cacheManager" value="default"/>
<property name="timeToLiveSeconds" value="600"/>
<property name="clearOnFlush" value="true"/>
<property name="eternal" value="true"/>
<property name="maxEntriesLocalHeap" value="10000"/>
<property name="maxEntriesLocalDisk" value="-1"/>
</cache>
</environment>
</environments>

<mappers>
<mapper resource="com/taobao/mybatis/mapping/UserMapper.xml"/>
</mappers>

<cache-contexts>
<cache-context type="web"/>
</cache-contexts>

<!-- Ehcache的配置文件 -->
<settings>
<setting name="defaultContextType" value="web"/>
</settings>
<caches>
<cache name="default">
<keyType>java.lang.String</keyType>
<valueType>java.lang.Object</valueType>
<expiryPolicyFactory type="custom">
<class>com.taobao.mybatis.common.CustomEternalExpiryPolicyFactory</class>
</expiryPolicyFactory>
<heap memoryStoreEvictionPolicy="LRU"/>
<timeToIdleSeconds>1200</timeToIdleSeconds>
<timeToLiveSeconds>1200</timeToLiveSeconds>
<diskPersistent>true</diskPersistent>
<diskStorePath>/data/cache</diskStorePath>
</cache>
</caches>
 ...
</configuration>

4.2 使用注解配置Ehcache缓存插件

import org.apache.ibatis.cache.decorators.SerializedCache;
import org.apache.ibatis.session.Configuration;
import org.springframework.beans.factory.annotation.Value;

public class MybatisConfig {
@Value("${cache.use}")
private boolean useCache;

// 添加到mybatis配置中
Configuration configuration;

/**
* 使用注解配置Ehcache缓存插件
*/
@PostConstruct
public void setMapperScanner() throws Exception {
if (useCache) {
// 添加缓存插件
SerializedCache serializedCache = new SerializedCache(new EhcacheCache("default"));
configuration.addCache(serializedCache);

// 用注解注册Mapper
configuration.addMapper(UserDaoImpl.class);
} else {
// 未启用缓存插件,直接注册Mapper
configuration.addMapper(UserDaoImpl.class);
}
}
}

以上为使用XML配置Ehcache缓存插件的代码实例,如果使用注解配置Ehcache缓存插件,只需修改第二步即可。

4.3 Mapper接口编写

在 UserMapper.java 接口中添加以下方法:

public interface UserMapper extends BaseMapper<User> {}

在 UserDaoImpl.java 实现类中添加以下方法:

import com.taobao.mybatis.dao.BaseMapper;
import com.taobao.mybatis.model.User;

public class UserDaoImpl implements BaseMapper<User> {
@Override
public List<User> getAllUsers() {
return Collections.emptyList();
}
}

以上代码为 UserMapper 和 UserDaoImpl 的简单实现。

4.4 测试

在 MyBatisConfig 中通过 Configuration 配置 MyBatis,并注册 UserDaoImpl 到 Configuration 中。最后启动 Tomcat,访问 http://localhost:8080/mybatis-anno/ 查看控制台输出结果,若看到下列输出结果,表明缓存已经生效。

INFO [main] [] org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
DEBUG [main] [com.taobao.mybatis.service.UserService] - ==>Preparing: select u.id,u.name,u.age from t_user u 
INFO [main] [com.taobao.mybatis.persistence.UserMapper.selectAll] - <==Total: 0
INFO [main] [] org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession

如果看到上述输出,表示缓存生效成功。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

禅与计算机程序设计艺术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值