天天看點

Spring Boot 使用 Spring Session 內建 Redis 實作Session共享

Spring Boot 使用 Spring Session 內建 Redis 實作Session共享

《Spring Boot 2.0極簡教程》—— 基于 Gradle + Kotlin的企業級應用開發最佳實踐

通常在web開發中,Session 會話管理是很重要的一部分,用于存儲與使用者相關的一些資料。在Java Web 系統中的 Session一般由 Tomcat 容器來管理。不過,使用特定的容器雖然可以很好地實作會話管理,但是基于Tomcat的會話插件實作tomcat-redis-session-manager 和tomcat-memcache-session-manager,會話統一由 NoSql 管理。對于項目本身來說,無須改動代碼,隻需要簡單的配置Tomcat的server.xml就可以解決問題。但是插件太依賴于容器,并且對于Tomcat各個版本的支援不是特别的好。重寫Tomcat的session管理,代碼耦合度高,不利于維護。而使用開源的Spring Session 架構,既不需要修改Tomcat配置,又無須重寫代碼,隻需要配置相應的參數即可完成分布式系統中的 Session 共享管理。

本章我們來介紹在 Spring Boot 應用中如何使用Spring Session 內建 Redis 實作分布式系統中的Session共享,進而實作 Spring Boot 應用的水準擴充。

1.1 集中式共享 Session 架構

我們通常優先采用水準擴充架構來提升系統的可用性和系統性能。但是更多的應用導緻管理更加複雜。對于Spring Boot 應用,會話管理是一個難點。 Spring Boot 應用水準擴充通常有如下兩個問題需要解決:

1.負載均衡。将使用者請求平均派發到水準部署的任意一台Spring Boot 應用伺服器上。可以用一個反向代理伺服器來實作,例如使用Nginx作為反向代理伺服器。在Spring Cloud 中,我們使用 Zuul(智能路由) 內建Eureka(服務發現)、 Hystrix(斷路器) 和 Ribbon(用戶端負載均衡)來實作。

2.共享 Session。 單個Spring Boot應用的Session由Tomcat來管理。如果部署多個Spring Boot應用,對于同一個使用者請求,實作在這些應用之間共享 Session 通常有如下兩種方式:

  a.Session 複制:Web伺服器通常都支援Session複制,一台應用的 Session 資訊改變将立刻複制到其他叢集的Web伺服器上。

  b.集中式 Session 共享 :所有 Web 伺服器都共享同一個Session,Session 通常存放在 Redis 資料庫伺服器上。

Session 複制的缺點是效率較低,性能差。 是以Spring Boot 應用采用集中式 Session 共享。架構圖如下:

上圖是一個通用的分布式系統架構,包含了三個獨立運作的微服務應用。微服務1部署在一台Tomcat伺服器上(IP1:9000),微服務2部署在兩台Tomcat伺服器(IP2:9001、IP3:9002)上采用水準擴充。架構采用Nginx作為反向代理,Nginx提供統一的入口。Spring Boot應用微服務1和微服務2,都采用 Spring Session實作各個子系統共享同一個 Session,該 Session 統一存放在 Redis中。微服務1和微服務2獨立部署的,支援水準擴充,最終整合成一個大的分布式系統。

1.2 Spring Session 介紹

Session 一直都是我們做分布式系統架構時需要解決的一個難題,過去我們可以從 Serlvet容器上解決,比如開源servlet容器-tomcat提供的tomcat-redis-session-manager、memcached-session-manager。 或者通過nginx之類的負載均衡做ip_hash,路由到特定的伺服器上。而使用 Spring Session 來管理分布式session,則完全實作了與具體的容器無關。Spring Session 是Spring的項目之一,GitHub位址:​​https://github.com/spring-projects/spring-session​​。

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

使用Spring Session 可以非常簡易地把 Session 存儲到第三方存儲容器,架構提供了redis、jvm 的 map、mongo、gemfire、hazelcast、jdbc等多種存儲 Session 的容器的方式。

1.3 Redis 簡介

本節介紹 Redis。Redis是目前使用的非常廣泛的記憶體資料庫,相比memcached,它支援更加豐富的資料類型。

1.3.1 Redis 是什麼

Redis 是完全開源免費的,遵守BSD協定,是一個高性能的key-value資料庫。

Redis支援資料的持久化,可以将記憶體中的資料儲存在磁盤中,重新開機的時候可以再次加載進行使用。Redis不僅僅支援簡單的key-value類型的資料,同時還提供list,set,zset,hash等資料結構的存儲。Redis支援資料的備份,即master-slave模式的資料備份。

Redis 優勢

 性能極高。 Redis能讀的速度是110000次/s,寫的速度是81000次/s 。

 豐富的資料類型 。 Redis支援二進制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 資料類型操作。

 原子性。 Redis的所有操作都是原子性的,意思就是要麼成功執行要麼失敗完全不執行。單個操作是原子性的。多個操作也支援事務,即原子性,通過MULTI和EXEC指令包起來。

 豐富的特性。 Redis還支援 publish/subscribe, 通知, key 過期等特性。

Redis運作在記憶體中但是可以持久化到磁盤,是以在對不同資料集進行高速讀寫時需要權衡記憶體,因為資料不能大于硬體記憶體。在記憶體資料庫方面的另一個優點是,相比在磁盤上相同的複雜的資料結構,在記憶體中操作起來非常簡單,這樣Redis可以做很多内部複雜性很強的事情。同時,在磁盤格式方面他們是緊湊的以追加的方式産生的,因為他們并不需要進行随機通路。

1.3.2 安裝Redis

使用下面的指令下載下傳安裝 redis:

$ wget http://download.redis.io/releases/redis-4.0.9.tar.gz
$ tar xzf redis-4.0.9.tar.gz
$ cd redis-4.0.9
$ make      

啟動 redis server 程序指令如下:

$ src/redis-server      

打開 redis client 指令

$ src/redis-cli
redis> set foo bar
OK
redis> get foo
"bar"
      

這樣我們就簡單完成了 redis 的環境配置。

如果需要在遠端 redis 服務上執行指令,同樣我們使用的也是 redis-cli 指令。文法格式如下

$ redis-cli -h host -p port -a password

代碼執行個體:

$redis-cli -h 127.0.0.1 -p 6379 -a "123456"

連接配接到主機為 127.0.0.1,端口為 6379 ,密碼為 123456 的 redis 服務上。

使用 * 号擷取所有配置項指令:

redis 127.0.0.1:6379> config get *
  1) "dbfilename"
  2) "dump.rdb"
  3) "requirepass"
  4) "123456"
  5) "masterauth"
  ...      

1.3.3 設定Redis密碼

通常我們會設定 redis 密碼,指令如下:

127.0.0.1:6379> config set requirepass 123456

OK

測試密碼:

127.0.0.1:6379> info

NOAUTH Authentication required.

127.0.0.1:6379> set x 0

(error) NOAUTH Authentication required.

提示無權限。使用密碼授權登陸:

127.0.0.1:6379> auth 123456

OK

127.0.0.1:6379> set x 0

OK

127.0.0.1:6379> get x

"0"

1.3.4 Redis 資料類型

Redis支援五種資料類型:string(字元串),hash(哈希),list(清單),set(集合)及zset(sorted set:有序集合)。

1.字元串string

string是redis最基本的類型,你可以了解成與Memcached一樣的類型,一個key對應一個value。string類型是二進制安全的。意思是redis的string可以包含任何資料。比如jpg圖檔或者序列化的對象 。

string類型是Redis最基本的資料類型,一個鍵最大能存儲512MB。

代碼執行個體

redis 127.0.0.1:6379> set name "Spring Boot Plus Kotlin"

OK

redis 127.0.0.1:6379> get name

"Spring Boot Plus Kotlin"

在以上執行個體中我們使用了 Redis 的 SET 和 GET 指令。鍵為 name,對應的值為 "Spring Boot Plus Kotlin"。

2.哈希Hash

Redis hash 是一個鍵值(key => value)對集合。Redis hash 是一個 string 類型的 field 和 value 的映射表,hash 适用于存儲對象。

代碼執行個體

redis> HMSET myhash field1 "Hello" field2 "World"

"OK"

redis> HGET myhash field1

"Hello"

redis> HGET myhash field2

"World"

以上執行個體中 hash 資料類型存儲了包含使用者腳本資訊的使用者對象。 執行個體中我們使用了 Redis HMSET, HGETALL 指令,user:1 為鍵值。每個 hash 可以存儲 2^32 -1 鍵值對(4294967295)。

3.清單 List

Redis 清單是簡單的字元串清單,按照插入順序排序。你可以添加一個元素到清單的頭部(左邊)或者尾部(右邊)。

代碼執行個體

127.0.0.1:6379> lpush mylist redis

(integer) 1

127.0.0.1:6379> lpush mylist springboot

(integer) 2

127.0.0.1:6379> lpush mylist kotlin

(integer) 3

127.0.0.1:6379> lpush mylist kotlin

(integer) 4

127.0.0.1:6379> lrange mylist 0 10

  1. "kotlin"
  2. "kotlin"
  3. "springboot"
  4. "redis"

    清單最多可存儲 2^32 - 1 元素 (4294967295)。

    4.集合 Set

    Redis的Set是string類型的無序集合。集合是通過哈希表實作的,是以添加,删除,查找的複雜度都是O(1)。

    使用sadd 指令添加一個 string 元素到 key 對應的 set 集合中,成功傳回1,如果元素已經在集合中傳回 0,如果 key 對應的 set 不存在則傳回錯誤。

    向集合添加一個或多個成員指令:

    SADD key member1 [member2]

    代碼示例:

    127.0.0.1:6379> sadd myset redis

    (integer) 1

    127.0.0.1:6379> sadd myset springboot

    (integer) 1

    127.0.0.1:6379> sadd myset kotlin

    (integer) 1

    127.0.0.1:6379> sadd myset kotlin

    (integer) 0

    擷取集合的成員數:

    SCARD key

    代碼示例:

    127.0.0.1:6379> scard myset

    (integer) 3

傳回集合中的所有成員:

SMEMBERS key

代碼示例:

127.0.0.1:6379> smembers myset

  1. "kotlin"
  2. "redis"
  3. "springboot"

注意:以上執行個體中 kotlin 添加了兩次,但根據集合内元素的唯一性,第二次插入的元素将被忽略。集合中最大的成員數為 2^32 - 1 (4294967295)。

5.有序集合(sorted set)

Redis 有序集合和集合一樣也是string類型元素的集合,且不允許重複的成員。不同的是每個元素都會關聯一個double類型的分數。redis 正是通過分數來為集合中的成員進行從小到大的排序。有序集合的成員是唯一的,但分數(score)卻可以重複。集合是通過哈希表實作的,是以添加,删除,查找的複雜度都是O(1)。 集合中最大的成員數為 2^32 - 1 (4294967295)。

代碼執行個體

127.0.0.1:6379> ZADD mysortedset 1 redis

(integer) 1

127.0.0.1:6379> ZADD mysortedset 2 mongodb

(integer) 1

127.0.0.1:6379> ZADD mysortedset 3 mysql

(integer) 1

127.0.0.1:6379> ZADD mysortedset 3 mysql

(integer) 0

127.0.0.1:6379> ZADD mysortedset 4 mysql

(integer) 0

127.0.0.1:6379> ZRANGE mysortedset 0 10 WITHSCORES

  1. "redis"
  2. "1"
  3. "mongodb"
  4. "2"
  5. "mysql"
  6. "4"

    在以上執行個體中我們通過指令 ZADD 向 redis 的有序集合中添加了三個值并關聯上分數。我們重複添加了 mysql,分數以最後添加的元素為準。

    1.3.5 Spring Boot 內建 Redis

    在項目中添加 spring-boot-starter-data-redis 依賴,然後在 application.properties 中配置 spring.redis.* 屬性即可使用 StringRedisTemplate模闆類來操作 Redis 了。 Spring Data Redis 是對通路redis用戶端的一個包裝适配,支援Jedis,JRedis,SRP,Lettuce四中開源的redis用戶端。RedisTemplate是對redis的CRUD的進階封裝,而RedisConnection提供了簡單封裝。

    一個簡單的代碼示例如下:

@RestController
class RedisTemplateController {
    @Autowired lateinit var stringRedisTemplate: StringRedisTemplate

    @RequestMapping(value = ["/redis/{key}/{value}"], method = [RequestMethod.GET])
    fun redisSave(@PathVariable key: String, @PathVariable value: String): String {

        val redisValue = stringRedisTemplate.opsForValue().get(key)

        if (StringUtils.isEmpty(redisValue)) {
            stringRedisTemplate.opsForValue().set(key, value)
            return String.format("設定[key=%s,value=%s]成功!", key, value)
        }

        if (redisValue != value) {
            stringRedisTemplate.opsForValue().set(key, value)
            return String.format("更新[key=%s,value=%s]成功!", key, value)
        }

        return String.format("redis中已存在[key=%s,value=%s]的資料!", key, value)
    }

    @RequestMapping(value = ["/redis/{key}"], method = [RequestMethod.GET])
    fun redisGet(@PathVariable key: String): String? {
        return stringRedisTemplate.opsForValue().get(key) // String 類型的 value
    }

    @RequestMapping(value = ["/redisHash/{key}/{field}"], method = [RequestMethod.GET])
    fun redisHashGet(@PathVariable key: String, @PathVariable field: String): String? {
        return stringRedisTemplate.opsForHash<String, String>().get(key, field) // Hash 類型的 value
    }
}


      

StringRedisTemplate 繼承了 RedisTemplate 。RedisTemplate是一個泛型類,而StringRedisTemplate則不是。StringRedisTemplate隻能對key=String,value=String的鍵值對進行操作,RedisTemplate可以對任何類型的key-value鍵值對操作。

StringRedisTemplate 封裝了對Redis的一些常用的操作。StringRedisTemplate 使用的是 StringRedisSerializer。RedisTemplate使用的序列類在在操作資料的時候,比如說存入資料會将資料先序列化成位元組數組,然後在存入Redis資料庫,這個時候打開Redis檢視的時候,你會看到你的資料不是以可讀的形式展現的。在使用StringRedisSerializer 操作redis資料類型的時候必須要set相對應的序列化。從StringRedisTemplate類的構造函數代碼可以看出

public class StringRedisTemplate extends RedisTemplate<String, String> {
    public StringRedisTemplate() {
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        setKeySerializer(stringSerializer);
        setValueSerializer(stringSerializer);
        setHashKeySerializer(stringSerializer);
        setHashValueSerializer(stringSerializer);
    }
    ...
}
      

StringRedisTemplate 和 RedisTemplate 各自序列化的方式不同,但最終都是得到了一個位元組數組,殊途同歸,StringRedisTemplate使用的是StringRedisSerializer類;RedisTemplate使用的是JdkSerializationRedisSerializer類。反序列化,則是一個得到String,一個得到Object。

測試 redis 操作

1.請求 ​​​http://127.0.0.1:9000/redis/x/1​​​,輸出:"更新[key=x,value=1]成功!"。

2.再次請求 ​​​http://127.0.0.1:9000/redis/x/1​​​,輸出:“redis中已存在[key=x,value=1]的資料!”。

3.請求 ​​​http://127.0.0.1:9000/redis/x​​​ , 輸出:1。

4.請求 ​​​http://127.0.0.1:9000/redisHash/spring:session:sessions:06830c1b-8157-46fc-b84a-a086aa8c8d45/lastAccessedTime​​,輸出一段不可讀的對象資料:“...java.lang.Long;...java.lang.Number...”。

提示:更多關于 Redis 的介紹參考

​​​https://redis.io/download​​​​http://try.redis.io/​​

1.4 項目實戰

本節通過完整的項目執行個體來介紹在 Spring Boot 應用中如何使用 Redis 來實作共享 Session。在分布式系統中,Sessiong 共享有很多的解決方案,其中使用 Redis 緩存是最常用的方案之一。

1.建立項目

建立兩個 Spring Boot 應用 demo_microservice_api_book、demo_microservice_api_user,它們的 Session 都使用同一個 Redis 資料庫存儲。

2.添加依賴

在build.gradle中添加 spring-session-data-redis 就可以使用 Redis來存儲 Session。

3.配置Redis

為了簡單起見,我們這裡就使用的單點 Redis 模式。在實際生産中,為了保障高可用性,通常是一個 Redis 叢集。在 application.properties中配置 Redis 資訊如下

spring.application.name=demo_microservice_api_user
server.port=9001
################# Redis 基礎配置 #################
spring.redis.host=127.0.0.1
spring.redis.password=123456
spring.redis.port=6379
#連接配接逾時時間 機關 ms(毫秒)
spring.redis.timeout=3000ms
################# Redis 線程池設定 #################
#連接配接池中的最大空閑連接配接,預設值是8。
spring.redis.jedis.pool.max-idle=10
#連接配接池中的最小空閑連接配接,預設值是0。
spring.redis.jedis.pool.min-idle=20
#連接配接池最大活躍數。預設值8。如果指派為-1,則表示不限制;如果pool已經配置設定了maxActive個jedis執行個體,則此時pool的狀态為exhausted(耗盡)。
spring.redis.jedis.pool.max-active=10
# 等待可用連接配接的最大時間,機關毫秒,預設值為-1ms,表示永不逾時。如果超過等待時間,則直接抛出JedisConnectionException
spring.redis.jedis.pool.max-wait=3000ms      

4.配置 Session 存儲類型

在 application.properties中配置存儲 Session的類型為 Redis:

################# 使用 Redis 存儲 Session 設定 #################
# Redis|JDBC|Hazelcast|none
spring.session.store-type=Redis      

spring-boot-autoconfigure 的源代碼中使用RedisAutoConfiguration來加載Redis的配置類RedisProperties。 其中RedisAutoConfiguration會加載 application.properties 檔案的字首為“spring.redis”的屬性。其中“spring.redis.sentinel”是哨兵模式的配置,“spring.redis.cluster”是叢集模式的配置。

當我們添加spring.session.store-type=Redis這行配置,指定 Session 的存儲方式為 Redis,可以看到控制台輸出的日志為:

c.e.s.d.SessionController : 
org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper
      

我們可以看到,Session 已經使用了 HttpSessionWrapper 這個包裝類實作,HttpSessionWrapper 背後真正負責 Session 管理的擴充卡類是 HttpSessionAdapter。RedisOperationsSessionRepository 是采用Redis存儲 Session 的核心業務邏輯實作。其中的變量DEFAULT_NAMESPACE = "spring:session"定義了Spring Session 存儲在 Redis 中的預設命名空間。其中的getSessionKey()方法如下:

String getSessionKey(String sessionId) {
    return this.namespace + "sessions:" + sessionId;
}      

通過方法源碼,我們可以知道 session id 存儲的 Key 是 spring:session:sessions:{sessionId}。這個我們稍後去 Redis 中檢視驗證。

按照上面的步驟在另一個項目中再次配置一次,啟動後,該項目也會自動進行了session共享。

5.測試 Session 資料

分别在兩個 Spring Boot 應用中編寫擷取 Session 資料的 Controller 類 SessionController,代碼相同,如下:

@RestController
class SessionController {
    val log = LoggerFactory.getLogger(SessionController::class.java)
    @RequestMapping(value = "/session")
    fun getSession(request: HttpServletRequest): SessionInfo {
        val session = request.session
        log.info(session.javaClass.canonicalName)
        log.info(session.id)

        val SessionInfo = SessionInfo()
        SessionInfo.id = session.id
        SessionInfo.creationTime = session.creationTime
        SessionInfo.lastAccessedTime = session.lastAccessedTime
        SessionInfo.maxInactiveInterval = session.maxInactiveInterval
        SessionInfo.isNew = session.isNew
        return SessionInfo
    }

    class SessionInfo {
        var id = ""
        var creationTime = 0L
        var lastAccessedTime = 0L
        var maxInactiveInterval = 0
        var isNew = false
    }
}      

在本機部署 demo_microservice_api_book,端口号為 9000。部署 demo_microservice_api_user 兩個運作執行個體,端口号分别為9001、9002。即使用 gradle bootJar 打可執行 jar 包,然後在指令行分别執行:

$ java -jar demo_microservice_api_user-0.0.1-SNAPSHOT.jar  --server.port=9001
$ java -jar demo_microservice_api_user-0.0.1-SNAPSHOT.jar  --server.port=9002

      

通路 ​​http://127.0.0.1:9000/session​​​,得到輸出

{

"id": "06830c1b-8157-46fc-b84a-a086aa8c8d45",

"creationTime": 1523693635249,

"lastAccessedTime": 1523697391616,

"maxInactiveInterval": 1800,

"new": false

}

通路 ​​​http://127.0.0.1:9001/session​​​,得到輸出

{

"id": "06830c1b-8157-46fc-b84a-a086aa8c8d45",

"creationTime": 1523693635249,

"lastAccessedTime": 1523697427153,

"maxInactiveInterval": 1800,

"new": false

}

通路 ​​​http://127.0.0.1:9002/session​​​,得到輸出

{

"id": "06830c1b-8157-46fc-b84a-a086aa8c8d45",

"creationTime": 1523693635249,

"lastAccessedTime": 1523697440377,

"maxInactiveInterval": 1800,

"new": false

}

我們可以看到,這3個獨立運作的應用,都共享了同一個 Session Id。通過 Redis 用戶端指令行 redis-cli 輸入如下指令,檢視所有“spring:session:”開頭的 keys:

127.0.0.1:6379> keys spring:session:*
...
15) "spring:session:sessions:expires:06830c1b-8157-46fc-b84a-a086aa8c8d45"
16) "spring:session:sessions:06830c1b-8157-46fc-b84a-a086aa8c8d45"
17) "spring:session:sessions:expires:d2193501-1d0b-4f1a-9b50-cd01949ce998"      

我們可以看到,spring:session:sessions的值跟我們在浏覽器中得到得到結果一樣。正如我們看到的一樣,session id 在 Redis 中存儲的 Key 是 spring:session:sessions:{sessionId}。

通過 redis-cli 檢視 Redis 存儲的所有 key 指令如下:

127.0.0.1:6379> keys *
…
4) "spring:session:sessions:c3304842-d3a1-42f5-936c-fb73606beda7"
5) "mylist"
6) "spring:session:sessions:expires:c3304842-d3a1-42f5-936c-fb73606beda7"
7) "spring:session:expirations:1523691300000"
…      

執行type指令可以擷取一個 key 存儲的資料類型,例如:

127.0.0.1:6379> type "spring:session:sessions:c3304842-d3a1-42f5-936c-fb73606beda7"
hash      

其中"spring:session:sessions:c3304842-d3a1-42f5-936c-fb73606beda7" 為其中的一個key值。表明出該key存儲在現在redis伺服器中的類型為 hash。此時操作這個資料就必須使用 hset、hget 等操作方法。否則會報錯:

127.0.0.1:6379> get "spring:session:sessions:c3304842-d3a1-42f5-936c-fb73606beda7"
(error) WRONGTYPE Operation against a key holding the wrong kind of value      

例如,擷取在哈希表中指定 key 為"spring:session:sessions:c4a6db26-d86d-47db-b53c-a10d3b997e40"的所有字段和值的指令如下:

127.0.0.1:6379> hgetall "spring:session:sessions:c4a6db26-d86d-47db-b53c-a10d3b997e40"
1) "maxInactiveInterval"
…
3) "lastAccessedTime"
…      

單獨擷取maxInactiveInterval、creationTime的值的指令如下:

127.0.0.1:6379> hget "spring:session:sessions:c4a6db26-d86d-47db-b53c-a10d3b997e40" maxInactiveInterval
…
127.0.0.1:6379> hget "spring:session:sessions:c4a6db26-d86d-47db-b53c-a10d3b997e40" creationTime
…      

繼續閱讀