温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么在Springboot2.0通过redis实现支持分布式的mybatis二级缓存

发布时间:2021-06-22 17:18:38 来源:亿速云 阅读:190 作者:chen 栏目:大数据

这篇文章主要介绍“怎么在Springboot2.0通过redis实现支持分布式的mybatis二级缓存”,在日常操作中,相信很多人在怎么在Springboot2.0通过redis实现支持分布式的mybatis二级缓存问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么在Springboot2.0通过redis实现支持分布式的mybatis二级缓存”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

最近领导要求在项目中加下mybatis二级缓存,由于当前项目是分布式微服务,且是多节点部署的,而司内缓存中间件使用的redis,那很自然的要用redis做分布式缓存支持,避免出现直接使用原生mybatis二级缓存造成缓存数据不一致等问题。下面会对基于redis的mybatis二级缓存实现做下简单介绍,涉及一些概念,同时一些坑点做下整理。

1. 一级缓存

一级缓存是在SqlSession级别的缓存,MyBatis默认开启一级缓存。即同一个SqlSession对象,相同参数多次调用同一个Mapper方法时,只执行一次SQL,第一次查询后数据被缓存起来,之后的调用在没有缓存刷新、超时情况下都是直接先从缓存中取数据,不再去查数据库。不同SqlSession间,缓存是隔离的。

怎么在Springboot2.0通过redis实现支持分布式的mybatis二级缓存

此外实际项目开发中,一级缓存存在很大的局限性,我们的项目一般是Spring+Mybatis集成开发,而Spring的事务管理在逻辑层,每个service对应不同的SqlSession(这是通过MapperScannerConfigurer类创建SqlSession自动注入到service中的), 每次查询之后都会关闭SqlSession,缓存数据就会被清空。所以Spring整合之后,如果没有事务,一级缓存是没有实际意义的。

2. 二级缓存

二级缓存是Mapper级别的缓存,Mybatis默认不开启二级缓存。二级缓存的作用域是mapper的namespace,即相同namespace的两个mapper将共用同一缓存区域;支持跨SqlSession,即多个SqlSession可以共享一个mapper缓存。实现上是基于PerpetualCache的HashMap做本地存储,也支持自定义三方存储如ehcache、redis、memcache等,用于支持分布式。在本地使用HashMap存储缓存时,key为hashCode+sqlId+Sql语句(查询参数好像也参与,demo用的selectAll,没怎么关注),其他三方存储时key也差不多。

怎么在Springboot2.0通过redis实现支持分布式的mybatis二级缓存

注意:开启二级缓存后

  • 所有在映射文件里的select 语句都将被缓存。

  • 所有在映射文件里insert,update 和delete 语句会清空缓存。

  • 缓存默认使用“最近很少使用”LRU算法来回收

  • 缓存不会被设定的时间所清空。

  • 每个缓存可以存储1024 个列表或对象的引用(不管查询出来的结果是什么)。

  • 缓存将作为“读/写”缓存,意味着获取的对象不是共享的且对调用者是安全的。不会有其它的调用干扰其他调用者或线程所做的潜在修改

实现步骤:

1、全局cache-enable开关设置,此开关默认为true(实践证明不设置也行)

  • 创建mybatis-config.xml的配置文件

<?xml version="1.0">
  • Mybatis配置SqlSessionFactory时加载该配置

factory.setConfigLocation(new ClassPathResource("mybatis-config.xml"));

注:通过mybatis-config.xml配置缓存开关,验证启停正常;通过配置属性mybatis.configuration.cache-enabled=true的设置不起作用,原因有待探究

2、mapper.xml中<cache/>缓存标签的开启

  • 这是二级缓存开启的关键,如下配置是mybatis本地缓存,作用于整个mapper的所有查询,若某个<select>不需要缓存,设置useCache=false即可

<cache eviction="FIFO"  flushInterval="60000"  size="512"  readOnly="true"/>
  • 若要自定义三方存储,需要自实现org.apache.ibatis.cache.Cache接口,并在<cache type="com.bkjk.growth.configs.MybatisRedisCache"/>标签中指定自定义实现。另外关于Spring的ApplicationContext上下文获取,简单提一句,即实现ApplicationContextAware接口即可切入上下文

@Slf4j
public class MybatisRedisCache implements Cache {
    // RedisTemplate实例的封装工具类
	RedisUtilHandler redisUtilHandler;
    private String id;
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public MybatisRedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
    }
    
    // 通过Spring上下文获取redis操作类
    // 个人理解mybatis的某些配置加载是工作在拦截器层,初始化会早于IOC容器的某些bean的加载,这会通过spring自动注入 
    // 是拿不到代理对象的,所以这里做后置延迟处理,调用时再从上下文的获取Bean
    private RedisUtilHandler getRedisHandler(){
        if(redisUtilHandler == null){
        	redisUtilHandler = SpringContextHolder.getBean("redisUtilHandler");
        }
        return redisUtilHandler;
    }
    @Override
    public void clear() {
        try {
        	RedisUtilHandler redisUtilHandler = getRedisHandler();
        	redisUtilHandler.flushCache(id);
        } catch (Exception e) {
            log.error("clear Exception: {}", e);
        } 
    }
    @Override
    public String getId() {
        return this.id;
    }
    @Override
    public void putObject(Object key, Object value) {
        try {
        	RedisUtilHandler redisUtilHandler = getRedisHandler();
        	redisUtilHandler.setCache(key.toString(), value, 1, TimeUnit.DAYS);
        } catch (Exception e) {
            log.error("putObject Exception: {}", e);
        } 
    }
    @Override
    public Object getObject(Object key) {
        Object result = null;
        try {
        	RedisUtilHandler redisUtilHandler = getRedisHandler();
        	result = redisUtilHandler.getCache(key.toString(), Object.class);
        } catch (Exception e) {
        	log.error("getObject Exception: key###{} {}", key, e);
        } 
        return result;
        
    }
    @Override
    public Object removeObject(Object key) {
        Object result = null;
        try {
        	RedisUtilHandler redisUtilHandler = getRedisHandler();
        	redisUtilHandler.delete(key.toString());
        } catch (Exception e) {
            log.error("clear Exception: {}", e);
        }
        return result;
    }

    @Override
    public int getSize() {
    	RedisUtilHandler redisUtilHandler = getRedisHandler();
        Long size = (Long) redisUtilHandler.getInstance().execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.dbSize();
            }
        });
        return size.intValue();

    }
    @Override
    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }
}
<mapper namespace="com.xxx.xxx.xxx.repository.mapper.UserMapper">
<cache type="com.xxx.xxx.MybatisRedisCache"/>
  <resultMap id="BaseResultMap" type="com.xxx.xxx.xxx.repository.model.User">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="union_id" jdbcType="VARCHAR" property="unionId" />
    <result column="user_id" jdbcType="VARCHAR" property="userId" />
    <result column="user_name" jdbcType="VARCHAR" property="userName" />
    ...
  </resultMap>
  <select id="selectUsers" resultMap="BaseResultMap">
  	SELECT * FROM table
  </select>
  <select id="selectUserById" resultMap="BaseResultMap" useCache="false">
  	SELECT * FROM table WHERE user_id = #{userId}
  </select>

3、Model实体类需要做序列化

public class User implements Serializable{
   private static final long serialVersionUID = -6596381461353742505L;
   ...

}

本文是以redis作为存储介质,在redis配置时即指定了key、value的序列化方式,所以我在这步时实体类上序列化可有可无(也有人说即使本地缓存也不需要)

执行示例结果:

怎么在Springboot2.0通过redis实现支持分布式的mybatis二级缓存

坑点整理:

在第二步实现时,我用了<cache/>标签来开启二级缓存,此处还可以在mybatis的mapper接口类上使用等效注解来开启二级缓存,注解如下:

@CacheNamespace(implementation = com.xxx.xxx.configs.MybatisRedisCache.class)

但使用注解和xml中<cache/>标签不能同时作用,也就是说使用注解时,只能在Mapper接口的方法上用@Select注解绑定执行SQL,缓存才有效;同样使用<cache/>标签,则只能在mapper.xml中定义<select>标签进行带缓存查询。两者同时存在也只会有一种起效,是哪种可以自己试试。

缺陷分析:

  • Mybatis自身的缓存天生不支持分布式,需要整合其他第三方缓存库

        好在本文既是以redis作为自定义缓存来实现的,可以解决这个问题

  • 由于二级缓存是基于mapper级别的,以命名空间(namespace)隔离,可能导致联表查询的数据脏读

        这样的情况会发生在做联表查询时,参与联合查询的表在被其中一个或者多个namespace做数据缓存时,都是存的彼此初次关联查询时的数据镜像,而这之后各个namespace下表数据的更新了,二级缓存是不知道的,也就造成了数据脏读。 

        能想到的处理方式:

        1、联表查询,关联的所有表的操作都必须在同一个namespace。这个很难保证

        2、缩小缓存有效时间,当前是基于redis的三方缓存,可以自行设定失效时间,应当在不影响业务性能的情况下尽量缩短缓存有效时间。但问题其实同样是治标不治本

至此,mybatis二级缓存应该是比较全的使用实现了。基于缺陷上还有一点思考,我们的项目是否真的需要用到mybatis的二级缓存?像其他使用者说的,mybatis可是默认关闭二级缓存的,所以由此你该多考虑一下;如果是某些必要场景,比如访问频次较高的大单表查询,或者是表数据更新频次不会太高,缓存时效可以覆盖变更频率的,二级缓存还是不错的选择。其他复杂场景的缓存建议还是自己做业务缓存或者直接上Spring Cache比较划得来。

附:mapper中配置的参数说明:

  • eviction(可用的收回策略)默认为 LRU

    • LRU – 最近最少使用的:移除最长时间不被使用的对象。

    • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

    • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。

    • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

  • flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

  • size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值1024。

  • readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。

到此,关于“怎么在Springboot2.0通过redis实现支持分布式的mybatis二级缓存”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI