1. 程式人生 > >spring boot學習(06):Redis 實現資料快取和 Session 共享

spring boot學習(06):Redis 實現資料快取和 Session 共享

前言

前面我們學習了redis的基本使用,我們知道redis最常用的應用場景,就是資料快取和session共享,Spring Boot 針對這兩個場景都做了一些優化,讓我們在實際專案中使用非常的方便。

資料快取

使用 Redis 做為資料快取是最常用的場景了。我們知道絕大多數的網站/系統,最先遇到的一個性能瓶頸就是資料庫,使用 Redis 做資料庫的前置快取,可以非常有效的降低資料庫的壓力,從而提升整個系統的響應效率和併發量。Spring Boot 也提供了非常簡單的解決方案,這裡給大家演示最核心的三個註解:@Cacheable、@CacheEvict、@CachePut 。

spring-boot-starter-cache

先了解一下元件:spring-boot-starter-cache

spring-boot-starter-cache 是 Spring Boot 提供快取支援的 starter 包。spring-boot-starter-cache 會進行快取的自動化配置和識別,Spring Boot 為 Redis 自動配置了 RedisCacheConfiguration 等資訊。spring-boot-starter-cache 中的註解也主要是使用了 Spring Cache 提供的支援。

@Cacheable

@Cacheable 是用來宣告方法是可快取的,將結果儲存到快取中以便後續使用相同引數呼叫時不需執行實際的方法,直接從快取中取值。@Cacheable 可以標記在一個方法上,也可以標記在一個類上。當標記在一個方法上時表示該方法是支援快取的,當標記在一個類上時則表示該類所有的方法都是支援快取的。

簡單例子:

@RequestMapping("/hello")
@Cacheable(value="neoCache")
public String hello(String name) {
    System.out.println("沒有走快取!");
    return "hello "+name;
}

專案啟動後,http://localhost:8080/hello?name=ljy訪問,發現第一次列印”沒有走快取!”,繼續訪問,發現不列印資料,說明沒有走controller方法,而是直接從快取中獲取的資料。

@Cacheable的引數

  • value:快取的名稱。
  • key:快取的 key,可以為空,如果指定要按照 SpEL 表示式編寫,如果不指定,則預設按照方法的所有引數進行組合。
  • condition:觸發條件,只有滿足條件的情況才會加入快取,預設為空,既表示全部都加入快取,支援 SpEL。

    資料庫例子:

    @RequestMapping("/getUsers")
    @Cacheable(value="usersCache",key="#nickname",condition="#nickname.length() <= 6")
    public List<User> getUsers(String nickname) {
        List<User> users=userRepository.findByNickname(nickname);
        System.out.println("執行了資料庫操作");
        return users;
    }

    執行以下語句測試:
    http://localhost:8080/getUsers?nickname=1234
    http://localhost:8080/getUsers?nickname=1234567
    我們發現結果不一樣,執行到:condition=”#nickname.length() <= 6” ,spring首先檢查condition條件是否滿足,滿足,在快取空間中查詢使用 key 儲存的物件,如果找到,將找到的結果返回,如果沒有找到執行方法,將方法的返回值以 key-value 物件的方式存入快取中,然後方法返回。

@CachePut

專案執行中會對資料庫的資訊進行更新,如果仍然使用 @Cacheable 就會導致資料庫的資訊和快取的資訊不一致。在以往的專案中,我們一般更新完資料庫後,再手動刪除掉 Redis 中對應的快取,以保證資料的一致性。Spring 提供了另外的一種解決方案,可以優雅的去更新快取。

與 @Cacheable 不同的是使用 @CachePut 標註的方法在執行前,不會去檢查快取中是否存在之前執行過的結果,而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的快取中。
新增一個 getPutUsers 方法,value、key 設定和 getUsers 方法保持一致,使用 @CachePut。

@RequestMapping("/getPutUsers")
@CachePut(value="usersCache",key="#nickname")
public List<User> getPutUsers(String nickname) {
    List<User> users=userRepository.findByNickname(nickname);
    System.out.println("執行了資料庫操作");
    return users;
}

重點內容@CachePut 配置方法

  • value 快取的名稱。
  • key 快取的 key,可以為空,如果指定要按照 SpEL 表示式編寫,如果不指定,則預設按照方法的所有引數進行組合。
  • condition 快取的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行快取。

@CacheEvict

@CacheEvict 是用來標註在需要清除快取元素的方法或類上的。當標記在一個類上時表示其中所有的方法的執行都會觸發快取的清除操作。

@CacheEvict 可以指定的屬性有 value、key、condition、allEntries 和 beforeInvocation。

其中 value、key 和 condition 的語義與 @Cacheable 對應的屬性類似。即 value 表示清除操作是發生在哪些 Cache 上的(對應 Cache 的名稱);key 表示需要清除的是哪個 key,如未指定則會使用預設策略生成的 key;condition 表示清除操作發生的條件。

allEntries 屬性:

allEntries 是 boolean 型別,表示是否需要清除快取中的所有元素。預設為 false,表示不需要。當指定了 allEntries 為 true 時,Spring Cache 將忽略指定的 key。有的時候我們需要 Cache 一下清除所有的元素,這比一個一個清除元素更有效率。

在上一個方法中我們使用註解:@CachePut(value=”usersCache”,key=”#nickname”) 來更新快取,但如果不寫 key=”#nickname”,Spring Boot 會以預設的 key 值去更新快取,導致最上面的方法 getUsers() 方法並沒有獲取最新的資料。但是現在使用 @CacheEvict 就可以解決這個問題了,它會將所有以 usersCache 為名的快取全部清除。

beforeInvocation 屬性:

清除操作預設是在對應方法成功執行之後觸發的,即方法如果因為丟擲異常而未能成功返回時也不會觸發清除操作。使用 beforeInvocation 可以改變觸發清除操作的時間,當我們指定該屬性值為 true 時,Spring 會在呼叫該方法之前清除快取中的指定元素。

總結

@Cacheable
Spring 在執行 @Cacheable 標註的方法前先檢視快取中是否有資料,如果有資料,則直接返回快取資料;若沒有資料,執行該方法並將方法返回值放進快取。 一般用於讀取資料。

@CachePut
和 @Cacheable 類似,但會把方法的返回值放入快取中, 主要用於資料新增和修改方法

@CacheEvict
方法執行成功後會從快取中移除相應資料。

session共享

什麼是session?

由於 HTTP 協議是無狀態的協議,所以服務端需要記錄使用者的狀態時,就需要用某種機制來識具體的使用者。Session 是另一種記錄客戶狀態的機制,不同的是 Cookie 儲存在客戶端瀏覽器中,而 Session 儲存在伺服器上。客戶端瀏覽器訪問伺服器的時候,伺服器把客戶端資訊以某種形式記錄在伺服器上。這就是 Session。客戶端瀏覽器再次訪問時只需要從該 Session 中查詢該客戶的狀態就可以了。

為什麼需要 Session 共享

這裡寫圖片描述
使用者的請求首先會到達前置閘道器,前置閘道器根據路由策略將請求分發到後端的伺服器,這就會出現第一次的請求會交給伺服器 1 處理,下次的請求可能會是服務B處理,如果不做 Session 共享的話,就有可能出現使用者在服務 2登入了,下次請求的時候到達服務 2 又要求使用者重新登入。

Spring Session

Spring Session 提供了一套建立和管理 Servlet HttpSession 的方案。Spring Session 提供了叢集 Session(Clustered Sessions)功能,預設採用外接的 Redis 來儲存 Session 資料(不用手動儲存到redis中),以此來解決 Session 共享的問題。

Spring Session 為企業級 Java 應用的 session 管理帶來了革新,使得以下的功能更加容易實現:

  • API 和用於管理使用者會話的實現。
  • HttpSession - 允許以應用程式容器(即 Tomcat)中性的方式替換 HttpSession。
  • 將 session 所儲存的狀態解除安裝到特定的外部 session 儲存中,如 Redis 或 Apache Geode
    中,它們能夠以獨立於應用伺服器的方式提供高質量的叢集。
  • 支援每個瀏覽器上使用多個 session,從而能夠很容易地構建更加豐富的終端使用者體驗。
  • 控制 session id 如何在客戶端和伺服器之間進行交換,這樣的話就能很容易地編寫 Restful API,因為它可以從 HTTP頭資訊中獲取 session id,而不必再依賴於 cookie。
  • 當用戶使用 WebSocket 傳送請求的時候,能夠保持 HttpSession 處於活躍狀態。

Spring 為 Spring Session 和 Redis 的整合提供了元件:spring-session-data-redis,下面演示如何使用。

快速整合

引入依賴包

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

Session 配置

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class SessionConfig {
}
maxInactiveIntervalInSeconds: 設定 Session 失效時間,使用 Redis Session 之後,原 Boot 的 server.session.timeout 屬性不再生效。

實現模擬登陸

新增登陸方法和註冊方法:

新增登入方法,登入成功後將使用者資訊存放到 Session 中。

@RequestMapping(value = "/login")
public String login (HttpServletRequest request,String userName,String password){
    String msg="登陸失敗!";
    User user= userRepository.findByUserName(userName);
    if (user!=null && user.getPassword().equals(password)){
        request.getSession().setAttribute("user",user);
        msg="登陸成功!";
    }
    return msg;
}

定義 index 方法,只有使用者登入之後才會看到:index content 這條資訊否則提示請先登入。

@RequestMapping(value = "/index")
public String index (HttpServletRequest request){
    String msg="首頁內容";
    Object user= request.getSession().getAttribute("user");
    if (user==null){
        msg="請先登入!";
    }
    return msg;
}

將專案複製,修改專案埠號:8899和8800

測試流程:
訪問:http://localhost:8800/indexhttp://localhost:8899/index都會提示先登入。
用8800視窗登陸:localhost:8800/login?userName=xiaoli&password=123456。
登陸成功後訪問首頁:顯示首頁內容,8899埠直接訪問首頁,我們發現顯示首頁內容,則實現了session共享。

退出登入:

@RequestMapping(value = "/logout")
    public String logout (HttpServletRequest request){
        HttpSession session=request.getSession();
        session.removeAttribute("user");
        return "";
    }