C#中跨库事务处理解决方案
最近新接手了一项业务,其中有一个方法,需要对业务表进行写入数据,之后记录到日志表中。这部分代码原先是前人写的,他没有采用任何方案,只是简单的调用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在两个库中,所以用这个方案问题不大。
总结:针对这个问题,我认为当初设计数据库时,能避免跨库就一定要避免。
如果大家有什么更好的解决方案的话,希望和大家多多交流和指教。