8.1 分佈式相關理論和算法 8.2 分佈式之zookeeper 8.3 SEATA分佈式事務源碼原理
8.1 分佈式相關理論和算法
分佈式負載均衡算法
1 輪詢算法
2 權重輪詢算法
3 一致性哈希
4 最少連接數
5 最快相應時間
等
都是字面意思不解釋了
分佈式ID算法
1 UUID (32位數的16進制數字,太長太佔空間,淘汰)
2 號段模式
3 滴滴 Tinyid
4 雪花算法
5 Redis實現
6 分佈式 zookeeper ID生成
7 分片表主鍵自增
等
分片表主鍵自增
可設置起始值,增長值不同來配置
但是每加如新機器,全部配置都得改動
每次只能向db獲取一個id,性能也較差
號段模式原理
批量提前提供ID,當應用時一一給具體業務分配ID
如提前提供0-1000一段ID,數據庫以樂觀鎖形式記錄號段長度和當前位置,
批量拿ID同時也能減小數據庫壓力
Tinyid原理
在號段模式基礎上,雙號段緩存優化。
隨機從兩個db上拿ID,一個db生成奇數ID,一個偶數ID,保證不會拿到重複值
雪花算法原理
64 bit long數字作爲 ID
1 bit 無意義(統一0爲正數)
41 bit 時間戳 (可表示69年時間)
5 bit 機房ID
5 bit 機器ID (機房ID+機器ID 可部署1024臺機器)
12 bit 序號 (相同1毫秒內的第幾個請求,id請求累加1序號)
經典基礎理論基礎
CAP
一致性(數據)
可用性(業務程序響應等)
容錯分區(網絡)
BASE
基本可用 (業務基本可用,支持分區失敗)
軟狀態 (允許數據延遲)
最終一致性
分佈式事務
TCC 截取此文章
TCC操作:
Try階段,嘗試執行業務,完成所有業務的檢查,實現一致性;預留必
須的業務資源,實現準隔離性。
Confirm階段:真正的去執行業務,不做任何檢查,僅適用Try階段預留的業務資
源,Confirm操作還要滿足冪等性。
Cancel階段:取消執行業務,釋放Try階段預留的業務資源,Cancel操作要滿足冪
等性。
2PC (幾乎所有主從中間件用到的方法,如zookeeper)
第三方等待所有程序預提交,收到一半以上的確認ACK信號則大家一起最終執行,
否則回滾
MQ
生產者執行時會修改發佈的名稱,讓訂閱的消費者無法消費。通過自我判斷成功
執行後修改回原先名稱,讓訂閱消費者繼續消費。具體將來kafka章節再說
分佈式一致性協議
Paxos算法 第一篇例子介紹的蠻好的
發佈/接受/學習
該算法主要是快速度選出老大
1 當接受者第一次遇到id 100的發佈者1時候,接受者將變成他鐵粉不再改動支持者
並且id小於100的發佈者理都不理,這樣會使得選舉很快。
2 當大於id100比如id200的發佈者2給那個接受者拉票時,接受者依然支持發佈者1
,並且id小於200的所有發佈者理都不理。這樣選舉速度更快了
3 當有發佈者拿到一半以上鐵粉就成爲老大了,其他發佈者也得跟隨這個老大
RAFT算法
主從,詳情看下面zk的ZAB算法,其實差不太多
8.2 分佈式之zookeeper
定義
一個擁有文件系統特點的數據庫
處理數據一致性問題的分佈式數據庫
具有發佈和訂閱功能的分佈式數據庫(watch機制)
集羣配置
1 vim 配置 zoo.cfg文件
clientPort xxxx //配置端口
server.1 = xxx
server.2 = xxx //配置集羣ip和port
2 啓動集羣
bin/zkServer.sh start//zk會自動選舉leader和follower
3 客戶端接入zk集羣
zk集羣下任意一節點bin目錄下,啓動一個客戶端接入即可
./zkCli.sh -server [client ip port]
zk常用命令
沒參考價值,略
Dubbo下zk註冊中心原理
圖片不知道怎麼正過來,我也很無奈
ZAB協議
領導者選舉,過半機制,2pc數據同步等共同組成ZAB協議
zk領導選舉
1 一開始大家都給自己投票
2 彼此之間互相比較zxid(即事務id),誰zxid大則這輪改票爲大的一方。如果zxid
一致則看誰myid(自己的id)大爲準
3 當一個節點得到超過一半數量的投票,則當選leader
領導選舉結束後又有新節點來,如果新節點zxid大於leader的zxid,則新節點需
要回滾,新節點必須和leader節點xzid同步,然後加入集羣
當leader掛掉,觸發領導者選舉
當有follwer掛掉導致領導者發現跟隨follower未超過一半,leader停止對外服務,
仍需要重新領導者選舉
zk如何處理數據
1 事務日誌按照順序添加
2 更新內存 DateTree (從內存中拿數據,不走磁盤,速度更快)
zk如何保證數據一致性
2pc事務一致性模式
1 leader生成事務
2 預提交
3 所有follower生成事務
4 ack返回
5 收到一半以上follower的ack則提交,否則回滾
6 提交成功,數據寫入內存DateTree
zk和redis分佈式鎖區別
1 zk有節點的watch機制,註冊個監聽器,不需要一直主動嘗試獲取鎖,性能開銷小
redis需要不停主動嘗試獲取鎖,性能開銷較大
2 redis拿到鎖的服務器掛了需要等到超時時間才釋放鎖,zk是臨時節點拿鎖,當服
務器掛了,立刻就會刪除臨時節點並且釋放鎖
8.3 SEATA分佈式事務源碼原理
場景
實際業務中,我們會遇到,訂單系統代碼層出錯,執行回滾。但是數據庫依然添加
了相關數據。爲什麼呢,因爲庫存系統和訂單系統是分開的。我們只回滾了訂單
系統,卻沒有回滾庫存系統。所以分佈式事務就產生了。
所以我們希望訂單系統能夠通知庫存系統一起跟着回滾
架構
RM(資源管理器) TM(事務管理器) TC(全局協調者)
RM 事務參與者
TM 全局事務管理者
TC:事務的協調者。保存全局事務,分支事務,全局鎖等記錄,然後通知各個RM回
滾或提交。
找的網圖
簡單實現思路
1 開啓全局事務
創建map
2 註冊分支事務
transactionIdMap.get(groupId).add(transactionId)
如果transactionType爲rollback則groupId下全部回滾
3 提交全局事務
sentMsg(groupId,"commit")
seata源碼基本思路
seata中維護三個表,branch_table/global_table/lock_table
branch_table維護了註冊的分支事務信息
global_table維護了註冊的全局事務信息
lock_table主要記錄業務表在被哪個寫事務執行上鎖,其他事務不能衝突執行
seata也有一個undo_log表,根據事務id(xid)以Json形式保存image鏡像到
undo_log內,將來可以根據鏡像進行2pc回滾
1 seata首先底層實現GlobalTranscationScanner.class的註解,這個註解類
implement AbstractAutoProxyCreator(SpringAOP頂級抽象父類接口) 和
InitializingBean(初始化RMClient和TMClient)
2 判斷是註解調用還是TCC調用等等,AOP生成代理類,內部織
入GlobalTransactionalInterceptor的註解攔截器
3 根據begin變量判斷是全局事務還是分支事務,TM和TC開啓信息交互
A 全局事務
1 getCurrentOrCreate創建TM
2 獲取全局事務信息
3 開啓全局事務beginTransaction(txInfo,tx);
4 調用JDBC往global_table內寫入全局事務數據
B 分支事務
beforeImage原先鏡像
根據增刪改查不同sql創建不同執行器,執行execute方法(需要從
lock_table中拿鎖,如果拿不到30ms拿一次,拿10次自旋拿鎖,一直拿不
到回滾到beforeImage)
afterImage(生成執行後鏡像)
prepareUndoLog(beforeImage,afterImage) 完整鏡像保存到undo_log
表內,可以供將來回滾使用
commit提交,branch_table寫入分支事務信息,與TC信息交互
4 提交本地事務並上傳成功或失敗狀態到TC
5 如果有本地事務提交失敗,全局事務鏈下所有事務全部回滾
簡單版seata源碼實現
//客戶端真正應用
@Service
public class DemoService{
@Autowired
DemoDao demoDao;
@GlobalTransaction(isStart=true) //我們自己實現的SEATA註解
@Transactional
public void test(){
//假設Dubbo下,demoDao,demoDao2很多個demoDao不在同一個系統
demoDao.inset("data1");
}
}
1 實現@GlobalTransaction(isStart=true)註解
@Target(~)
@Retention(~)
public @interface GlobalTransaction{
boolean isStart() default false;//控制是否是全局事務,默認不是
}
2 加切面
@Aspect //對應Seata攔截器
@Component
public class GlobalTransactionAspect implements Ordered{
@Around("@annoation(~.annoation.GlobalTransaction)")//所有加了這個註解的都要攔截走這個切面
public void invoke(ProceedigJoinPoint point){
before邏輯:
//拿point方法,拿GlobalTransaction對象,
//如果isStart是true,表示這是全局事務,開啓創建全局事務組 GlobalTransactionManager.createGroup()
point.proceed(); //spring切面下@Transaction切面真正方法
after邏輯
//創建註冊事務分支到事務組 TM GlobalTransactionalManager下方法
//如果proceed成功,創建的事務分支的lbTransaction.Type爲提交屬性,不成功設爲回滾屬性
//如果狀態回滾,TC通知全局事務組下所有事務全部回滾
}
}
RM資源管理層(提交本地事務)
public class LbConnection implements Connection{
通過構造方法接收Spring實現的Connection,利用它完成大多數重寫方法,
我們只需要改變提交方法邏輯
Connection connection;
LbTransaction lbTransaction;
@Override
~commit(){
new Thread{//不阻塞後面的註冊
lbTransaction的線程wait;
//等待NettyClientHandler設置transactionType然後執行本地提交或回滾
if(lbTransaction.transactionType爲commit本地提交){
connection.commit();
}else{
connection.rollback();
}
}
}
}
對Spring底層*javax.sql.DataSource.getConnection(...)加切面
@Around(...)
~ proceed ~{
Connection connection = point.proceed() //Spring本身實現類
return new LbConnection(connection); //SPring會使用我們自己重寫的實現類
}
事務管理者TM
public class GlobalTransactionManager{
NettyClient nettyClient;
ThreadLocal<LbTransaction> current = new ~;
ThreadLocal<String> currentGroupId = new ~;
Map<String,LbTransaction> LB_TRANSACTION_MAP = new ~;
//創建全局事務組
public String getOrCreateGroup{
1 利用分佈式ID算法創建一個唯一ID GroupId
2 以Json形式 nettyClient.send(JSONObject)發送給NettyServer
的NettyClientHandler保存事務信息
3 currentGroupId.set(groupId);
}
//註冊全局事務組
public ~~~~
設置事務Type,事務Id,GroupId等然後以Json形式傳給NettyServer
//提交全局事務組
//創建分支事務
public ~~~
創建lbTransaction,以Json形式 nettyClient.send(JSONObject)發送
給NettyServer
//註冊分支事務
public ~~~~
map.put(groupId,lbTransaction)
}
public class LbTransaction{
public String transactionId;
public TransactionType transactionType;
構造方法 ~
}
public class NettyClientHandler{
~ channelRead(~){
LbTransaction transaction = GlobalTransactionManager.getlbTransaction(groupId);
如果GlobalTransactionAspect下point.proceed執行成功,這裏會
傳入成功指令,設置transaction.transactionType爲commit。然後
我們配置的RM下LbConnection會執行commit方法本地提交,否則本地回滾
}
}