Spring?data?jpa緩存機制使用總結(jié)
Spring data jpa緩存機制
Spring data jpa 的使用讓我們操作數(shù)據(jù)庫變得非常簡單,開發(fā)人員只需要編寫repository接口,Spring將自動提供實現(xiàn),尤其是基礎(chǔ)的的CURD 操作,為我們封裝好的同時也做了一些性能上的優(yōu)化。
但也正因為如此,這些基礎(chǔ)的操作的背后并不是那么簡單,稍有不慎就會得到我們意料之外的結(jié)果,接下來列舉一些工作中遇到的問題。
一、案例
項目中遇到過這樣一個問題,repository繼承了CrudRepository接口,直接使用save(S entity) 方法進(jìn)行數(shù)據(jù)保存,但是因為某個字段的唯一約束沖突了,導(dǎo)致保存失敗并拋出了異常,但是save方法后的代碼邏輯卻執(zhí)行了,將數(shù)據(jù)保存到redis,這導(dǎo)致了數(shù)據(jù)庫和redis數(shù)據(jù)不一致。
代碼代碼大概是這樣子:
@Override
@Transactional
public void save(SomeThingVo vo){
SomeThingEntity entity = new SomeThingEntity();
BeanUtils.copyProperties(vo,entity);
//保存至數(shù)據(jù)庫
someThingRepository.save(entity);
//緩存
cacheSomeThing(entity);
//做一些其他事
doSomeThingElse();
}然后對這個操作進(jìn)行了debug,發(fā)現(xiàn)到save方法結(jié)束,是沒有拋出異常的,然后繼續(xù)進(jìn)行保存redis等操作,直到方法結(jié)束才拋出了異常。
這時注意到了@Transactional注解加在了這個方法之上,那就是事務(wù)提交時才會報出 唯一約束沖突的異常,再聯(lián)想到Spring data Jpa的是用Hibernate實現(xiàn)的 , Hibernate是有緩存機制的,猜想不使用jpa自帶的save方法,就可以在保存時直接拋異常,而不執(zhí)行之后的代碼,然后進(jìn)行嘗試,的確如此;還有一種解決方式是使用saveAndFlush方法,立馬將緩存中的實體bean刷入數(shù)據(jù)庫。
二、分析
Hibernate緩存包括兩大類:一級緩存和二級緩存。
一級緩存又稱為“Session的緩存”,它是內(nèi)置的,不能被卸載(不能被卸載的意思就是這種緩存不具有可選性,必須有的功能,不可以取消session緩存)。由于Session對象的生命周期通常對應(yīng)一個數(shù)據(jù)庫事務(wù)或者一個應(yīng)用事務(wù),因此它的緩存是事務(wù)范圍的緩存在第一級緩存中,持久化類的每個實例都具有唯一的OID。我們使用@Transactional 注解時,JpaTransactionManager會在開啟事務(wù)前打開一個session,將事務(wù)綁定在這個session上,事務(wù)結(jié)束session關(guān)閉,所以后續(xù)內(nèi)容將以粗略以事務(wù)作為一級緩存的生存時段。
二級緩存又稱為“SessionFactory的緩存”,由于SessionFactory對象的生命周期和應(yīng)用程序的整個過程對應(yīng),因此二級緩存是進(jìn)程范圍或者集群范圍的緩存,有可能出現(xiàn)并發(fā)問題,因此需要采用適當(dāng)?shù)牟l(fā)訪問策略。第二級緩存是可選的,是一個可配置的插件,在默認(rèn)情況下,SessionFactory不會啟用這個插件,二級緩存應(yīng)用場景局限性比較大,適用于數(shù)據(jù)要求的實時性和準(zhǔn)確性不高、變動很少的情況,此次我們僅針對一級緩存進(jìn)行詳細(xì)說明。
我們使用CrudRepository.save() 方法保存或更新對象的流程如下

從上圖可以看出每次save方法執(zhí)行時都會用主鍵向數(shù)據(jù)庫發(fā)起一次查詢,來判斷是更新還是插入,此時spring data jpa 不會立馬向數(shù)據(jù)庫發(fā)送命令,而是將這條數(shù)據(jù)保存在一級緩存之中,然后返回緩存中實體對象,接下來繼續(xù)執(zhí)行后續(xù)的代碼。
如果想更新這條數(shù)據(jù)的值,可以直接修改這個實體對象,jpa會在事前提交之前的某個點(具體后面會說明)自動將這些變更的數(shù)據(jù)保存至數(shù)據(jù)庫,并且在事務(wù)期間查詢這條數(shù)據(jù)都是優(yōu)先從緩存中獲取數(shù)據(jù)。
一級緩存的作用還是很明顯的,在整個事務(wù)中,在對同一條數(shù)據(jù)進(jìn)行了保存更新查詢操作都會以盡量少地請求數(shù)據(jù)庫的方式進(jìn)行優(yōu)化,降低了網(wǎng)絡(luò)io開銷。
三、聯(lián)想
有利就有弊,就像第一部分描述的,因為延遲提交 ,數(shù)據(jù)的正確性驗證(數(shù)據(jù)庫限制方面,比如約束)并沒有立馬執(zhí)行,有時候完全是我們不能承受的,我們想要的效果并不是這樣。
接下來設(shè)想一下其他場景:
1、何時會將數(shù)據(jù)提交至數(shù)據(jù)庫?
實際上這中情況是不存在的。
測試代碼和結(jié)果如下:
@Transactional(rollbackFor = {Exception.class})
public SomeThingEntity save(SomeThingVo vo) {
SomeThingEntity entity = new SomeThingEntity();
BeanUtils.copyProperties(vo,entity);
SomeThingEntity someThingEntity = someThingRepository.save(entity);
log.info("保存方法結(jié)束");
String code = "GOODS_" + someThingEntity.getCode() ;
someThingEntity.setCode(code);
log.info("開始查找");
SomeThingEntity searchThing = someThingRepository.searchByCode(code);
log.info("查找結(jié)果:{}" , searchThing);
SomeThingEntity getThing = someThingRepository.getOne(someThingEntity.getId());
log.info("執(zhí)行了一次JPA查詢\n\r" +
"someThingEntity == getThing : {}\n\r" +
"searchThing == getThing :{}" , someThingEntity == getThing , searchThing == getThing );
return someThingEntity;
}
打印日志:
1 Hibernate: select somethinge0_.id as id1_3_0_, somethinge0_.code as code2_3_0_, somethinge0_.description as descript3_3_0_, somethinge0_.price as price4_3_0_ from tb_something somethinge0_ where somethinge0_.id=?
2 保存方法結(jié)束
3 開始查找
4 Hibernate: insert into tb_something (code, description, price, id) values (?, ?, ?, ?)
5 Hibernate: update tb_something set code=?, description=?, price=? where id=?
6 Hibernate: select somethinge0_.id as id1_3_, somethinge0_.code as code2_3_, somethinge0_.description as descript3_3_, somethinge0_.price as price4_3_ from tb_something somethinge0_ where somethinge0_.code=?
7 查找結(jié)果:SomeThingEntity(id=5, code=GOODS_005, price=100, description=書包)
8 執(zhí)行了一次JPA查詢
9 someThingEntity == getThing : true
10 searchThing == getThing :true
11 Hibernate: update tb_something set code=?, description=?, price=? where id=?
從日志可見:
- save()方法執(zhí)行時只打印了一個查詢sql
- someThingRepository.searchByCode()方法執(zhí)行前各打印了一條插入sql和更新sql
- someThingRepository.searchByCode() 進(jìn)行了查詢
- getOne()并沒有打印sql,直接獲取緩存中的對象
最后比對這些實體都是同一個對象,即緩存中的對象。
將代碼中someThingRepository.searchByCode方法改為其他讀寫語句,嘗試多次,得出以下結(jié)論:
(1)未提交至數(shù)據(jù)庫的操作會在下次請求到數(shù)據(jù)庫時一起提交至數(shù)據(jù)庫執(zhí)行
(2)在事務(wù)提交前存在未提交的數(shù)據(jù),會提交至數(shù)據(jù)庫執(zhí)行
2、實體對象加入緩存后
我們寫sql更新數(shù)據(jù),再用自己的sql獲取這條數(shù)據(jù),得到的是緩存中的數(shù)據(jù)還是更新后的數(shù)據(jù)
這次測試代碼和結(jié)果如下:
@Transactional(rollbackFor = {Exception.class})
public SomeThingEntity save(SomeThingVo vo) {
SomeThingEntity entity = new SomeThingEntity();
BeanUtils.copyProperties(vo,entity);
SomeThingEntity someThingEntity = someThingRepository.save(entity);
log.info("開始更新");
Integer fenPrice = entity.getPrice() * 100;
someThingRepository.updatePriceByCode(someThingEntity.getCode(),fenPrice);
//Session session = (Session) entityManger.getDelegate();
//session.clear();
SomeThingEntity searchThing = someThingRepository.searchByCode(someThingEntity.getCode());
log.info("searchThing = {}",searchThing);
log.info("searchThing == someThingEntity {}",searchThing == someThingEntity);
//someThingEntity.setDescription("");
return someThingEntity;
}傳入?yún)?shù):{id=20,code='GOODS_020",price=100,description="書包"}
打印日志:
1Hibernate: select somethinge0_.id as id1_3_0_, somethinge0_.code as code2_3_0_, somethinge0_.description as descript3_3_0_, somethinge0_.price as price4_3_0_ from tb_something somethinge0_ where somethinge0_.id=?
2 開始更新
3 Hibernate: insert into tb_something (code, description, price, id) values (?, ?, ?, ?)
4 Hibernate: update tb_something set price=? where code=?
5 Hibernate: select * from tb_something where code = ?
6 searchThing = SomeThingEntity(id=20, code=GOODS_020, price=100, description=書包)
7 searchThing == someThingEntity true
數(shù)據(jù)庫結(jié)果:{id=20,code='GOODS_020",price=10000,description="書包"}
從日志中可見:
someThingRepository.updatePriceByCode(someThingEntity.getCode(),fenPrice) 執(zhí)行打印了相關(guān)更新sql(第4行日志),目的 將price由100 改為10000
我們的查詢方法向數(shù)據(jù)庫發(fā)起了查詢;
打印的結(jié)果不是我們更新后的結(jié)果,price仍然為100;
查詢的結(jié)果對象和緩存中的對象比較,是同一個對象;
測試說明:
執(zhí)行我們的查詢方法后,jpa返回給我們的仍然是緩存中的值,這樣子的話我們在這個事務(wù)中怎么查詢都拿不到我們變更后的值! jpa不會根據(jù)我們的update方法自動刷新緩存,后邊查詢出來的數(shù)據(jù)也不會覆蓋緩存中的數(shù)據(jù)。
那么一些同學(xué)可能會把一個事務(wù)涵蓋內(nèi)容的比較多,在頂層的service就加了@Transactional ,就可能在一些操作上進(jìn)入了這樣的場景,在緩存存在的情況,手動update,后續(xù)有去查詢使用,最終使用了錯誤的數(shù)據(jù)。
如果非要在當(dāng)前事務(wù)中查詢到正確數(shù)據(jù)的話,那就手動清除session中的緩存吧(上述代碼中 10、11行)。
另外,放開上述代碼中的15行,最終保存在數(shù)據(jù)庫的結(jié)果為 {id=20,code='GOODS_020",price=100,description=""} ,price的值會被緩存中的覆蓋。
總結(jié)
Spring data jpa 的這些操作都是簡單常用而又容易忽視的,我們在使用時要考慮一下是否得當(dāng)。
對于這樣的緩存機制我們要做的是 將事務(wù)控制在合適的范圍,將不需要在事務(wù)中執(zhí)行的內(nèi)容就移出去;在需要sql明確執(zhí)行好的情況,就主要避開使用會延遲提交的方法。
規(guī)范的代碼和設(shè)計是質(zhì)量的一個重要保證之一。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot+WebMagic+MyBatis爬蟲框架的使用
本文是對spring boot+WebMagic+MyBatis做了整合,使用WebMagic爬取數(shù)據(jù),然后通過MyBatis持久化爬取的數(shù)據(jù)到mysql數(shù)據(jù)庫。具有一定的參考價值,感興趣的可以了解一下2021-08-08
SpringBoot連接Microsoft SQL Server實現(xiàn)登錄驗證
本文主要介紹了SpringBoot連接Microsoft SQL Server實現(xiàn)登錄驗證,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-05-05
java去除集合中重復(fù)元素示例分享 java去除重復(fù)
這篇文章主要介紹了java去除集合中重復(fù)元素示例,大家參考使用吧2014-01-01
Java實現(xiàn)發(fā)起HTTP請求的四種方法實現(xiàn)與適用場景
Java中發(fā)起HTTP請求有多種方式,包括原生庫、第三方庫及現(xiàn)代API,以下是常見的幾種實現(xiàn)方法,本文介紹了它們的具體實現(xiàn)代碼以及性能適用場景,感興趣的可以了解下2025-06-06
Java中JUC包(java.util.concurrent)下的常用子類
相信大家已經(jīng)對并發(fā)機制中出現(xiàn)的很多的常見知識點進(jìn)行了總結(jié),下面這篇文章主要給大家介紹了關(guān)于Java中JUC包(java.util.concurrent)下的常用子類的相關(guān)資料,文中通過圖文以及示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12

