mybatis緩存,從一個“靈異”事件說起


剛准備下班走人,被一開發同事叫住,讓幫看一個比較奇怪的問題:Mybatis同一個Mapper接口的查詢方法,第一次返回與第二次返回結果不一樣,百思不得其解!

問題

Talk is cheap. Show me the code. 該問題涉及的主要代碼實現包括

1.mapper接口定義

public interface GoodsTrackMapper extends BaseMapper<GoodsTrack> {
    List<GoodsTrackDTO> listGoodsTrack(@Param("criteria") GoodsTrackQueryCriteria criteria);
}

2.xml定義

<select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO">
    SELECT ...
</select>

3.service定義

@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class GoodsTrackService extends BaseService<GoodsTrack, GoodsTrackDTO> {
     @Autowired
    private GoodsTrackMapper goodsTrackMapper;

    public List<GoodsTrackDTO> listGoodsTrack(GoodsTrackQueryCriteria criteria){
         return goodsTrackMapper.listGoodsTrack(criteria);
    }


    public List<GoodsTrackDTO> goodsTrackList(GoodsTrackQueryCriteria criteria){
        List<GoodsTrackDTO> listGoodsTrack = goodsTrackMapper.listGoodsTrack(criteria);
        Map<String, GoodsTrackDTO> goodsTrackDTOMap = new HashMap<String, GoodsTrackDTO>();
        for (GoodsTrackDTO goodsTrackDTO : listGoodsTrack){
            String goodsId = String.valueOf(goodsTrackDTO.getGoodsId());
            if (!goodsTrackDTOMap.containsKey(goodsId)){
                goodsTrackDTOMap.put(goodsId, goodsTrackDTO);
            }else {
                GoodsTrackDTO goodsTrack = goodsTrackDTOMap.get(goodsId);
                int num = goodsTrack.getGoodsNum() + goodsTrackDTO.getGoodsNum();
                goodsTrack.setGoodsNum(num);
            }
        }
        List<GoodsTrackDTO>  list = new ArrayList(goodsTrackDTOMap.values());
        return list;
    }
}

@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class GoodsOrderService extends BaseService<GoodsOrder, GoodsOrderDTO> {
    @Autowired
    private GoodsTrackService goodsTrackService;

    @Override
    public GoodsOrderDTO create(GoodsOrderDTO goodsOrderDTO) {
        //...
        List<GoodsTrackDTO> rs1 = goodsTrackList(criteria);
        //...
        List<GoodsTrackDTO> rs2 = listGoodsTrack(criteria);
        //...
    }
}

大致邏輯就是在 GoodsTrackService 定義了兩個查詢方法,一個是直接從數據庫中獲取數據,第二個是從數據庫中獲取數據后進行了一些加工(通過某個字段進行合並累加,類似sum group by),然后在GoodsOrderService 的同一個方法(該方法是一個事務方法 )中調用這兩個查詢,發現rs2中的數據存在問題, 期望是都應該與數據庫表的數據一致,但其中部分數據卻與查出后進行了修改的rs1中的一致。

定位

初步看,listGoodsTrack 方法直接調用的mapper方法 goodsTrackMapper.listGoodsTrack(criteria) 沒做任何應用層的處理,第一反應是緩存的原因。 我問前面的查詢有沒有改變查詢返回的結果(一開始沒細看具體實現),答曰沒有。折騰一陣后,返過去細看 goodsTrackList 的實現,果然還是眼見為實、耳聽為虛。在該方法中,通過goodsId對返回的列表進行分組,對goodsNum進行累加,最后返回累加后的幾個對象。但是在累加的時候,是直接作用於返回結果對象的,明明就是改變了查詢結果(居然說沒有?!!)。 這就是問題所在了,mybatis在同一個事務中,對同一個查詢(同樣的sql,同樣的參數)的返回結果進行了緩存(稱為一級緩存),下一次做同樣的查詢時,如果中間沒有任何更新操作,則直接返回緩存的數據,而在本例中因為對緩存數據做了人為的修改,所以最后導致查出的數據與數據庫不一致。

mybatis緩存機制

簡單介紹下mybatis的兩級緩存機制

  • 一級緩存:一級緩存包括SqlSession與STATEMENT兩種級別,默認在 SqlSession 中實現。在一次會話中,如果兩次查詢sql相同,參數相同,且中間沒有任何更新操作,則第二次查詢會直接返回第一次查詢緩存的結果,不再請求數據庫。如果中間存在更新操作,則更新操作會清除掉緩存,后面的查詢就會訪問數據庫了。STATEMENT級別則每次查詢都會清掉一級緩存,每次查詢都會進行數據庫訪問。

  • 二級緩存:二級緩存則是在同一個namesapce的多個 SqlSession 間共享的緩存,默認未開啟。當開啟二級緩存后,數據查詢的流程就是 二級緩存 ——> 一級緩存 ——> 數據庫, 同一個namespace下的更新操作,會影響同一個Cache。

如何開啟二級緩存

1.需要在mybatis-config.xml中設置:

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

2.然后在mapper的xml文件的<mapper>下設置cache相關配置:

<cache 
	eviction="LRU"  
	flushInterval="60000" 
	size="512" 
	readOnly="true"/> 

支持的屬性:

  • type:cache使用的類型,默認是PerpetualCache
  • eviction: 回收的策略,常見的有LRU,FIFO
  • flushInterval: 配置一定時間自動刷新緩存,單位毫秒
  • size: 最多緩存的對象個數
  • readOnly: 是否只讀,若配置為可讀寫,則需要對應的實體類實現Serializable接口
  • blocking: 如果緩存中找不到對應的key,是否會一直blocking,直到有對應的數據進入緩存

也可以使用 <cache-ref namespace="mapper.UserMapper"/> 來與另一個mapper共享二級緩存

解決

已經定位到是由於mybatis的一級緩存導致,那如何解決本文提到的問題呢? 基本上有三個解決方向。

1.使用緩存的方案

既然要使用緩存,那就不能更改緩存的數據,此時我們可以在需要更改數據的地方把數據做一次副本拷貝,使其不改變緩存數據本身, 如

for (GoodsTrackDTO goodsTrackDTO : listGoodsTrack){
    String goodsId = String.valueOf(goodsTrackDTO.getGoodsId());
    if (!goodsTrackDTOMap.containsKey(goodsId)){
        goodsTrackDTOMap.put(goodsId, ObjectUtil.clone(goodsTrackDTO));
    }else {
        GoodsTrackDTO goodsTrack = goodsTrackDTOMap.get(goodsId);
        int num = goodsTrack.getGoodsNum() + goodsTrackDTO.getGoodsNum();
        goodsTrack.setGoodsNum(num);
    }
}

使用ObjectUtil.clone()方法(hutool工具包中提供)對需要更改的數據做副本拷貝。

2.禁用緩存的方案

在xml的sql定義中添加 flushCache="true" 的配置,使該查詢不使用緩存,如下

<select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO" flushCache="true"> 
    SELECT ...
</select>

禁用緩存的另一種方案是將一級緩存直接設置為STATEMENT來進行全局禁用,在mybatis-config.xml中配置:

<settings>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

3.避開緩存的方案

再定義一個實現相同查詢的mapper方法,id不一樣來避開使用相同的緩存,這種做法就不怎么優雅了。

<select id="listGoodsTrack2" resultType="xxx.GoodsTrackDTO" flushCache="true"> 
    SELECT ...
</select>

避開緩存的另一種做法是不使用事務,使兩個查詢不在一個SqlSession中,但有時候事務是必須的,所以得分場景來。

另外由於mybatis的緩存都是基於本地的,在分布式環境下可能導致讀取的數據與數據庫不一致,比如一個服務實例兩次讀取中間,另一個服務實例對數據進行了更新,則后一次讀取由於緩存還是讀取的舊數據,而不是更新后的數據,可能導致問題。這時可以通過將緩存設置為STATEMENT級別來禁用mybatis緩存,通過Redis,MemCached等來提供分布式的全局緩存。

認真生活,快樂分享
歡迎關注微信公眾號:空山新雨的技術空間
公眾號二維碼
獲取更多關於Spring Boot,Spring Cloud, Docker等企業實戰技術


免責聲明!

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



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