作者:禅与计算机程序设计艺术
1.简介
MyBatis Cache 是 MyBatis 框架的一个重要的扩展插件,它提供了一整套缓存解决方案,可有效地提升系统的运行效率。 MyBatis Cache 通过设置缓存配置并通过注解的方式来将数据查询结果进行缓存,缓存数据包括增删改查的数据和对象,提供强大的查询缓存功能。由于 MyBatis Cache 具有良好的扩展性、通用性、灵活性、可定制化能力等特点,能够很好地满足不同应用场景下的缓存需求。
本文将会详细阐述 MyBatis Cache 的内部机制,重点介绍其工作原理、配置参数、高级特性以及相关案例,力争打造一个易于理解、实用的 MyBatis Cache 技术分享。
阅读本文,您将了解到:
- 为什么需要 MyBatis Cache?
- MyBatis Cache 基本概念、术语和配置参数介绍
- MyBatis Cache 内部机制解析
- MyBatis Cache 高级特性介绍
- 实战:基于 SpringBoot + MyBatis + MyBatis Cache 的缓存案例
2.为什么需要 MyBatis Cache?
2.1 查询性能优化
在实际业务开发中,数据库查询操作是应用最频繁的操作之一,因此优化数据库查询操作的性能至关重要。而 MyBatis Cache 就是为了提升数据库查询性能而产生的一种解决方案。
2.2 数据一致性保障
在分布式环境下,对于同一份数据在不同的节点上可能存在不一致的情况。如果两个节点都查询到了过期的数据,就可能出现数据不一致的现象。这时候就需要引入缓存机制来保证数据的一致性。
3. MyBatis Cache 基本概念、术语和配置参数介绍
3.1 缓存分类
Mybatis Cache 有四种缓存类型:
- LOCAL: 只对当前 Session 中的数据生效,当 Session 关闭后失效。
- SESSION: 对当前 Session 中的所有数据生效,只要不是过期就永远不会失效。
- STATEMENT: 对整个过程中的所有语句生效,只要语句没有变化,就会命中缓存,否则就重新执行查询。
- FULL: 对所有的查询结果都进行缓存。
3.1.1 默认缓存策略
默认情况下, MyBatis 会采用 STATEMENT 缓存策略,即只针对每个 SQL 语句的结果进行缓存。
3.1.2 XML 配置 cache 属性
<cache type="org.mybatis.caches.ehcache.EhcacheCache" />
<!--
设置本地缓存 (Session)
默认缓存级别
可选级别:
NONE = 不缓存
BASIC = 缓存方法输入参数值
DEFAULT = 基本+历史结果缓存(DEFAULT_2NDLEVEL_CACHE)
INFINITE = 一直缓存
使用自定义缓存实现类时需要同时指定自定义缓存器 class
eg:<cache type="com.company.YourCustomCachingProvider"/>
-->
<cache type="org.apache.ibatis.cache.decorators.LruCache" eviction="LRU">
<!--
最大缓存元素个数 (Optional),默认为 1024
0 表示无限制,建议设定合理大小
可以通过如下参数进行设置:
size = "1024" | "unlimited"
-->
</cache>
<!--
指定装饰器顺序 (Optional),默认为 FIFO(先进先出)
可选顺序:FIFO(先进先出),LRU(最近最少使用),SOFT(软引用),WEAK (弱引用)。
注意:只有 LRU 和 SOFT 支持统计缓存命中次数。
-->
<cache-eviction/>
<!--
设置缓存回收策略
淘汰算法
如果容器满了之后该如何处理,默认是 LRU(最近最少使用)
参数:
type = “LeastRecentlyUsed”
| “FirstInFirstOut”
| “LeastFrequentlyUsed”
| “MostFrequentlyUsed”
| “RRWQueue”
| “RandomAccess”
maxEntries = “1024” //设置最大缓存条目数量
timeToLiveSeconds = “0”//缓存超时时间,0代表永不过期
-->
3.1.3 Annotation 配置 @CacheNamespace 和 @CacheNamespaceRef
package org.apache.ibatis.annotations;
import java.lang.annotation.*;
/**
* The Cache annotation is used to specify the name of a cache to be used for caching queries and/or results.
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Cache {
String value() default "";
}
/**
* Specifies that all the methods in a class or interface should share the same namespace.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CacheNamespace {
String value();
}
/**
* Used with {@link CacheNamespace} to reference another namespace by its alias.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface CacheNamespaceRef {
String value();
}
3.2 高级特性
3.2.1 查询预防CacheKey
查询预防CacheKey 是指根据查询条件生成cacheKey 。有了cacheKey后,系统可以判断查询缓存是否存在,如果存在则直接从缓存中取出数据,避免重复查询数据库,提高查询效率。
3.2.2 缓存共享
缓存共享 是指多个模块共用一个缓存,使得各个模块之间的缓存数据一致。缓存共享是MyBatis Cache 提供的高级特性,通过在注解或者xml文件中定义namespace ,然后使用@CacheNamespaceRef注解或者cache-ref标签即可实现。
3.2.3 分布式缓存
分布式缓存 是指缓存服务器部署在不同的服务器上,用于缓解单机缓存服务器的访问压力。分布式缓存使得缓存的生命周期更长,并提供了容错恢复机制,降低缓存服务的单点故障风险。
4. MyBatis Cache 内部机制解析
MyBatis Cache 内部实现主要由三大模块构成:缓存管理器、缓存存储、缓存键生成器。缓存管理器负责管理缓存的各种配置信息和缓存实例;缓存存储负责存储缓存的数据;缓存键生成器负责根据用户请求的参数生成CacheKey。下面我们逐一来看一下这些模块的实现原理和流程。
4.1 缓存管理器
4.1.1 创建缓存管理器
// createCache 方法创建缓存管理器实例
private static final CacheManager cacheManager = new CacheManager(settings);
4.1.2 设置缓存配置
private static final Configuration config = new Configuration();
private static void setCacheConfig() {
Properties settings = new Properties();
settings.setProperty("defaultCache", "true");
settings.setProperty("cache.type", "org.apache.ibatis.cache.ehcache.EhcacheCache");
settings.setProperty("cache.ehcache.configLocation", "/path/to/ehcache.xml");
EhcacheCacheFactory factory = new EhcacheCacheFactory(settings);
DefaultSqlSession sqlSession = new DefaultSqlSession(config, null);
LocalCacheScope localCacheScope = new LocalCacheScope(sqlSession, factory);
config.setLocalCacheScope(localCacheScope);
}
4.2 缓存存储
4.2.1 创建缓存存储
// 通过缓存配置信息获取缓存的实现类
final Ehcache cache = (Ehcache)cacheFactory.createCache(properties.getProperty("cache.id"));
4.2.2 添加、更新、删除缓存数据
if (!keys.isEmpty()) {
// 添加缓存数据
List<String> addKeyList = new ArrayList<>();
for (Object key : keys) {
Object value = entry.getValue().get(key);
try {
cache.put(new Element(generateKey(key), serializer.serialize(value)));
addKeyList.add((String) key);
} catch (IOException ex) {
throw new CacheException("Add failed.", ex);
}
}
// 从列表中删除已经成功添加到缓存中的key
removeFromList(entry.getKey(), addKeyList, true);
} else {
// 删除缓存数据
try {
cache.remove(generateKey(key));
} catch (IllegalStateException ise) {
// Ignore this exception as it means there was no element found with that key.
}
}
4.3 缓存键生成器
4.3.1 生成CacheKey
return generateKey(statementId, parameterObject, rowBounds, resultHandler, boundSql);
4.3.2 根据请求参数生成CacheKey
private String generateKey(String statementId, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StringBuilder keyBuilder = new StringBuilder(statementId);
ParameterHandler handler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
keyBuilder.append(":")
.append(handler.getParameterMapping());
if (rowBounds!= RowBounds.DEFAULT) {
keyBuilder.append(":").append(rowBounds);
}
if (resultHandler!= null) {
keyBuilder.append(":").append(System.identityHashCode(resultHandler));
}
return keyBuilder.toString();
}
5. MyBatis Cache 高级特性介绍
MyBatis Cache 提供的高级特性:查询预防CacheKey、缓存共享以及分布式缓存。下面我们来详细介绍一下这三个特性的内部机制。
5.1 查询预防CacheKey
5.1.1 原理
查询预防CacheKey 是MyBatis Cache 的一个高级特性,通过计算请求参数生成CacheKey ,来判断查询缓存是否存在。
5.1.2 操作步骤
-
在配置文件中启用查询预防CacheKey功能
<setting name="cacheEnabled" value="true"/>
-
在XML或注解中指定 @CacheNamespaceRef 属性
<cache-ref namespace="myNS"/>
-
生成CacheKey
private String generateCacheKey() {
StringBuffer sb = new StringBuffer();
sb.append(“select “).append(”*”).append(" from myTable where id=‘“).append(userId).append(”’ “);
if(!condition.equals(”“)) {
sb.append(“and condition='”).append(condition).append(”’ ");
}
return sb.toString();
}
4. 通过CacheKey 查询缓存
```java
BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
CacheKey cacheKey = new CacheKey(ms.getId(), mappedStatement.getConfiguration(), parameterObject, RowBounds.DEFAULT, boundSql);
Object cachedObj = cache.getObject(cacheKey);
上面的代码通过CacheKey 查询缓存。
5.2 缓存共享
5.2.1 原理
缓存共享 是MyBatis Cache 的一个高级特性,允许多个模块共用一个缓存,使得各个模块之间的缓存数据一致。
5.2.2 操作步骤
-
在 XML 或注解 中定义 @CacheNamespace 和 @CacheNamespaceRef 属性
<cache-namespace alias="ns1" /> <cache-ref namespace="ns1" property="myCache"/> <cache-ref namespace="ns1" property="yourCache"/>
-
在使用缓存的地方注入缓存
@Autowired private YourCache yourCache;
-
使用缓存
BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey cacheKey = new CacheKey(ms.getId(), ms.getConfiguration(), parameterObject, RowBounds.DEFAULT, boundSql); Object cachedObj = yourCache.getObject(cacheKey);
上面的代码通过CacheKey 获取缓存数据。
5.3 分布式缓存
5.3.1 原理
分布式缓存 是MyBatis Cache 的一个高级特性,通过将缓存数据存放在分布式缓存服务器上,来缓解单机缓存服务器的访问压力。
5.3.2 操作步骤
-
安装分布式缓存服务器,如 Memcached
-
修改配置文件
<cache-namespace alias="ns1" > <property name="distributed" value="true" /> <property name="serialization" value="java"/> </cache-namespace>
-
在修改后的配置文件中,设置分布式缓存相关属性
<setting name="cachePlugin" value="com.domain.yourplugin.YourPlugin"/> <setting name="cacheRegistry" value="com.domain.yourregistry.YourRegistry"/> <setting name="cacheSerializer" value="com.domain.yourserializer.YourSerializer"/>
-
编写分布式缓存插件和注册中心
public class YourPlugin implements CachePlugin {
// Implementation goes here...
}
public class YourRegistry extends CacheRegistry {
// Implementation goes here...
}
public class YourSerializer implements CacheSerializer {
// Implementation goes here...
}
- 使用缓存
BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
CacheKey cacheKey = new CacheKey(mappedStatement.getId(), mappedStatement.getConfiguration(), parameterObject, RowBounds.DEFAULT, boundSql);
Object cachedObj = cache.getObject(cacheKey);
上面的代码通过CacheKey 获取缓存数据。
6. 实战:基于 SpringBoot + MyBatis + MyBatis Cache 的缓存案例
6.1 准备环境
6.1.1 数据库
本文使用的数据库为 MySQL。
首先创建一个空数据库,例如,命名为 mybatis_test
。然后在 MySQL 命令行模式下,依次执行以下命令:
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
age INT NOT NULL
);
6.1.2 Maven
在项目的 pom.xml 文件中加入 MyBatis 和 MyBatis Cache 的依赖项。
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- MyBatis Cache -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-cachae</artifactId>
<version>${mybatis.cache.version}</version>
</dependency>
其中 ${mybatis.version}
和 ${mybatis.cache.version}
需要替换为正确的版本号。
6.1.3 Spring Boot starter
在 pom.xml 文件中加入 Spring Boot Starter Web 的依赖项。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
6.2 简单查询
首先,编写一个简单的查询接口 /users
,返回数据库中的所有用户记录。
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
@Autowired
private SqlSessionFactory sessionFactory;
@Autowired
private UserService userService;
@GetMapping("/users")
public List<User> getUsers() throws Exception {
return userService.selectAll();
}
}
接着,编写一个 UserService 来处理数据库的查询逻辑。
package com.example.demo.service;
import com.example.demo.entity.User;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.cache.CacheKey;
import org.mybatis.cache.defaults.DefaultCache;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserService {
@Resource
private SqlSessionFactory sessionFactory;
public List<User> selectAll() throws Exception {
// 获取 SqlSession 对象
SqlSession session = sessionFactory.openSession();
// 根据 ID 查询用户记录
List<User> users = session.selectList("com.example.demo.mapper.UserMapper.selectAll");
// 关闭 SqlSession 对象
session.close();
return users;
}
}
最后,编写 UserMapper.xml 文件,用来映射 SQL 语句。
<?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">
<mapper namespace="com.example.demo.mapper.UserMapper">
<select id="selectAll" resultType="com.example.demo.entity.User">
SELECT id, name, age FROM user ORDER BY id DESC
</select>
</mapper>
以上配置表示,启动 Spring Boot 时,自动扫描 @Service
注解的 Bean,并根据注解中指定的名称,将 Bean 注册到 Spring 容器中。并且,Spring Boot 将自动加载 MyBatis 配置文件。
6.3 加入 MyBatis Cache
前面配置了一个简单的查询接口,现在增加缓存支持,需要做如下几步:
6.3.1 启用 MyBatis Cache
打开 application.yml 文件,启用 MyBatis Cache 功能。
mybatis:
configuration:
cache-enabled: true
6.3.2 配置缓存
配置 MyBatis 的缓存插件,并注册缓存实例到 MyBatis 中。
package com.example.demo.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.MappedTransactionFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.type.TypeAliasRegistry;
import org.mybatis.cache.CacheManager;
import org.mybatis.cache.CachingExecutor;
import org.mybatis.cache.impl.PerpetualCacheAdapter;
import org.mybatis.logging.LogImpl;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
@Configuration
@MapperScan(basePackages = {"com.example.demo.mapper"})
public class DemoApplicationConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
sqlSessionFactoryBean.setTypeAliasesPackage("com.example.demo.entity");
ClassPathResource[] mapperLocations = new ClassPathResource[]{new ClassPathResource("mybatis/UserMapper.xml")};
sqlSessionFactoryBean.setMapperLocations(mapperLocations);
Properties properties = new Properties();
properties.load(Resources.getResourceAsStream("mybatis/mybatis-config.xml"));
Environment environment = new Environment("development", new JdbcTransactionFactory(), dataSource());
configuration(environment, properties);
sqlSessionFactoryBean.setConfiguration(configuration(environment, properties));
return sqlSessionFactoryBean;
}
private Configuration configuration(Environment environment, Properties properties) {
Configuration configuration = new Configuration(environment);
configuration.setDefaultFetchSize(Integer.parseInt(properties.getProperty("mybatis.default-fetch-size")));
configuration.setLazyLoadingEnabled(Boolean.parseBoolean(properties.getProperty("mybatis.lazy-loading")));
configuration.setUseColumnLabel(Boolean.parseBoolean(properties.getProperty("mybatis.use-column-label")));
configuration.setCacheEnabled(Boolean.parseBoolean(properties.getProperty("mybatis.cache-enabled")));
configuration.setLocalCacheScope(properties.getProperty("mybatis.cache.local.scope"));
configuration.setImplementation(new PerpetualCacheAdapter(cache()));
return configuration;
}
private Map<String, Cache> caches() {
HashMap<String, Cache> map = new HashMap<>();
map.put("local-cache", localCache());
return map;
}
private Cache localCache() {
return CachingExecutor.getLocalCache();
}
private Cache cache() {
Caffeine<Object, Object> caffeine = Caffeine.<Object, Object>newBuilder()
.maximumSize(Long.valueOf(10))
.build();
return new PerpetualCache("default-cache", caffeine);
}
}
此处配置了一个名为 local-cache
的本地缓存,最大缓存条目数为 10。
6.3.3 增加 @CacheNamespace 和 @CacheNamespaceRef
按照上面文档所说的配置 @CacheNamespace
和 @CacheNamespaceRef
,这样 MyBatis 将把所有方法共享同一个缓存空间。
package com.example.demo.entity;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.LoggingCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.cache.jcache.JCacheCache;
import org.apache.ibatis.cache.jcache.JCacheRegionFactory;
import org.apache.ibatis.cache.redis.RedisCache;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.type.TypeAliasRegistry;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.hibernate.validator.internal.engine.ValidatorFactoryImpl;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.spi.CachingProvider;
import java.lang.reflect.Proxy;
import java.time.Duration;
import java.util.Locale;
import java.util.Properties;
@SuppressWarnings({"unchecked","rawtypes"})
public class UserEntity implements ValidatorFactoryImpl.Holder{
private JCacheRegionFactory regionFactory;
private boolean useClassCache = false;
private boolean useMinimalPuts = Boolean.FALSE;
private int batchSize = Integer.MIN_VALUE;
private long expirationMillis = Long.MAX_VALUE;
private String jcacheName = "default";
private boolean readThrough = false;
private boolean writeThrough = false;
private Locale locale = Locale.getDefault();
private LoggingCache loggingCache;
private Cache cache;
private TypeAliasRegistry typeAliasRegistry;
private TypeHandlerRegistry typeHandlerRegistry;
public UserEntity(){
initJCacheRegionFactory();
MetaClass metaClass = SystemMetaObject.forObject(this).getMetaClass();
Proxy proxy = (Proxy)metaClass.getConstructor(new Class[]{JCacheRegionFactory.class}).newInstance(regionFactory);
cache = (Cache)proxy;
loggingCache = new LoggingCache(cache);
}
private void initJCacheRegionFactory() {
Properties p = new Properties();
p.setProperty("org.apache.commons.logging.Log", LogImpl.class.getName());
CachingProvider provider = Caching.getCachingProvider();
CacheManager manager = provider.getCacheManager();
regionFactory = new JCacheRegionFactory(manager, p);
}
public Cache getCache() {
return cache;
}
public void setCache(Cache cache) {
this.cache = cache;
}
public boolean isUseClassCache() {
return useClassCache;
}
public void setUseClassCache(boolean useClassCache) {
this.useClassCache = useClassCache;
}
public boolean isUseMinimalPuts() {
return useMinimalPuts;
}
public void setUseMinimalPuts(boolean useMinimalPuts) {
this.useMinimalPuts = useMinimalPuts;
}
public int getBatchSize() {
return batchSize;
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
public long getExpirationMillis() {
return expirationMillis;
}
public void setExpirationMillis(long expirationMillis) {
this.expirationMillis = expirationMillis;
}
public String getJcacheName() {
return jcacheName;
}
public void setJcacheName(String jcacheName) {
this.jcacheName = jcacheName;
}
public boolean isReadThrough() {
return readThrough;
}
public void setReadThrough(boolean readThrough) {
this.readThrough = readThrough;
}
public boolean isWriteThrough() {
return writeThrough;
}
public void setWriteThrough(boolean writeThrough) {
this.writeThrough = writeThrough;
}
public Locale getLocale() {
return locale;
}
public void setLocale(Locale locale) {
this.locale = locale;
}
protected void finalize() throws Throwable {
regionFactory.destroy();
}
}
6.3.4 测试
启动 Spring Boot 项目,测试缓存效果。
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
@Autowired
private SqlSessionFactory sessionFactory;
@Autowired
private UserService userService;
@GetMapping("/users")
public List<User> getUsers() throws Exception {
List<User> users = userService.selectAll();
users.stream().forEach(u -> u.setName("name_" + Math.random()));
return users;
}
}
第一次调用接口/users
,会先查询数据库,并放入缓存中,再返回给客户端。第二次调用接口/users
,会直接从缓存中取出数据,无需再查询数据库。