談談我的網站的創建(八)—— 緩存的使用

歡迎訪問個人網站http://www.wenzhihuai.com/ 。感謝,若是能夠,但願能在GitHub上給個star,GitHub地址https://github.com/Zephery/newbloghtml

1、概述

 1.1 緩存介紹

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

1.2 本站緩存架構

從沒有使用緩存,到使用mybatis緩存,而後使用了ehcache,再而後是mybatis+redis緩存。
mysql


步驟:
(1)用戶發送一個請求到nginx,nginx對請求進行分發。
(2)請求進入controller,service,service中查詢緩存,若是命中,則直接返回結果,不然去調用mybatis。
(3)mybatis的緩存調用步驟:二級緩存->一級緩存->直接查詢數據庫。
(4)查詢數據庫的時候,mysql做了主主備份。

2、Mybatis緩存

2.1 mybatis一級緩存

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

咱們來查看一下源碼的類圖,具體的源碼分析簡單歸納一下: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對象也一併釋放掉。
git

2.2 mybatis二級緩存

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

2.2.1 MyBatis二級緩存的劃分

MyBatis並非簡單地對整個Application就只有一個Cache緩存對象,它將緩存劃分的更細,便是Mapper級別的,即每個Mapper均可以擁有一個Cache對象,具體以下:
a.爲每個Mapper分配一個Cache緩存對象(使用 節點配置);
b.多個Mapper共用一個Cache緩存對象(使用 節點配置);
redis

2.2.2 二級緩存的開啓

在mybatis的配置文件中添加:算法

<settings>
   <!--開啓二級緩存-->
    <setting name="cacheEnabled" value="true"/>
</settings>

而後再須要開啓二級緩存的mapper.xml中添加(本站使用了LRU算法,時間爲120000毫秒):spring

<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的二級緩存有三個選擇: sql

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

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

2.3 Mybatis在分佈式環境下髒讀問題

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

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

3、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。第二條實現方式簡單,缺陷是依舊須要查詢數據庫,想了想仍是放棄了。緩存的初衷是對請求頻繁又不易變的數據,實際使用中不多會反覆的請求同一頁的數據(查詢條件也相同),固然對數據中某些字段作緩存仍是有必要的。

4、如何解決髒讀?

對於文章來講,內容是不常常更新的,沒有涉及到緩存一致性,可是對於文章的閱讀量,用戶每點擊一次,就應該更新瀏覽量的。對於文章的緩存,常規的設計是將文章存儲進數據庫中,而後讀取的時候放入緩存中,而後將瀏覽量以文章ID+瀏覽量的結構實時的存入redis服務器中。本站當初設計不合理,直接將瀏覽量做爲一個字段,用戶每點擊一次的時候就異步更新瀏覽量,可是此處沒有更新緩存,若是手動更新緩存的話,基本上每點擊一次都得執行更新操做,一樣也不合理。因此,目前本站,大家在頁面上看到的瀏覽量和數據庫中的瀏覽量並非一致的。有興趣的能夠點擊個人網站玩玩~~

5、題外話

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

我的網站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.《深刻分佈式緩存:從原理到實踐》

相關文章
相關標籤/搜索