dubbo負載均衡策略和集群容錯策略都有哪些?
負載均衡策略
-
Random
(隨機)默認情況下,dubbo是random load balance隨機調用實現負載均衡,可以對provider不同實例設置不同的權重,會按照權重來負載均衡,權重越大分配流量越高,一般就用這個默認的就可以了
-
Round Robin
(輪詢)還有roundrobin loadbalance,這個的話默認就是均勻地將流量打到各個機器上去,但是如果各個機器的性能不一樣,容易導致性能差的機器負載過高。所以此時需要調整權重,讓性能差的機器承載權重小一些,流量少一些
-
Least Active
(最少活躍)這個就是自動感知一下,如果某個機器性能越差,那么接收的請求越少,越不活躍,此時就會給不活躍的性能差的機器更少的請求
-
Consistent Hash
(一致性哈希)一致性Hash算法,相同參數的請求一定分發到一個provider上去,provider掛掉的時候,會基於虛擬節點均勻分配剩余的流量,抖動不會太大
集群容錯策略
-
Fail over
失敗自動切換,自動重試其他機器,默認就是這個,常見於讀操作
-
Fail fast
一次調用失敗就立即失敗,常見於寫操作
-
Fail safe
出現異常時忽略掉,常用於不重要的接口調用,比如記錄日志
-
Fail back
失敗了后台自動記錄請求,然后定時重發,比較適合於寫消息隊列這種
-
Forking
並行調用多個provider,只要一個成功就立即返回
-
Broadcacst
逐個調用所有的provider
動態代理策略呢?
默認使用javassist動態字節碼生成,創建代理類
但是可以通過spi擴展機制配置自己的動態代理策略
dubbo的spi思想是什么?
簡單來說就是service provider interface,說白了是什么意思呢,比如你有個接口,現在這個接口有3個實現類,那么在系統運行的時候對這個接口到底選擇哪個實現類呢?這就需要spi了,需要根據指定的配置或者是默認的配置,去找到對應的實現類加載進來,然后用這個實現類的實例對象。
接口A -> 實現A1,實現A2,實現A3
配置一下,接口A = 實現A2
在系統實際運行的時候,會加載你的配置,用實現A2實例化一個對象來提供服務
比如說你要通過jar包的方式給某個接口提供實現,然后你就在自己jar包的META-INF/services/目錄下放一個跟接口同名的文件,里面指定接口的實現里是自己這個jar包里的某個類。ok了,別人用了一個接口,然后用了你的jar包,就會在運行的時候通過你的jar包的那個文件找到這個接口該用哪個實現類。
這是jdk提供的一個功能。
比如說你有個工程A,有個接口A,接口A在工程A里是沒有實現類的 -> 系統在運行的時候,怎么給接口A選擇一個實現類呢?
你就可以自己搞一個jar包,META-INF/services/,放上一個文件,文件名就是接口名,接口A,接口A的實現類=com.xxx.service.實現類A2。讓工程A來依賴你的這個jar包,然后呢在系統運行的時候,工程A跑起來,對接口A,就會掃描自己依賴的所有的jar包,在每個jar里找找,有沒有META-INF/services文件夾,如果有,在里面找找,有沒有接口A這個名字的文件,如果有在里面找一下你指定的接口A的實現是你的jar包里的哪個類?
SPI機制,一般來說用在哪兒?插件擴展的場景,比如說你開發的是一個給別人使用的開源框架,如果你想讓別人自己寫個插件,插到你的開源框架里面來,擴展某個功能。
經典的思想體現,大家平時都在用,比如JDBC
java定義了一套jdbc的接口,但是java是沒有提供jdbc的實現類
但是實際上項目跑的時候,要使用JDBC接口的哪些實現類呢?一般來說,們要根據自己使用的數據庫,比如MySQL,你就將mysql-jdbc-connector.jar,引入進來;oracle,你就將oracle-jdbc-connector.jar,引入進來。
在系統跑的時候,碰到你使用JDBC的接口,他會在底層使用你引入的那個jar中提供的實現類
但是dubbo也用了spi思想,不過沒有用jdk的spi機制,是自己實現的一套spi機制。
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
Protocol接口,dubbo要判斷一下,在系統運行的時候,應該選用這個Protocol接口的哪個實現類來實例化對象來使用呢?
他會去找一個你配置的Protocol,他就會將你配置的Protocol實現類,加載到jvm中來,然后實例化對象,就用你的那個Protocol實現類就可以了
微內核,可插拔,大量的組件,Protocol負責rpc調用的東西,你可以實現自己的rpc調用組件,實現Protocol接口,給自己的一個實現類即可。
這行代碼就是dubbo里大量使用的,就是對很多組件,都是保留一個接口和多個實現,然后在系統運行的時候動態根據配置去找到對應的實現類。如果你沒配置,那就走默認的實現好了,沒問題。
@SPI("dubbo") public interface Protocol { int getDefaultPort(); @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy(); }
在dubbo自己的jar里,在/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol文件中:
dubbo = com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol http = com.alibaba.dubbo.rpc.protocol.http.HttpProtocol hessian = com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
所以說,這就看到了dubbo的spi機制默認是怎么玩兒的了,其實就是Protocol接口,@SPI(“dubbo”)說的是,通過SPI機制來提供實現類,實現類是通過dubbo作為默認key去配置文件里找到的,配置文件名稱與接口全限定名一樣的,通過dubbo作為key可以找到默認的實現了就是com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
。
dubbo的默認網絡通信協議,就是dubbo
協議,用的DubboProtocol
如果想要動態替換掉默認的實現類,需要使用@Adaptive
接口,Protocol
接口中,有兩個方法加了@Adaptive
注解,就是說那倆接口會被代理實現。
比如這個Protocol
接口搞了倆@Adaptive
注解標注了方法,在運行的時候會針對Protocol
生成代理類,這個代理類的那倆方法里面會有代理代碼,代理代碼會在運行的時候動態根據url
中的protocol
來獲取那個key,默認是dubbo
,你也可以自己指定,你如果指定了別的key,那么就會獲取別的實現類的實例了。
通過這個url中的參數不通,就可以控制動態使用不同的組件實現類
好吧,那下面來說說怎么來自己擴展dubbo中的組件
自己寫個工程,要是那種可以打成jar包的,里面的src/main/resources
目錄下,搞一個META-INF/services,里面放個文件叫:com.alibaba.dubbo.rpc.Protocol
,文件里搞一個my=com.xxx.MyProtocol
。自己把jar弄到nexus私服里去。
然后自己搞一個dubbo provider工程,在這個工程里面依賴你自己搞的那個jar,然后在spring配置文件里給個配置:
<dubbo:protocol name=”my” port=”20000” />
這個時候provider啟動的時候,就會加載到們jar包里的my=com.xxx.MyProtocol
這行配置里,接着會根據你的配置使用你定義好的MyProtocol了,這個就是簡單說明一下,你通過上述方式,可以替換掉大量的dubbo內部的組件,就是扔個你自己的jar包,然后配置一下即可。
dubbo里面提供了大量的類似上面的擴展點,就是說,你如果要擴展一個東西,只要自己寫個jar,讓你的consumer或者是provider工程,依賴你的那個jar,在你的jar里指定目錄下配置好接口名稱對應的文件,里面通過key=實現類。
如何基於dubbo進行服務治理、服務降級、失敗重試以及超時重試?
服務治理
-
調用鏈路自動生成
一個大型的分布式系統,或者說是用現在流行的微服務架構來說吧,分布式系統由大量的服務組成。那么這些服務之間互相是如何調用的?調用鏈路是啥?說實話,幾乎到后面沒人搞的清楚了,因為服務實在太多了,可能幾百個甚至幾千個服務。
那就需要基於dubbo做的分布式系統中,對各個服務之間的調用自動記錄下來,然后自動將各個服務之間的依賴關系和調用鏈路生成出來,做成一張圖,顯示出來,大家才可以看到對吧。
服務A -> 服務B -> 服務C
-> 服務E
-> 服務D
-> 服務F
-> 服務W
-
服務訪問壓力以及時長統計
需要自動統計各個接口和服務之間的調用次數以及訪問延時,而且要分成兩個級別。一個級別是接口粒度,就是每個服務的每個接口每天被調用多少次,TP50,TP90,TP99,三個檔次的請求延時分別是多少;第二個級別是從源頭入口開始,一個完整的請求鏈路經過幾十個服務之后,完成一次請求,每天全鏈路走多少次,全鏈路請求延時的TP50,TP90,TP99,分別是多少。
這些東西都搞定了之后,后面才可以來看當前系統的壓力主要在哪里,如何來擴容和優化
-
服務可用性
服務分層(避免循環依賴),調用鏈路失敗監控和報警,服務鑒權,每個服務的可用性的監控(接口調用成功率?幾個9?)99.99%,99.9%,99%
服務降級
比如說服務A調用服務B,結果服務B掛掉了,服務A重試幾次調用服務B,還是不行,直接降級,走一個備用的邏輯,給用戶返回響應
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <dubbo:application name="dubbo-consumer" /> <dubbo:registry address="zookeeper://127.0.0.1:2181" /> <dubbo:reference id="fooService" interface="com.test.service.FooService" timeout="10000" check="false" mock="return null"> </dubbo:reference> </beans>
可以將mock修改為true,然后在跟接口同一個路徑下實現一個Mock類,命名規則是接口名稱加Mock后綴。然后在Mock類里實現自己的降級邏輯。
public class HelloServiceMock implements HelloService { public void sayHello() { // 降級邏輯 } }
失敗重試
所謂失敗重試,就是consumer調用provider要是失敗了,比如拋異常了,此時應該是可以重試的,或者調用超時了也可以重試。
<dubbo:reference id="xxxx" interface="xx" check="true" async="false" retries="3" timeout="2000"/>
某個服務的接口,要耗費5s,你這邊不能干等着,你這邊配置了timeout之后,等待2s,還沒返回,直接就撤了,不能干等你
如果是超時了,timeout就會設置超時時間;如果是調用失敗了自動就會重試指定的次數
你就結合你們公司的具體的場景來說說你是怎么設置這些參數的,timeout,一般設置為200ms,們認為不能超過200ms還沒返回
retries,3次,設置retries,還一般是在讀請求的時候,比如你要查詢個數據,你可以設置個retries,如果第一次沒讀到,報錯,重試指定的次數,嘗試再次讀取2次
分布式服務接口的冪等性如何設計(比如不能重復扣款)?
保證冪等性主要三點
-
對於每個請求必須有一個唯一的標識
-
每次處理完請求之后,必須有一個記錄標識這個請求處理過了
-
每次接收請求需要進行判斷之前是否處理過的邏輯處理
如何自己設計一個類似dubbo的rpc框架?
-
注冊中心
使用能夠存儲鍵值對的某種存儲工具,比如zk redis 數據庫等等,和ip:port:接口名和服務提供方的具體實現類綁定起來,然后服務端啟動socket監聽
-
動態代理服務消費者
消費端引入服務端提供的接口,對該接口進行動態代理,讓其代理對象使用注冊中心提供的IP:PORT:接口名,向服務端發起socket長連接,寫入需要調用的方法名,方法參數,然后遠程調用服務端的該方法,服務端將結果通過socket寫入返回.
-
自定義通信協議
在socket數據傳輸過程中,可以定義傳輸協議,是使用json還是java serializable還是hessian還是protobuf等協議,這一協議,服務提供者和消費者都要遵守
-
請求負載均衡
假如有多個服務提供者提供相同的服務,需要分散消費者的遠程調用請求.
分布式服務框架
zk都有哪些使用場景?
-
分布式協調(zk節點Watch機制)
這個其實是zk很經典的一個用法,簡單來說,就好比,你A系統發送個請求到mq,然后B消息消費之后處理了。那A系統如何知道B系統的處理結果?用zk就可以實現分布式系統之間的協調工作。A系統發送請求之后可以在zk上對某個節點的值注冊個監聽器,一旦B系統處理完了就修改zk那個節點的值,A立馬就可以收到通知,完美解決。
-
分布式鎖(創建有序臨時節點)
對某一個數據連續發出兩個修改操作,兩台機器同時收到了請求,但是只能一台機器先執行另外一個機器再執行。那么此時就可以使用zk分布式鎖,一個機器接收到了請求之后先獲取zk上的一把分布式鎖,就是可以去創建一個znode,接着執行操作;然后另外一個機器也嘗試去創建那個znode,結果發現自己創建不了,因為被別人創建了。。。。那只能等着,等第一個機器執行完了自己再執行
-
配置信息管理
通過客戶端向zk注冊一個配置節點監聽器,一旦zk上的配置節點新增了配置或者修改了配置,客戶端都會受到zk發來的消息
-
高可用性
比如hadoop、hdfs、yarn等很多大數據系統,都選擇基於zk來開發HA高可用機制,就是一個重要進程一般會做主備兩個,主進程掛了立馬通過zk感知到切換到備用進程
分布式鎖
一般實現分布式鎖都有哪些方式?
-
redis單機實現(setNX)
最普通的實現方式,如果就是在redis里創建一個key算加鎖
SET my:lock 隨機值 NX PX 30000,這個命令就ok,這個的NX的意思就是只有key不存在的時候才會設置成功,PX 30000的意思是30秒后鎖自動釋放。別人創建的時候如果發現已經有了就不能加鎖了。
釋放鎖就是刪除key,但是一般可以用lua腳本刪除,判斷value一樣才刪除:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
為啥要用隨機值呢?因為如果某個客戶端獲取到了鎖,但是阻塞了很長時間才執行完,此時可能已經自動釋放鎖了,此時可能別的客戶端已經獲取到了這個鎖,要是你這個時候直接刪除
key
的話會有問題,所以得用隨機值加上面的
lua
腳本來釋放鎖
-
redis分布式實現(RedLock算法) -----不推薦使用
在Redis的分布式環境中,們假設有N個Redis master。這些節點完全互相獨立,不存在主從復制或者其他集群協調機制。們確保將在N個實例上使用與在Redis單實例下相同方法獲取和釋放鎖。現在們假設有5個Redis master節點,同時們需要在5台服務器上面運行這些Redis實例,這樣保證他們不會同時都宕掉。
為了取到鎖,客戶端應該執行以下操作:
-
獲取當前Unix時間,以毫秒為單位。
-
依次嘗試從5個實例,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當向Redis請求獲取鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間為10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該盡快嘗試去另外一個Redis實例請求獲取鎖。
-
客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(N/2+1,這里是3個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖才算獲取成功。
-
如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
-
如果因為某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖(即便某些Redis實例根本就沒有加鎖成功,防止某些節點獲取到鎖但是客戶端沒有得到響應而導致接下來的一段時間不能被重新獲取鎖)。
-
-
zookeeper實現
zk分布式鎖,其實可以做的比較簡單,就是某個節點嘗試創建臨時znode,此時創建成功了就獲取了這個鎖;這個時候別的客戶端來創建鎖會失敗,只能注冊個監聽器監聽這個鎖。釋放鎖就是刪除這個znode,一旦釋放掉就會通知客戶端,然后有一個等待着的客戶端就可以再次重新枷鎖
使用zk來設計分布式鎖可以嗎?
package com.zookeeper.java.distributed_lock; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.ZooKeeper; import java.util.*; import java.util.concurrent.CountDownLatch; /** * zk實現分布式鎖 * * @author zhuliang * @date 2019/6/19 12:17 */ public class DistributedLock { private ZooKeeper zooKeeper; private String root = "/LOCKS"; private String lockId; private int sessionTimeout; private byte[] data = {1, 2}; private CountDownLatch latch = new CountDownLatch(1); public DistributedLock() throws Exception { this.zooKeeper = ZookeeperFactory.getInstance(); this.sessionTimeout = ZookeeperFactory.getSessionTimeout(); } public boolean lock() { //創建新的臨時有序節點 將其自動生成的序號返回值作為 鎖id try { lockId = zooKeeper.create(root + "/", data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); System.out.println(Thread.currentThread().getName() + "-> 成功創建的lock節點[" + lockId + "] 開始競爭鎖"); //獲取root下所有的子節點 List<String> children = zooKeeper.getChildren(root, true); //利用treeSet排序特性將其排序 SortedSet<String> sortedSet = new TreeSet<>(); //並將其元素前面拼接上父節點路徑 for (String s : children) { sortedSet.add(root + "/" + s); } //獲取最小的節點 如果最小的節點存在 且等於lockId 則可以獲取鎖 if (sortedSet.first().equals(lockId)) { System.out.println(Thread.currentThread().getName() + "-> 成功獲得鎖 lock節點[" + lockId + "]"); return true; } //如果不等於lockId SortedSet<String> lessThanLockId = sortedSet.headSet(lockId); if (!lessThanLockId.isEmpty()) { //獲取比當前lockId小的上一個節點 其實這個preLockId就是正在被使用的鎖的id String preLockId = lessThanLockId.last(); //然后給這個正在被使用的鎖 添加一個watcher 當這個鎖被調用delete,get,set的時候就會觸發watch時間 zooKeeper.exists(preLockId, new LockWatcher(latch)); // latch.await(sessionTimeout, TimeUnit.MILLISECONDS); latch.await(); //上面這段代碼意味着如果會話超時或者節點被刪除了 System.out.println(Thread.currentThread().getName() + "-> 成功獲得鎖[" + lockId + "]"); } return true; } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return false; } public boolean unlock() { System.out.println(Thread.currentThread().getName() + "-> 開始施放鎖[" + lockId + "]"); try { System.out.println("節點[" + lockId + "]成功被刪除"); zooKeeper.delete(lockId, -1); return true; } catch (InterruptedException e) { e.printStackTrace(); } catch (KeeperException e) { e.printStackTrace(); } return false; } public static void main(String[] args) { CountDownLatch latch = new CountDownLatch(10); Random random = new Random(); for (int i = 0; i < 10; i++) { new Thread(() -> { DistributedLock lock = null; try { lock = new DistributedLock(); latch.countDown(); latch.await(); lock.lock(); Thread.sleep(random.nextInt(3000)); } catch (Exception e) { e.printStackTrace(); } finally { if (lock != null) { lock.unlock(); } } }).start(); } } } /* * 最終結果 獲得鎖->釋放鎖 是按順序進行的 * Thread-3-> 成功創建的lock節點[/LOCKS/0000000090] 開始競爭鎖 Thread-2-> 成功創建的lock節點[/LOCKS/0000000091] 開始競爭鎖 Thread-7-> 成功創建的lock節點[/LOCKS/0000000093] 開始競爭鎖 Thread-6-> 成功創建的lock節點[/LOCKS/0000000094] 開始競爭鎖 Thread-0-> 成功創建的lock節點[/LOCKS/0000000092] 開始競爭鎖 Thread-4-> 成功創建的lock節點[/LOCKS/0000000095] 開始競爭鎖 Thread-1-> 成功創建的lock節點[/LOCKS/0000000096] 開始競爭鎖 Thread-8-> 成功創建的lock節點[/LOCKS/0000000097] 開始競爭鎖 Thread-9-> 成功創建的lock節點[/LOCKS/0000000098] 開始競爭鎖 Thread-5-> 成功創建的lock節點[/LOCKS/0000000099] 開始競爭鎖 Thread-3-> 成功獲得鎖 lock節點[/LOCKS/0000000090] Thread-3-> 開始施放鎖[/LOCKS/0000000090] 節點[/LOCKS/0000000090]成功被刪除 Thread-2-> 成功獲得鎖[/LOCKS/0000000091] Thread-2-> 開始施放鎖[/LOCKS/0000000091] 節點[/LOCKS/0000000091]成功被刪除 Thread-0-> 成功獲得鎖[/LOCKS/0000000092] Thread-0-> 開始施放鎖[/LOCKS/0000000092] 節點[/LOCKS/0000000092]成功被刪除 Thread-7-> 成功獲得鎖[/LOCKS/0000000093] Thread-7-> 開始施放鎖[/LOCKS/0000000093] 節點[/LOCKS/0000000093]成功被刪除 Thread-6-> 成功獲得鎖[/LOCKS/0000000094] Thread-6-> 開始施放鎖[/LOCKS/0000000094] 節點[/LOCKS/0000000094]成功被刪除 Thread-4-> 成功獲得鎖[/LOCKS/0000000095] Thread-4-> 開始施放鎖[/LOCKS/0000000095] 節點[/LOCKS/0000000095]成功被刪除 Thread-1-> 成功獲得鎖[/LOCKS/0000000096] Thread-1-> 開始施放鎖[/LOCKS/0000000096] 節點[/LOCKS/0000000096]成功被刪除 Thread-8-> 成功獲得鎖[/LOCKS/0000000097] Thread-8-> 開始施放鎖[/LOCKS/0000000097] 節點[/LOCKS/0000000097]成功被刪除 Thread-9-> 成功獲得鎖[/LOCKS/0000000098] Thread-9-> 開始施放鎖[/LOCKS/0000000098] 節點[/LOCKS/0000000098]成功被刪除 Thread-5-> 成功獲得鎖[/LOCKS/0000000099] Thread-5-> 開始施放鎖[/LOCKS/0000000099] 節點[/LOCKS/0000000099]成功被刪除 */
這兩種分布式鎖的實現方式哪種效率比較高?
redis分布式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能
zk分布式鎖,獲取不到鎖,注冊個監聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷較小
另外一點就是,如果是redis獲取鎖的那個客戶端bug了或者掛了,那么只能等待超時時間之后才能釋放鎖;而zk的話,因為創建的是臨時znode,只要客戶端掛了,znode就沒了,此時就自動釋放鎖
redis分布式鎖比較麻煩,遍歷上鎖,計算時間等等。。。zk的分布式鎖語義清晰實現簡單
所以先不分析太多的東西,就說這兩點,個人實踐認為zk的分布式鎖比redis的分布式鎖牢靠、而且模型簡單易用
分布式事務
-
兩階段提交(2PC)
兩階段提交(Two-phase Commit,2PC),通過引入協調者(Coordinator)來協調參與者的行為,並最終決定這些參與者是否要真正執行事務。
1. 運行過程
1.1 准備階段
協調者詢問參與者事務是否執行成功,參與者發回事務執行結果。
1.2 提交階段
如果事務在每個參與者上都執行成功,事務協調者發送通知讓參與者提交事務;否則,協調者發送通知讓參與者回滾事務。
需要注意的是,在准備階段,參與者執行了事務,但是還未提交。只有在提交階段接收到協調者發來的通知后,才進行提交或者回滾。
2. 存在的問題
2.1 同步阻塞 所有事務參與者在等待其它參與者響應的時候都處於同步阻塞狀態,無法進行其它操作。
2.2 單點問題 協調者在 2PC 中起到非常大的作用,發生故障將會造成很大影響。特別是在階段二發生故障,所有參與者會一直等待狀態,無法完成其它操作。
2.3 數據不一致 在階段二,如果協調者只發送了部分 Commit 消息,此時網絡發生異常,那么只有部分參與者接收到 Commit 消息,也就是說只有部分參與者提交了事務,使得系統數據不一致。
2.4 太過保守 任意一個節點失敗就會導致整個事務失敗,沒有完善的容錯機制。
-
補償事務(TCC)
TCC 其實就是采用的補償機制,其核心思想是:針對每個操作,都要注冊一個與其對應的確認和補償(撤銷)操作。它分為三個階段:
-
Try 階段主要是對業務系統做檢測及資源預留
-
Confirm 階段主要是對業務系統做確認提交,Try階段執行成功並開始執行 Confirm階段時,默認 Confirm階段是不會出錯的。即:只要Try成功,Confirm一定成功。
-
Cancel 階段主要是在業務執行錯誤,需要回滾的狀態下執行的業務取消,預留資源釋放。
舉個例子,假入 Bob 要向 Smith 轉賬,思路大概是: 們有一個本地方法,里面依次調用
-
首先在 Try 階段,要先調用遠程接口把 Smith 和 Bob 的錢給凍結起來。
-
在 Confirm 階段,執行遠程調用的轉賬的操作,轉賬成功進行解凍。
-
如果第2步執行成功,那么轉賬成功,如果第二步執行失敗,則調用遠程凍結接口對應的解凍方法 (Cancel)。
優點: 跟2PC比起來,實現以及流程相對簡單了一些,但數據的一致性比2PC也要差一些
缺點: 缺點還是比較明顯的,在2,3步中都有可能失敗。TCC屬於應用層的一種補償方式,所以需要程序員在實現的時候多寫很多補償的代碼,在一些場景中,一些業務流程可能用TCC不太好定義及處理。
-
-
本地消息表(異步確保)
本地消息表與業務數據表處於同一個數據庫中,這樣就能利用本地事務來保證在對這兩個表的操作滿足事務特性,並且使用了消息隊列來保證最終一致性。
-
在分布式事務操作的一方完成寫業務數據的操作之后向本地消息表發送一個消息,本地事務能保證這個消息一定會被寫入本地消息表中。
-
之后將本地消息表中的消息轉發到 Kafka 等消息隊列中,如果轉發成功則將消息從本地消息表中刪除,否則繼續重新轉發。
-
在分布式事務操作的另一方從消息隊列中讀取一個消息,並執行消息中的操作。
優點: 一種非常經典的實現,避免了分布式事務,實現了最終一致性。
缺點: 消息表會耦合到業務系統中,如果沒有封裝好的解決方案,會有很多雜活需要處理。
-
-
可靠消息最終一致性方案(MQ 事務消息)
這個的意思,就是干脆不要用本地的消息表了,直接基於MQ來實現事務。比如阿里的RocketMQ就支持消息事務。
大概的意思就是:
1)A系統先發送一個prepared消息到mq,如果這個prepared消息發送失敗那么就直接取消操作別執行了
3)如果發送了確認消息,那么此時B系統會接收到確認消息,然后執行本地的事務
4)mq會自動定時輪詢所有prepared消息回調你的接口,問你,這個消息是不是本地事務處理失敗了,所有沒發送確認消息?那是繼續重試還是回滾?一般來說這里你就可以查下數據庫看之前本地事務是否執行,如果回滾了,那么這里也回滾吧。這個就是避免可能本地事務執行成功了,別確認消息發送失敗了。
5)這個方案里,要是系統B的事務失敗了咋辦?重試咯,自動不斷重試直到成功,如果實在是不行,要么就是針對重要的資金類業務進行回滾,比如B系統本地回滾后,想辦法通知系統A也回滾;或者是發送報警由人工來手工回滾和補償
這個還是比較合適的,目前國內互聯網公司大都是這么玩兒的,要不你舉用RocketMQ支持的,要不你就自己基於類似ActiveMQ?RabbitMQ?自己封裝一套類似的邏輯出來,總之思路就是這樣子的
優點: 實現了最終一致性,不需要依賴本地數據庫事務。
缺點: 實現難度大,主流MQ不支持,RocketMQ事務消息部分代碼也未開源。
-
最大努力通知方案
這個方案的大致意思就是:
1)系統A本地事務執行完之后,發送個消息到MQ
2)這里會有個專門消費MQ的最大努力通知服務,這個服務會消費MQ然后寫入數據庫中記錄下來,或者是放入個內存隊列也可以,接着調用系統B的接口
3)要是系統B執行成功就ok了;要是系統B執行失敗了,那么最大努力通知服務就定時嘗試重新調用系統B,反復N次,最后還是不行就放棄
-
阿里GTS(Global Transaction Service)
詳情見官網
分布式事務總結
其實用任何一個分布式事務的這么一個方案,都會導致你那塊兒代碼會復雜10倍。很多情況下,系統A調用系統B、系統C、系統D,們可能根本就不做分布式事務。如果調用報錯會打印異常日志。
每個月也就那么幾個bug,很多bug是功能性的,體驗性的,真的是涉及到數據層面的一些bug,一個月就幾個,兩三個?如果你為了確保系統自動保證數據100%不能錯,上了幾十個分布式事務,代碼太復雜;性能太差,系統吞吐量、性能大幅度下跌。
99%的分布式接口調用,不要做分布式事務,直接就是監控(發郵件、發短信)、記錄日志(一旦出錯,完整的日志)、事后快速的定位、排查和出解決方案、修復數據。
每個月,每隔幾個月,都會對少量的因為代碼bug,導致出錯的數據,進行人工的修復數據,自己臨時動手寫個程序,可能要補一些數據,可能要刪除一些數據,可能要修改一些字段的值。
比你做50個分布式事務,成本要來的低上百倍,低幾十倍
trade off,權衡,要用分布式事務的時候,一定是有成本,代碼會很復雜,開發很長時間,性能和吞吐量下跌,系統更加復雜更加脆弱反而更加容易出bug;好處,如果做好了,TCC、可靠消息最終一致性方案,一定可以100%保證你那快數據不會出錯。
1%,0.1%,0.01%的業務,資金、交易、訂單,們會用分布式事務方案來保證,會員積分、優惠券、商品信息,其實不要這么搞了
分布式會話
分布式session如何實現?
-
Tomcat + Redis
使用session的代碼跟以前一樣,還是基於tomcat原生的session支持即可,然后就是用一個叫做Tomcat RedisSessionManager的東西,讓所有們部署的tomcat都將session數據存儲到redis即可。
在tomcat的配置文件中,配置一下
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" /> <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager" host="{redis.host}" port="{redis.port}" database="{redis.dbnum}" maxInactiveInterval="60"/>
搞一個類似上面的配置即可,你看是不是就是用了RedisSessionManager,然后指定了redis的host和 port就ok了。
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" /> <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager" sentinelMaster="mymaster" sentinels="<sentinel1-ip>:26379,<sentinel2-ip>:26379,<sentinel3-ip>:26379" maxInactiveInterval="60"/>
還可以用上面這種方式基於redis哨兵支持的redis高可用集群來保存session數據,都是ok的
-
Spring Session + Redis
分布式會話的這個東西重耦合在tomcat中,如果要將web容器遷移成jetty,難道你重新把jetty都配置一遍嗎?
因為上面那種tomcat + redis的方式好用,但是會嚴重依賴於web容器,不好將代碼移植到其他web容器上去,尤其是你要是換了技術棧咋整?比如換成了spring cloud或者是spring boot之類的。還得好好思忖思忖。
所以現在比較好的還是基於java一站式解決方案,spring了。人家spring基本上包掉了大部分的們需要使用的框架了,spirng cloud做微服務了,spring boot做腳手架了,所以用sping session是一個很好的選擇。
pom.xml
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> <version>1.2.1.RELEASE</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.8.1</version> </dependency>
spring配置文件中
<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <property name="maxInactiveIntervalInSeconds" value="600"/> </bean> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxTotal" value="100" /> <property name="maxIdle" value="10" /> </bean> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy"> <property name="hostName" value="${redis_hostname}"/> <property name="port" value="${redis_port}"/> <property name="password" value="${redis_pwd}" /> <property name="timeout" value="3000"/> <property name="usePool" value="true"/> <property name="poolConfig" ref="jedisPoolConfig"/> </bean>
web.xml
<filter> <filter-name>springSessionRepositoryFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSessionRepositoryFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
示例代碼
@Controller @RequestMapping("/test") public class TestController { @RequestMapping("/putIntoSession") @ResponseBody public String putIntoSession(HttpServletRequest request, String username){ request.getSession().setAttribute("name", “leo”); return "ok"; } @RequestMapping("/getFromSession") @ResponseBody public String getFromSession(HttpServletRequest request, Model model){ String name = request.getSession().getAttribute("name"); return name; } }
上面的代碼就是ok的,給spring session配置基於redis來存儲session數據,然后配置了一個spring session的過濾器,這樣的話,session相關操作都會交給spring session來管了。接着在代碼中,就用原生的session操作,就是直接基於spring sesion從redis中獲取數據了。
實現分布式的會話,有很多種很多種方式,說的只不過比較常見的兩種方式,tomcat + redis早期比較常用;近些年,重耦合到tomcat中去,通過spring session來實現。
-
不使用session(使用JWT)
使用 JWT(Java Web Token)儲存用戶身份,然后再從數據庫或者 cache 中獲取其他的信息。這樣無論請求分配到哪個服務器都無所謂
如何設計一個高並發系統?
其實所謂的高並發,如果要理解這個問題呢,其實就得從高並發的根源出發,為什么會有高並發?
說的淺顯一點,很簡單,就是因為剛開始系統都是連接數據庫的,但是要知道數據庫支撐到每秒並發兩三千的時候,基本就快完了。所以才有說,很多公司,剛開始干的時候,技術比較low,結果業務發展太快,有的時候系統扛不住壓力就掛了。
當然會掛了,憑什么不掛?數據庫如果瞬間承載每秒5000,8000,甚至上萬的並發,一定會宕機,因為比如mysql就壓根兒扛不住這么高的並發量。
所以為啥高並發牛逼?就是因為現在用互聯網的人越來越多,很多app、網站、系統承載的都是高並發請求,可能高峰期每秒並發量幾千,很正常的。如果是什么雙十一了之類的,每秒並發幾萬幾十萬都有可能。
高並發架構
(1)系統拆分,將一個系統拆分為多個子系統,用dubbo來搞。然后每個系統連一個數據庫,這樣本來就一個庫,現在多個數據庫,不也可以抗高並發么。
(2)緩存,必須得用緩存。大部分的高並發場景,都是讀多寫少,那完全可以在數據庫和緩存里都寫一份,然后讀的時候大量走緩存不就得了。畢竟人家redis輕輕松松單機幾萬的並發啊。沒問題的。所以可以考慮考慮你的項目里,那些承載主要請求的讀場景,怎么用緩存來抗高並發。
(3)MQ,必須得用MQ。可能還是會出現高並發寫的場景,比如說一個業務操作里要頻繁搞數據庫幾十次,增刪改增刪改,瘋了。那高並發絕對搞掛你的系統,你要是用redis來承載寫那肯定不行,人家是緩存,數據隨時就被LRU了,數據格式還無比簡單,沒有事務支持。所以該用mysql還得用mysql啊。那咋辦?用MQ吧,大量的寫請求灌入MQ里,排隊慢慢玩兒,后邊系統消費后慢慢寫,控制在mysql承載范圍之內。所以你得考慮考慮你的項目里,那些承載復雜寫業務邏輯的場景里,如何用MQ來異步寫,提升並發性。MQ單機抗幾萬並發也是ok的,這個之前還特意說過。
(4)分庫分表,可能到了最后數據庫層面還是免不了抗高並發的要求,好吧,那么就將一個數據庫拆分為多個庫,多個庫來抗更高的並發;然后將一個表拆分為多個表,每個表的數據量保持少一點,提高sql跑的性能。
(5)讀寫分離,這個就是說大部分時候數據庫可能也是讀多寫少,沒必要所有請求都集中在一個庫上吧,可以搞個主從架構,主庫寫入,從庫讀取,搞一個讀寫分離。讀流量太多的時候,還可以加更多的從庫。
(6)Elasticsearch,可以考慮用es。es是分布式的,可以隨便擴容,分布式天然就可以支撐高並發,因為動不動就可以擴容加機器來抗更高的並發。那么一些比較簡單的查詢、統計類的操作,可以考慮用es來承載,還有一些全文搜索類的操作,也可以考慮用es來承載。
上面的6點,基本就是高並發系統肯定要干的一些事兒,大家可以仔細結合之前講過的知識考慮一下,到時候你可以系統的把這塊闡述一下,然后每個部分要注意哪些問題,之前都講過了,你都可以闡述闡述,表明對這塊是有點積累的。
說句實話,畢竟真正厲害的一點,不是在於弄明白一些技術,或者大概知道一個高並發系統應該長什么樣?其實實際上在真正的復雜的業務系統里,做高並發要遠遠比這個圖復雜幾十倍到上百倍。需要考慮,哪些需要分庫分表,哪些不需要分庫分表,單庫單表跟分庫分表如何join,哪些數據要放到緩存里去啊,放哪些數據再可以抗掉高並發的請求,需要完成對一個復雜業務系統的分析之后,然后逐步逐步的加入高並發的系統架構的改造,這個過程是務必復雜的,一旦做過一次,一旦做好了,在這個市場上就會非常的吃香。