MyBatis一级二级缓存

MyBatis一级二级缓存

MyBatis一级二级缓存

在Spring Boot整合MyBatis框架下,MyBatis的缓存机制分为一级缓存和二级缓存。

1. 一级缓存(Local Cache)

一级缓存使用了PerpetualCache,内置HashMap。
该缓存是本地缓存,存放在内存中,session关闭后自动消失,不共享。一个HashMap实现 ,Key通常是SQL语句加上参数值的组合(哈希值),Value是查询结果对象列表。

image.png

一级缓存是SqlSession级别的缓存,意味着在一个SqlSession的生命周期内,对于相同的SQL查询请求,MyBatis会优先查看一级缓存中是否存在结果。

  • 何时有效:只要在一个SqlSession实例中执行相同SQL查询,且SQL语句、参数完全一致,无论查询多少次,只要缓存中已有结果,就不会再次查询数据库。

  • 何时失效

    • SqlSession关闭时,一级缓存会被清空。
    • 执行任何增删改操作(INSERT、UPDATE、DELETE)后,MyBatis会清理一级缓存,因为修改了数据库状态可能导致之前缓存的结果不再准确。
    • 当查询条件或参数发生变化时,一级缓存也不会命中。

2. 二级缓存(Secondary Cache)

image.png

二级缓存是跨SqlSession级别的缓存,允许不同SqlSession实例共享查询结果。二级缓存需要显式开启并在全局或Mapper级别配置。

  • 何时有效

    • 在二级缓存开启的前提下,当一级缓存未命中且二级缓存存在对应查询结果时,MyBatis会从二级缓存中返回数据,而不去数据库查询。
    • 在新的SqlSession中执行相同的查询,如果二级缓存中存在相关数据,也会命中缓存。
  • 何时失效

    • 当执行了影响二级缓存中相关数据的增删改操作后,需要手动清除或刷新相关的缓存区域,否则会导致缓存中的数据与数据库不一致。
    • 在MyBatis中,默认并不自动同步二级缓存,开发者需要在业务代码中配合事务处理来保证数据一致性。
    • 若全局或Mapper级别的二级缓存配置发生改变,原有缓存也将失效。
  • 何时清空

    • 当执行了可能影响缓存数据的增删改操作后,需要手动清除或刷新缓存,可通过编程方式或利用第三方缓存工具(如Ehcache、Redis等)的特性来实现。
    • 关闭应用服务器,所有缓存自然失效。
  • 何时设置缓存

    • 查询结果被成功执行且满足缓存条件时,MyBatis会自动将结果放入二级缓存中。开启二级缓存时,需要配置好缓存插件(如Ehcache)并在全局或Mapper级别启用缓存功能。

总结起来,在Spring Boot+MyBatis的应用中,缓存有效的情况通常发生在:

  • 对于同一SqlSession内的重复查询,一级缓存有效。
  • 对于不同SqlSession间的重复查询且二级缓存已启用情况下,二级缓存有效。

而缓存失效的情况主要在于:

  • SqlSession结束或执行了增删改操作后,一级缓存失效。
  • 执行了影响缓存数据的增删改操作后,二级缓存需要适当处理才能保持数据一致性。

二级缓存框架(Ehcache、Redis、Memcached等)

MyBatis的二级缓存本身并不提供具体的缓存实现,而是提供了一个缓存接口,开发者可以根据自身需求选择合适的缓存框架进行集成。常用的二级缓存实现框架包括:

  1. Ehcache:

    • Ehcache是一个广泛使用的Java进程内缓存库,它支持分布式缓存,且与MyBatis兼容良好。
    • 在MyBatis中启用Ehcache作为二级缓存,需要添加Ehcache的依赖,并在MyBatis的配置文件中指定EhcacheCache插件。

    在Spring Boot整合MyBatis并使用Ehcache作为二级缓存的实现时,需要经过以下几个步骤:

  2. 添加依赖
    在项目的pom.xml文件中添加Ehcache和MyBatis-Ehcache的依赖。例如,对于Maven项目:

    <dependencies>
        <!-- Spring Boot MyBatis starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version> <!-- 根据实际版本调整 -->
        </dependency>
        
        <!-- Ehcache -->
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>3.x.x</version> <!-- 根据实际版本调整 -->
        </dependency>
        
        <!-- MyBatis-Ehcache plugin -->
        <dependency>
            <groupId>org.mybatis.caches</groupId>
            <artifactId>mybatis-ehcache</artifactId>
            <version>1.1.0</version> <!-- 根据实际版本调整 -->
        </dependency>
    </dependencies>
  3. 配置MyBatis二级缓存
    application.ymlapplication.properties中启用全局二级缓存,并指定使用Ehcache作为缓存实现:

    # application.yml
    mybatis:
      configuration:
        cache-enabled: true
      mapper-locations: classpath:mappers/*.xml
    
      #
      type-aliases-package: com.example.yourproject.model
      # 使用Ehcache作为二级缓存
      default-cache: org.mybatis.caches.ehcache.EhcacheCache

    # application.properties
    mybatis.configuration.cache-enabled=true
    mybatis.mapper-locations=classpath:mappers/*.xml
    mybatis.type-aliases-package=com.example.yourproject.model
    # 使用Ehcache作为二级缓存
    mybatis.default-cache=org.mybatis.caches.ehcache.EhcacheCache
  4. 配置Ehcache
    在资源文件目录下创建一个Ehcache的配置文件,如ehcache.xml,定义缓存的详细配置(如缓存名、缓存有效期、最大元素数量等)。

    <?xml version="1.0" encoding="UTF-8"?>
    <config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
            xmlns='http://www.ehcache.org/v3'>
    
        <cache alias="yourCacheAlias">
            <key-type>java.lang.Long</key-type>
            <value-type>com.example.yourproject.modeldto.YourModelDto</value-type>
            <expiry>
                <ttl unit="seconds">3600</ttl> <!-- 缓存有效期1小时 -->
            </expiry>
            <heap size="1000" unit="entries"/>
        </cache>
    
    </config>
  5. 在Mapper接口或全局配置中启用二级缓存
    若要在特定的Mapper接口上启用二级缓存,可以在对应的Mapper XML文件中声明cache-ref指向Ehcache配置的别名,或者在Mapper接口上使用@CacheNamespace注解。

    <!-- UserMapper.xml -->
    <mapper namespace="com.example.yourproject.mapper.UserMapper">
        <cache-ref namespace="yourCacheAlias" />
     
    
      <!-- 带有缓存的查询 -->
     <select id="selectByUserId" parameterType="java.lang.Long" resultType="com.example.yourproject.modeldto.YourModelDto">
         SELECT * FROM users WHERE id = #{id}
     </select>
    
    </mapper>

    或者使用注解:

    import org.apache.ibatis.annotations.CacheNamespaceRef;
    import org.apache.ibatis.annotations.Select;
    
    @CacheNamespaceRef(EhcacheCache.class)
    public interface UserMapper {
        @Select("...")
        YourModelDto selectByUserId(Long id);
        // 其他方法...
    }

注意:这里的resultType="com.example.yourproject.modeldto.YourModelDto"必须与Ehcache配置文件中 定义的类型一致,这样查询结果才会被正确地存入缓存中。
只有设置了缓存的查询方法在执行时才会依据缓存策略决定是否从缓存中获取数据,而不是所有的SQL查询都会自动纳入缓存。

完成以上步骤后,Ehcache将作为MyBatis的二级缓存框架进行工作。当执行查询操作时,如果缓存中已经有结果,则MyBatis会直接从缓存中返回数据,减少对数据库的访问。
同时,需要注意在执行增删改操作后,根据业务逻辑及时更新或清除缓存,以保证数据的一致性。

  1. Redis:

    • Redis是一个开源的、高性能的键值对存储系统,常被用作分布式缓存。
    • 虽然MyBatis本身并未直接提供对Redis的支持,但是可以通过第三方插件如mybatis-redis-cache(或者其他类似的组件)将Redis作为MyBatis二级缓存的实现。

配置MyBatis二级缓存

在MyBatis的配置中启用二级缓存,并指定Redis缓存插件:default-cache: com.example.redis.MybatisRedisCache

mybatis:
  configuration:
    cache-enabled: true
  mapper-locations: classpath:mappers/*.xml
  type-aliases-package: com.example.yourproject.model
  default-cache: com.example.redis.MybatisRedisCache

自定义实现MybatisRedisCache.java,创建一个类继承org.apache.ibatis.cache.Cache接口,并在Spring Boot中注册这个缓存插件,实现Redis缓存逻辑。

import org.apache.commons.collections.CollectionUtils;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * MybatisRedisCache 缓存工具类
 * Title: MybatisRedisCache
 * Description:
 *
 * @author wstv
 */
@Service
public class MybatisRedisCache implements Cache {
    private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final String id; // cache instance id
    private RedisTemplate redisTemplate;

    private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间

    public MybatisRedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    /**
     * Put query result to redis
     *
     * @param key
     * @param value
     */
    @Override
    public void putObject(Object key, Object value) {
        try {
            redisTemplate = getRedisTemplate();
            if (value != null) {
                redisTemplate.opsForValue().set(key.toString(), value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
            }
            logger.debug("Put query result to redis");
        } catch (Throwable t) {
            logger.error("Redis put failed", t);
        }


    }

    /**
     * Get cached query result from redis
     *
     * @param key
     * @return
     */
    @Override
    public Object getObject(Object key) {
        try {
            redisTemplate = getRedisTemplate();
            logger.debug("Get cached query result from redis");
            return redisTemplate.opsForValue().get(key.toString());
        } catch (Throwable t) {
            logger.error("Redis get failed, fail over to db", t);
            return null;
        }
    }

    /**
     * Remove cached query result from redis
     *
     * @param key
     * @return
     */
    @Override
    @SuppressWarnings("unchecked")
    public Object removeObject(Object key) {
        try {
            redisTemplate = getRedisTemplate();
            redisTemplate.delete(key.toString());
            logger.debug("Remove cached query result from redis");
        } catch (Throwable t) {
            logger.error("Redis remove failed", t);
        }
        return null;
    }

    /**
     * Clears this cache instance
     */
    @Override
    public void clear() {
        redisTemplate = getRedisTemplate();
        Set<String> keys = redisTemplate.keys("*:" + this.id + "*");
        if (!CollectionUtils.isEmpty(keys)) {
            redisTemplate.delete(keys);
        }
        logger.debug("Clear all the cached query result from redis");
    }

    /**
     * This method is not used
     *
     * @return
     */
    @Override
    public int getSize() {
        return 0;
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }

    private RedisTemplate getRedisTemplate() {
        if (redisTemplate == null) {
            redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
        }
        return redisTemplate;
    }
}

在Mapper接口或XML文件中启用缓存

在对应的Mapper XML文件或接口上启用缓存,与Ehcache或其他二级缓存实现相似:

<!-- UserMapper.xml -->
<mapper namespace="com.example.yourproject.mapper.UserMapper">
    <cache type="com.example.redis.MybatisRedisCache" eviction="LRU" flushInterval="60000" size="512" readOnly="false"/>
    <!-- 带有缓存的查询 -->
    <select id="selectUserById" parameterType="java.lang.Long" resultType="com.example.yourproject.model.User">
        SELECT * FROM users WHERE id = #{id}
    </select>
</mapper>

或者在Mapper接口上使用注解全局启用缓存:

@CacheNamespace(implementation = MybatisRedisCache.class, eviction = CacheConcurrencyStrategy.LRU)
public interface UserMapper {

 // 对特定查询启用Redis缓存,同时也启用缓存注解选项

 @Options(useCache = true) // 启用缓存
 YourModel selectUserById(YourParam param);

 // 其他无缓存的查询方法
 // ...

}

在MyBatis的Mapper配置文件中,可以通过添加 <cache> 标签来启用并配置二级缓存,该标签具有多个可设置的属性:

  1. eviction属性

    • LRU (Least Recently Used): 最近最少使用策略,当缓存容量满时,移除最近最少使用的对象。
    • FIFO (First In First Out): 先进先出策略,按照对象进入缓存的顺序进行移除。
    • SOFT: 软引用策略,当内存不足且对象只有软引用时,根据垃圾回收器的状态来移除对象。
    • WEAK: 弱引用策略,相比软引用更积极地根据垃圾回收器状态和弱引用规则移除对象。
      默认的缓存回收策略是 LRU
  2. flushInterval属性

    • 设置缓存刷新间隔的毫秒数。默认情况下,MyBatis不设置刷新间隔,这意味着缓存仅在执行更改数据的操作(如增删改SQL)时才进行刷新。
  3. size属性

    • 用于指定缓存的最大引用对象数目。超过这个数目后,缓存将根据选定的回收策略进行对象移除,以避免内存溢出的风险。
  4. readOnly属性

    • true: 表示只读缓存。在这种模式下,缓存返回给所有调用者的都是相同对象实例,因此这些对象不可被修改。这能带来显著的性能提升,但要注意确保缓存的对象是线程安全或不可变的。
    • false: 表示读写缓存。在这种模式下,缓存会在返回对象时创建其副本(通常通过序列化实现)。这种方式虽然相对较慢,但更为安全,因为它避免了多线程环境下的数据争用问题。默认情况下,readOnly属性为 false
  5. Memcached(省略):

    • Memcached也是一个流行的分布式缓存系统。
    • 同样,MyBatis原生不支持Memcached,但可以通过类似mybatis-memcached-cache这样的第三方插件将Memcached与MyBatis集成,实现二级缓存。
  6. Caffeine(省略):

    • Caffeine是Java 8及以上版本的新一代本地缓存库,性能优秀。
    • 也可以通过MyBatis的适配插件将其作为二级缓存的实现。
  7. Hazelcast(省略):

    • Hazelcast是一个开源的内存数据网格,既可以做分布式缓存,也可以做内存数据库。通过相应的适配器,可以将其与MyBatis结合使用。
  8. 自定义缓存实现(类似redis):

    • 如果上述缓存方案都不能满足需求,MyBatis还允许开发者自定义缓存实现,只需要实现org.apache.ibatis.cache.Cache接口即可。

请注意,启用二级缓存前务必充分考虑数据的一致性问题,特别是对于高并发、强一致性要求的场景,需要仔细权衡是否启用二级缓存以及如何正确配置缓存刷新策略。

原文链接 https://www.hanyuanhun.cn | https://node.hanyuanhun.cn

  • 13
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值