Mybatis自定義分布式二級緩存實現與遇到的一些問題解決方案!


先說兩句:

  我們都知道Mybatis緩存分兩類: 一級緩存(同一個Session會話內) & 二級緩存(基於HashMap實現的以 namespace為范圍的緩存)

  今天呢, 我們不談一級緩存, 我們來談一談 二級緩存, 通過查看Mybatis源碼發現, 他的二級緩存實現真的十分簡單, 默認的實現類是 org.apache.ibatis.cache.impl.PerpetualCache 這里貼一下他的源碼吧:

  
/**
 *    Copyright 2009-2015 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

/**
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {

  private String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }

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

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

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

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}
PerpetualCache.java

  那么既然他都已經有一個實現了, 我們為什么還要自定義實現呢? 

  原因很簡單, 默認實現是(HashMap)本地緩存, 不支持分布式緩存, 而我們現在大多數項目都是以集群的方式部署, 這種情況下, 使用本地緩存會出現很嚴重的臟讀問題, 特定情況下更新可直接導致數據不一致的問題.

  如果讓緩存實現支持分布式呢? 方案有很多, 基本圍繞着NoSQL數據庫實現, 最常用的就是Redis了, 接下來我們就來用Redis實現自定義緩存類

  說干就干吧, 開始我們的自定義緩存實現之路

 

實現過程:

  首先: Mybatis自定義實現緩存類的配置方式十分簡單, 我們只需要開啟二級緩存, 並在 mapper.xml 中修改如下配置:

  

  其中 type 屬性值就是我們自定義的緩存實現辣!

 

  接下來我們來看一看實現類內部是怎么寫的:

  
package com.cardgame.demo.game.component.db;

import com.cardgame.demo.component.redis.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;

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

import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME;

/**
 * @author yjy
 * 2018-08-06 15:46
 */
@Slf4j
public class MybatisRedisCache implements Cache {

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private String id;

    public MybatisRedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("id can not be null");
        }
        log.debug("MyBatisRedisCache:id=" + id);
        this.id = id;
    }

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

    @Override
    public int getSize() {
        int size = (int) RedisUtils.getInstance().getHashSizeObj(getKey());
        log.debug("MybatisCache getSize > {}", size);
        return size;
    }

    @Override
    public void putObject(Object key, Object value) {
        RedisUtils.getInstance().setHashObj(getKey(), key, value);
        log.debug("MybatisCache put > key: {}, val: {}", key, value);
    }
    @Override
    public Object getObject(Object key) {
        Object val = RedisUtils.getInstance().getHashObj(getKey(), key);
        log.debug("MybatisCache get > key: {}, val: {}", key, val);
        return val;
    }

    @Override
    public Object removeObject(Object key) {
        // 移除指定緩存
        Object obj = RedisUtils.getInstance().getHashObj(getKey(), key);
        RedisUtils.getInstance().delHashObj(getKey(), key);
        log.debug("MybatisCache remove > key: {}, val: {}", key, obj);
        return obj;
    }

    @Override
    public void clear() {
        // 清除所有緩存
        RedisUtils.getInstance().delObj(getKey());
        log.debug("MybatisCache clear > hashKey : {}", getKey());
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }
    
    protected String getKey() {
        return REDIS_KEY_GAME + "mybatis_cache_" + id;
    }

}
MybatisRedisCache

  自定義緩存實現的要求很低, 只需要 實現 org.apache.ibatis.cache.Cache 就可以了. 這里我們用到了 RedisUtils 工具類, 還請同學們 對應到自己的項目中的工具類.

  至此: 我們的Mybatis二級緩存就支持分布式啦!

 

問題來臨:

  今天我偶然想起, 是否需要給二級緩存 設置一個過期時間?

  我們來想一下不設置過期時間會有什么問題:

    當有一天我們不得不手動修改數據庫的數據時(別問為啥要動數據庫, 因為我在本地測試需要修改), 如果相應的 namespace 沒有 插入和更新操作, 那么他的緩存將一直有有效, 然后查出來的數據一直是修改前的數據, 而且我們使用的Redis做的緩存, 即使重啟了系統緩存依然還是在, 只能從redis中找到指定緩存並清除, 實在是頭疼

  有沒有辦法解決呢? 

  你可能已經想到了, cache標簽不是支持 flushInterval 屬性的嗎? 設置一個 flushInterval = "10000", 這樣不就行了嗎?

  我開始也是這么認為的, 直到有一天我發現這個設置完全沒有起作用, 緩存一直是有效的, 那問題就來了, 為什么呢?

  百思不得其解的我決定去瞄一瞄Mybatis的源碼, 最終讓我發現了其中的奧秘, 我們來看一下下面的代碼:

  

  這是Mybatis初始化二級緩存中的一段代碼, 我們可以看到, flushInterval 屬性對於自定義實現類是不起作用的, 而 Mybatis 實現的緩存過期時間的原理則是利用 設計模式(裝飾器模式) 在默認的 緩存實現類上封裝了一層 ScheduleCache, 也正是此類實現了緩存的有效期設置.

 

  完了完了, 那不能設置咋辦啊, 這不是坑爹嗎?

  冷靜下來, 先別忙. 既然 不能通過設置解決, 那我們就自己想辦法解決吧

  既然你Mybatis不幫我封裝 ScheduleCache, 那我就自己封裝一個 ScheduleRedisCache, 原有的MybatisRedisCache不變. 我們看一下 ScheduleRedisCache的代碼:

  
package com.cardgame.demo.game.component.db;

import com.cardgame.demo.component.redis.RedisUtils;
import org.apache.ibatis.cache.Cache;

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

import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME;

/**
 *
 * 二級緩存 過期時間 實現
 *
 * @author yjy
 * 2018-08-13 13:21
 */
public class ScheduleRedisCache implements Cache {

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private String id;
    private long flushInterval = 10000; // 緩存刷新時間, 單位毫秒
    private Cache delegate; // 委派緩存類

    public ScheduleRedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("id can not be null");
        }
        this.id = id;
        this.delegate = new MybatisRedisCache(id);
    }

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

    @Override
    public void putObject(Object key, Object value) {
        // 記錄過期時間
        long timeout = System.currentTimeMillis() + flushInterval;
        RedisUtils.getInstance().setHashObj(getKey(), key, timeout);
        delegate.putObject(key, value);
    }

    @Override
    public Object getObject(Object key) {
        Object timeout = RedisUtils.getInstance().getHashObj(getKey(), key);
        // if 未過期
        if (timeout != null && (long) timeout > System.currentTimeMillis()) {
            // 更新過期時間
            RedisUtils.getInstance().setHashObj(getKey(), key, System.currentTimeMillis() + flushInterval);
            return delegate.getObject(key);
        }
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        RedisUtils.getInstance().delHashObj(getKey(), key);
        return delegate.removeObject(key);
    }

    @Override
    public void clear() {
        RedisUtils.getInstance().delObj(getKey());
        delegate.clear();
    }

    @Override
    public int getSize() {
        return (int) RedisUtils.getInstance().getHashSizeObj(getKey());
    }

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

    protected String getKey() {
        return REDIS_KEY_GAME + "schedule_cache_" + id;
    }

}
ScheduleRedisCache.java

  代碼簡單易懂, 說白了就是增加一個 緩存管理, 專門 緩存二級緩存的 過期時間. 在適當的時候 更新過期時間 / 清除過期時間

  需要注意的是, 既然我們封裝了 MybatisRedisCache, 那么 mapper.xml 中就需要 改一改了, 如下: 

  

  

  這樣, 我們就實現了 二級緩存的超時自動過期功能了!!! 

 

結論: 謝謝大家!666

  

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM