在開發中,爲了下降單點壓力,一般會根據業務狀況進行分表分庫,將表分佈在不一樣的庫中(庫可能分佈在不一樣的機器上),可是一個業務場景可能會同時處理兩個表的操做。在這種場景下,事務的提交會變得相對複雜,由於多個節點(庫)的存在,可能存在部分節點提交失敗的狀況,即事務的ACID特性須要在各個不一樣的數據庫實例中保證。好比更新db1庫的A表時,必須同步更新db2庫的B表,兩個更新造成一個事務,要麼都成功,要麼都失敗。
那麼咱們如何利用mysql實現分佈式數據庫的事務呢?php
mysql好像是從5.0開始支持分佈式事務html
這裏先聲明兩個概念:mysql
資源管理器(resource manager):用來管理系統資源,是通向事務資源的途徑。數據庫就是一種資源管理器。資源管理還應該具備管理事務提交或回滾的能力。
事務管理器(transaction manager):事務管理器是分佈式事務的核心管理者。事務管理器與每一個資源管理器(resource
manager)進行通訊,協調並完成事務的處理。事務的各個分支由惟一命名進行標識。
mysql在執行分佈式事務(外部XA)的時候,mysql服務器至關於xa事務資源管理器,與mysql連接的客戶端至關於事務管理器。sql
分佈式事務原理:分段式提交
分佈式事務一般採用2PC協議,全稱Two Phase Commitment Protocol。該協議主要爲了解決在分佈式數據庫場景下,全部節點間數據一致性的問題。分佈式事務經過2PC協議將提交分紅兩個階段:數據庫
prepare;
commit/rollback
階段一爲準備(prepare)階段。即全部的參與者準備執行事務並鎖住須要的資源。參與者ready時,向transaction manager報告已準備就緒。
階段二爲提交階段(commit)。當transaction manager確認全部參與者都ready後,向全部參與者發送commit命令。
服務器
事務協調者transaction manager
由於XA 事務是基於兩階段提交協議的,因此須要有一個事務協調者(transaction manager)來保證全部的事務參與者都完成了準備工做(第一階段)。若是事務協調者(transaction manager)收到全部參與者都準備好的消息,就會通知全部的事務均可以提交了(第二階段)。MySQL 在這個XA事務中扮演的是參與者的角色,而不是事務協調者(transaction manager)。app
Mysql的XA事務分爲外部XA和內部XA
外部XA用於跨多MySQL實例的分佈式事務,須要應用層做爲協調者,通俗的說就是好比咱們在PHP中寫代碼,那麼PHP書寫的邏輯就是協調者。應用層負責決定提交仍是回滾,崩潰時的懸掛事務。MySQL數據庫外部XA能夠用在分佈式數據庫代理層,實現對MySQL數據庫的分佈式事務支持,例如開源的代理工具:網易的DDB,淘寶的TDDL等等。
內部XA事務用於同一實例下跨多引擎事務,由Binlog做爲協調者,好比在一個存儲引擎提交時,須要將提交信息寫入二進制日誌,這就是一個分佈式內部XA事務,只不過二進制日誌的參與者是MySQL自己。Binlog做爲內部XA的協調者,在binlog中出現的內部xid,在crash recover時,由binlog負責提交。(這是由於,binlog不進行prepare,只進行commit,所以在binlog中出現的內部xid,必定可以保證其在底層各存儲引擎中已經完成prepare)。分佈式
mysql xa事務的語法ide
一、首先要確保mysql開啓XA事務支持工具
SHOW VARIABLES LIKE '%xa%'
若是innodb_support_xa的值是ON就說明mysql已經開啓對XA事務的支持了。
若是不是就執行:
SET innodb_support_xa = ON
主要有:
XA START 'any_unique_id'; // 'any_unique_id' 是用戶給的,全局惟一在一臺mysql中開啓一個XA事務
XA END 'any_unique_id '; //標識XA事務的操做結束
XA PREPARE 'any_unique_id'; //告知mysql 準備提交這個xa事務
XA COMMIT 'any_unique_id'; //告知mysql提交這個xa事務
XA ROLLBACK 'any_unique_id'; //告知mysql回滾這個xa事務
XA RECOVER;//查看本機mysql目前有哪些xa事務處於prepare狀態
XA事務恢復
若是執行分佈式事務的mysql crash了,mysql 按照以下邏輯進行恢復:
a. 若是這個xa事務commit了,那麼什麼也不用作
b. 若是這個xa事務尚未prepare,那麼直接回滾它
c. 若是這個xa事務prepare了,還沒commit, 那麼把它恢復到prepare的狀態,由用戶去決定commit或rollback
當mysql crash後從新啓動以後,執行「XA RECOVER;」查看當前處於prepare狀態的xa事務,而後commit或rollback它們。
使用限制
a. XA事務和本地事務以及鎖表操做是互斥的
開啓了xa事務就沒法使用本地事務和鎖表操做
mysql> xa start 't1xa'; Query OK, 0 rows affected (0.04 sec)
mysql> begin; ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
mysql> lock table t1 read; ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
開啓了本地事務就沒法使用xa事務
mysql> begin; Query OK, 0 rows affected (0.00 sec)
mysql> xa start 'rrrr'; ERROR 1400 (XAE09): XAER_OUTSIDE: Some work is done outside global transaction
b. xa start 以後必須xa end, 不然不能執行xa commit 和xa rollback
因此若是在執行xa事務過程當中有語句出錯了,你也須要先xa end一下,而後才能xarollback。
注意事項
a. mysql只是提供了xa事務的接口,分佈式事務中的mysql實例之間是互相獨立的不感知的。 因此用戶必須
本身實現分佈式事務的調度器
b. xa事務有一些使用上的bug, 參考http://www.mysqlops.com/2012/02/24/mysql-xa-optimize.html
主要是
「MySQL數據庫的主備數據庫的同步,經過Binlog的複製完成。而Binlog是MySQL數據庫內部XA事務的協調者,而且MySQL數據庫爲binlog作了優化——binlog不寫prepare日誌,只寫commit日誌。
全部的參與節點prepare完成,在進行xa commit前crash。crash recover若是選擇commit此事務。因爲binlog在prepare階段未寫,所以主庫中看來,此分佈式事務最終提交了,可是此事務的操做並未 寫到binlog中,所以也就未能成功複製到備庫,從而致使主備庫數據不一致的狀況出現。
而crash recover若是選rollback, 那麼就會出現全局不一致(該分佈式事務對應的節點,部分已經提交,沒法回滾,而部分節點回滾。最終致使同一分佈式事務,在各參與節點,最終狀態不一致)」
參考的那篇blog中給出的辦法是修改mysql代碼,這個沒法在DBScale中使用。 因此可選的替代方案是不使用
主從複製進行備份,而是直接使用xa事務實現同步寫來做爲備份。
php+mysql實現分佈式事務案例
保證數據表是innodb的
//db_finance庫下
CREATE TABLE `t_user_account` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `username` varchar(255) NOT NULL DEFAULT '' COMMENT '用戶名', `money` int(11) NOT NULL DEFAULT '0' COMMENT '帳戶金額', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
//db_order庫下
CREATE TABLE `t_user_orders` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `username` varchar(255) NOT NULL DEFAULT '', `money` int(11) NOT NULL DEFAULT '0' COMMENT '訂單扣款金額', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8;
php代碼
$username = '溫柔的風'; $order_money = 100; $addOrder_success = addOrder($username,$order_money); $upAccount_success = updateAccount($username,$order_money); if($addOrder_success['state'] =="yes" && $upAccount_success['state']=="yes"){ commitdb($addOrder_success['xa']); commitdb1($upAccount_success['xa']); }else{ rollbackdb($addOrder_success['xa']); rollbackdb1($upAccount_success['xa']); } die; function addOrder ($username, $order_money){ $xa = uniqid(""); $sql_xa = "XA START '$xa'"; $db = Yii::app()->dborder_readonly; $db->createCommand($sql_xa)->execute(); $insert_sql = "INSERT INTO t_user_orders (`username`,`money`) VALUES ($username,$order_money)"; $id = $db->createCommand($insert_sql)->execute(); $db->createCommand("XA END '$xa'")->execute(); if ($id) { $db->createCommand("XA PREPARE '$xa'")->execute(); return ['state' => 'yes', 'xa' => $xa]; }else { return ['state' => 'no', 'xa' => $xa]; } } function updateAccount($username, $order_money){ $xa = uniqid(""); $sql_xa = "XA START '$xa'"; $db = Yii::app()->db_finance; $db->createCommand($sql_xa)->execute(); $sql = "update t_user_account set money=money-".$order_money." where username='$username'"; $id = $db->createCommand($sql)->execute(); $db->createCommand("XA END '$xa'")->execute(); if ($id) { $db->createCommand("XA PREPARE '$xa'")->execute(); return ['state' => 'yes', 'xa' => $xa]; }else { return ['state' => 'no', 'xa' => $xa]; } } //提交事務!
function commitdb($xa){ $db = Yii::app()->dborder_readonly; return $db->createCommand("XA COMMIT '$xa'")->execute(); } //回滾事務
function rollbackdb($xa){ $db = Yii::app()->dborder_readonly; return $db->createCommand("XA COMMIT '$xa'")->execute(); } //提交事務!
function commitdb1($xa){ $db = Yii::app()->db_finance; return $db->createCommand("XA COMMIT '$xa'")->execute(); } //回滾事務
function rollbackdb1($xa){ $db = Yii::app()->db_finance; return $db->createCommand("XA ROLLBACK '$xa'")->execute(); }