剛準(zhǔn)備下班走人,被一開(kāi)發(fā)同事叫住,讓幫看一個(gè)比較奇怪的問(wèn)題:Mybatis同一個(gè)Mapper接口的查詢(xún)方法,第一次返回與第二次返回結(jié)果不一樣,百思不得其解!
問(wèn)題
Talk is cheap. Show me the code. 該問(wèn)題涉及的主要代碼實(shí)現(xiàn)包括
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
定義了兩個(gè)查詢(xún)方法,一個(gè)是直接從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù),第二個(gè)是從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)后進(jìn)行了一些加工(通過(guò)某個(gè)字段進(jìn)行合并累加,類(lèi)似sum group by),然后在
GoodsOrderService 的同一個(gè)方法(該方法是一個(gè)事務(wù)方法 )中調(diào)用這兩個(gè)查詢(xún),發(fā)現(xiàn)rs2中的數(shù)據(jù)存在問(wèn)題,
期望是都應(yīng)該與數(shù)據(jù)庫(kù)表的數(shù)據(jù)一致,但其中部分?jǐn)?shù)據(jù)卻與查出后進(jìn)行了修改的rs1中的一致。
定位
初步看,listGoodsTrack 方法直接調(diào)用的mapper方法 goodsTrackMapper.listGoodsTrack(criteria)
沒(méi)做任何應(yīng)用層的處理,第一反應(yīng)是緩存的原因。 我問(wèn)前面的查詢(xún)有沒(méi)有改變查詢(xún)返回的結(jié)果(一開(kāi)始沒(méi)細(xì)看具體實(shí)現(xiàn)),答曰沒(méi)有。折騰一陣后,返過(guò)去細(xì)看
goodsTrackList
的實(shí)現(xiàn),果然還是眼見(jiàn)為實(shí)、耳聽(tīng)為虛。在該方法中,通過(guò)goodsId對(duì)返回的列表進(jìn)行分組,對(duì)goodsNum進(jìn)行累加,最后返回累加后的幾個(gè)對(duì)象。但是在累加的時(shí)候,是直接作用于返回結(jié)果對(duì)象的,明明就是改變了查詢(xún)結(jié)果(居然說(shuō)沒(méi)有??。。?。
這就是問(wèn)題所在了,mybatis在同一個(gè)事務(wù)中,對(duì)同一個(gè)查詢(xún)(同樣的sql,同樣的參數(shù))的返回結(jié)果進(jìn)行了緩存(稱(chēng)為一級(jí)緩存),下一次做同樣的查詢(xún)時(shí),如果中間沒(méi)有任何更新操作,則直接返回緩存的數(shù)據(jù),而在本例中因?yàn)閷?duì)緩存數(shù)據(jù)做了人為的修改,所以最后導(dǎo)致查出的數(shù)據(jù)與數(shù)據(jù)庫(kù)不一致。
mybatis緩存機(jī)制
簡(jiǎn)單介紹下mybatis的兩級(jí)緩存機(jī)制
*
一級(jí)緩存:一級(jí)緩存包括SqlSession與STATEMENT兩種級(jí)別,默認(rèn)在 SqlSession
中實(shí)現(xiàn)。在一次會(huì)話(huà)中,如果兩次查詢(xún)sql相同,參數(shù)相同,且中間沒(méi)有任何更新操作,則第二次查詢(xún)會(huì)直接返回第一次查詢(xún)緩存的結(jié)果,不再請(qǐng)求數(shù)據(jù)庫(kù)。如果中間存在更新操作,則更新操作會(huì)清除掉緩存,后面的查詢(xún)就會(huì)訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)了。STATEMENT級(jí)別則每次查詢(xún)都會(huì)清掉一級(jí)緩存,每次查詢(xún)都會(huì)進(jìn)行數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)。
*
二級(jí)緩存:二級(jí)緩存則是在同一個(gè)namesapce的多個(gè) SqlSession 間共享的緩存,默認(rèn)未開(kāi)啟。當(dāng)開(kāi)啟二級(jí)緩存后,數(shù)據(jù)查詢(xún)的流程就是 二級(jí)緩存
——> 一級(jí)緩存 ——> 數(shù)據(jù)庫(kù), 同一個(gè)namespace下的更新操作,會(huì)影響同一個(gè)Cache。
如何開(kāi)啟二級(jí)緩存
1.需要在mybatis-config.xml中設(shè)置:
<settings> <setting name="cacheEnabled" value="true"/> </settings>
2.然后在mapper的xml文件的<mapper>下設(shè)置cache相關(guān)配置:
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
支持的屬性:
* type:cache使用的類(lèi)型,默認(rèn)是PerpetualCache
* eviction: 回收的策略,常見(jiàn)的有LRU,F(xiàn)IFO
* flushInterval: 配置一定時(shí)間自動(dòng)刷新緩存,單位毫秒
* size: 最多緩存的對(duì)象個(gè)數(shù)
* readOnly: 是否只讀,若配置為可讀寫(xiě),則需要對(duì)應(yīng)的實(shí)體類(lèi)實(shí)現(xiàn)Serializable接口
* blocking: 如果緩存中找不到對(duì)應(yīng)的key,是否會(huì)一直blocking,直到有對(duì)應(yīng)的數(shù)據(jù)進(jìn)入緩存
也可以使用 <cache-ref namespace="mapper.UserMapper"/> 來(lái)與另一個(gè)mapper共享二級(jí)緩存
解決
已經(jīng)定位到是由于mybatis的一級(jí)緩存導(dǎo)致,那如何解決本文提到的問(wèn)題呢? 基本上有三個(gè)解決方向。
1.使用緩存的方案
既然要使用緩存,那就不能更改緩存的數(shù)據(jù),此時(shí)我們可以在需要更改數(shù)據(jù)的地方把數(shù)據(jù)做一次副本拷貝,使其不改變緩存數(shù)據(jù)本身, 如
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工具包中提供)對(duì)需要更改的數(shù)據(jù)做副本拷貝。
2.禁用緩存的方案
在xml的sql定義中添加 flushCache="true" 的配置,使該查詢(xún)不使用緩存,如下
<select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO" flushCache="true">
SELECT ... </select>
禁用緩存的另一種方案是將一級(jí)緩存直接設(shè)置為STATEMENT來(lái)進(jìn)行全局禁用,在mybatis-config.xml中配置:
<settings> <setting name="localCacheScope" value="STATEMENT"/> </settings>
3.避開(kāi)緩存的方案
再定義一個(gè)實(shí)現(xiàn)相同查詢(xún)的mapper方法,id不一樣來(lái)避開(kāi)使用相同的緩存,這種做法就不怎么優(yōu)雅了。
<select id="listGoodsTrack2" resultType="xxx.GoodsTrackDTO" flushCache="true">
SELECT ... </select>
避開(kāi)緩存的另一種做法是不使用事務(wù),使兩個(gè)查詢(xún)不在一個(gè)SqlSession中,但有時(shí)候事務(wù)是必須的,所以得分場(chǎng)景來(lái)。
另外由于mybatis的緩存都是基于本地的,在分布式環(huán)境下可能導(dǎo)致讀取的數(shù)據(jù)與數(shù)據(jù)庫(kù)不一致,比如一個(gè)服務(wù)實(shí)例兩次讀取中間,另一個(gè)服務(wù)實(shí)例對(duì)數(shù)據(jù)進(jìn)行了更新,則后一次讀取由于緩存還是讀取的舊數(shù)據(jù),而不是更新后的數(shù)據(jù),可能導(dǎo)致問(wèn)題。這時(shí)可以通過(guò)將緩存設(shè)置為STATEMENT級(jí)別來(lái)禁用mybatis緩存,通過(guò)Redis,MemCached等來(lái)提供分布式的全局緩存。
認(rèn)真生活,快樂(lè)分享
歡迎關(guān)注微信公眾號(hào):空山新雨的技術(shù)空間
獲取更多關(guān)于Spring Boot,Spring Cloud, Docker等企業(yè)實(shí)戰(zhàn)技術(shù)
熱門(mén)工具 換一換
