剛準(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ù)

          友情鏈接
          ioDraw流程圖
          API參考文檔
          OK工具箱
          云服務(wù)器優(yōu)惠
          阿里云優(yōu)惠券
          騰訊云優(yōu)惠券
          京東云優(yōu)惠券
          站點(diǎn)信息
          問(wèn)題反饋
          郵箱:[email protected]
          QQ群:637538335
          關(guān)注微信

                顾美玲勾引管家 | 日韩无遮挡无码A片免费看 | 我与俩邻居少妇的性事 | 台湾性生生活1 | AAAA黄色片 |