關于MySQL與Golan分布式事務經典的七種解決方案
前言:
隨著業(yè)務的快速發(fā)展、業(yè)務復雜度越來越高,幾乎每個公司的系統(tǒng)都會從單體走向分布式,特別是轉向微服務架構。隨之而來就必然遇到分布式事務這個難題。
這篇文章首先介紹了相關的基礎理論,然后總結了最經典的事務方案,最后給出了子事務亂序執(zhí)行(冪等、空補償、懸掛問題)的解決方案,分享給大家。
1、基礎理論
在講解具體方案之前,我們先了解一下分布式事務所涉及到的基礎理論知識。
我們拿轉賬作為例子,A需要轉100元給B,那么需要給A的余額-100元,給B的余額+100元,整個轉賬要保證,A-100和B+100同時成功,或者同時失敗??纯丛诟鞣N場景下,是如何解決這個問題的。
1.1 事務
把多條語句作為一個整體進行操作的功能,被稱為數(shù)據庫事務。數(shù)據庫事務可以確保該事務范圍內的所有操作都可以全部成功或者全部失敗。
事務具有 4 個屬性:原子性
、一致性
、隔離性
、持久性
。這四個屬性通常稱為 ACID 特性。
Atomicity
(原子性):一個事務中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環(huán)節(jié)。事務在執(zhí)行過程中發(fā)生錯誤,會被恢復到事務開始前的狀態(tài),就像這個事務從來沒有執(zhí)行過一樣。Consistency
(一致性):在事務開始之前和事務結束以后,數(shù)據庫的完整性沒有被破壞。完整性包括外鍵約束、應用定義的等約束不會被破壞。Isolation
(隔離性):數(shù)據庫允許多個并發(fā)事務同時對其數(shù)據進行讀寫和修改的能力,隔離性可以防止多個事務并發(fā)執(zhí)行時由于交叉執(zhí)行而導致數(shù)據的不一致。Durability
(持久性):事務處理結束后,對數(shù)據的修改就是永久的,即便系統(tǒng)故障也不會丟失。
假如我們的業(yè)務系統(tǒng)不復雜,可以在一個數(shù)據庫、一個服務內對數(shù)據進行修改,完成轉賬,那么,我們可以利用數(shù)據庫事務,保證轉賬業(yè)務的正確完成。
1.2 分布式事務
銀行跨行轉賬業(yè)務是一個典型分布式事務場景,假設A需要跨行轉賬給B
,那么就涉及兩個銀行的數(shù)據,無法通過一個數(shù)據庫的本地事務保證轉賬的ACID
,只能夠通過分布式事務來解決。
分布式事務就是指事務的發(fā)起者、資源及資源管理器和事務協(xié)調者分別位于分布式系統(tǒng)的不同節(jié)點之上。在上述轉賬的業(yè)務中,用戶A-100
操作和用戶B+100
操作不是位于同一個節(jié)點上。本質上來說,分布式事務就是為了保證在分布式場景下,數(shù)據操作的正確執(zhí)行。
分布式事務在分布式環(huán)境下,為了滿足可用性、性能與降級服務的需要,降低一致性與隔離性的要求,一方面遵循 BASE 理論(BASE相關理論,涉及內容非常多,感興趣的同學,可以參考BASE理論):
- 基本業(yè)務可用性(
Basic Availability
) - 柔性狀態(tài)(
Soft state
) - 最終一致性(
Eventual consistency
)
同樣的,分布式事務也部分遵循 ACID 規(guī)范:
- 原子性:嚴格遵循
- 一致性:事務完成后的一致性嚴格遵循;事務中的一致性可適當放寬
- 隔離性:并行事務間不可影響;事務中間結果可見性允許安全放寬
- 持久性:嚴格遵循
2、分布式事務的解決方案
由于分布式事務方案,無法做到完全的ACID
的保證,沒有一種完美的方案,能夠解決掉所有業(yè)務問題。因此在實際應用中,會根據業(yè)務的不同特性,選擇最適合的分布式事務方案。
2.1 兩階段提交/XA
XA是由X/Open
組織提出的分布式事務的規(guī)范,XA規(guī)范主要定義了(全局)事務管理器(TM)和(局部)資源管理器(RM)之間的接口。本地的數(shù)據庫如mysql
在XA中扮演的是RM角色
XA一共分為兩階段:
- 第一階段(
prepare
):即所有的參與者RM準備執(zhí)行事務并鎖住需要的資源。參與者ready
時,向TM報告已準備就緒。 - 第二階段 (
commit/rollback
):當事務管理者(TM)確認所有參與者(RM)都ready后,向所有參與者發(fā)送commit
命令。
目前主流的數(shù)據庫基本都支持XA事務,包括mysql
、oracle
、sqlserver
、postgre
XA 事務由一個或多個資源管理器(RM)、一個事務管理器(TM)和一個應用程序(ApplicationProgram
)組成。
這里的RM、TM、AP三個角色是經典的角色劃分,會貫穿后續(xù)Saga、Tcc等事務模式。
把上面的轉賬作為例子,一個成功完成的XA事務時序圖如下:
如果有任何一個參與者prepare
失敗,那么TM會通知所有完成prepare
的參與者進行回滾。
XA事務的特點是:
- 簡單易理解,開發(fā)較容易
- 對資源進行了長時間的鎖定,并發(fā)度低
如果讀者想要進一步研究XA
,go
語言以及PHP
、Python
、Java
、C#
、Node
等都可參考DTM
2.2 SAGA
Saga是這一篇數(shù)據庫論文sagas
提到的一個方案。其核心思想是將長事務拆分為多個本地短事務,由Saga事務協(xié)調器協(xié)調,如果正常結束那就正常完成,如果某個步驟失敗,則根據相反順序一次調用補償操作。
把上面的轉賬作為例子,一個成功完成的SAGA事務時序圖如下:
Saga
一旦到了Cancel
階段,那么Cancel
在業(yè)務邏輯上是不允許失敗了。如果因為網絡或者其他臨時故障,導致沒有返回成功,那么TM會不斷重試,直到Cancel
返回成功。
Saga事務的特點:
- 并發(fā)度高,不用像XA事務那樣長期鎖定資源
- 需要定義正常操作以及補償操作,開發(fā)量比XA大
- 一致性較弱,對于轉賬,可能發(fā)生A用戶已扣款,最后轉賬又失敗的情況
論文里面的SAGA
內容較多,包括兩種恢復策略,包括分支事務并發(fā)執(zhí)行,我們這里的討論,僅包括最簡單的SAGA
SAGA
適用的場景較多,長事務適用,對中間結果不敏感的業(yè)務場景適用
如果讀者想要進一步研究SAGA,可參考DTM,里面包括了SAGA成功、失敗回滾的例子,還包括各類網絡異常的處理。
2.3 TCC
關于 TCC
(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年發(fā)表的一篇名為《Life beyond Distributed Transactions:an Apostate's Opinion》的論文提出。
TCC分為3個階段:
Try
階段:嘗試執(zhí)行,完成所有業(yè)務檢查(一致性), 預留必須業(yè)務資源(準隔離性)Confirm
階段:確認執(zhí)行真正執(zhí)行業(yè)務,不作任何業(yè)務檢查,只使用 Try 階段預留的業(yè)務資源,Confirm
操作要求具備冪等設計,Confirm
失敗后需要進行重試。Cancel
階段:取消執(zhí)行,釋放 Try 階段預留的業(yè)務資源。Cancel
階段的異常和Confirm
階段異常處理方案基本上一致,要求滿足冪等設計。
把上面的轉賬作為例子,通常會在Try里面凍結金額,但不扣款,Confirm
里面扣款,Cancel
里面解凍金額,
一個成功完成的TCC事務時序圖如下:
TCC
的Confirm/Cancel
階段在業(yè)務邏輯上是不允許返回失敗的,如果因為網絡或者其他臨時故障,導致不能返回成功,TM會不斷的重試,直到Confirm/Cancel
返回成功。
TCC特點如下:
- 并發(fā)度較高,無長期資源鎖定。
- 開發(fā)量較大,需要提供
Try/Confirm/Cancel
接口。 - 一致性較好,不會發(fā)生
SAGA
已扣款最后又轉賬失敗的情況 TCC
適用于訂單類業(yè)務,對中間狀態(tài)有約束的業(yè)務
如果讀者想要進一步研究TCC
,可參考DTM
2.4 本地消息表
本地消息表這個方案最初是 ebay
架構師 Dan Pritchett
在 2008 年發(fā)表給 ACM
的文章。設計核心是將需要分布式處理的任務通過消息的方式來異步確保執(zhí)行。
大致流程如下:
寫本地消息和業(yè)務操作放在一個事務里,保證了業(yè)務和發(fā)消息的原子性,要么他們全都成功,要么全都失敗。
容錯機制:
- 扣減余額事務 失敗時,事務直接回滾,無后續(xù)步驟
- 輪序生產消息失敗, 增加余額事務失敗都會進行重試
本地消息表的特點:
- 長事務僅需要分拆成多個任務,使用簡單
- 生產者需要額外的創(chuàng)建消息表
- 每個本地消息表都需要進行輪詢
- 消費者的邏輯如果無法通過重試成功,那么還需要更多的機制,來回滾操作
適用于可異步執(zhí)行的業(yè)務,且后續(xù)操作無需回滾的業(yè)務
2.5 事務消息
在上述的本地消息表方案中,生產者需要額外創(chuàng)建消息表,還需要對本地消息表進行輪詢,業(yè)務負擔較重。阿里開源的RocketMQ 4.3
之后的版本正式支持事務消息,該事務消息本質上是把本地消息表放到RocketMQ
上,解決生產端的消息發(fā)送與本地事務執(zhí)行的原子性問題。
事務消息發(fā)送及提交:
發(fā)送消息(half消息)
服務端存儲消息,并響應消息的寫入結果
根據發(fā)送結果執(zhí)行本地事務(如果寫入失敗,此時half
消息對業(yè)務不可見,本地邏輯不執(zhí)行)
根據本地事務狀態(tài)執(zhí)行Commit
或者Rollback
(Commit
操作發(fā)布消息,消息對消費者可見)
正常發(fā)送的流程圖如下:
補償流程:
對沒有Commit/Rollback
的事務消息(pending
狀態(tài)的消息),從服務端發(fā)起一次“回查”
Producer收到回查消息,返回消息對應的本地事務的狀態(tài),為Commit
或者Rollback
事務消息方案與本地消息表機制非常類似,區(qū)別主要在于原先相關的本地表操作替換成了一個反查接口
事務消息特點如下:
- 長事務僅需要分拆成多個任務,并提供一個反查接口,使用簡單
- 消費者的邏輯如果無法通過重試成功,那么還需要更多的機制,來回滾操作
適用于可異步執(zhí)行的業(yè)務,且后續(xù)操作無需回滾的業(yè)務
2.6 最大努力通知
發(fā)起通知方通過一定的機制最大努力將業(yè)務處理結果通知到接收方。具體包括:
有一定的消息重復通知機制。因為接收通知方可能沒有接收到通知,此時要有一定的機制對消息重復通知。
消息校對機制。如果盡最大努力也沒有通知到接收方,或者接收方消費消息后要再次消費,此時可由接收方主動向通知方查詢消息信息來滿足需求。
前面介紹的的本地消息表和事務消息都屬于可靠消息,與這里介紹的最大努力通知有什么不同?
可靠消息一致性,發(fā)起通知方需要保證將消息發(fā)出去,并且將消息發(fā)到接收通知方,消息的可靠性關鍵由發(fā)起通知方來保證。
最大努力通知,發(fā)起通知方盡最大的努力將業(yè)務處理結果通知為接收通知方,但是可能消息接收不到,此時需要接收通知方主動調用發(fā)起通知方的接口查詢業(yè)務處理結果,通知的可靠性關鍵在接收通知方。
解決方案上,最大努力通知需要:
- 提供接口,讓接受通知放能夠通過接口查詢業(yè)務處理結果
- 消息隊列
ACK
機制,消息隊列按照間隔1min
、5min
、10min
、30min
、1h
、2h
、5h
、10h
的方式,逐步拉大通知間隔 ,直到達到通知要求的時間窗口上限。之后不再通知
最大努力通知適用于業(yè)務通知類型,例如微信交易的結果,就是通過最大努力通知方式通知各個商戶,既有回調通知,也有交易查詢接口
2.7 AT事務模式
這是阿里開源項目seata中的一種事務模式,在螞蟻金服也被稱為FMT。優(yōu)點是該事務模式使用方式,類似XA模式,業(yè)務無需編寫各類補償操作,回滾由框架自動完成,缺點也類似XA,存在較長時間的鎖,不滿足高并發(fā)的場景。從性能的角度看,AT模式會比XA更高一些,但也帶來了臟回滾這樣的新問題。
3、異常處理
在分布式事務的各個環(huán)節(jié)都有可能出現(xiàn)網絡以及業(yè)務故障等問題,這些問題需要分布式事務的業(yè)務方做到防空回滾,冪等,防懸掛三個特性。
3.1 異常情況
下面以TCC事務說明這些異常情況:
空回滾:
在沒有調用 TCC 資源 Try 方法的情況下,調用了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然后直接返回成功。
出現(xiàn)原因是當一個分支事務所在服務宕機或網絡異常,分支事務調用記錄為失敗,這個時候其實是沒有執(zhí)行Try階段,當故障恢復后,分布式事務進行回滾則會調用二階段的Cancel方法,從而形成空回滾。
冪等:
由于任何一個請求都可能出現(xiàn)網絡異常,出現(xiàn)重復請求,所以所有的分布式事務分支,都需要保證冪等性
懸掛:
懸掛就是對于一個分布式事務,其二階段 Cancel
接口比 Try 接口先執(zhí)行。
出現(xiàn)原因是在 RPC 調用分支事務try時,先注冊分支事務,再執(zhí)行RPC調用,如果此時 RPC 調用的網絡發(fā)生擁堵,RPC 超時以后,TM就會通知RM回滾該分布式事務,可能回滾完成后,Try 的 RPC 請求才到達參與者真正執(zhí)行。
下面看一個網絡異常的時序圖,更好的理解上述幾種問題
- 業(yè)務處理請求4的時候,
Cancel
在Try之前執(zhí)行,需要處理空回滾 - 業(yè)務處理請求6的時候,
Cancel
重復執(zhí)行,需要冪等 - 業(yè)務處理請求8的時候,Try在
Cancel
后執(zhí)行,需要處理懸掛
面對上述復雜的網絡異常情況,目前看到各家建議的方案都是業(yè)務方通過唯一鍵,去查詢相關聯(lián)的操作是否已完成,如果已完成則直接返回成功。相關的判斷邏輯較復雜,易出錯,業(yè)務負擔重。
3.2 子事務屏障
在項目https://github.com/yedf/dtm中,出現(xiàn)了一種子事務屏障技術,使用該技術,能夠達到這個效果,看示意圖:
所有這些請求,到了子事務屏障后:不正常的請求,會被過濾;正常請求,通過屏障。開發(fā)者使用子事務屏障之后,前面所說的各種異常全部被妥善處理,業(yè)務開發(fā)人員只需要關注實際的業(yè)務邏輯,負擔大大降低。
子事務屏障提供了方法ThroughBarrierCall,方法的原型為:
func ThroughBarrierCall(db *sql.DB, transInfo *TransInfo, busiCall BusiFunc)
業(yè)務開發(fā)人員,在busiCall里面編寫自己的相關邏輯,調用該函數(shù)。ThroughBarrierCall
保證,在空回滾、懸掛等場景下,busiCall
不會被調用;在業(yè)務被重復調用時,有冪等控制,保證只被提交一次。
子事務屏障會管理TCC、SAGA、事務消息等,也可以擴展到其他領域
3.3 子事務屏障原理
子事務屏障技術的原理是,在本地數(shù)據庫,建立分支事務狀態(tài)表sub_trans_barrier
,唯一鍵為全局事務id-子事務id-子事務分支名稱(try|confirm|cancel
)
- 開啟事務
- 如果是Try分支,則那么
insert ignore
插入gid-branchid-try
,如果成功插入,則調用屏障內邏輯 - 如果是Confirm分支,那么
insert ignore
插入gid-branchid-confirm
,如果成功插入,則調用屏障內邏輯 - 如果是Cancel分支,那么
insert ignore
插入gid-branchid-try
,再插入gid-branchid-cancel
,如果try未插入并且cancel插入成功,則調用屏障內邏輯 - 屏障內邏輯返回成功,提交事務,返回成功
- 屏障內邏輯返回錯誤,回滾事務,返回錯誤
在此機制下,解決了網絡異常相關的問題
- 空補償控制--如果Try沒有執(zhí)行,直接執(zhí)行了Cancel,那么
Cancel
插入gid-branchid-try
會成功,不走屏障內的邏輯,保證了空補償控制 - 冪等控制--任何一個分支都無法重復插入唯一鍵,保證了不會重復執(zhí)行
- 防懸掛控制--Try在Cancel之后執(zhí)行,那么插入的
gid-branchid-try
不成功,就不執(zhí)行,保證了防懸掛控制
對于SAGA、事務消息等,也是類似的機制。
3.4 子事務屏障小結
子事務屏障技術,為https://github.com/yedf/dtm首創(chuàng),它的意義在于設計簡單易實現(xiàn)的算法,提供了簡單易用的接口,在首創(chuàng),它的意義在于設計簡單易實現(xiàn)的算法,提供了簡單易用的接口,在這兩項的幫助下,開發(fā)人員徹底的從網絡異常的處理中解放出來。
該技術目前需要搭配yedf/dtm事務管理器,目前SDK已經提供給Go、Python語言的開發(fā)者。其他語言的sdk正在規(guī)劃中。對于其他的分布式事務框架,只要提供了合適的分布式事務信息,能夠按照上述原理,快速實現(xiàn)該技術。
4、分布式事務實踐
我們以前面介紹的SAGA事務為例,以DTM作為事務框架,來完成一個具體的分布式事務。本例子采用Go語言,如果您對此不感興趣,可以直接跳到文章最后的小結。
4.1 一個SAGA事務
我們先編寫核心業(yè)務代碼,調整用戶的賬戶余額
func qsAdjustBalance(uid int, amount int) (interface{}, error) { _, err := dtmcli.SdbExec(sdbGet(), "update dtm_busi.user_account set balance = balance + ? where user_id = ?", amount, uid) return dtmcli.ResultSuccess, err }
下面我們來編寫具體的正向操作/補償操作的處理函數(shù)
app.POST(qsBusiAPI+"/TransIn", common.WrapHandler(func(c *gin.Context) (interface{}, error) { return qsAdjustBalance(2, 30) })) app.POST(qsBusiAPI+"/TransInCompensate", common.WrapHandler(func(c *gin.Context) (interface{}, error) { return qsAdjustBalance(2, -30) })) app.POST(qsBusiAPI+"/TransOut", common.WrapHandler(func(c *gin.Context) (interface{}, error) { return qsAdjustBalance(1, -30) })) app.POST(qsBusiAPI+"/TransOutCompensate", common.WrapHandler(func(c *gin.Context) (interface{}, error) { return qsAdjustBalance(1, 30) }))
到此各個子事務的處理函數(shù)已經OK了,然后是開啟SAGA事務,進行分支調用
req := &gin.H{"amount": 30} // 微服務的載荷 // DtmServer為DTM服務的地址 saga := dtmcli.NewSaga(DtmServer, dtmcli.MustGenGid(DtmServer)). // 添加一個TransOut的子事務,正向操作為url: qsBusi+"/TransOut", 逆向操作為url: qsBusi+"/TransOutCompensate" Add(qsBusi+"/TransOut", qsBusi+"/TransOutCompensate", req). // 添加一個TransIn的子事務,正向操作為url: qsBusi+"/TransOut", 逆向操作為url: qsBusi+"/TransInCompensate" Add(qsBusi+"/TransIn", qsBusi+"/TransInCompensate", req) // 提交saga事務,dtm會完成所有的子事務/回滾所有的子事務 err := saga.Submit()
至此,一個完整的SAGA分布式事務編寫完成。
如果您想要完整運行一個成功的示例,那么按照yedf/dtm項目的說明搭建好環(huán)境之后,通過下面命令運行saga的例子即可:
go run app/main.go quick_start
4.2 處理網絡異常
假設提交給dtm的事務中,調用轉入操作時,出現(xiàn)短暫的故障怎么辦?按照SAGA事務的協(xié)議,dtm會重試未完成的操作,這時我們要如何處理?故障有可能是轉入操作完成后出網絡故障,也有可能是轉入操作完成中出現(xiàn)機器宕機。如何處理才能夠保障賬戶余額的調整是正確無問題的?
我們使用了子事務屏障功能,保證多次重試,只會有一次成功提交。
我們把處理函數(shù)調整為:
func sagaBarrierAdjustBalance(sdb *sql.Tx, uid int, amount int) (interface{}, error) { _, err := dtmcli.StxExec(sdb, "update dtm_busi.user_account set balance = balance + ? where user_id = ?", amount, uid) return dtmcli.ResultSuccess, err } func sagaBarrierTransIn(c *gin.Context) (interface{}, error) { return dtmcli.ThroughBarrierCall(sdbGet(), MustGetTrans(c), func(sdb *sql.Tx) (interface{}, error) { return sagaBarrierAdjustBalance(sdb, 1, reqFrom(c).Amount) }) } func sagaBarrierTransInCompensate(c *gin.Context) (interface{}, error) { return dtmcli.ThroughBarrierCall(sdbGet(), MustGetTrans(c), func(sdb *sql.Tx) (interface{}, error) { return sagaBarrierAdjustBalance(sdb, 1, -reqFrom(c).Amount) }) }
這里的dtmcli.TroughBarrierCall
調用會使用子事務屏障技術,保證第三個參數(shù)里的回調函數(shù)僅被處理一次。
您可以嘗試多次調用這個TransIn服務,僅有一次余額調整。您可以運行以下命令,運行新的處理方式:
go run app/main.go saga_barrier
4.3 處理回滾
假如銀行將金額準備轉入用戶2時,發(fā)現(xiàn)用戶2的賬戶異常,返回失敗,會怎么樣?我們調整處理函數(shù),讓轉入操作返回失敗
func sagaBarrierTransIn(c *gin.Context) (interface{}, error) { return dtmcli.ResultFailure, nil }
我們給出事務失敗交互的時序圖
這里有一點,TransIn
的正向操作什么都沒有做,就返回了失敗,此時調用TransIn
的補償操作,會不會導致反向調整出錯了呢?
不用擔心,前面的子事務屏障技術,能夠保證TransIn
的錯誤如果發(fā)生在提交之前,則補償為空操作;TransIn
的錯誤如果發(fā)生在提交之后,則補償操作會將數(shù)據提交一次;如果TransIn
還在進行中,則補償操作會等待TransIn最終提交/回滾,然后再提交補償/空回滾。
您可以將返回錯誤的TransIn改成:
func sagaBarrierTransIn(c *gin.Context) (interface{}, error) { dtmcli.ThroughBarrierCall(sdbGet(), MustGetTrans(c), func(sdb *sql.Tx) (interface{}, error) { return sagaBarrierAdjustBalance(sdb, 1, 30) }) return dtmcli.ResultFailure, nil }
最后的結果余額依舊沒有問題
5、總結
本文介紹了分布式事務的一些基礎理論,并對常用的分布式事務方案進行了講解;在文章的后半部分還給出了事務異常的原因、分類以及優(yōu)雅的解決方案;最后以一個可運行的分布式事務例子,將前面介紹的內容以簡短的程序進行演示。
到此這篇關于關于MySQL與Golan分布式事務經典的七種解決方案的文章就介紹到這了,更多相關分布式事務經典的七種解決方案內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
在Windows主機上定時備份遠程VPS(CentOS)數(shù)據的批處理
我想在自己的 Windows7 下每天/周運行一次備份,就有了這個小工具2012-05-05詳解Mysql如何實現(xiàn)數(shù)據同步到Elasticsearch
要通過Elasticsearch實現(xiàn)數(shù)據檢索,首先要將Mysql中的數(shù)據導入Elasticsearch,并實現(xiàn)數(shù)據源與Elasticsearch數(shù)據同步,這里使用的數(shù)據源是Mysql數(shù)據庫。目前Mysql與Elasticsearch常用的同步機制大多是基于插件實現(xiàn)的,希望這篇文章能對大家有所幫助2021-11-11MySQL 百萬級分頁優(yōu)化(Mysql千萬級快速分頁)
MySql 性能到底能有多高?用了php半年多,真正如此深入的去思考這個問題還是從前天開始。有過痛苦有過絕望,到現(xiàn)在充滿信心2012-11-11老鳥帶你開發(fā)專業(yè)規(guī)范的MySQL啟動腳本
這篇文章主要介紹了老鳥帶你開發(fā)專業(yè)規(guī)范的MySQL啟動腳本,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-09-09