簡介

這是一篇關于Redis使用的總結類型文章,會先簡單的談一下緩存的應用場景、緩存的使用邏輯及注意事項,然后是Redis緩存與數據庫間結合以進行系統優化,當然文章的最后也會給出具體的代碼實現,不至于看到文章的你一頭霧水,理論要講,項目代碼也要分享,這是我寫博客的基本出發點。

應用場景

Redis能做什么呢?

這是個好問題,不同的人可能會給出不同的答案,因為它的應用場景真的很多,作為一個優秀的nosql數據庫可以結合其他產品做很多事情,比如:tomcat集群的session同步、與nginx和lua結合做限流工具、基于Redis的分布式鎖實現、分布式系統唯一主鍵生成策略、秒殺場景中也會看到它、它還能夠作為一個消息隊列.....

Redis的應用場景很多很多,以上也只是列舉了一部分而已,由于本文是圍繞我的開源項目perfect-ssm來寫的,所以在本文的場景就是一個緩存中間層,對于讀多寫少的應用場景,我們經常使用緩存來進行優化以提高系統性能。

我曾經寫過一篇《一次線上Mysql數據庫崩潰事故的記錄》的文章,里面記錄了Web請求是如何毫不留情的摧垮mysql數據庫,進而導致網站應用無法正常運轉。當時的情況就是數據庫讀請求太多,事故的主要原因也是這個,后續的解決方案也就是在項目中添加緩存層,使得熱點數據得以存入緩存,不會重復的去讀取mysql,將大部分請求壓力轉移至Redis緩存中以減輕mysql的負擔。

接入緩存后的處理邏輯

請求過來后,首先判斷Redis里面有沒有,有數據則直接返回Redis中的數據給用戶,沒有則查詢數據庫,如果數據庫中也沒有則返回空或者提醒語句即可。

當然,針對不同的操作,對于Redis和mysql的操作也是不同的:

添加操作

如果是需要放入緩存的數據,那么在向mysql數據庫中插入成功后,生成對應的key至,并存入Redis中。

修改操作

向mysql數據庫中修改成功后,修改Redis中的數據,但是Redis并沒有更新語句,所以只能先刪除,再添加完成更新操作。

需要注意的是,考慮到程序對于Redis的操作可能會失敗,這時mysql中的數據已經修改,但是Redis中的數據依然是上一次的數據,導致數據不一致的問題,所以是先操作Redis還是先操作mysql需要慎重考慮。

刪除操作

與修改操作相同,先刪除數據,再更新緩存,但是同樣會有出現數據不一致問題的可能性需要注意,如果數據庫中的數據刪除了,但是Redis中的數據沒刪除,又會出現業務問題。

查詢操作

首先通過Redis查詢,如果緩存中已經存在數據則直接返回即可,此時就不再需要通過mysql數據庫來獲取數據,減少對mysql的請求,如果緩存中不存在數據,則依然通過mysql數據庫查詢,查詢到數據后,存入Redis緩存中。

本項目中的代碼是先操作mysql,再操作Redis,有概率會出現上文中提到的數據庫與緩存數據不一致的情況,所以需要注意,本文的代碼只做參考,用到實際項目中還是需要根據具體的業務邏輯進行合理的修改。

使用緩存的建議

緩存存儲策略:

可以緩存的數據的特征基本上是以下幾點:

  • 熱點數據
  • 實時性要求不高的數據
  • 業務邏輯簡單的數據

至于什么數據,不同的系統、不同的項目要求肯定不同,這里不做過多討論,只簡單的說一下自己的想法,結合以上的特征總結如下:

  • 1.首頁數據、分類數據這些數據屬于熱點數據,首頁數據更是熱得發燙,而且這類數據一般實時性不高,不會頻繁的去操作,比較適合放入緩存。
  • 2.詳情數據,比如文章詳情、商品詳情、廣告詳情、個人信息詳情,這些數據庫中單條的的數據可以以其id生成不同的key保存到Redis,操作比較簡單明了,在更新或者刪除的時候需要同步更新Redis中的數據,這類數據也適合放入緩存中。
  • 3.列表數據不是特別推薦,除非是實時性和改變頻率真的很低的情況下,因為列表往往牽涉的數據和操作很多,處理起來比較復雜,如果對實時性要求低的話、或者部分字段更新頻率低的話,可以換成這部分數據。

緩存存儲策略的制定說難也難,說容易也容易,主要是根據具體的業務場景合理的操作即可,以上只是做了一個簡單的總結。

緩存失效策略:

失效策略一定要做好,血的教訓。

  • 定時刪除

含義:在設置key的過期時間的同時,為該key創建一個定時器,讓定時器在key的過期時間來臨時,對key進行刪除

優點:保證內存被盡快釋放

缺點: 若過期key很多,刪除這些key會占用很多的CPU時間,在CPU時間緊張的情況下,CPU不能把所有的時間用來做要緊的事兒,還需要去花時間刪除這些key 定時器的創建耗時,若為每一個設置過期時間的key創建一個定時器(將會有大量的定時器產生),性能影響嚴重

  • 惰性刪除

含義:key過期的時候不刪除,每次從數據庫獲取key的時候去檢查是否過期,若過期則刪除,返回null。

優點:刪除操作只發生在從數據庫取出key的時候發生,而且只刪除當前key,所以對CPU時間的占用是比較少的,而且此時的刪除是已經到了非做不可的地步(如果此時還不刪除的話,我們就會獲取到了已經過期的key了)

缺點:若大量的key在超出超時時間后,很久一段時間內,都沒有被獲取過,那么可能發生內存泄露(無用的垃圾占用了大量的內存)

  • 定期刪除

含義:每隔一段時間執行一次刪除過期key操作

優點: 通過限制刪除操作的時長和頻率,來減少刪除操作對CPU時間的占用--處理"定時刪除"的缺點 定期刪除過期key--處理"惰性刪除"的缺點

缺點 在內存友好方面,不如"定時刪除" 在CPU時間友好方面,不如"惰性刪除" 難點 合理設置刪除操作的執行時長(每次刪除執行多長時間)和執行頻率(每隔多長時間做一次刪除)(這個要根據服務器運行情況來定了)

參考《Redis設計與實現》

緩存操作順序策略:

在上文中已經講到了操作順序的問題,是先操作mysql呢?還是先操作Redis呢?這個需要根據自己的業務邏輯來考量,盡量選擇影響較小且結合友好的方案來做。

代碼實現:

這里只貼出主要的邏輯代碼,想要完整實現的可以到代碼倉庫去取。

//添加
@Override
    public int addArticle(Article article) {
        if (articleDao.insertArticle(article) > 0) {
            log.info("insert article success,save article to Redis");
            RedisUtil.put(Constants.ARTICLE_CACHE_KEY + article.getId(), article);
            return 1;
        }
        return 0;
    }

//修改
    @Override
    public int updateArticle(Article article) {
        if (article.getArticleTitle() == null || article.getArticleContent() == null || getTotalArticle(null) > 90 || article.getArticleContent().length() > 50000) {
            return 0;
        }
        if (articleDao.updArticle(article) > 0) {
            log.info("update article success,delete article in Redis and save again");
            RedisUtil.del(Constants.ARTICLE_CACHE_KEY + article.getId());
            RedisUtil.put(Constants.ARTICLE_CACHE_KEY + article.getId(), article);
            return 1;
        }
        return 0;
    }

//刪除
    @Override
    public int deleteArticle(String id) {
        RedisUtil.del(Constants.ARTICLE_CACHE_KEY + id);
        return articleDao.delArticle(id);
    }

//查詢
    @Override
    public Article findById(String id) {
        log.info("get article by id:" + id);
        Article article = (Article) RedisUtil.get(Constants.ARTICLE_CACHE_KEY + id, Article.class);
        if (article != null) {
            log.info("article in Redis");
            return article;
        }
        Article articleFromMysql = articleDao.getArticleById(id);
        if (articleFromMysql != null) {
            log.info("get article from mysql and save article to Redis");
            RedisUtil.put(Constants.ARTICLE_CACHE_KEY + articleFromMysql.getId(), articleFromMysql);
            return articleFromMysql;
        }
        return null;
    }

結語

首發于我的個人博客,新的項目演示地址:perfect-ssm,登錄賬號:admin,密碼:123456 如果有問題或者有一些好的創意,歡迎給我留言,也感謝向我指出項目中存在問題的朋友。

如果你想繼續了解該項目可以查看整個系列文章Spring+SpringMVC+MyBatis+easyUI整合系列文章,也可以到我的GitHub倉庫或者開源中國代碼倉庫中查看源碼及項目文檔。