微服務(四)-Redis、Ribbon、會話保持、單點登錄(session共享、Token令牌)


1 Redis

  Redis下載:蒼老師網站

1.1 什么是Redis?

  Redis就是一個能夠將信息或數據保存在內存中的緩存數據庫。

  Redis是一個使用ANSI C編寫的開源、支持網絡、基於內存、可選持久性的鍵值對存儲數據庫。目前Redis的開發由Redis Labs贊助。根據月度排行網站DB-Engines.com的數據,Redis是最流行的鍵值對存儲數據庫

  Redis是一個開發好的軟件,有固定的使用方式。

Redis的特征:

  • Redis是一個內存(緩存)數據庫,因為數據保存在內存中,所以速度快,每秒可執行10萬次讀寫操作。

  • 雖然Redis是一個內存數據庫,但是它允許將數據保存在硬盤上,以便出現運行異常時恢復(Redis數據保存到硬盤上的策略有兩種:AOF和RDB,可同時開啟)

  • Redis保存數據使用key-value的格式,類似java中的Map類型集合,這樣使用key-value保存數據的數據庫統稱為"非關系型數據庫"英文"no-sql"(關系型數據庫,sql,通過外鍵等建立關系)

  • Redis的key為string類型,value支持各種類型:string、list、set、zset、hash。

  • Redis支持微服務系統需要的分布式部署,支持master-slave(一主多從)的模式,以達到"高並發、高可用、高性能"的目的

  • Redis的競品軟件memcached,關於它們的區別可以自學

  • Redis是一個緩存數據庫,以鍵值對的形式將數據保存到內存中,屬於非關系型數據庫(nosql),特點是快速響應

    • 內存數據斷電消失

    • Redis將數據保存到硬盤的兩種方式:AOF、RDB,可同時進行

    • 有些數據庫軟件也以key-value形式將數據保存到硬盤上,但是是關系型數據庫(sql),如:MongoDB

    • 關系型數據庫主要通過外鍵等建立關系,一般是存在硬盤上,主要用途是用來保存全部數據,如:MySQL、Oracle、MongoDB、ServerSocket

    • 非關系型數據庫主要以鍵值對形式將數據保存到內存中,主要是輔助關系型數據庫使用,特點是響應速度快

    • 注意:並非所有的非關系型數據庫數據都存在內存中,也有存在硬盤中的

    • Redis的競品memcached,就性能而言,memcached性能好,但是redis可以進行備份到硬盤

    • redis可用於數據進行增減的場合,如商品搶購

    • redis數據存在內存中,只有重啟電腦數據才會清除,一般是1小時無人訪問就會自動回收,但是這不是絕對的,一般redis會根據內存分配靈活配置

1.2 為什么需要Redis?

  如下圖所示,當faq模塊由多台服務器組成時,每個服務器都要緩存一份所有標簽,這樣會造成緩存冗余,造成內存的浪費。我們可以使用Redis保存所有標簽,當任何faq服務器需要時直接從Redis中獲取即可,這樣節省了內存,提高了服務器性能。

1.3 Redis的安裝及初步使用

解壓運行下載的Redis,文件夾內容如下所示:

  • 雙擊redis-start.bat文件可以啟動redis,出現一個界面,這個界面不能關,一關redis就停止了

  • redis-cli.exe可以運行操作Redis的客戶端

由於每次開機都要啟動redis,界面還不能關,不方便。

我們可以實現每次開機自動啟動,需要運行下面的文件:

  1. 先運行: service-installing.bat---安裝Redis服務到操作系統

  2. 再運行:service-start.bat---啟動Redis服務

  3. 如果想停止服務: service-stop.bat

  4. 如果想卸載: service-uninstalling.bat

  運行上面的步驟1和2即可,正常情況下,每次開機redis都會自動開啟了,然后打開redis-cli.exe,啟動后輸入info,輸出下面的信息就表示redis啟動成功了,之后可以在此界面進行代碼的編寫:

1.4 Redis基本操作

Redis支持如下幾種數據類型:

  我們主要使用string字符串類型,string類型基本操作如下:

 127.0.0.1:6379> set mystr "hello world!" //保存字符串類型 
 127.0.0.1:6379> get mystr //讀取字符串類型

  除了保存字符串,Redis還特別適合保存頻繁變化的數字。因為如果頻繁修改硬盤數據庫(mysql)中的數字的話,每次都是硬盤操作,效率低;如果修改的是redis中的數據,那么支持的並發會較大。

  除此之外,Redis是一個單線程的程序,沒有線程安全問題,所有即使是高並發的程序也能夠正確響應數字的變化。

下面是對數字增減的專門命令:

 127.0.0.1:6379> set mynum "2"   //創建數字,也可直接寫數字保存:set mynum 2,默認保存數字為字符串類型 
 OK
 127.0.0.1:6379> get mynum       //查詢數字
 "2"
 127.0.0.1:6379> incr mynum     //數字增加
 (integer) 3
 127.0.0.1:6379> get mynum     //查詢數字
 "3"
 127.0.0.1:6379> decr mynum     //數字減少
 (integer) 2
 127.0.0.1:6379> get mynum     //查詢數字
 "2"

Redis基本命令補充:

 List 列表
  常用命令: lpush,rpush,lpop,rpop,lrange等
  Redis的list在底層實現上並不是數組而是鏈表,Redis list 的應用場景非常多,也是Redis最重要的數據結構之一,比如微博的關注列表,粉絲列表,消息列表等功能都可以用Redis的 list 結構來實現。
 Redis list 的實現為一個雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過帶來了部分額外的內存開銷。
  另外可以通過 lrange 命令,就是從某個元素開始讀取多少個元素,可以基於 list 實現分頁查詢,這個很棒的一個功能,基於 redis 實現簡單的高性能分頁,可以做類似微博那種下拉不斷分頁的東西(一頁一頁的往下走),性能高。
  lists的常用操作包括LPUSH、RPUSH、LRANGE、RPOP等。可以用LPUSH在lists的左側插入一個新元素,用RPUSH在lists的右側插入一個新元素,用LRANGE命令從lists中指定一個范圍來提取元素,RPOP從右側彈出數據。來看幾個例子::
 //新建一個list叫做mylist,並在列表頭部插入元素"Tom"
 127.0.0.1:6379> lpush mylist "Tom"
 
 //返回當前mylist中的元素個數
 (integer) 1
 
 //在mylist右側插入元素"Jerry"
 127.0.0.1:6379> rpush mylist "Jerry"
 (integer) 2
 
 //在mylist左側插入元素"Andy"
 127.0.0.1:6379> lpush mylist "Andy"
 (integer) 3
 
 //列出mylist中從編號0到編號1的元素
 127.0.0.1:6379> lrange mylist 0 1
 1) "Andy"
 2) "Tom"
 
 //列出mylist中從編號0到倒數第一個元素
 127.0.0.1:6379> lrange mylist 0 -1
 1) "Andy"
 2) "Tom"
 3) "Jerry"
 
 //從右側取出最后一個數據
 127.0.0.1:6379> rpop mylist
 "Jerry"
 
 //再次列出mylist中從編號0到倒數第一個元素
 127.0.0.1:6379> lrange mylist 0 -1
 1) "Andy"
 2) "Tom"
 
 Set 集合
  常用命令: sadd,smembers,sunion 等
  set 是無序不重復集合,list是有序可以重復集合,當你需要存儲一個列表數據,又不希望出現重復數據時,set是一個很好的選擇,並且set提供了判斷某個成員是否在一個set集合內的重要功能,這個也是list所不能提供的。
  可以基於 set 輕易實現交集、並集、差集的操作。比如:在微博應用中,可以將一個用戶所有的關注人存在一個集合中,將其所有粉絲存在一個集合Redis可以非常方便的實現如共同關注、共同粉絲、共同喜好等功能,也就是求交集的過程。set具體命令如下:
 //向集合myset中加入一個新元素"Tom"
 127.0.0.1:6379> sadd myset "Tom"
 (integer) 1
 127.0.0.1:6379> sadd myset "Jerry"
 (integer) 1
 
 //列出集合myset中的所有元素
 127.0.0.1:6379> smembers myset
 1) "Jerry"
 2) "Tom"
 
 //判斷元素Tom是否在集合myset中,返回1表示存在
 127.0.0.1:6379> sismember myset "Tom"
 (integer) 1
 
 //判斷元素3是否在集合myset中,返回0表示不存在
 127.0.0.1:6379> sismember myset "Andy"
 (integer) 0
 
 //新建一個新的集合yourset
 127.0.0.1:6379> sadd yourset "Tom"
 (integer) 1
 127.0.0.1:6379> sadd yourset "John"
 (integer) 1
 127.0.0.1:6379> smembers yourset
 1) "Tom"
 2) "John"
 
 //對兩個集合求並集
 127.0.0.1:6379> sunion myset yourset
 1) "Tom"
 2) "Jerry"
 3) "John"
 
 Sorted Set 有序集合
  常用命令: zadd,zrange,zrem,zcard等
  和set相比,sorted set增加了一個權重參數score,使得集合中的元素能夠按score進行有序排列。
 在直播系統中,實時排行信息包含直播間在線用戶列表,各種禮物排行榜,彈幕消息(可以理解為按消息維度的消息排行榜)等信息,適合使用 Redis 中的 SortedSet 結構進行存儲。
  很多時候,我們都將redis中的有序集合叫做zsets,這是因為在redis中,有序集合相關的操作指令都是以z開頭的,比如zrange、zadd、zrevrange、zrangebyscore等等
 來看幾個生動的例子:
 //新增一個有序集合hostset,加入一個元素baidu.com,給它賦予score:1
 127.0.0.1:6379> zadd hostset 1 baidu.com
 (integer) 1
 
 //向hostset中新增一個元素bing.com,賦予它的score是30
 127.0.0.1:6379> zadd hostset 3 bing.com
 (integer) 1
 
 //向hostset中新增一個元素google.com,賦予它的score是22
 127.0.0.1:6379> zadd hostset 22 google.com
 (integer) 1
 
 //列出hostset的所有元素,同時列出其score,可以看出myzset已經是有序的了。
 127.0.0.1:6379> zrange hostset 0 -1 with scores
 1) "baidu.com"
 2) "1"
 3) "google.com"
 4) "22"
 5) "bing.com"
 6) "30"
 
 //只列出hostset的元素
 127.0.0.1:6379> zrange hostset 0 -1
 1) "baidu.com"
 2) "google.com"
 3) "bing.com"
 
 Hash
  常用命令: hget,hset,hgetall 等。
  Hash 是一個 string 類型的 field 和 value 的映射表,hash 特別適合用於存儲對象,后續操作的時候,你可以直接僅僅修改這個對象中的某個字段的值。 比如我們可以Hash數據結構來存儲用戶信息,商品信息等等。比如下面我就用 hash 類型存放了我本人的一些信息:
 //建立哈希,並賦值
 127.0.0.1:6379> HMSET user:001 username antirez password P1pp0 age 34
 OK
 
 //列出哈希的內容
 127.0.0.1:6379> HGETALL user:001
 1) "username"
 2) "antirez"
 3) "password"
 4) "P1pp0"
 5) "age"
 6) "34"
 
 //更改哈希中的某一個值
 127.0.0.1:6379> HSET user:001 password 12345
 (integer) 0
 
 //再次列出哈希的內容
 127.0.0.1:6379> HGETALL user:001
 1) "username"
 2) "antirez"
 3) "password"
 4) "12345"
 5) "age"
 6) "34"

1.5 SpringBoot操作Redis

  添加依賴:像mysql一樣,java可以操作mysql數據庫,就可以操作Redis。底層我們使用jdbc操作mysql,redis方面底層使用Jedis操作Redis,但是和jdbc操作數據庫一樣,使用Jedis操作Redis步驟比較繁瑣。

  我們可以使用Spring Boot Redis操作Redis,這樣會很簡單,先添加必要依賴,然后才能使用Spring Boot Redis框架,在knows-faq模塊的pom.xml文件添加如下:

 <!--Spring連接Redis的依賴,上面為底層,下面為封裝優化-->
 <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.data</groupId>
     <artifactId>spring-data-redis</artifactId>
 </dependency>

application.properties文件中需要配置Redis的ip地址和端口號,就像我們連接數據庫也要提供這些資料一樣。

 # 配置Redis的ip和端口,localhost即127.0.0.1
 spring.redis.host=localhost
 spring.redis.port=6379

1.6 基本操作

  我們可以在測試類中編寫代碼測試是否可以成功操作redis:

 //我們添加Spring Redis的依賴就是向Spring容器中添加了一個可以操作Redis的對象
 // RedisTemplate<[key的類型],[value的類型]>
 @Autowired
 RedisTemplate<String,String> redisTemplate;
 @Test
 public void redis(){
     // 向Redis中保存(添加)數據
     redisTemplate.opsForValue().set("myname","東方不敗");
     System.out.println("ok");
 }
 
 @Test
 public void getValue(){
     //讀取Redis中的信息
     String name=redisTemplate.opsForValue().get("myname");
     System.out.println(name);
 }

輸出結果:

 ok
 
 東方不敗

1.7 優化標簽緩存

  我們學習了怎么操作Redis,下面我們就將TagServiceImpl實現類中獲得所有標簽的方法修改為從Redis中獲取。

轉到knows-faq模塊: TagServiceImpl 實現類代碼修改如下:

 @Service
 public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {
     //RedisTemplate源代碼注入只注入了兩個類型
     //RedisTemplate<String, String>
     //RedisTemplate<Object, Object>
     //如果使用@Autowired自動裝配,因為泛型類型不匹配,所以會報錯
     //但是@Resource是id,注入主要參數名稱是redisTemplate就可以注入,成功
     @Resource
     private RedisTemplate<String,List<Tag>> redisTemplate;
 
     //從Spring容器中取TagMapper
     @Autowired
     private TagMapper tagMapper;
 
     //全查所有Tags
     @Override //重寫方法
     public List<Tag> getTags() {
         //先從Redis中獲得所有標簽的集合
         List<Tag> tags = redisTemplate.opsForValue().get("tags");
         //如果上面的獲取失敗,說明Redis中沒有所要標簽
         //那么就是第一次請求,需要連接數據庫新增
         if(tags==null || tags.isEmpty()){
             //連接數據庫查詢所有標簽
             tags = tagMapper.selectList(null);
             //將全查出來的標簽保存到redis中
             redisTemplate.opsForValue().set("tags",tags);
             System.out.println("Redis已加載所有標簽");
        }
         //千萬別忘了修改返回值!!!!
         return tags;
    }
 
     //全查所有Tags放在Map中
     @Override
     public Map<String, Tag> getTagMap() {
         Map<String,Tag> tagMap = new HashMap<>();
         for(Tag t:getTags()){
             tagMap.put(t.getName(),t);
        }
         //千萬別忘了修改返回值!!!!
         return tagMap;
    }
 }
 

  重新啟動knows-faq服務,訪問學生首頁,路徑為:http://localhost:8080/index_student.html,輸出效果如下:

  控制台輸出內容如下:

  之后刷新或重啟服務,不再輸出該語句,因為數據已經存到redis緩沖中去了,除非重啟電腦,重啟后加載一次即可,以后不再需要重復加載,直接使用即可。

 

2 使用Ribbon實現服務間調用

  注冊和查詢所有標簽我們已經完成了,下面要完成登錄功能,登錄功能涉及很多知識點和代碼,先來學習Ribbon。

2.1 什么Ribbon?

  Ribbon是SpringCloud提供的一個組件,它能夠實現微服務之間的互相調用。它的使用不用添加額外依賴,因為使用的非常頻繁,在spring-cloud-starter-alibaba-nacos-discovery這個依賴中已經集成了。

2.2 Ribbon基本使用

步驟1 : 明確服務的提供者(方法的定義)

  服務的提供者也叫生產者,本次調用我們將sys模塊作為生成者,需要定義一個方法作為被調用的方法,必須是一個控制器方法才能被Ribbon調用,我們將/v1/auth/demo這個路徑的方法作為調用目標。

步驟2 : 在發起調用的一方添加Ribbon的支持

  本次調用的發起者是faq模塊,在SpringBoot啟動類中注入一個能夠發起Ribbon請求的對象

 @SpringBootApplication
 @EnableDiscoveryClient
 @MapperScan("cn.tedu.knows.faq.mapper")
 public class KnowsFaqApplication {
 
     public static void main(String[] args) {
         SpringApplication.run(KnowsFaqApplication.class, args);
    }
 
     // @Bean表示將下面方法的返回值保存到Spring容器中
     @Bean
     // 保存到Spring容器的對象支持負載均衡的Ribbon調用
     @LoadBalanced
     // 這個方法的返回值是實現Ribbon調用的對象,使用它來發起跨服務器的調用請求
     public RestTemplate restTemplate(){
         return new RestTemplate();
    }
 }

步驟3 : 發起調用

  使用剛剛保存到Spring容器中的RestTemplate對象調用方法,我們先使用測試類來調用。實際開發中經常會在業務邏輯層中發起,測試類代碼如下:

 @Autowired
 RestTemplate restTemplate;
 @Test
 public void ribbon(){
     // 聲明要調用的控制器的路徑:
     // sys-service:要調用的微服務注冊到Nacos的名稱
     // /v1/auth/demo:要調用的控制器的訪問路徑
     String url="http://sys-service/v1/auth/demo";
 
     // ribbon調用
     // 參數url:是上面定義的字符串
     // 參數String.class:定義返回值類型的反射,根據實際情況編寫即可
     String str=restTemplate.getForObject(url,String.class);
     System.out.println(str);
 }

測試結果:

 sys:Hello World!!!

關系示意圖如下:

2.3 使用Ribbon通過用戶名獲得對象

  我們下面將剛編寫的Ribbon升級,添加了參數,返回值變為了User。faq模塊還是請求的發起者(消費者),根據Ribbon的規則,我們首先要在sys模塊中定義一個根據用戶名返回用戶對象的控制器方法,沒有這個方法就要編寫這個方法,從業務邏輯層開始。

轉到knows-sys模塊:

(1)IUserService接口中添加方法:

 // 根據用戶名查詢用戶對象
 User getUserByUsername(String username);

UserServiceImpl類中實現業務邏輯層方法:

 //根據用戶名查找用戶對象的邏輯層實現
 @Override
 public User getUserByUsername(String username) {
     return userMapper.findUserByUsername(username);
 }

(2)控制層代碼:AuthController添加方法

 @Resource
 private IUserService userService;
 @GetMapping("/user")//測試路徑:http://localhost:8002/v1/auth/user
 public User getUser(String username){
     return userService.getUserByUsername(username);
 }

在faq模塊中進行測試,測試執行前保證sys模塊重新啟動過!

 @Test
 public void getUser(){
     // 有參數的Ribbon調用
     // url請求的路徑寫完之后,使用?分割開始編寫參數列表
     // 參數的值不能寫死要用{1},{2}....這種方式占位
     String url="http://sys-service/v1/auth/user?username={1}";
 
     // 調用帶參數的方法
     // 從第三個參數開始,給{1}賦值,第四個參數給{2}賦值,以此類推
     User user=restTemplate.getForObject(url, User.class,"st2");
     System.out.println(user);
 }

測試結果:

  需要大家下載一個軟件:postman(郵遞員)

  下載地址:https://www.postman.com/downloads/

  這個軟件用於向服務器發送各種請求,get\post均可,還可以攜帶參數。

 

3 微服務的會話保持

3.1 什么是會話保持

  會話就是多個請求和響應的集合,一般來講,打開瀏覽器到關閉瀏覽器就是一次會話。

  會話對應java中的HttpSession對象,所謂會話保持,就是多次請求過程中,服務器都可以獲得session中的信息。

  單體項目中會話保持是依靠session對象的。

3.2 微服務項目的會話保持問題

  因為微服務具有多個項目,每個項目都有自己的session,在一個服務器中登錄並不能共享給其它項目,這樣單體項目中的會話保持方式就不能使用了。

  如下圖所示,sys模塊登錄成功並不能把登錄信息發送給faq模塊,這樣就無法實現會話保持,微服務項目中有專門的會話保持技術稱之為"單點登錄"。

 

3.3 單點登錄實現思路

  上次課我們講到了微服務項目因為有多個服務器組成,遇到了會話保持問題。

  在一個服務器上登錄,能夠讓所有服務器知道當前用戶的信息的解決方案就是單點登錄

單點登錄的辦法有很多,但是實現思路主要有兩種:

  1. Session共享

  1. Token令牌

3.3.1 方法一:Session共享

原理:讓用戶的登錄信息共享給所有模塊,用戶登錄時共享session。

下圖表示使用Session共享實現單點登錄:

基本思路:將用戶信息保存到Redis中,哪個模塊需要用戶信息從redis中取。

優點:

  1. 安全性高

  1. 框架支持比較完善,開發代碼量小

缺點:

  1. 每個模塊還有Redis都要消耗較多內存保存用戶信息

  1. 當用戶信息更新時,需要比較復雜的操作

3.3.2 方法二:Token令牌

原理:當用戶登錄時,將一個加密的令牌發送給客戶端,客戶端保存這個令牌,令牌中包含用戶信息,訪問其它模塊時,使用令牌表名身份。

下圖是Token令牌的解決方案:

基本思路:用戶登錄成功頒發令牌,由客戶端保存,訪問其它服務器時,客戶端提供令牌,服務器驗證。

優點:

  1. 解放session,服務器不需要再因為用戶信息占用內存

  1. 客戶端保存令牌,方便響應客戶端變化

缺點:

  1. 解析和頒發令牌需要Cpu消耗算力

  1. 絕對安全的業務,需要再次驗證

在達內知道項目中,我們使用第二種方式實現微服務的單點登錄,Token令牌這種方式有很多需要我們解決的問題,我們通過一些業界成熟的規范和標准實現這個過程。

 


免責聲明!

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



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