第16章 集羣高可用
學習目標
-
理解集羣流程
-
理解分佈式概念
-
能實現Eureka集羣[集羣配置]
-
能實現Redis集羣[Redis集羣配置、哨兵策略(案例)、Redis擊穿問題]
1.Redis集羣的原理 2.Redis集羣會用->在java代碼中能鏈接集羣服務 3.哨兵策略->監控集羣的健康狀態[作用] 4.Redis擊穿->如何解決擊穿問題 5.如何解決Redis雪崩問題->多級緩存
-
RabbitMQ集羣
1.集羣概述
1.1什麼是集羣
1.1.1集羣概念
集羣是一種計算機系統, 它通過一組鬆散集成的計算機軟件和/或硬件連接起來高度緊密地協作完成計算工作。在某種意義上,他們可以被看作是一臺計算機。集羣系統中的單個計算機通常稱爲節點,通常通過局域網連接,但也有其它的可能連接方式。集羣計算機通常用來改進單個計算機的計算速度和/或可靠性。一般情況下集羣計算機比單個計算機,比如工作站或超級計算機性能價格比要高得多。
1.1.2集羣的特點
集羣擁有以下兩個特點:
- 可擴展性:集羣的性能不限制於單一的服務實體,新的服務實體可以動態的添加到集羣,從而增強集羣的性能。
- 高可用性:集羣當其中一個節點發生故障時,這臺節點上面所運行的應用程序將在另一臺節點被自動接管,消除單點故障對於增強數據可用性、可達性和可靠性是非常重要的。
1.1.3集羣的兩大能力
集羣必須擁有以下兩大能力:
- 負載均衡:負載均衡把任務比較均勻的分佈到集羣環境下的計算和網絡資源,以提高數據吞吐量。
- 錯誤恢復:如果集羣中的某一臺服務器由於故障或者維護需要無法使用,資源和應用程序將轉移到可用的集羣節點上。這種由於某個節點的資源不能工作,另一個可用節點中的資源能夠透明的接管並繼續完成任務的過程,叫做錯誤恢復。
負載均衡和錯誤恢復要求各服務實體中有執行同一任務的資源存在,而且對於同一任務的各個資源來說,執行任務所需的信息視圖必須是相同的。
1.2集羣與分佈式的區別
說到集羣,可能大家會立刻聯想到另一個和它很相近的一個詞----“分佈式”。那麼集羣和分佈式是一回事嗎?有什麼聯繫和區別呢?
相同點:
分佈式和集羣都是需要有很多節點服務器通過網絡協同工作完成整體的任務目標。
不同點:
分佈式是指將業務系統進行拆分,即分佈式的每一個節點都是實現不同的功能。而集羣每個節點做的是同一件事情。
如下圖,每個人都有不同的分工,一起協作幹一件事,叫做“分佈式”
再看下圖:每個划槳人乾的都是一樣的活,叫做集羣。
分佈式的每一個節點也可以做成集羣。其實這個賽龍舟的圖,總整體來看屬於分佈式,包括打鼓和划槳兩個分佈式節點,而划槳的節點又是集羣的形態。
2 Eureka集羣
2.1 Eureka簡介
2.1.1什麼是Eureka
Eureka是一種基於REST(Representational State Transfer)的服務,主要用於AWS,用於定位服務,以實現中間層服務器的負載平衡和故障轉移。我們將此服務稱爲Eureka Server。Eureka還附帶了一個基於Java的客戶端組件Eureka Client,它使與服務的交互變得更加容易。客戶端還有一個內置的負載均衡器,可以進行基本的循環負載均衡。在Netflix,一個更復雜的負載均衡器包裝Eureka,根據流量,資源使用,錯誤條件等多種因素提供加權負載平衡,以提供卓越的彈性。
理解:
Eureka是一個服務註冊與發現的註冊中心。類似於dubbo中的zookeeper.
官網地址:
https://github.com/Netflix/eureka/wiki/Eureka-at-a-glance
2.1.2Eureka的架構
application Service :相當於服務提供者
application Client :相當於服務消費者
make remote call :服務調用過程
us-east-1c d e 都是region:us-east-1 的可用區域。
簡單可以理解爲:
每一個erurak都是一個節點,默認啓動時就是以集羣的方式。
區別:
erurak:集羣,各個節點的數據一致,各個節點都屬於同等級別的註冊中心,不存在leader的概念。
zookeeper:Zookeeper集羣存在Leader節點,並且會進行Leader選舉,Leader具有最高權限。
2.2 搭建Eureka集羣
配置host文件C:\Windows\System32\drivers\etc\hosts
文件,添加映射
127.0.0.1 eureka-server1
127.0.0.1 eureka-server2
127.0.0.1 eureka-server3
2.2.1 application.yml配置
第1臺application.yml:
server:
port: 8761
eureka:
instance:
hostname: eureka-server1
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://eureka-server2:8762/eureka/,http://eureka-server3:8763/eureka/
第2臺application.yml
server:
port: 8762
eureka:
instance:
hostname: eureka-server2
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://eureka-server1:8761/eureka/,http://eureka-server3:8763/eureka/
第3臺application.yml配置:
server:
port: 8763
eureka:
instance:
hostname: eureka-server3
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://eureka-server1:8761/eureka/,http://eureka-server2:8762/eureka/
2.2.2 效果
http://localhost:8761/
http://localhost:8762/
http://localhost:8763/
項目中使用的時候,將多個寫到一起,隔開即可,代碼如下:
上圖代碼如下:
eureka:
client:
service-url:
defaultZone: http://eureka-server1:8761/eureka/,http://eureka-server2:8761/eureka/,http://eureka-server3:8763/eureka/
3 Redis Cluster
4.1 Redis-Cluster簡介
4.1.1 什麼是Redis-Cluster
爲何要搭建Redis集羣。Redis是在內存中保存數據的,而我們的電腦一般內存都不大,這也就意味着Redis不適合存儲大數據,適合存儲大數據的是Hadoop生態系統的Hbase或者是MogoDB。Redis更適合處理高併發,一臺設備的存儲能力是很有限的,但是多臺設備協同合作,就可以讓內存增大很多倍,這就需要用到集羣。
Redis集羣搭建的方式有多種,例如使用客戶端分片、Twemproxy、Codis等,但從redis 3.0之後版本支持redis-cluster集羣,它是Redis官方提出的解決方案,Redis-Cluster採用無中心結構,每個節點保存數據和整個集羣狀態,每個節點都和其他所有節點連接。其redis-cluster架構圖如下:
客戶端與 redis 節點直連,不需要中間 proxy 層.客戶端不需要連接集羣所有節點連接集羣中任何一個可用節點即可。
所有的 redis 節點彼此互聯(PING-PONG 機制),內部使用二進制協議優化傳輸速度和帶寬.
4.1.2分佈存儲機制-槽
(1)redis-cluster 把所有的物理節點映射到[0-16383]slot 上,cluster 負責維護
node<->slot<->value
(2)Redis 集羣中內置了 16384 個哈希槽,當需要在 Redis 集羣中放置一個 key-value 時,redis 先對 key 使用 crc16 算法算出一個結果,然後把結果對 16384 求餘數,這樣每個key 都會對應一個編號在 0-16383 之間的哈希槽,redis 會根據節點數量大致均等的將哈希槽映射到不同的節點。
例如三個節點:槽分佈的值如下:
SERVER1: 0-5460
SERVER2: 5461-10922
SERVER3: 10923-16383
4.1.3容錯機制-投票
(1)選舉過程是集羣中所有master參與,如果半數以上master節點與故障節點通信超過(cluster-node-timeout),認爲該節點故障,自動觸發故障轉移操作. 故障節點對應的從節點自動升級爲主節點
(2)什麼時候整個集羣不可用(cluster_state:fail)?
如果集羣任意master掛掉,且當前master沒有slave.集羣進入fail狀態,也可以理解成集羣的slot映射[0-16383]不完成時進入fail狀態.
4.2搭建Redis-Cluster
4.2.1搭建要求
需要 6 臺 redis 服務器。搭建僞集羣。
需要 6 個 redis 實例。
需要運行在不同的端口 7001-7006
4.2.2準備工作
(1)安裝gcc 【此步省略】
Redis 是 c 語言開發的。安裝 redis 需要 c 語言的編譯環境。如果沒有 gcc 需要在線安裝。
yum install gcc-c++
(2)使用yum命令安裝 ruby (我們需要使用ruby腳本來實現集羣搭建)【此步省略】
yum install ruby
yum install rubygems
----- 知識點小貼士 -----
Ruby,一種簡單快捷的面向對象(面向對象程序設計)腳本語言,在20世紀90年代由日本人松本行弘(Yukihiro Matsumoto)開發,遵守GPL協議和Ruby License。它的靈感與特性來自於 Perl、Smalltalk、Eiffel、Ada以及 Lisp 語言。由 Ruby 語言本身還發展出了JRuby(Java平臺)、IronRuby(.NET平臺)等其他平臺的 Ruby 語言替代品。Ruby的作者於1993年2月24日開始編寫Ruby,直至1995年12月才正式公開發佈於fj(新聞組)。因爲Perl發音與6月誕生石pearl(珍珠)相同,因此Ruby以7月誕生石ruby(紅寶石)命名
RubyGems簡稱gems,是一個用於對 Ruby組件進行打包的 Ruby 打包系統
(3)將redis源碼包上傳到 linux 系統 ,解壓redis源碼包
(4)編譯redis源碼 ,進入redis源碼文件夾
make
看到以下輸出結果,表示編譯成功
(5)創建目錄/usr/local/redis-cluster目錄, 安裝6個redis實例,分別安裝在以下目錄
/usr/local/redis-cluster/redis-1
/usr/local/redis-cluster/redis-2
/usr/local/redis-cluster/redis-3
/usr/local/redis-cluster/redis-4
/usr/local/redis-cluster/redis-5
/usr/local/redis-cluster/redis-6
以第一個redis實例爲例,命令如下
make install PREFIX=/usr/local/redis-cluster/redis-1
出現此提示表示成功,按此方法安裝其餘5個redis實例
(6)複製配置文件 將 /redis-3.0.0/redis.conf 複製到redis下的bin目錄下
[root@localhost redis-3.0.0]# cp redis.conf /usr/local/redis-cluster/redis-1/bin
[root@localhost redis-3.0.0]# cp redis.conf /usr/local/redis-cluster/redis-2/bin
[root@localhost redis-3.0.0]# cp redis.conf /usr/local/redis-cluster/redis-3/bin
[root@localhost redis-3.0.0]# cp redis.conf /usr/local/redis-cluster/redis-4/bin
[root@localhost redis-3.0.0]# cp redis.conf /usr/local/redis-cluster/redis-5/bin
[root@localhost redis-3.0.0]# cp redis.conf /usr/local/redis-cluster/redis-6/bin
4.2.3配置集羣
(1)修改每個redis節點的配置文件redis.conf
修改運行端口爲7001 (7002 7003 …)
將cluster-enabled yes 前的註釋去掉(632行)
集羣:
6個節點
3主
3從
1)創建6個節點 7001-7006
2)開啓集羣
3)串聯集羣[將集羣鏈接到一起]
(2)啓動每個redis實例
以第一個實例爲例,命令如下
cd /usr/local/redis-cluster/redis-1/bin/
./redis-server redis.conf
把其餘的5個也啓動起來,然後查看一下是不是都啓動起來了
[root@localhost ~]# ps -ef | grep redis
root 15776 15775 0 08:19 pts/1 00:00:00 ./redis-server *:7001 [cluster]
root 15810 15784 0 08:22 pts/2 00:00:00 ./redis-server *:7002 [cluster]
root 15831 15813 0 08:23 pts/3 00:00:00 ./redis-server *:7003 [cluster]
root 15852 15834 0 08:23 pts/4 00:00:00 ./redis-server *:7004 [cluster]
root 15872 15856 0 08:24 pts/5 00:00:00 ./redis-server *:7005 [cluster]
root 15891 15875 0 08:24 pts/6 00:00:00 ./redis-server *:7006 [cluster]
root 15926 15895 0 08:24 pts/7 00:00:00 grep redis
(3)上傳redis-3.0.0.gem ,安裝 ruby用於搭建redis集羣的腳本。
[root@localhost ~]# gem install redis-3.0.0.gem
Successfully installed redis-3.0.0
1 gem installed
Installing ri documentation for redis-3.0.0...
Installing RDoc documentation for redis-3.0.0...
(4)使用 ruby 腳本搭建集羣。
進入redis源碼目錄中的src目錄 執行下面的命令 redis-trib.rb
ruby工具,可以實現Redis集羣,create
創建集羣,--replicas
創建主從關係 1:是否隨機創建(是)。
./redis-trib.rb create --replicas 1 192.168.25.140:7001 192.168.25.140:7002 192.168.25.140:7003
192.168.25.140:7004 192.168.25.140:7005 192.168.25.140:7006
出現下列提示信息
>>> Creating cluster
Connecting to node 192.168.25.140:7001: OK
Connecting to node 192.168.25.140:7002: OK
Connecting to node 192.168.25.140:7003: OK
Connecting to node 192.168.25.140:7004: OK
Connecting to node 192.168.25.140:7005: OK
Connecting to node 192.168.25.140:7006: OK
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
192.168.25.140:7001
192.168.25.140:7002
192.168.25.140:7003
Adding replica 192.168.25.140:7004 to 192.168.25.140:7001
Adding replica 192.168.25.140:7005 to 192.168.25.140:7002
Adding replica 192.168.25.140:7006 to 192.168.25.140:7003
M: 1800237a743c2aa918ade045a28128448c6ce689 192.168.25.140:7001
slots:0-5460 (5461 slots) master
M: 7cb3f7d5c60bfbd3ab28800f8fd3bf6de005bf0d 192.168.25.140:7002
slots:5461-10922 (5462 slots) master
M: 436e88ec323a2f8bb08bf09f7df07cc7909fcf81 192.168.25.140:7003
slots:10923-16383 (5461 slots) master
S: c2a39a94b5f41532cd83bf6643e98fc277c2f441 192.168.25.140:7004
replicates 1800237a743c2aa918ade045a28128448c6ce689
S: b0e38d80273515c84b1a01820d8ecee04547d776 192.168.25.140:7005
replicates 7cb3f7d5c60bfbd3ab28800f8fd3bf6de005bf0d
S: 03bf6bd7e3e6eece5a02043224497c2c8e185132 192.168.25.140:7006
replicates 436e88ec323a2f8bb08bf09f7df07cc7909fcf81
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join....
>>> Performing Cluster Check (using node 192.168.25.140:7001)
M: 1800237a743c2aa918ade045a28128448c6ce689 192.168.25.140:7001
slots:0-5460 (5461 slots) master
M: 7cb3f7d5c60bfbd3ab28800f8fd3bf6de005bf0d 192.168.25.140:7002
slots:5461-10922 (5462 slots) master
M: 436e88ec323a2f8bb08bf09f7df07cc7909fcf81 192.168.25.140:7003
slots:10923-16383 (5461 slots) master
M: c2a39a94b5f41532cd83bf6643e98fc277c2f441 192.168.25.140:7004
slots: (0 slots) master
replicates 1800237a743c2aa918ade045a28128448c6ce689
M: b0e38d80273515c84b1a01820d8ecee04547d776 192.168.25.140:7005
slots: (0 slots) master
replicates 7cb3f7d5c60bfbd3ab28800f8fd3bf6de005bf0d
M: 03bf6bd7e3e6eece5a02043224497c2c8e185132 192.168.25.140:7006
slots: (0 slots) master
replicates 436e88ec323a2f8bb08bf09f7df07cc7909fcf81
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
4.3連接Redis-Cluster
4.3.1客戶端工具連接
Redis-cli 連接集羣:
redis-cli -p ip地址 -p 端口 -c
-c:代表連接的是 redis 集羣
測試值的存取:
(1)從本地連接到集羣redis 使用7001端口 加 -c 參數
(2)存入name值爲abc ,系統提示此值被存入到了7002端口所在的redis (槽是5798)
(3)提取name的值,可以提取。
(4)退出(quit)
(5)再次以7001端口進入 ,不帶-c
(6)查詢name值,無法獲取,因爲值在7002端口的redis上
(7)我們以7002端口進入,獲取name值發現是可以獲取的,而以其它端口進入均不能獲取
4.3.2 springboot連接redis集羣
(1)創建工程 ,打包方式jar包,命名爲:changgou-redis-demo
(2)添加redis起步依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.itheima</groupId>
<artifactId>changgou-redis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>changgou-redis-demo</name>
<description>redis</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3)配置application.yml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/changgou_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: itcast
application:
name: redis-demo
#redis配置
# rabbitmq:
# addresses: 192.168.25.130:5672,192.168.25.134:5672
# username: guest
# password: guest
redis:
cluster:
nodes:
- 192.168.25.153:7001
- 192.168.25.153:7002
- 192.168.25.153:7003
- 192.168.25.153:7004
- 192.168.25.153:7005
- 192.168.25.153:7006
server:
ssl:
enabled: false
port: 9008
mybatis:
configuration:
map-underscore-to-camel-case: true
(4)創建測試類進行測試:
package com.itheima.changgouredisdemo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ChanggouRedisDemoApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void contextLoads() {
redisTemplate.boundValueOps("key111").set("123");
System.out.println(redisTemplate.boundValueOps("key111").get());
}
}
4.4 Redis的持久化
4.4.1 redis的持久化介紹
Redis的數據都放在內存中。如果機器掛掉,內存的數據就不存在,數據不能恢復,嚴重影響使用。那麼redis本身給我們提供了持久化機制。即時出現這樣的問題,也能恢復數據。接下來我們來看下redis的兩種持久化方
4.4.2 開啓RDB
RDB: 快照形式 (定期數據保存磁盤中)會產生一個dump.rdb文件,redis默認開啓了RDB的持久化方式。
特點:會存在數據丟失,性能較好,用於數據備份。
如圖:有一個文件產生
如圖:redis.conf中 的默認的RDB的配置:
解釋:
# 在 900 秒內最少有 1 個 key 被改動,或者 300 秒內最少有 10 個 key 被改動,又或者 60 秒內最少有 1000 個 key 被改動,以上三個條件隨便滿足一個,就觸發一次保存操作。
# if(在60秒之內有10000個keys發生變化時){
# 進行鏡像備份
# }else if(在300秒之內有10個keys發生了變化){
# 進行鏡像備份
# }else if(在900秒之內有1個keys發生了變化){
# 進行鏡像備份
# }
4.4.3 開啓AOF
AOF : append only file . 所有對redis的操作命令記錄在.aof文件中,如果想恢復數據,重新加載文件,執行文件中的命令即可。默認的情況下 redis沒有開啓,要想開啓,必須配置。
特點:每秒保存,數據完整性比較好,耗費性能。
開啓AOF: 如圖 去掉註釋
配置 AOF的執行策略:
always:總是執行
everysec:每秒鐘執行(默認)
no:不執行。
如果隨着時間的推移,AOF文件中的數據越來越大,所以需要進行重寫也就是壓縮。
如圖所示:自動壓縮的比例爲:
100:上一次AOF文件達到100%的時候進行壓縮
64mb :壓縮時最小的文件大小。
4.4.4 模式的抉擇應用場景介紹
AOF 和RDB對比:
命令 | RDB | AOF |
---|---|---|
啓動優先級 | 低 | 高 |
體積 | 小 | 大 |
恢復速度 | 快 | 慢 |
數據安全性 | 丟數據 | 根據策略決定 |
RDB的最佳策略:
- 關閉
- 集中管理(用於備份數據)
- 主從模式,從開。
AOF的最佳策略:
- 建議 開 每秒刷盤->aof日誌文件中
- AOF重寫集中管理
最佳的策略:
- 小分片(max_memery 4G左右)
- 監控機器的負載
4.6 Redis哨兵模式
Redis在使用過程中服務器毫無徵兆的宕機,是一個麻煩的事情,如何保證備份的機器是原始服務器的完整備份呢?這時候就需要哨兵和複製。
Sentinel(哨兵)可以管理多個Redis服務器,它提供了監控,提醒以及自動的故障轉移的功能,
Replication(複製)則是負責讓一個Redis服務器可以配備多個備份的服務器。
Redis也是利用這兩個功能來保證Redis的高可用的
4.6.1 Redis的主從複製實現高可用
如圖,通過主從架構,一個主節點,兩個從節點。
通過手動監控的方式,監控master的宕機,以及出現故障將故障轉移的方式可以做到高可用。
比如:如果主節點宕機,我們手動監控到主節點的宕機,並將某一個Slave變成主節點。 但是這樣話,如何手動監控也是很麻煩的事情。所以使用sentinel機制就可以解決了這個問題,Sentinel(哨兵)是Redis 的高可用性解決方案。
- 它能自動進行故障轉移。
- 客戶端連接sentinel,不需要關係具體的master。
- 當master地址改變時由sentinel更新到客戶端。
架構原理如圖:
1.多個sentinel 發現並確認master有問題。
2.sentinel內部選舉領導
3.選舉出slave作爲新的master
4.通知其餘的slave成爲新master的slave
5.通知客戶端 主從變化
6.如果老的master重新復活,那麼成爲新的master的slave
要實現上邊的功能的主要細節主要有以下三個定時任務:
- 每10秒,哨兵會向master和slave發送INFO命令(目的就是監控每一個節點信息)
- 每2秒,哨兵會向master庫和slave的頻道(sentinel:hello)發送自己的信息 (sentinel節點通過__sentinel__:hello頻道進行信息交換,比如加入哨兵集羣,分享自己節點數據)
- 每1秒,哨兵會向master和slave以及其他哨兵節點發送PING命令(目的就是 redis節點的狀態監控,還有領導選舉,主備切換選擇等)
策略總結:
1.儘量爲 每一個節點部署一個哨兵
2.哨兵也要搭建集羣(防止哨兵單點故障)
3.每一個節點都同時設置quorum的值超過半數(N/2)+1
面試常問的問題:
主從複製,以及哨兵 和集羣之間區別。
主從複製 是redis實現高可用的一個策略。將會有主節點 和從節點,從節點的數據完整的從主節點中複製一份。
哨兵:當系統節點異常宕機的時候,開發者可以手動進行故障轉移恢復,但是手動比較麻煩,所以通過哨兵機制自動進行監控和恢復。爲了解決哨兵也會單點故障的問題,可以建立哨兵集羣。
集羣:即使使用哨兵,redis每個實例也是全量存儲,每個redis存儲的內容都是完整的數據,浪費內存且有木桶效應。爲了最大化利用內存,可以採用集羣,就是分佈式存儲。這個時候可以使用redis集羣。將不同的數據分配到不同的節點中,這樣就可以橫向擴展,擴容。
4.7 redis緩存擊穿問題解決
4.7.1 什麼是緩存擊穿
如圖:
1. 當用戶根據key 查詢數據時,先查詢緩存,如果緩存有命中,返回,
2. 但是如果緩存沒有命中直接穿過緩存層,訪問數據層 如果有,則存儲指緩存,
3. 但是同樣如果沒有命中,(也就是數據庫中也沒有數據)直接返回用戶,但是不緩存
這就是緩存的穿透。如果某一個key 請求量很大,但是存儲層也沒有數據,大量的請求都會達到存儲層就會造成數據庫壓力巨大,有可能宕機的情況。
4.7.2 緩存擊穿的解決方案
如圖:
1.當緩存中沒有命中的時候,從數據庫中獲取
2.當數據庫中也沒有數據的時候,我們直接將null 作爲值設置redis中的key上邊。
3.此時如果沒有數據,一般情況下都需要設置一個過期時間,例如:5分鐘失效。(爲了避免過多的KEY 存儲在redis中)
4.返回給用戶,
5.用戶再次訪問時,已經有KEY了。此時KEY的值是null而已,這樣就可以在緩存中命中,解決了緩存穿透的問題。
(2)例如:代碼如下:
注意:緩存空對象會有兩個問題:
第一,空值做了緩存,意味着緩存層中存了更多的鍵,需要更多的內存空間 ( 如果是攻擊,問題更嚴重 ),比較有效的方法是針對這類數據設置一個較短的過期時間,讓其自動剔除。
第二,緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有一定影響。例如過期時間設置爲 5分鐘,如果此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時可以利用消息系統或者其他方式清除掉緩存層中的空對象。
4.8 Redis緩存雪崩問題解決(作業)
4.8.1 什麼是緩存雪崩
如果緩存集中在一段時間內失效,發生大量的緩存穿透,所有的查詢都落在數據庫上,造成了緩存雪崩。
4.8.2 如何解決
這個沒有完美解決辦法,但可以分析用戶行爲,儘量讓失效時間點均勻分佈。
- 限流 加鎖排隊
在緩存失效後,通過對某一個key加鎖或者是隊列 來控制key的線程訪問的數量。例如:某一個key 只允許一個線程進行 操作。
- 限流
在緩存失效後,某一個key 做count統計限流,達到一定的閾值,直接丟棄,不再查詢數據庫。例如:令牌桶算法。等等。
- 數據預熱
在緩存失效應當儘量避免某一段時間,可以先進行數據預熱,比如某些熱門的商品。提前在上線之前,或者開放給用戶使用之前,先進行loading 緩存中,這樣用戶使用的時候,直接從緩存中獲取。要注意的是,要更加業務來進行過期時間的設置 ,儘量均勻。
- 做緩存降級(二級緩存策略)
當分佈式緩存失效的時候,可以採用本地緩存,本地緩存沒有再查詢數據庫。這種方式,可以避免很多數據分佈式緩存沒有,就直接打到數據庫的情況。
4.8.3二級緩存解決雪崩的案例
分析:
基本的思路:通過redis緩存+mybatis的二級緩存整合ehcache來實現。
EhCache 是一個純Java的進程內緩存框架,具有快速、精幹等特點,是Hibernate中默認的CacheProvider。
(1)在原來的工程中加入依賴
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.1.0</version>
</dependency>
(2)創建dao的接口 使用XML的方式
@Mapper
public interface TbUserMapper {
/**
* 根據用戶名查詢用戶的信息
* @param username
* @return
*/
public TbUser findOne(String username);
}
(3)創建TbUserMapper.xml,如圖加入echache的配置 開啓二級緩存
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.changgouredisdemo.dao.TbUserMapper">
<!--加入使用緩存-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<!--緩存自創建日期起至失效時的間隔時間一個小時-->
<property name="timeToIdleSeconds" value="3600"/>
<!--緩存創建以後,最後一次訪問緩存的日期至失效之時的時間間隔一個小時-->
<property name="timeToLiveSeconds" value="3600"/>
<!--設置在緩存中保存的對象的最大的個數,這個按照業務進行配置-->
<property name="maxEntriesLocalHeap" value="1000"/>
<!--設置在磁盤中最大實體對象的個數-->
<property name="maxEntriesLocalDisk" value="10000000"/>
<!--緩存淘汰算法-->
<property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>
<select id="findOne" resultType="com.itheima.changgouredisdemo.pojo.TbUser" parameterType="string">
SELECT * from tb_user where username = #{username}
</select>
</mapper>
(4)配置application.yml
mybatis:
configuration:
map-underscore-to-camel-case: true
# 指定mapper映射文件目錄
mapper-locations: classpath:mapper/*Mapper.xml
(5)創建controller service 來進行測試:
package com.itheima.changgouredisdemo.controller;
import com.itheima.changgouredisdemo.pojo.TbUser;
import com.itheima.changgouredisdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 描述
*
* @author 三國的包子
* @version 1.0
* @package com.itheima.changgouredisdemo.controller *
* @since 1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/findOne/{username}")
public TbUser findOne(@PathVariable String username) {
return userService.findOne(username);
}
}
package com.itheima.changgouredisdemo.service.impl;
import com.itheima.changgouredisdemo.dao.TbUserMapper;
import com.itheima.changgouredisdemo.pojo.TbUser;
import com.itheima.changgouredisdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 描述
*
* @author 三國的包子
* @version 1.0
* @package com.itheima.changgouredisdemo.service.impl *
* @since 1.0
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private TbUserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
@Override
public TbUser findOne(String username) {
TbUser user = (TbUser) redisTemplate.boundValueOps(username).get();
if (redisTemplate.hasKey(username)) {
return user;
} else {
//沒有key 數據庫中查詢
TbUser one = userMapper.findOne(username);
System.out.println("第一次查詢數據庫");
redisTemplate.boundValueOps(username).set(one);
if (one == null) {
redisTemplate.expire(username, 30, TimeUnit.SECONDS);
}
return one;
}
}
}
(6)測試:
已知: 數據庫中有zhangsan
準備好redis
瀏覽器輸入
http://localhost:9008/user/findOne/zhangsan
效果:
redis中: 也有數據
此時:修改數據庫的數據zhangsan爲zhangsan5,並清空redis緩存。
再次輸入瀏覽器地址:
http://localhost:9008/user/findOne/zhangsan
此時數據庫總已經沒有zhangsan,但是效果卻是
說明數據從二級緩存中也就是本地緩存中獲取到了,測試成功。
第5章 RabbitMQ集羣
在使用RabbitMQ的過程中,如果只有一個節點,但是一旦單機版宕機,服務不可用,影響比較嚴重,所以這裏我們演示下如何搭建rabbitmq的集羣,集羣就能避免單點故障的問題。
RabbitMQ 集羣分爲兩種 普通集羣 和 鏡像集羣
5.1普通集羣
以兩個節點(rabbit01、rabbit02)爲例來進行說明。
rabbit01和rabbit02兩個節點僅有相同的元數據,即隊列的結構,但消息實體只存在於其中一個節點rabbit01(或者rabbit02)中。
當消息進入rabbit01節點的Queue後,consumer從rabbit02節點消費時,RabbitMQ會臨時在rabbit01、rabbit02間進行消息傳輸,把A中的消息實體取出並經過B發送給consumer。
所以consumer應儘量連接每一個節點,從中取消息,即對於同一個邏輯隊列,要在多個節點建立物理Queue;否則無論consumer連rabbit01或rabbit02,出口總在rabbit01,會產生瓶頸。
當rabbit01節點故障後,rabbit02節點無法取到rabbit01節點中還未消費的消息實體。如果做了消息持久化,那麼得等rabbit01節點恢復,然後纔可被消費;如果沒有持久化的話,就會產生消息丟失的現象。
5.2 鏡像集羣
在普通集羣的基礎上,把需要的隊列做成鏡像隊列,消息實體會主動在鏡像節點間同步,而不是在客戶端取數據時臨時拉取,也就是說多少節點消息就會備份多少份。該模式帶來的副作用也很明顯,除了降低系統性能外,如果鏡像隊列數量過多,加之大量的消息進入,集羣內部的網絡帶寬將會被這種同步通訊大大消耗掉。所以在對可靠性要求較高的場合中適用
由於鏡像隊列之間消息自動同步,且內部有選舉master機制,即使master節點宕機也不會影響整個集羣的使用,達到去中心化的目的,從而有效的防止消息丟失及服務不可用等問題。
5.3 集羣搭建
準備兩臺虛擬機,虛擬機的地址分別爲192.168.25.130, 192.168.25.134
設置192.168.25.130的別名爲A:修改如下文件 名稱改爲A
vi /etc/hostname
設置192.168.25.134的別名爲B,修改如下文件 名稱改爲B
vi /etc/hostname
修改每一個虛擬機的hosts
vi /etc/hosts
加入如下代碼:
192.168.25.130 A
192.168.25.134 B
如下圖所示:
重啓系統。
5.3.1 安裝erlang
192.168.25.130上安裝如下:
輸入命令,下載erlang:
wget http://www.rabbitmq.com/releases/erlang/erlang-18.1-1.el6.x86_64.rpm
安裝erlang,root用戶使用rpm安裝:
rpm -ihv erlang-18.1-1.el6.x86_64.rpm
5.3.2 下載和安裝RabbitMQ
下載RabbitMQ:
wget https://github.com/rabbitmq/rabbitmq-server/releases/download/rabbitmq_v3_6_12/rabbitmq-server-3.6.12-1.el6.noarch.rpm
安裝RabbitMQ,
- 先安裝socat
yum install -y socat
- root用戶使用rpm安裝 安裝rabbitmq
rpm -ihv rabbitmq-server-3.6.12-1.el6.noarch.rpm
驗證是否安裝成功:
輸入命令:
[root@A ~]# rabbitmq-server
出現如下界面,說明OK
守護進程啓動方式:
rabbitmq-server -detached
其他相關指令:
啓動服務:rabbitmq-server -detached
查看狀態:rabbitmqctl status
關閉服務:rabbitmqctl stop
列出角色:rabbitmqctl list_users
改密碼: rabbimqctlchange_password {username} {newpassword}
刪除用戶: rabbitmqctl delete_user xx
5.3.3配置RabbitMQ
(1)創建rabbitmq的賬號
[root@A ~]# rabbitmqctl add_user admin admin
(2)將admin賬號賦予管理員權限
[root@A ~]# rabbitmqctl set_user_tags admin administrator
(3)設置權限
[root@A ~]# rabbitmqctl set_permissions -p '/' admin '.*' '.*' '.*'
安裝可視化界面
rabbitmq-plugins enable rabbitmq_management
5.4 配置rabbitmq集羣
5.4.1 安裝rabbitmq
安裝方法如上邊的方法進行在 192.168.25.134上進行
5.4.2 保證相同的erlang cookie
erlang.cookie是erlang分佈式的token文件,集羣內所有的設備要持有相同的.erlang.cookie文件才允許彼此通信。
scp /var/lib/rabbitmq/.erlang.cookie root@B:/var/lib/rabbitmq
[root@A rabbitmq]# scp /var/lib/rabbitmq/.erlang.cookie root@A:/var/lib/rabbitmq
The authenticity of host 'a (192.168.25.134)' can't be established.
ECDSA key fingerprint is cd:17:22:d1:10:81:d6:98:be:ad:cc:13:e0:0c:c3:03.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'a,192.168.25.134' (ECDSA) to the list of known hosts.
root@a's password:
.erlang.cookie 100% 20 0.0KB/s 00:00
[root@A rabbitmq]#
5.4.5 運行各個rabbitmq節點
先停止每一個服務器的服務
輸入命令停止0
[root@A rabbitmq]# rabbitmqctl stop
再輸入命令啓動:
[root@A rabbitmq]# rabbitmq-server -detached
5.4.6 查看狀態
[root@A rabbitmq]# rabbitmqctl cluster_status
5.4.7 將節點連接成集羣
每一個服務器都關閉防火牆:
[root@B ~]# systemctl stop firewalld
B加入到集羣A中,在B中執行如下命令:
[root@B ~]# rabbitmqctl stop_app
[root@B ~]# rabbitmqctl join_cluster rabbit@A
[root@B ~]# rabbitmqctl start_app
A不需要加入到自己
查看集羣狀態:在任意服務器上執行:
rabbitmqctl cluster_status
添加集羣之前:
添加集羣之後:
5.4.8 配置單web插件查看集羣
在A服務器執行命令:
rabbitmq-plugins enable rabbitmq_management
在B服務器執行命令:
rabbitmq-plugins enable rabbitmq_management
在瀏覽器中輸入A服務器的rabbitmq服務器地址:
http://192.168.25.130:15672
輸入用戶名和密碼登錄,如果登錄失敗,採取如下措施:
- 執行命令 設置guest爲管理員
rabbitmqctl set_user_tags guest administrator
- 執行命令 設置權限
rabbitmqctl set_permissions -p / guest '.*' '.*' '.*'
- 在rabbitmq的配置文件目錄下創建一個rabbitmq.config文件
cd /etc/rabbitmq
vi rabbitmq.config
輸入如下文本,並保存:
[{rabbit, [{loopback_users, []}]}].
重新啓動服務:
service rabbitmq-server restart
再次輸入地址 出現界面,說明集羣搭建成功。
如有需要,同理 對服務B的配置如上邊一樣配置一遍即可。
5.5 配置鏡像隊列
如上,我們已經搭建好了集羣,但是並不能做到高可用,所以需要配置升級爲鏡像隊列。
在任意的節點(A或者B)中執行如下命令:
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
解釋
rabbitmqctl set_policy
用於設置策略
ha-all
表示設置爲鏡像隊列並策略爲所有節點可用 ,意味着 隊列會被(同步)到所有的節點,當一個節點被加入到集羣中時,也會同步到新的節點中,此策略比較保守,性能相對低,對接使用半數原則方式設置(N/2+1),例如:有3個結點 此時可以設置爲:ha-two 表示同步到2個結點即可。
"^" 表示針對的隊列的名稱的正則表達式,此處表示匹配所有的隊列名稱
'{"ha-mode":"all"}' 設置一組key/value的JSON 設置爲高可用模式 匹配所有exchange
此時查看web管理界面:添加一個隊列itcast111,如下圖已經可以出現結果爲有一個結點,並且是ha-all模式(鏡像隊列模式)
5.6 springboot整合rabbitmq集羣使用(作業)
修改原來redis測試的項目,
(1)加入pom.xml依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
(2)配置application.yml
spring:
rabbitmq:
addresses: 192.168.25.130:5672,192.168.25.134:5672
username: guest
password: guest
(3)創建controller
package com.itheima.changgouredisdemo.controller;
import com.itheima.changgouredisdemo.pojo.TbUser;
import com.itheima.changgouredisdemo.service.UserService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 描述
*
* @author 三國的包子
* @version 1.0
* @package com.itheima.changgouredisdemo.controller *
* @since 1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/findOne/{username}")
public TbUser findOne(@PathVariable String username) {
return userService.findOne(username);
}
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping("/send")
public String ok(){
rabbitTemplate.convertAndSend("itcast111","hello");
return "ok";
}
}
(4)設置監聽:
代碼:
package com.itheima.changgouredisdemo.listener;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 描述
*
* @author 三國的包子
* @version 1.0
* @package com.itheima.changgouredisdemo.listener *
* @since 1.0
*/
@Component
@RabbitListener(queues = "itcast111")
public class Lisnter {
@RabbitHandler
public void getInfo(String message){
System.out.println("123131");
System.out.println(message);
}
}
測試,在瀏覽器中輸入:
http://localhost:9008/user/send
測試當宕機一臺rabbitmq也能發送成功。
如下效果。