溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么在Springboot2.0通過(guò)redis實(shí)現(xiàn)支持分布式的mybatis二級(jí)緩存

發(fā)布時(shí)間:2021-06-22 17:18:38 來(lái)源:億速云 閱讀:205 作者:chen 欄目:大數(shù)據(jù)

這篇文章主要介紹“怎么在Springboot2.0通過(guò)redis實(shí)現(xiàn)支持分布式的mybatis二級(jí)緩存”,在日常操作中,相信很多人在怎么在Springboot2.0通過(guò)redis實(shí)現(xiàn)支持分布式的mybatis二級(jí)緩存問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”怎么在Springboot2.0通過(guò)redis實(shí)現(xiàn)支持分布式的mybatis二級(jí)緩存”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

最近領(lǐng)導(dǎo)要求在項(xiàng)目中加下mybatis二級(jí)緩存,由于當(dāng)前項(xiàng)目是分布式微服務(wù),且是多節(jié)點(diǎn)部署的,而司內(nèi)緩存中間件使用的redis,那很自然的要用redis做分布式緩存支持,避免出現(xiàn)直接使用原生mybatis二級(jí)緩存造成緩存數(shù)據(jù)不一致等問(wèn)題。下面會(huì)對(duì)基于redis的mybatis二級(jí)緩存實(shí)現(xiàn)做下簡(jiǎn)單介紹,涉及一些概念,同時(shí)一些坑點(diǎn)做下整理。

1. 一級(jí)緩存

一級(jí)緩存是在SqlSession級(jí)別的緩存,MyBatis默認(rèn)開(kāi)啟一級(jí)緩存。即同一個(gè)SqlSession對(duì)象,相同參數(shù)多次調(diào)用同一個(gè)Mapper方法時(shí),只執(zhí)行一次SQL,第一次查詢后數(shù)據(jù)被緩存起來(lái),之后的調(diào)用在沒(méi)有緩存刷新、超時(shí)情況下都是直接先從緩存中取數(shù)據(jù),不再去查數(shù)據(jù)庫(kù)。不同SqlSession間,緩存是隔離的。

怎么在Springboot2.0通過(guò)redis實(shí)現(xiàn)支持分布式的mybatis二級(jí)緩存

此外實(shí)際項(xiàng)目開(kāi)發(fā)中,一級(jí)緩存存在很大的局限性,我們的項(xiàng)目一般是Spring+Mybatis集成開(kāi)發(fā),而Spring的事務(wù)管理在邏輯層,每個(gè)service對(duì)應(yīng)不同的SqlSession(這是通過(guò)MapperScannerConfigurer類(lèi)創(chuàng)建SqlSession自動(dòng)注入到service中的), 每次查詢之后都會(huì)關(guān)閉SqlSession,緩存數(shù)據(jù)就會(huì)被清空。所以Spring整合之后,如果沒(méi)有事務(wù),一級(jí)緩存是沒(méi)有實(shí)際意義的。

2. 二級(jí)緩存

二級(jí)緩存是Mapper級(jí)別的緩存,Mybatis默認(rèn)不開(kāi)啟二級(jí)緩存。二級(jí)緩存的作用域是mapper的namespace,即相同namespace的兩個(gè)mapper將共用同一緩存區(qū)域;支持跨SqlSession,即多個(gè)SqlSession可以共享一個(gè)mapper緩存。實(shí)現(xiàn)上是基于PerpetualCache的HashMap做本地存儲(chǔ),也支持自定義三方存儲(chǔ)如ehcache、redis、memcache等,用于支持分布式。在本地使用HashMap存儲(chǔ)緩存時(shí),key為hashCode+sqlId+Sql語(yǔ)句(查詢參數(shù)好像也參與,demo用的selectAll,沒(méi)怎么關(guān)注),其他三方存儲(chǔ)時(shí)key也差不多。

怎么在Springboot2.0通過(guò)redis實(shí)現(xiàn)支持分布式的mybatis二級(jí)緩存

注意:開(kāi)啟二級(jí)緩存后

  • 所有在映射文件里的select 語(yǔ)句都將被緩存。

  • 所有在映射文件里insert,update 和delete 語(yǔ)句會(huì)清空緩存。

  • 緩存默認(rèn)使用“最近很少使用”LRU算法來(lái)回收

  • 緩存不會(huì)被設(shè)定的時(shí)間所清空。

  • 每個(gè)緩存可以存儲(chǔ)1024 個(gè)列表或?qū)ο蟮囊茫ú还懿樵兂鰜?lái)的結(jié)果是什么)。

  • 緩存將作為“讀/寫(xiě)”緩存,意味著獲取的對(duì)象不是共享的且對(duì)調(diào)用者是安全的。不會(huì)有其它的調(diào)用干擾其他調(diào)用者或線程所做的潛在修改

實(shí)現(xiàn)步驟:

1、全局cache-enable開(kāi)關(guān)設(shè)置,此開(kāi)關(guān)默認(rèn)為true(實(shí)踐證明不設(shè)置也行)

  • 創(chuàng)建mybatis-config.xml的配置文件

<?xml version="1.0">
  • Mybatis配置SqlSessionFactory時(shí)加載該配置

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

注:通過(guò)mybatis-config.xml配置緩存開(kāi)關(guān),驗(yàn)證啟停正常;通過(guò)配置屬性mybatis.configuration.cache-enabled=true的設(shè)置不起作用,原因有待探究

2、mapper.xml中<cache/>緩存標(biāo)簽的開(kāi)啟

  • 這是二級(jí)緩存開(kāi)啟的關(guān)鍵,如下配置是mybatis本地緩存,作用于整個(gè)mapper的所有查詢,若某個(gè)<select>不需要緩存,設(shè)置useCache=false即可

<cache eviction="FIFO"  flushInterval="60000"  size="512"  readOnly="true"/>
  • 若要自定義三方存儲(chǔ),需要自實(shí)現(xiàn)org.apache.ibatis.cache.Cache接口,并在<cache type="com.bkjk.growth.configs.MybatisRedisCache"/>標(biāo)簽中指定自定義實(shí)現(xiàn)。另外關(guān)于Spring的ApplicationContext上下文獲取,簡(jiǎn)單提一句,即實(shí)現(xiàn)ApplicationContextAware接口即可切入上下文

@Slf4j
public class MybatisRedisCache implements Cache {
    // RedisTemplate實(shí)例的封裝工具類(lèi)
	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;
    }
    
    // 通過(guò)Spring上下文獲取redis操作類(lèi)
    // 個(gè)人理解mybatis的某些配置加載是工作在攔截器層,初始化會(huì)早于IOC容器的某些bean的加載,這會(huì)通過(guò)spring自動(dòng)注入 
    // 是拿不到代理對(duì)象的,所以這里做后置延遲處理,調(diào)用時(shí)再?gòu)纳舷挛牡墨@取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實(shí)體類(lèi)需要做序列化

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

}

本文是以redis作為存儲(chǔ)介質(zhì),在redis配置時(shí)即指定了key、value的序列化方式,所以我在這步時(shí)實(shí)體類(lèi)上序列化可有可無(wú)(也有人說(shuō)即使本地緩存也不需要)

執(zhí)行示例結(jié)果:

怎么在Springboot2.0通過(guò)redis實(shí)現(xiàn)支持分布式的mybatis二級(jí)緩存

坑點(diǎn)整理:

在第二步實(shí)現(xiàn)時(shí),我用了<cache/>標(biāo)簽來(lái)開(kāi)啟二級(jí)緩存,此處還可以在mybatis的mapper接口類(lèi)上使用等效注解來(lái)開(kāi)啟二級(jí)緩存,注解如下:

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

但使用注解和xml中<cache/>標(biāo)簽不能同時(shí)作用,也就是說(shuō)使用注解時(shí),只能在Mapper接口的方法上用@Select注解綁定執(zhí)行SQL,緩存才有效;同樣使用<cache/>標(biāo)簽,則只能在mapper.xml中定義<select>標(biāo)簽進(jìn)行帶緩存查詢。兩者同時(shí)存在也只會(huì)有一種起效,是哪種可以自己試試。

缺陷分析:

  • Mybatis自身的緩存天生不支持分布式,需要整合其他第三方緩存庫(kù)

        好在本文既是以redis作為自定義緩存來(lái)實(shí)現(xiàn)的,可以解決這個(gè)問(wèn)題

  • 由于二級(jí)緩存是基于mapper級(jí)別的,以命名空間(namespace)隔離,可能導(dǎo)致聯(lián)表查詢的數(shù)據(jù)臟讀

        這樣的情況會(huì)發(fā)生在做聯(lián)表查詢時(shí),參與聯(lián)合查詢的表在被其中一個(gè)或者多個(gè)namespace做數(shù)據(jù)緩存時(shí),都是存的彼此初次關(guān)聯(lián)查詢時(shí)的數(shù)據(jù)鏡像,而這之后各個(gè)namespace下表數(shù)據(jù)的更新了,二級(jí)緩存是不知道的,也就造成了數(shù)據(jù)臟讀。 

        能想到的處理方式:

        1、聯(lián)表查詢,關(guān)聯(lián)的所有表的操作都必須在同一個(gè)namespace。這個(gè)很難保證

        2、縮小緩存有效時(shí)間,當(dāng)前是基于redis的三方緩存,可以自行設(shè)定失效時(shí)間,應(yīng)當(dāng)在不影響業(yè)務(wù)性能的情況下盡量縮短緩存有效時(shí)間。但問(wèn)題其實(shí)同樣是治標(biāo)不治本

至此,mybatis二級(jí)緩存應(yīng)該是比較全的使用實(shí)現(xiàn)了?;谌毕萆线€有一點(diǎn)思考,我們的項(xiàng)目是否真的需要用到mybatis的二級(jí)緩存?像其他使用者說(shuō)的,mybatis可是默認(rèn)關(guān)閉二級(jí)緩存的,所以由此你該多考慮一下;如果是某些必要場(chǎng)景,比如訪問(wèn)頻次較高的大單表查詢,或者是表數(shù)據(jù)更新頻次不會(huì)太高,緩存時(shí)效可以覆蓋變更頻率的,二級(jí)緩存還是不錯(cuò)的選擇。其他復(fù)雜場(chǎng)景的緩存建議還是自己做業(yè)務(wù)緩存或者直接上Spring Cache比較劃得來(lái)。

附:mapper中配置的參數(shù)說(shuō)明:

  • eviction(可用的收回策略)默認(rèn)為 LRU

    • LRU – 最近最少使用的:移除最長(zhǎng)時(shí)間不被使用的對(duì)象。

    • FIFO – 先進(jìn)先出:按對(duì)象進(jìn)入緩存的順序來(lái)移除它們。

    • SOFT – 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象。

    • WEAK – 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象。

  • flushInterval(刷新間隔)可以被設(shè)置為任意的正整數(shù),而且它們代表一個(gè)合理的毫秒形式的時(shí)間段。默認(rèn)情況是不設(shè)置,也就是沒(méi)有刷新間隔,緩存僅僅調(diào)用語(yǔ)句時(shí)刷新。

  • size(引用數(shù)目)可以被設(shè)置為任意正整數(shù),要記住你緩存的對(duì)象數(shù)目和你運(yùn)行環(huán)境的可用內(nèi)存資源數(shù)目。默認(rèn)值1024。

  • readOnly(只讀)屬性可以被設(shè)置為 true 或 false。只讀的緩存會(huì)給所有調(diào)用者返回緩存對(duì)象的相同實(shí)例。因此這些對(duì)象不能被修改。這提供了很重要的性能優(yōu)勢(shì)。可讀寫(xiě)的緩存會(huì)返回緩存對(duì)象的拷貝(通過(guò)序列化)。這會(huì)慢一些,但是安全,因此默認(rèn)是false。

到此,關(guān)于“怎么在Springboot2.0通過(guò)redis實(shí)現(xiàn)支持分布式的mybatis二級(jí)緩存”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI