程序员的自我救赎---1.3:事务的使用
《事务的使用》
关于事务,我今天要把自己放在一个初学者的心态来写这篇文章。之前几篇文章大多讲的是对于Winner的应用,今天要从根本上来讲
一下“事务”,以及事务在Winner中的应用。
首先从基础讲起,什么是“事务”?事务能帮我们解决哪些问题? 摘录百度上的一段话教科书式的文字:
“数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。”
其实很好理解,比如说我们的商品购物流程中支付成功之后要做的几步操作:
1,修改订单表该状态;
2,修改库存表库存数量;
3,添加物流表发货信息;
三个操作必须一气呵成,这时候就需要串联事务,当一个操作失败之后,事务就回滚整个业务失败。当操作成功之后,所有操作才最终持久化执行。
假设我们没有事务的话,会怎么样?
还是上面三个流程,没有联事务就有可能出现以下情况:
步骤一: 修改订单表该状态 (完成)
步骤二:修改库存表库存数量 (完成)
步骤三:添加物流发货信息(失败)
当步骤三失败,由于没有事务回滚,程序中就必须得通过程序判断步骤三得到失败后,再操作“步骤二”中库存订单数量回到未修改前的值。
同事还要还原“步骤一”中订单表的订单状态。
而且,如果数据库健壮性不够,有可能导致二次修改步骤一,步骤二失败,造出数据库一片混乱。
这就是为什么我们要使用事务,事务有四大特性(百度摘录):
原子性:事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。
一致性:事务在完成时,必须使所有的数据都保持一致状态。
隔离性:由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。
持久性:事务完成之后,它对于系统的影响是永久性的。
这些其实我也早忘了,毕竟工作多年以后也不会有这样的考试,读书那会还是记得挺清楚的,只是那会不能感受到
事务的重要性,那时候的老师也不管那么多久照本宣科的讲,那些是以后工作生涯的重点,有时候老师自己都不知道
造出了我们可能花很多时间去理解“游标”,“函数” 这些压根用不着几次的东西。
开始事物:begin transaction
提交事物:commit transaction
回滚事物:rollback transaction
begin transaction declare @errorSum int --定义局部变量 set @errorSum=0 --初始化临时变量 update bank set currentMoneycurrentMoney= currentMoney-1000 where customerName='张三' set @errorSum=@errorSum+@@error --累计是否有错误 update bank set currentMoneycurrentMoney= currentMoney+1000 where customerName='李四' set @errorSum=@errorSum+@@error --累计是否有错误 if @errorSum<>0 --如果有错误 begin rollback transaction end else begin commit transaction end go
这里我就偷个懒,不自己去屑个事务的案例,直接从网络上摘录,出处与:http://database.51cto.com/art/201108/283348.htm。
每个数据库语法略又有不同,大整体差不到哪去我这里就不详细解释每个关键字上面意思了,自行百度吧! 再说这还是比较基础的知识,我就一笔带过。
最后在事务基础知识再补充一点,使用事务时一定要谨慎,事务必须 “一开一关”,开启了一个事务必须要关闭这个事务,无论是提交(commit ) 还是 回滚(roolback)。
必须要有关闭操作,如果没有关闭事务,则会造成事务挂起。数据库就会被锁,一旦数据数据被锁,轻则导致该表不能操作,重则导致整个数据库不能操作,至使整个
程序奔溃不能运行。 这里一定要谨慎,在我工作了8年后我任然很多次看到我们系统会出现锁表的情况,都是有个别程序员对事务应用不当,导致事务挂起,数据库死锁。
Winner一直使用的是Oracle数据库,这里贴一个我们常用的Sql工具:”锁表侦探”
SELECT ROOT, L.SID_BLOCKED, L.TYPE, L.LMODE, SINFO.* FROM (SELECT ROWNUM ORDERNO, CONNECT_BY_ROOT SID_WAITING ROOT, T.* FROM (SELECT B.SID SID_BLOCKED, W.SID SID_WAITING, W.TYPE, W.LMODE FROM V$LOCK B, V$LOCK W WHERE B.ID1 = W.ID1 AND B.ID2 = W.ID2 AND B.BLOCK = 1 AND W.REQUEST > 0 UNION ALL SELECT NULL, SID, TYPE, LMODE FROM V$LOCK B WHERE B.BLOCK = 1 AND SID NOT IN (SELECT DISTINCT W.SID SID_WAITING FROM V$LOCK B, V$LOCK W WHERE B.ID1 = W.ID1 AND B.ID2 = W.ID2 AND B.BLOCK = 1 AND W.REQUEST > 0)) T START WITH SID_BLOCKED IS NULL CONNECT BY SID_BLOCKED = PRIOR SID_WAITING) L LEFT JOIN (SELECT S.SID, SERIAL#, O.OBJECT_NAMES, T.START_TIME, S.STATUS, ST.SQL_TEXT, S.MACHINE, S.PROGRAM, S.USERNAME, S.LOGON_TIME FROM V$SESSION S JOIN (SELECT SESSION_ID, SUBSTR(SYS_CONNECT_BY_PATH(OBJECT_NAME, ','), 2) OBJECT_NAMES FROM (SELECT ROW_NUMBER() OVER(PARTITION BY SESSION_ID ORDER BY OBJECT_NAME) AS RN, LO.*, O.* FROM V$LOCKED_OBJECT LO LEFT JOIN DBA_OBJECTS O ON LO.OBJECT_ID = O.OBJECT_ID) T WHERE CONNECT_BY_ISLEAF = 1 START WITH RN = 1 CONNECT BY SESSION_ID = PRIOR SESSION_ID AND RN = PRIOR RN + 1) O ON S.SID = O.SESSION_ID LEFT JOIN V$TRANSACTION T ON S.TADDR = T.ADDR LEFT JOIN V$SQLAREA ST ON ST.ADDRESS = S.SQL_ADDRESS) SINFO ON SINFO.SID = L.SID_WAITING ORDER BY ORDERNO;
在后面的篇章中会讲到“报表系统”,我习惯把锁表侦探添加到报表系统中,每次遇到锁表情况的时候就上报表系统查看是哪个项目锁表。
这里有人就会问了,锁表侦探能查出具体哪张表所了,那怎么监控数据库有没有锁表的迹象? 这里要推荐第二个工具:“Spotligth”
翻译过来叫“聚光灯”, Spotligth有很多版本,有监控 服务器的,有监控数据库的(主流都支持)
我上班的时候 是两台电脑,一台办公,另外一台则挂着Spotligth 实时监控着我们的数据库,一飘红里面上报表系统查“锁表侦探”
然后通知到相应的技术员,当然有时候还免不了要对犯错误的技术员 “指点”几句。
=======================华丽的分割线=======================
基础知识就到这里了,下面就是Winner的干货了。 在整个Winner中,我觉得最牛逼的当属“事务”这一块,能想出这种方式并开发出来了的真的很厉害。
最初我在上家公司任职时,我的老大(William )他跟我讲事务的时候我就觉得太屌了,而这个事务就是由他开发的。
其他的不多说,贴一张图就知道Winner中的事务有多好用:
真的超级好用,一来不用写一句Sql,二来业务流程清晰,尤其是当程序需要调试的时候,这种方式能让程序员清晰的看到业务逻辑的每一个流程。
这里运用到一个“职责分离”的思想,我们设定数据库的职责就是:“持久化存储数据” 复杂的业务逻辑由程序去处理。
我刚参加工作那会任职过的几家公司,就没有这种思想(可能也是因为去的都是单一的项目型公司)。 最常见的就是 一旦涉及业务流程处理的他们
就习惯性的以“存储过程”去处理,这样就使得开发变得繁琐,一会要写C#代码,一会有要去写sql代码,最重要的是数据库的不同又造成程序员要熟悉
各种数据库的sql语法来写存储过程、事务、函数等等。
“职责分离”的思想跟设计模式六大原则中的“单一职责”有点类似,但是“单一职责”更多的是指在程序中一个类只负责一项职责。“职责分离” 相当于“单一职责”
的抽象版,程序做程序的负责业务逻辑,数据库做数据库的数据存储。
我曾经也见过,有的公司一开始用的sqlserver数据库,然后开发方式还是当时特牛气的 Html + Ajax + C# + 存储过程,后来因为业务关系更换到MySql,大量的存储
过程写在了数据库里面,特别是有些关键字Mysql是没有或不支持的,致使他们痛苦不堪。
==============================华丽的分割线========================
我们来看看Winner是如何实现的,首先Winner的业务类对象都基础了 FacadeBase 这个基类。 (关于Winner解决方案不清楚的可以《解决方案命名规范》)
using System; using Winner.Framework.Core.DataAccess; using Winner.Framework.Core.Interface; using Winner.Framework.Utils; namespace Winner.Framework.Core.Facade { public class FacadeBase : IDisposable, IPromptInfo { public IChangePage ChangePage; public FacadeBase(); public PromptInfo PromptInfo { get; } public Transaction Transaction { get; } public virtual void Dispose(); public void ReferenceTransactionFrom(Transaction transaction); protected void Alert(ResultType restulType); protected void Alert(PromptInfo result); protected void Alert(string msg); protected void Alert(ResultType restulType, PromptInfo result); protected void Alert(ResultType restulType, string msg); protected void Alert(string msg, PromptInfo result); protected void Alert(ResultType restulType, string msg, PromptInfo result); protected void BeginTransaction(); protected void Commit(); protected void RealRollback(); protected void Rollback(); } }
FacadeBase在Winner.Framework.Core 程序集中,关于 FacadeBase其他的方法后面的篇章中再详细讲,今天重点讲事务这一块。
public Transaction Transaction { get; } 定义事务对象; 对象由
public void ReferenceTransactionFrom(Transaction transaction); 串联事务;
protected void BeginTransaction(); 开启事务;
protected void Rollback(); 回滚事务;
protected void Commit(); 提交事务;
protected void RealRollback(); 强制回滚事务;
using System; using Winner.Framework.Core.DataAccess; using Winner.Framework.Core.Interface; using Winner.Framework.Utils; namespace Winner.Framework.Core.Facade { /// <summary> /// 通用三层架构的业务处理层(BLL)基类 /// </summary> public class FacadeBase : IDisposable, IPromptInfo { #region 事务 /// <summary> /// 事物对象 /// </summary> public Transaction Transaction { get; private set; } /// <summary> /// 开启事务 /// </summary> protected void BeginTransaction() { if (this.Transaction == null) { this.Transaction = new Winner.Framework.Core.DataAccess.Transaction(); } this.Transaction.BeginTransaction(); } /// <summary> /// 提交事务 /// </summary> protected void Commit() { this.Transaction.Commit(); } /// <summary> /// 强制回滚事物 /// </summary> protected void RealRollback() { this.Transaction.RealRollback(); } /// <summary> /// 事物串联 /// </summary> /// <param name="transaction">事物对象</param> public void ReferenceTransactionFrom(Transaction transaction) { this.Transaction = transaction; } /// <summary> /// 回滚事物 /// </summary> protected void Rollback() { this.Transaction.Rollback(); } #endregion } }
为了更清楚的单一讲清楚事务,FacadeBase我精剪掉了其他方法,只剩下事务有关的方法,会看到FacadeBase作为调用实现几个基本的操作
整个Winner事务的核心在Winner.Framework.Core.DataAccess.Transaction 这个对象中。
下面贴一些阿杰开发的Winner2.0 的事务对象,写的非常漂亮。
using System; using System.Data; using System.Data.Common; using System.Diagnostics; using Winner.Framework.Core.CustomException; using Winner.Framework.Core.Delegate; using Winner.Framework.Utils; namespace Winner.Framework.Core.DataAccess { /// <summary> /// 数据库事务机制 /// </summary> /// <remarks> /// <![CDATA[ /// 四大特性:原子性、一致性、隔离性、持久性 /// ]]> /// </remarks> [DebuggerDisplay("事务状态={Status},计数器={Counter}")] public class Transaction { #region Event /// <summary> /// 开启事务时触发 /// </summary> public event BeginTransaction BeginTransactionEvent; /// <summary> /// 提交事务时触发 /// </summary> public event CommitTransaction CommitEvent; /// <summary> /// 强制回滚时触发 /// </summary> public event RealRollbackTransaction RealRollbackEvent; /// <summary> /// 回滚事务时触发 /// </summary> public event RollbackTransaction RollbackEvent; #endregion #region Property /// <summary> /// 是否已开启事务 /// </summary> public bool IsBegin { get; private set; } /// <summary> /// 是否提交 /// </summary> public bool IsCommit { get; private set; } /// <summary> /// 是否强制回滚 /// </summary> public bool IsRealRollback { get; private set; } /// <summary> /// 是否回滚 /// </summary> public bool IsRollback { get; private set; } /// <summary> /// 事务计数器 /// </summary> public int Counter { get; private set; } /// <summary> /// 事务状态 /// </summary> public TransactionStatus Status { get; internal set; } /// <summary> /// 获取或设置连接数据库对象 /// </summary> internal IDbConnection DbConnection { get; set; } /// <summary> /// 获取或设置事务机制对象 /// </summary> internal IDbTransaction DBTransaction { get; set; } #endregion #region Member /// <summary> /// 开启事务(此处不真正开事务,会导致性能问题,所以在使用ADO.NET对象时才开启事务) /// </summary> public void BeginTransaction() { try { if (this.Counter == 0) { this.Status = TransactionStatus.已启动事务; this.IsBegin = true; this.IsCommit = this.IsRollback = this.IsRealRollback = false; if (this.BeginTransactionEvent != null) { this.BeginTransactionEvent(this); } } this.Counter++; OutupRunLog("BeginTransaction() Counter: " + this.Counter); } catch (Exception e) { if (!Debuger.IsDebug) { Log.Error("开启事务时出现异常", e); } throw new TransactionException(e); } } /// <summary> /// 提交事务 /// </summary> public void Commit() { try { if (this.IsRealRollback) { return; } switch (this.Counter) { case 0: throw new TransactionException(this.Status.ToString()); case 1: if (!this.DBTransaction.IsNull()) { this.DBTransaction.Commit(); } if (!this.DbConnection.IsNull()) { this.DbConnection.Close(); } this.Status = TransactionStatus.事务已提交; this.IsCommit = true; this.IsBegin = this.IsRollback = this.IsRealRollback = false; if (this.CommitEvent != null) { this.CommitEvent(this); } break; } OutupRunLog("Commit() Counter: " + this.Counter); this.Counter--; } catch (Exception e) { if (!Debuger.IsDebug) { Log.Error("提交事务时出现异常", e); } throw e; } } /// <summary> /// 强制回滚事务 /// </summary> public void RealRollback() { try { if (this.IsRollback) { return; } if (!this.DBTransaction.IsNull()) { this.DBTransaction.Rollback(); } if (!this.DbConnection.IsNull()) { this.DbConnection.Close(); } this.Status = TransactionStatus.事务已强制回滚; this.IsRollback = this.IsRealRollback = true; this.IsBegin = this.IsCommit = false; if (this.RealRollbackEvent != null) { this.RealRollbackEvent(this); } OutupRunLog("RealRollback()"); } catch (Exception ex) { if (!Debuger.IsDebug) { Log.Error("强制回滚事务出现异常", ex); } throw new TransactionException(ex); } finally { this.IsRealRollback = true; } } /// <summary> /// 回滚事务 /// </summary> public void Rollback() { try { if (this.IsRealRollback) { return; } switch (this.Counter) { case 0: throw new TransactionException(this.Status.ToString()); case 1: if (!this.DBTransaction.IsNull()) { this.DBTransaction.Rollback(); } if (!this.DbConnection.IsNull()) { this.DbConnection.Close(); } this.Status = TransactionStatus.事务已回滚; this.IsRollback = true; this.IsBegin = this.IsCommit = this.IsRealRollback = false; if (this.RollbackEvent != null) { this.RollbackEvent(this); } break; } OutupRunLog("Rollback() Counter: " + this.Counter); this.Counter--; } catch (Exception e) { if (!Debuger.IsDebug) { Log.Error("回滚事务时出现异常", e); } throw e; } } #endregion /// <summary> /// 输出运行日志 /// </summary> /// <param name="msg"></param> private void OutupRunLog(string msg) { Debug.WriteLine(msg); Console.WriteLine(msg); } } /// <summary> /// 事务状态 /// </summary> public enum TransactionStatus { /// <summary> /// 未开启事务 /// </summary> 未启动事务 = 0, /// <summary> /// 已开启事务,但未操作数据库 /// </summary> 已启动事务 = 1, /// <summary> /// 已开启事务,并有数据库事务被挂起 /// </summary> 事务已挂起 = 2, /// <summary> /// /// </summary> 事务已提交 = 3, /// <summary> /// 事务已回滚 /// </summary> 事务已回滚 = 4, /// <summary> /// 事务已强制回滚 /// </summary> 事务已强制回滚 = 5, } }
原理不复杂,都是调用System.Data 的 IDbTransaction 去完成的,经典的地方在于这个 Counter 事务计数器累加事务。
而且阿杰的代码有个很优先的地方,并不是调用 BeginTransaction(),就去开启事务,这里是一个非常巧妙巧妙的设计,有效的
避免了个别马虎的技术员开启了事务却有在代币流程中忘记提交or回滚造成的数据库死锁。
下午阿杰过来和我聊了会,我说我再写关于事务的博客,阿杰说了很多。包括他当初为什么这样设计,以及他综合了动软基础框架的
事务,还有微软的分布式事务。只是我一时半会还没办法转换成自己的语言写成博客,同时也要我经历像阿杰这样的创作过程,才能
像他那样富含底蕴的讲述他的思考逻辑。
行了,今天我就写到这里。关于对事务这一块的理解,如果有机会我希望阿杰也能写一篇博客,比我更详细的阐述事务。他一定写的比我
出彩,毕竟我只是一个使用者,而他才是创这者。
关于Winner,我新建了一个QQ群,有兴趣的可以加我们QQ群,阿杰,jason都在群中。我们可以一起探讨Winner,群号:261083244
也可以扫描博客左侧二维码加群。