最近新接手了一項業務,其中有一個方法,需要對業務表進行寫入數據,之後記錄到日志表中。這部分代碼原先是前人寫的,他沒有采用任何方案,只是簡單的調用Ado.net執行了兩次寫庫操作。因此經常出現系統使用者不斷發郵件說數據有問題,經過查看原因就是在於寫庫操作中,有某個表寫入失敗,但是其他表寫入成功,導致出現了數據不一致的問題。後來本想改用事務,但發現日志表和業務表不在同一個數據庫下,甚至不在同一個IP下,對於這個問題,我想到了有以下解決方案。
由ado.net管理的事務改為自己手動提交事務和Commit或者RollBack操作:
step1:按照連接字符串和sql分類,存入Dictionary<string,string>中,Key為連接字符串,Value為針對此數據庫的Sql語句,多條用分號隔開;
step2:遍歷此Dictionary,打開這些連接;
step3:對於每個連接,打開事務;
step4:執行針對每個連接的sql,出現錯誤則全部rollback,否則全部commit;
step5:關閉連接,記錄運行情況,記錄日志。
具體代碼如下:
1 //提交事務用的sql 2 public const string MultiTran = @"BEGIN TRAN 3 {0}"; 4 5 /// <summary> 6 /// 事務返回的信息 7 /// </summary> 8 public struct TransInfo 9 { 10 /// <summary> 11 /// sql總條數 12 /// </summary> 13 public int Total; 14 /// <summary> 15 /// 事務執行是否成功 16 /// </summary> 17 public bool IsSuccess; 18 /// <summary> 19 /// 失敗時的sql 20 /// </summary> 21 public string WrongMessage; 22 } 23 24 /// <summary> 25 /// 跨庫事務異常對象 26 /// </summary> 27 public class TransException : Exception 28 { 29 public TransException(string message) : base(message) 30 { 31 } 32 33 public string wrongSQL { get; set; } 34 public string wrongAt { get; set; } 35 /// <summary> 36 /// 已經打開的連接 37 /// </summary> 38 public List<SqlConnection> DoneConnection = new List<SqlConnection>(); 39 /// <summary> 40 /// 出現錯誤的連接 41 /// </summary> 42 public SqlConnection CurrentConnection; 43 /// <summary> 44 /// 覆蓋Exception中的Message字段,使其可寫 45 /// </summary> 46 public new string Message { get; set; } 47 } 48 49 /// <summary> 50 /// 多操作sql,使用事務,用於多庫事務 51 /// <para> 52 /// 返回值TransInfo字段:IsSuccess 是否成功, 53 /// Total sql總條數, 54 /// WrongAt 失敗的sql語句 55 /// </para> 56 /// </summary> 57 /// <param name="sqlwithconn">執行的sql和連接字符串列表key:sql,value:連接字符串</param> 58 /// <param name="connectionString">連接字符串</param> 59 /// <returns>sadf</returns> 60 public static TransInfo RunSqlInTrans(Dictionary<string, string> sqlwithconn) 61 { 62 var sqltable = new Dictionary<string, string>(); 63 var conntable = new Dictionary<string, SqlConnection>(); 64 65 foreach (var i in sqlwithconn) 66 { 67 if (!sqltable.Keys.Contains(i.Value)) 68 { 69 sqltable.Add(i.Value, i.Key); //sqltable的key是連接字符串,value是sql語句 70 conntable.Add(i.Value, new SqlConnection(i.Value)); //key是連接字符串,value是連接對象 71 } 72 else 73 { 74 sqltable[i.Value] += ";" + i.Key; 75 } 76 } 77 78 try 79 { 80 var wrongEx = new TransException(""); 81 foreach (var i in sqltable) 82 { 83 //遵照晚開早關原則,在此處打開數據庫連接 84 conntable[i.Key].Open(); 85 //連接打開後,將連接對象放入異常處理對象中做記錄 86 wrongEx.DoneConnection.Add(conntable[i.Key]); 87 var dc = new SqlCommand(string.Format(MultiTran, i.Value), conntable[i.Key]); 88 try 89 { 90 dc.ExecuteNonQuery(); 91 } 92 catch (Exception ex) 93 { 94 //出現異常,拋出異常處理對象 95 wrongEx.CurrentConnection = conntable[i.Key]; 96 wrongEx.wrongAt = i.Key; 97 wrongEx.wrongSQL = sqltable[i.Key]; 98 wrongEx.Message = ex.Message; 99 throw wrongEx; 100 } 101 } 102 //全部執行完畢沒有發現錯誤,提交事務 103 foreach (var i in conntable) 104 { 105 var dc = new SqlCommand("COMMIT TRAN", i.Value); 106 dc.ExecuteNonQuery(); 107 i.Value.Close(); 108 } 109 return new TransInfo() 110 { 111 IsSuccess = true, 112 Total = sqlwithconn.Count, 113 WrongMessage = "" 114 }; 115 116 } 117 catch (TransException e) //1.回滾所有操作2.關閉所有已經打開的數據庫連接4.生成錯誤對象 118 { 119 foreach (var i in e.DoneConnection) 120 { 121 if (!i.Equals(e.CurrentConnection)) 122 { 123 var dc = new SqlCommand("ROLLBACK TRAN", i); 124 dc.ExecuteNonQuery(); 125 } 126 i.Close(); 127 } 128 return new TransInfo() 129 { 130 IsSuccess = false, 131 Total = sqlwithconn.Count, 132 WrongMessage = string.Format("在連接{0}中,操作{1}出現錯誤,錯誤信息:{2}", e.wrongAt, e.wrongSQL, e.Message) 133 }; 134 } 135 }
這樣解決了跨庫數據表處理有時因為網絡問題或其他偶然性問題導致的數據不一致的問題。但是這個解決方案最大的問題就是在於性能問題上,比如如果有多個庫假設為A,B,C,D,其中C庫的數據修改寫入比較復雜,那麼在A,B庫開啟事務後,必須等待C和D庫完成或失敗後,事務才可以結束,連接才能釋放,這個時候,A庫和B庫就是處於掛起狀態,如果處於高IO的生產環境中的話,這個性能的損失可能是致命的,所以這個方案只能用於簡單的sql處理,而且處理sql不能太多或者太復雜。而且出現網絡波動的話,損失會更大。幸運的是我所接手的這個業務,是在內網環境中,同時只用兩句sql在兩個庫中,所以用這個方案問題不大。
總結:針對這個問題,我認為當初設計數據庫時,能避免跨庫就一定要避免。
如果大家有什麼更好的解決方案的話,希望和大家多多交流和指教。