歡迎訪問我的網站http://www.wenzhihuai.com/ 。感謝,如果可以,希望能在GitHub上給個star,GitHub地址https://github.com/Zephery/newblog 。
一、概述
1.1 緩存介紹
系統的性能指標一般包括響應時間、延遲時間、吞吐量,並發用戶數和資源利用率等。在應用運行過程中,我們有可能在一次數據庫會話中,執行多次查詢條件完全相同的SQL,MyBatis提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提高性能。
緩存常用語:
數據不一致性、緩存更新機制、緩存可用性、緩存服務降級、緩存預熱、緩存穿透
可查看Redis實戰(一) 使用緩存合理性
1.2 本站緩存架構
從沒有使用緩存,到使用mybatis緩存,然后使用了ehcache,再然后是mybatis+redis緩存。
二、Mybatis緩存
2.1 mybatis一級緩存
Mybatis的一級緩存是指Session回話級別的緩存,也稱作本地緩存。一級緩存的作用域是一個SqlSession。Mybatis默認開啟一級緩存。在同一個SqlSession中,執行相同的查詢SQL,第一次會去查詢數據庫,並寫到緩存中;第二次直接從緩存中取。當執行SQL時兩次查詢中間發生了增刪改操作,則SqlSession的緩存清空。Mybatis 默認支持一級緩存,不需要在配置文件中配置。
我們來查看一下源碼的類圖,具體的源碼分析簡單概括一下:SqlSession實際上是使用PerpetualCache來維護的,PerpetualCache中定義了一個HashMap<k,v>來進行緩存。
(1)當會話開始時,會創建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象,Executor對象中持有一個新的PerpetualCache對象;
(2)對於某個查詢,根據statementId,params,rowBounds來構建一個key值,根據這個key值去緩存Cache中取出對應的key值存儲的緩存結果。如果命中,則返回結果,如果沒有命中,則去數據庫中查詢,再將結果存儲到cache中,最后返回結果。如果執行增刪改,則執行flushCacheIfRequired方法刷新緩存。
(3)當會話結束時,SqlSession對象及其內部的Executor對象還有PerpetualCache對象也一並釋放掉。
2.2 mybatis二級緩存
Mybatis的二級緩存是指mapper映射文件,為Application應用級別的緩存,生命周期長。二級緩存的作用域是同一個namespace下的mapper映射文件內容,多個SqlSession共享。Mybatis需要手動設置啟動二級緩存。在同一個namespace下的mapper文件中,執行相同的查詢SQL。實現二級緩存,關鍵是要對Executor對象做文章,Mybatis給Executor對象加上了一個CachingExecutor,使用了設計模式中的裝飾者模式,
2.2.1 MyBatis二級緩存的划分
MyBatis並不是簡單地對整個Application就只有一個Cache緩存對象,它將緩存划分的更細,即是Mapper級別的,即每一個Mapper都可以擁有一個Cache對象,具體如下:
a.為每一個Mapper分配一個Cache緩存對象(使用
b.多個Mapper共用一個Cache緩存對象(使用
2.2.2 二級緩存的開啟
在mybatis的配置文件中添加:
<settings>
<!--開啟二級緩存-->
<setting name="cacheEnabled" value="true"/>
</settings>
然后再需要開啟二級緩存的mapper.xml中添加(本站使用了LRU算法,時間為120000毫秒):
<cache eviction="LRU"
type="org.apache.ibatis.cache.impl.PerpetualCache"
flushInterval="120000"
size="1024"
readOnly="true"/>
2.2.3 使用第三方支持的二級緩存的實現
MyBatis對二級緩存的設計非常靈活,它自己內部實現了一系列的Cache緩存實現類,並提供了各種緩存刷新策略如LRU,FIFO等等;另外,MyBatis還允許用戶自定義Cache接口實現,用戶是需要實現org.apache.ibatis.cache.Cache接口,然后將Cache實現類配置在
- MyBatis自身提供的緩存實現;
- 用戶自定義的Cache接口實現;
- 跟第三方內存緩存庫的集成;
具體的實現,可參照:SpringMVC + MyBatis + Mysql + Redis(作為二級緩存) 配置
MyBatis中一級緩存和二級緩存的組織如下圖所示(圖片來自深入理解mybatis原理):
2.3 Mybatis在分布式環境下臟讀問題
(1)如果是一級緩存,在多個SqlSession或者分布式的環境下,數據庫的寫操作會引起臟數據,多數情況可以通過設置緩存級別為Statement來解決。
(2)如果是二級緩存,雖然粒度比一級緩存更細,但是在進行多表查詢時,依舊可能會出現臟數據。
(3)Mybatis的緩存默認是本地的,分布式環境下出現臟讀問題是不可避免的,雖然可以通過實現Mybatis的Cache接口,但還不如直接使用集中式緩存如Redis、Memcached好。
下面將介紹使用Redis集中式緩存在個人網站的應用。
三、Redis緩存
Redis運行於獨立的進程,通過網絡協議和應用交互,將數據保存在內存中,並提供多種手段持久化內存的數據。同時具備服務器的水平拆分、復制等分布式特性,使得其成為緩存服務器的主流。為了與Spring更好的結合使用,我們使用的是Spring-Data-Redis。此處省略安裝過程和Redis的命令講解。
3.1 Spring Cache
Spring 3.1 引入了激動人心的基於注釋(annotation)的緩存(cache)技術,它本質上不是一個具體的緩存實現方案(例如EHCache 或者 OSCache),而是一個對緩存使用的抽象,通過在既有代碼中添加少量它定義的各種 annotation,即能夠達到緩存方法的返回對象的效果。Spring 的緩存技術還具備相當的靈活性,不僅能夠使用 SpEL(Spring Expression Language)來定義緩存的 key 和各種 condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存例如 EHCache 集成。
下面是Spring Cache常用的注解:
(1)@Cacheable
@Cacheable 的作用 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存
屬性 | 介紹 | 例子 |
---|---|---|
value | 緩存的名稱,必選 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 緩存的key,可選,需要按照SpEL表達式填寫 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 緩存的條件,可以為空,使用 SpEL 編寫,只有為 true 才進行緩存 | @Cacheable(value=”testcache”,key=”#userName”) |
(2)@CachePut
@CachePut 的作用 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存,和 @Cacheable 不同的是,它每次都會觸發真實方法的調用
屬性 | 介紹 | 例子 |
---|---|---|
value | 緩存的名稱,必選 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 緩存的key,可選,需要按照SpEL表達式填寫 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 緩存的條件,可以為空,使用 SpEL 編寫,只有為 true 才進行緩存 | @Cacheable(value=”testcache”,key=”#userName”) |
(3)@CacheEvict
@CachEvict 的作用 主要針對方法配置,能夠根據一定的條件對緩存進行清空
屬性 | 介紹 | 例子 |
---|---|---|
value | 緩存的名稱,必選 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 緩存的key,可選,需要按照SpEL表達式填寫 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 緩存的條件,可以為空,使用 SpEL 編寫,只有為 true 才進行緩存 | @Cacheable(value=”testcache”,key=”#userName”) |
allEntries | 是否清空所有緩存內容,默認為false | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法執行前就清空,缺省為 false | @CachEvict(value=”testcache”,beforeInvocation=true) |
但是有個問題:
Spring官方認為:緩存過期時間由各個產商決定,所以並不提供緩存過期時間的注解。所以,如果想實現各個元素過期時間不同,就需要自己重寫一下Spring cache。
3.2 引入包
一般是Spring常用的包+Spring data redis的包,記得注意去掉所有沖突的包,之前才過坑,Spring-data-MongoDB已經有SpEL的庫了,和自己新引進去的沖突,搞得我以為自己是配置配錯了,真是個坑,注意,開發過程中一定要去除掉所有沖突的包!!!
3.3 ApplicationContext.xml
需要啟用緩存的注解開關,並配置好Redis。序列化方式也要帶上,否則會碰到幽靈bug。
<!-- 啟用緩存注解開關,此處可自定義keyGenerator -->
<cache:annotation-driven/>
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${host}"/>
<property name="port" value="${port}"/>
<property name="password" value="${password}"/>
<property name="database" value="${redis.default.db}"/>
<property name="timeout" value="${timeout}"/>
<property name="poolConfig" ref="jedisPoolConfig"/>
<property name="usePool" value="true"/>
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
<!-- 序列化方式 建議key/hashKey采用StringRedisSerializer。 -->
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
<property name="hashValueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
</bean>
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg name="redisOperations" ref="redisTemplate" />
<!--統一過期時間-->
<property name="defaultExpiration" value="${redis.defaultExpiration}"/>
</bean>
3.5 自定義KeyGenerator
在分布式系統中,很容易存在不同類相同名字的方法,如A.getAll(),B.getAll(),默認的key(getAll)都是一樣的,會很容易產生問題,所以,需要自定義key來實現分布式環境下的不同。
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object o, Method method, Object... objects) {
StringBuilder sb = new StringBuilder();
sb.append(o.getClass().getName());
sb.append(".");
sb.append(method.getName());
for (Object obj : objects) {
sb.append(obj.toString());
}
return sb.toString();
}
}
之后,存儲的key就變為:com.myblog.service.impl.BlogServiceImpl.getBanner。
3.4 添加注解
在所需要的方法上添加注解,比如,首頁中的那幾張幻燈片,每次進入首頁都需要查詢數據庫,這里,我們直接放入緩存里,減少數據庫的壓力,還有就是那些熱門文章,訪問量比較大的,也放進數據庫里。
@Override
@Cacheable(value = "getBanner", keyGenerator = "customKeyGenerator")
public List<Blog> getBanner() {
return blogMapper.getBanner();
}
@Override
@Cacheable(value = "getBlogDetail", key = "'blogid'.concat(#blogid)")
public Blog getBlogDetail(Integer blogid) {
Blog blog = blogMapper.selectByPrimaryKey(blogid);
if (blog == null) {
return null;
}
Category category = categoryMapper.selectByPrimaryKey(blog.getCategoryid());
blog.setCategory(category);
List<Tag> tags = tagMapper.getTagByBlogId(blog.getBlogid());
blog.setTags(tags.size() > 0 ? tags : null);
asyncService.updatebloghits(blogid);//異步更新閱讀次數
logger.info("沒有走緩存");
return blog;
}
3.5 測試
我們調用一個getBlogDetail(獲取博客詳情)100次來對比一下時間。連接的數據庫在深圳,本人在廣州,還是有那么一丟丟的網路延時的。
public class SpringTest {
@Test
public void init() {
ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:spring-test.xml");
IBlogService blogService = (IBlogService) ctx.getBean("blogService");
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
blogService.getBlogDetail(615);
}
System.out.println(System.currentTimeMillis() - startTime);
}
}
為了做一下對比,我們同時使用mybatis自身緩存來進行測試。
3.6 實驗結果
統計出結果如下:
沒有使用任何緩存(mybatis一級緩存沒有關閉):18305
使用遠程Redis緩存:12727
使用Mybatis緩存:6649
使用本地Redis緩存:5818
由結果看出,緩存的使用大大較少了獲取數據的時間。
部署進個人博客之后,redis已經緩存的數據:
3.7 分頁的數據怎么辦
個人網站中共有兩個欄目,一個是技術雜談,另一個是生活筆記,每點擊一次欄目的時候,會根據頁數從數據庫中查詢數據,百度了下,大概有三種方法:
(1)以頁碼作為Key,然后緩存整個頁面。
(2)分條存取,只從數據庫中獲取分頁的文章ID序列,然后從service(緩存策略在service中實現)中獲取。
第一種,由於使用了第三方的插件PageHelper,分頁獲取的話會比較麻煩,同時整頁緩存對內存壓力也蠻大的,畢竟服務器只有2g。第二條實現方式簡單,缺陷是依舊需要查詢數據庫,想了想還是放棄了。緩存的初衷是對請求頻繁又不易變的數據,實際使用中很少會反復的請求同一頁的數據(查詢條件也相同),當然對數據中某些字段做緩存還是有必要的。
四、如何解決臟讀?
對於文章來說,內容是不經常更新的,沒有涉及到緩存一致性,但是對於文章的閱讀量,用戶每點擊一次,就應該更新瀏覽量的。對於文章的緩存,常規的設計是將文章存儲進數據庫中,然后讀取的時候放入緩存中,然后將瀏覽量以文章ID+瀏覽量的結構實時的存入redis服務器中。本站當初設計不合理,直接將瀏覽量作為一個字段,用戶每點擊一次的時候就異步更新瀏覽量,但是此處沒有更新緩存,如果手動更新緩存的話,基本上每點擊一次都得執行更新操作,同樣也不合理。所以,目前本站,你們在頁面上看到的瀏覽量和數據庫中的瀏覽量並不是一致的。有興趣的可以點擊我的網站玩玩~~
五、題外話
兄弟姐妹們啊,個人網站只是個小項目,純屬為了學習而用的,文章可以看看,但是,就不要抓取了吧。。。。一個小時抓取6萬次寶寶心臟真的受不了,雖然服務器一切都還穩定==
個人網站:http://www.wenzhihuai.com
個人網站源碼,希望能給個star:https://github.com/Zephery/newblog
參考:
1.《深入理解mybatis原理》 MyBatis的一級緩存實現詳解
2.《深入理解mybatis原理》 MyBatis的二級緩存的設計原理
3.聊聊Mybatis緩存機制
4.Spring思維導圖
5.SpringMVC + MyBatis + Mysql + Redis(作為二級緩存) 配置
6.《深入分布式緩存:從原理到實踐》