談談個人網站的建立(八)—— 緩存的使用


歡迎訪問我的網站http://www.wenzhihuai.com/ 。感謝,如果可以,希望能在GitHub上給個star,GitHub地址https://github.com/Zephery/newblog

一、概述

1.1 緩存介紹

系統的性能指標一般包括響應時間、延遲時間、吞吐量,並發用戶數和資源利用率等。在應用運行過程中,我們有可能在一次數據庫會話中,執行多次查詢條件完全相同的SQL,MyBatis提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提高性能。
緩存常用語:
數據不一致性、緩存更新機制、緩存可用性、緩存服務降級、緩存預熱、緩存穿透
可查看Redis實戰(一) 使用緩存合理性

1.2 本站緩存架構

從沒有使用緩存,到使用mybatis緩存,然后使用了ehcache,再然后是mybatis+redis緩存。

![](http://image.wenzhihuai.com/images/20180121034503.png)
步驟: (1)用戶發送一個請求到nginx,nginx對請求進行分發。 (2)請求進入controller,service,service中查詢緩存,如果命中,則直接返回結果,否則去調用mybatis。 (3)mybatis的緩存調用步驟:二級緩存->一級緩存->直接查詢數據庫。 (4)查詢數據庫的時候,mysql作了主主備份。

二、Mybatis緩存

2.1 mybatis一級緩存

Mybatis的一級緩存是指Session回話級別的緩存,也稱作本地緩存。一級緩存的作用域是一個SqlSession。Mybatis默認開啟一級緩存。在同一個SqlSession中,執行相同的查詢SQL,第一次會去查詢數據庫,並寫到緩存中;第二次直接從緩存中取。當執行SQL時兩次查詢中間發生了增刪改操作,則SqlSession的緩存清空。Mybatis 默認支持一級緩存,不需要在配置文件中配置。

![](http://image.wenzhihuai.com/images/20180120015614.png)

我們來查看一下源碼的類圖,具體的源碼分析簡單概括一下: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對象也一並釋放掉。

![](http://image.wenzhihuai.com/images/20180120022427.png)

2.2 mybatis二級緩存

Mybatis的二級緩存是指mapper映射文件,為Application應用級別的緩存,生命周期長。二級緩存的作用域是同一個namespace下的mapper映射文件內容,多個SqlSession共享。Mybatis需要手動設置啟動二級緩存。在同一個namespace下的mapper文件中,執行相同的查詢SQL。實現二級緩存,關鍵是要對Executor對象做文章,Mybatis給Executor對象加上了一個CachingExecutor,使用了設計模式中的裝飾者模式,

![](http://image.wenzhihuai.com/images/20180120030017.png)

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實現類配置在 節點的type屬性上即可;除此之外,MyBatis還支持跟第三方內存緩存庫如Memecached、Redis的集成,總之,使用MyBatis的二級緩存有三個選擇:

  1. MyBatis自身提供的緩存實現;
  2. 用戶自定義的Cache接口實現;
  3. 跟第三方內存緩存庫的集成;
    具體的實現,可參照:SpringMVC + MyBatis + Mysql + Redis(作為二級緩存) 配置

MyBatis中一級緩存和二級緩存的組織如下圖所示(圖片來自深入理解mybatis原理):

![](http://image.wenzhihuai.com/images/20180120120015.png)

2.3 Mybatis在分布式環境下臟讀問題

(1)如果是一級緩存,在多個SqlSession或者分布式的環境下,數據庫的寫操作會引起臟數據,多數情況可以通過設置緩存級別為Statement來解決。
(2)如果是二級緩存,雖然粒度比一級緩存更細,但是在進行多表查詢時,依舊可能會出現臟數據。
(3)Mybatis的緩存默認是本地的,分布式環境下出現臟讀問題是不可避免的,雖然可以通過實現Mybatis的Cache接口,但還不如直接使用集中式緩存如Redis、Memcached好。

下面將介紹使用Redis集中式緩存在個人網站的應用。

三、Redis緩存

Redis運行於獨立的進程,通過網絡協議和應用交互,將數據保存在內存中,並提供多種手段持久化內存的數據。同時具備服務器的水平拆分、復制等分布式特性,使得其成為緩存服務器的主流。為了與Spring更好的結合使用,我們使用的是Spring-Data-Redis。此處省略安裝過程和Redis的命令講解。

![](http://image.wenzhihuai.com/images/20180119110640.png)

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已經緩存的數據:

![](http://image.wenzhihuai.com/images/20180120052757.png)

3.7 分頁的數據怎么辦

個人網站中共有兩個欄目,一個是技術雜談,另一個是生活筆記,每點擊一次欄目的時候,會根據頁數從數據庫中查詢數據,百度了下,大概有三種方法:
(1)以頁碼作為Key,然后緩存整個頁面。
(2)分條存取,只從數據庫中獲取分頁的文章ID序列,然后從service(緩存策略在service中實現)中獲取。
第一種,由於使用了第三方的插件PageHelper,分頁獲取的話會比較麻煩,同時整頁緩存對內存壓力也蠻大的,畢竟服務器只有2g。第二條實現方式簡單,缺陷是依舊需要查詢數據庫,想了想還是放棄了。緩存的初衷是對請求頻繁又不易變的數據,實際使用中很少會反復的請求同一頁的數據(查詢條件也相同),當然對數據中某些字段做緩存還是有必要的。

四、如何解決臟讀?

對於文章來說,內容是不經常更新的,沒有涉及到緩存一致性,但是對於文章的閱讀量,用戶每點擊一次,就應該更新瀏覽量的。對於文章的緩存,常規的設計是將文章存儲進數據庫中,然后讀取的時候放入緩存中,然后將瀏覽量以文章ID+瀏覽量的結構實時的存入redis服務器中。本站當初設計不合理,直接將瀏覽量作為一個字段,用戶每點擊一次的時候就異步更新瀏覽量,但是此處沒有更新緩存,如果手動更新緩存的話,基本上每點擊一次都得執行更新操作,同樣也不合理。所以,目前本站,你們在頁面上看到的瀏覽量和數據庫中的瀏覽量並不是一致的。有興趣的可以點擊我的網站玩玩~~

五、題外話

兄弟姐妹們啊,個人網站只是個小項目,純屬為了學習而用的,文章可以看看,但是,就不要抓取了吧。。。。一個小時抓取6萬次寶寶心臟真的受不了,雖然服務器一切都還穩定==

![](http://image.wenzhihuai.com/images/20180119044345.png)

個人網站http://www.wenzhihuai.com
個人網站源碼,希望能給個starhttps://github.com/Zephery/newblog

參考:
1.《深入理解mybatis原理》 MyBatis的一級緩存實現詳解
2.《深入理解mybatis原理》 MyBatis的二級緩存的設計原理
3.聊聊Mybatis緩存機制
4.Spring思維導圖
5.SpringMVC + MyBatis + Mysql + Redis(作為二級緩存) 配置
6.《深入分布式緩存:從原理到實踐》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM