剛準備下班走人,被一開發同事叫住,讓幫看一個比較奇怪的問題:Mybatis同一個Mapper接口的查詢方法,第一次返回與第二次返回結果不同,百思不得其解!java
Talk is cheap. Show me the code. 該問題涉及的主要代碼實現包括sql
mapper接口定義數據庫
public interface GoodsTrackMapper extends BaseMapper<GoodsTrack> {
List<GoodsTrackDTO> listGoodsTrack(@Param("criteria") GoodsTrackQueryCriteria criteria);
}複製代碼
xml定義緩存
<select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO">
SELECT ...
</select>複製代碼
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中的一致。mybatis
初步看,listGoodsTrack
方法直接調用的mapper方法 goodsTrackMapper.listGoodsTrack(criteria)
沒作任何應用層的處理,第一反應是緩存的緣由。 我問前面的查詢有沒有改變查詢返回的結果(一開始沒細看具體實現),答曰沒有。折騰一陣後,返過去細看 goodsTrackList
的實現,果真仍是眼見爲實、耳聽爲虛。在該方法中,經過goodsId對返回的列表進行分組,對goodsNum進行累加,最後返回累加後的幾個對象。可是在累加的時候,是直接做用於返回結果對象的,明明就是改變了查詢結果(竟然說沒有?!!)。 這就是問題所在了,mybatis在同一個事務中,對同一個查詢(一樣的sql,一樣的參數)的返回結果進行了緩存(稱爲一級緩存),下一次作一樣的查詢時,若是中間沒有任何更新操做,則直接返回緩存的數據,而在本例中由於對緩存數據作了人爲的修改,因此最後致使查出的數據與數據庫不一致。app
簡單介紹下mybatis的兩級緩存機制分佈式
一級緩存:一級緩存包括SqlSession與STATEMENT兩種級別,默認在 SqlSession 中實現。在一次會話中,若是兩次查詢sql相同,參數相同,且中間沒有任何更新操做,則第二次查詢會直接返回第一次查詢緩存的結果,再也不請求數據庫。若是中間存在更新操做,則更新操做會清除掉緩存,後面的查詢就會訪問數據庫了。STATEMENT級別則每次查詢都會清掉一級緩存,每次查詢都會進行數據庫訪問。ide
二級緩存:二級緩存則是在同一個namesapce的多個 SqlSession 間共享的緩存,默認未開啓。當開啓二級緩存後,數據查詢的流程就是 二級緩存 ——> 一級緩存 ——> 數據庫, 同一個namespace下的更新操做,會影響同一個Cache。工具
如何開啓二級緩存
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>複製代碼
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/> 複製代碼
支持的屬性:
也可使用 <cache-ref namespace="mapper.UserMapper"/>
來與另外一個mapper共享二級緩存
已經定位到是因爲mybatis的一級緩存致使,那如何解決本文提到的問題呢? 基本上有三個解決方向。
既然要使用緩存,那就不能更改緩存的數據,此時咱們能夠在須要更改數據的地方把數據作一次副本拷貝,使其不改變緩存數據自己, 如
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工具包中提供)對須要更改的數據作副本拷貝。
在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>複製代碼
再定義一個實現相同查詢的mapper方法,id不同來避開使用相同的緩存,這種作法就不怎麼優雅了。
<select id="listGoodsTrack2" resultType="xxx.GoodsTrackDTO" flushCache="true">
SELECT ...
</select>複製代碼
避開緩存的另外一種作法是不使用事務,使兩個查詢不在一個SqlSession中,但有時候事務是必須的,因此得分場景來。
另外因爲mybatis的緩存都是基於本地的,在分佈式環境下可能致使讀取的數據與數據庫不一致,好比一個服務實例兩次讀取中間,另外一個服務實例對數據進行了更新,則後一次讀取因爲緩存仍是讀取的舊數據,而不是更新後的數據,可能致使問題。這時能夠經過將緩存設置爲STATEMENT級別來禁用mybatis緩存,經過Redis,MemCached等來提供分佈式的全局緩存。
做者:空山新雨,一枚仍在學習路上的IT老兵 近期做者寫了幾十篇技術博客,內容包括Java、Spring Boot、Spring Cloud、Docker,技術管理心得等
歡迎關注做者微信公衆號:空山新雨的技術空間,一塊兒學習成長