日期:2014-05-16  浏览次数:20475 次

浅谈数据库事务

概要

??? 该问讲述了我对数据库事务的一些理解,由于本人才疏学浅,无法保证这些内容的正确性望各位明鉴,希望能够误导你,哦是帮到你!
文章后面列出了一些大牛的文章,供大家参考。

事务的定义
??? 啥是数据库事务?事务就是….,(此处略去200字)

一个事务的典型例子(中西结合版)
?? Mr张三给Mr李四转帐….(此出略去1000字)

?

到底嘛是事务

? 上面分别给出了事务的定义与一个典型场境,相信无论你是菜鸟还是小鸟看到上面这段文时时都会笑而不语 (嘿嘿………哥在这里已经看见你们邪恶的表情了),好了下面我们来点别的。

测试如下问题:
? 有一张表MyTable有100万条记录,假设使用Delete语句执行删除全部记录要20分钟,你在查询分析器里输入如下代码 “Delete From MyTable” 然后按F5, 执行5分钟左右你把电源拔了(注意这个动作要猛、准、狠). 问题是:当你重新启动电脑后,你的MyTable表里还有多少记录呢?

………
……….
哥在这里休息5分钟再写,以便您能完成上面的测试…
………
……………..
您好!很高兴您成功启动了电脑,如果一切正常(关于太阳带电粒子的问题我们这里先不考虑)您的MyTable表中应该还有100万条记录。
这就是传说中的原子性--A,要么全部删完,要么一条多没删除,顺便提下你的任何SQL 即使没加Begin Tran 都是在事务环境下远行的,因为上面那个转帐的例子导致哥许多年来天真的认为不加Begin Tran的SQL语句是没有事务的,其实
Delete From MyTable

Begin Tran
? Delete from Mytable
? If @@Error >0
??? Rollback;
Coomit;
是等价的,前一个语句就是那个天杀的SQL自动事务模式.?好了我们继续,您在次输入 Delete From MyTable,注意饿(请确保您的MyTable表中有100万条记录),不然一下就删除没了。
然后新开个查询窗口输入Select * from MyTableA, 接着F5运行,你会发现运行正常,查询语句显示MyTableA表中的28条记录,注意我上面说的是MyTableA, 如果这个时候你执行Select * From MyTable 会显示什么呢…答案是啥都没有,只显示[正在执行查询....]直接到您的Delete完成,然后显示的结果是没有任何记录的MyTable表。
这就是所谓的隔离性-I,注意上面的Select * From MyTable也是在事务环境下运行的,别拿查询不当事务饿,而上面的操作我们模拟了两个并发运行的事务,在Delete完成或取消前,
Select被挡在了外面,以免您的Select语句整出54万条或别的什么数目的记录来。
对数据库来说你执行Delete* From MyTable,后只会有两种结果,成功或失败,如果执行成功,那么在执行删除前语句Select * From MyTable应该显示100万,而在删除执行后所有的查询只能看到0条记录,就是不允许你看到它删除中的54万或38万条记录什么的,这就体现了一致性-C,这个比较抽象请先忽略掉把。
好了说下持久性-D,说到持久我们就会想到硬盘,我们上面的Delete语句执行后被删除的数据就应该从.mdf文件中清除以实现持久化,不过在执行删除前后对比.mdf文件大小您可能要失望了,您会发现.mdf没有变小甚至可能变大,造成功这个现象的原因是,数据库文件是按8K一页为单位来组织管理的,数据库记录就写在这些页上,在您删除数据后这些页面没有被删除或者说将磁盘空间还给操作系统,只是将这些页标记为空以便以后使用,不过您确实能观察到.ldf文件变大了。ACID(事务),可以看作是关系数据库的一个标准,就像HTML一样,比方w3c定义了一个A标签,那么你做浏览器开发的就要在程序上实现对a标签的支持,而通常来说关系数据库产品要实现ACID标准一般多是通过”日志”与”锁”技术。

?现在我们来大致来整理下删除的具体过程

1.查询语句解析:数据库接到您的删除命令后计算出这次删除要涉及的数据或记录(页面)
2.读取对应页面到缓存并锁定:数据库引擎会读取这些页面并加上工享锁,防止它事务(也就是其他人)进行写操作。
3.写日志:将这些将被删除页面做个备份写到log中(这个操作是真真切切要先搞完的),
4.获取排它锁禁止其它进程的一切读写操作,这步完成时就是你看到上面那个Select 一直处于执行中的原因了.
4.修改缓存中的页面,并记录事务点到日志中,释放排它锁


至于缓存的页面啥时候写到.mdf中就不是事务要关心的事情了,反正读写数据都是针对缓存来说,而缓存与数据库文件之间的一致性,也是通过日志和一个叫检查点的机制来实现的。

好了针对上面的执行模型再考虑我们那100万条记录的删除情况,当执行到5分钟时根本没到事务提交点(.ldf中没记录这次删除完成的点),因此当您重新启动电脑后,数据库服务器会根据日志进行一致性检测,根据日志,将上面那个删除了一半的操作撤消(因为日志里保存了删除的页面,如果有必要系统会据此来还原数据),当然如果您在执行了19分59秒时把电源拔了那么事务可能已经完成并提交了,但是缓存中的页面没写入.mdf,不过这个也不是问题,根据日志文件的记录,数据库会再做一次“删除”,以保证.mdf处于一直状态,为了避免干扰在完成这些工作前您是无法访问数据库的,当然数据库在进行上面的操作时也会写日志,防止某些变态的人又突然把电源拔掉。


下面我们看下另外一个场景

有个卖火车票的网站,上面有个可卖火车票的列表,有很多群众不停的刷新,一旦发现他要的票,就双击进入购卖详细页(ticketDetail.aspx)然后[提交购买]。
好了那个数据表是 : ticket(Id,status,buyerId,….),字段说明:车票编号、状态(可买,已买)、购买者编号.

[提交购买]对应的操作是
Update ticket status=’已买’,buyerId=@buyerId where Id=@Id

聪明绝顶的你一看这个可能不行的,这一语句的问题是没检测车票是否已被卖出,这样后面的提交的总是覆盖掉前面的提交。

改进一

If (Select count(*) from ticket where status=’可买’ and Id=@Id )=1
Update ticket status=’已买’,buyerId=@buyerId where Id=@Id

但是由于群众太多上面的代码有些时候会出问题,张三,李四,王五他们同时买Id=1的车票,页面提示他们全都购买成功了,不过最后事实是李四拿到了票,因为buyerId记录了李四的编号。

改进二
Update ticket status=’已买’ ,buyerId=@buyerId where Id=@Id and @status=’可买’
我们通过update 返回值是否 >0来判断是否购买成功,这个方案加入了版本控制字段status(SQL2005中可以使用timestamp来达到同样的目的),而且语句只有一句了。
现在我们分析下
当张三跟李四同时提交时,出现了A,B两个并发事务,
缓存中首先加载where条件对应的数据页,并将更新锁分配给A或B中的一个,这里假设是A获得了更新锁(更新锁之间互不兼容)
A将U锁升级成X锁完成更新操作。而B在A进程完成后再次尝试获取对应where条件的数据页的更新锁,但是无法找到对应条件的数据页了,因此更新失败。

?

1 楼 jyjava 2012-01-17  
lz,这个文章怎么到头版的啊,讲了事务的隔离和一个sql的执行的大概过程,不知道跟缓存有啥关系