本文将针对以下几个问题给大家解答:
1)什么是事务?事务有哪些特性?
2)不同隔离级别的事务,有什么区别?
3)了解一下数据库锁:共享锁,更新锁,排它锁
4)数据库事务和锁之间有什么关系?
5)拓展:什么是分布式事务?有哪些解决方案?
事务
通常是指包含了多个数据库执行操作(select,update,delete,insert)的一个程序执行单元。
事务的四大特性ACID:
1.原子性(Atomicity):事务中所有数据库操作要么全部执行成功,要么全部执行失败。
2.一致性(Consistency):事务执行前后数据库状态保持一致。比如付款操作:A账户减少100元,相应的B账户增加100元。
3.隔离性(Isolation):事务与事务之间相互隔离,互不影响,保证了事务之间数据处理的独立性。
4.持久性(Durability):事务一旦提交成功,那么对数据库数据的影响必将持久化到数据库中,不会因为外在环境,比如断电,服务器宕机等因素而影响对数据库数据的变化。事务提交成功之后,会首先记录到数据库日志文件中,即使断电重启后,也会继续读取日志完成事务操作。
事务分类:根据事务中涉及的数据库操作实例数量区分
本地事务:事务仅针对同一个数据实例进行操作, 例如:.Net中具体实现如SQLtransaction
分布式事务:事务中针对多个数据库实例进行操作。例如:.Net中的DTC分布式事务,具体实现TransactionScope
事务的隔离性,将事务分为不同隔离级别
1.未提交读(Read UnCommitted) :允许该事务读取其他事务未提交的数据。
存在问题:脏读
2.已提交读 (Read Committed):允许该事务读取其他事务已提交的数据。
解决了脏读的问题
存在问题:不可重复读,事务中前后读取数据不一致。
3.可重复读 (Repeatable Read):同一个查询,保证该事务读取前后数据一致。
解决了不可重复度的问题
存在问题:幻读 ,可能读取到其他事务新增的数据。
4.串行读(Serializable):要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。
解决了幻读的问题
SQLServer数据库事务语法:
SET TRANSATION ISOLATION LEVEL READ UNCOMMITTED--设置事务隔离级别 BEGIN TRANSACTION ... ...--事务执行内容 ... COMMIT/ROLLBACK--提交或者回滚
数据库锁
1.共享锁(S)
1)保护读取的数据,读取的过程中,其他并发事务不能修改,删除数据,可以并发读取。
2)当事务隔离级别低于“可重复读”时,一旦数据读取结束,立即释放共享锁,和事务是否结束无关。
2.排它锁(X)
1)保护修改的数据,获取当前被修改资源的锁,该资源同一时刻只能被一个排它锁占有,不允许其他并发事务操作该资源(读取和修改)。
2)排它锁和共享锁不能兼容。当修改资源时,会自动由共享锁升级为排它锁,因此必须等待资源释放共享锁,才能获得排它锁。
3)如果排它锁存在于事务中,需等事务结束才能释放锁。
3.更新锁(U)
1)更新锁时介于共享锁和排它锁中间的混合。更新锁通过uplock手动添加,分两个阶段:1.在修改操作之前,通过更新锁获取资源对象 ,其他事务线程则只能读取不能操作 2.然后执行修改操作,将更新锁升级为排它锁。在修改操作前实现对资源锁定,避免了死锁。
2)更新锁和共享锁可以共存,因此使用更新锁比使用排它锁解决死锁问题,性能更优。
3)持有更新锁的资源,允许被其他并发事务select。
4)如果更新存在于事务中,需等事务结束才能释放更新锁。
事务与锁之间的关系
首先我们说事务和数据库锁之间没有必然的联系,但是事务的执行时间会影响锁持有的时间,间接影响了数据执行效率。
1.通常在执行select查询的时候会持有共享锁,查询一旦执行结束,则立即释放共享锁。如果在事务中执行查询,且隔离级别低于可重复读,即使事务没有执行结束,也不会影响共享锁的释放。
2.更新锁和排它锁,通常在语句执行结束后会立即释放锁资源。如果在事务中执行相关锁操作,需等待事务执行结束,才能释放锁。
3.update语句天然就会持有排它锁,即使不在事务中执行update操作也会持有锁。
4.事务隔离级别未提交读(Read UnCommitted)等同于select查询添加with(nolock),允许读取“脏数据”
语法:
select * from sys.objects with(nolock) where name='sysrscols';
5.事务隔离级别中的未提交读、已提交读和可重复读都是行级别锁,对条件范围内的行数据持有锁。
6.事务如何通过隔离级别处理并发,以“可重复读”隔离级别为例:
1)事务A,默认隔离级别“已提交读”,查询操作5秒后执行更新操作
BEGIN TRAN SELECT * FROM [ORDER] WHERE ID='10' WAITFOR DELAY '00:00:05' UPDATE [ORDER] SET PRICE=30 WHERE ID='10' COMMIT TRAN
2)事务B,隔离级别“可重复读”,前后两次查询间隔10秒
SET TRAN ISOLATION LEVEL SERIALIZABLE BEGIN TRAN SELECT * FROM [ORDER] WHERE ID='10' WAITFOR DELAY '00:00:10' SELECT * FROM [ORDER] WHERE ID='10' COMMIT TRAN
3)启动事务A后,立即启动事务B,事务具体执行顺序如下:
a)事务A执行查询,并持有ID=10数据的共享锁,查询结束后释放锁,并等待5秒
b)事务B执行查询,并持有ID=10数据的共享锁,由于隔离级别是可重复读,因此查询结束后也一直持有共享锁资源,并等待10秒。
c)事务A5秒等待结束后,执行update申请持有ID=10数据的排它锁,此时事务B已经持有了共享锁,由于共享锁和排它锁互斥,所以事务A申请排它锁失败,继续等待事务B释放锁资源。这里其实也正是“可重复读”隔离级别解决“不可能重复”问题的关键。
d)事务B10秒等待结束后,继续执行下一个select查询,并申请持有ID=10数据的共享锁,由于共享锁可以共存,事务B申请成功并完成查询,前后查询数据保持一致。
e)事务A在事务B执行结束后,立即获取到ID=10数据的排它锁,并执行成功,事务A结束释放锁资源。
拓展:
锁分类:
悲观锁:使用数据库锁机制,牺牲并发性能,保持事务一致性。
乐观锁:不使用数据库锁机制,通常可通过操作前后校验数据的方式,比如字段中增加一个版本号version,如果更新前后版本号一致,则执行成功 否则返回错误提示,也能保证事务的一致性。
分布式事务
本地事务都是操作的本地单数据库,分布式事务中不同的操作可能涉及多个不同服务器上的数据库实例,因此本地事务不再满足。
解决方案:
1.两阶段提交(2PC)
准备阶段:事务协调者询问事务中的每个数据库参与者是否都执行事务成功,如果成功则进入下阶段。
提交阶段:事务协调者通知事务中每个参与者提交执行操作。
目前.Net中分布式事务支持TransactionScope 需启动MSDTC服务,即事务协调者。
特点:需要事务中涉及的每个参与者反馈成功才能最终提交,满足事务一致性,但是阻塞较长。
2.事务补偿机制(TCC)
该机制将事务中每个操作都注册对应的确认和补偿操作:分为三个阶段
1.try阶段:主要是对业务系统做检测和预留
2.confirm阶段:做确认提交,一旦try阶段成功,则默认confirm成功
3.cancel取消阶段:如果步骤执行失败,执行回滚。
3.本地事务+MQ消息
将分布式事务分为多个本地事务,不同服务器上的本地事务通过MQ消息发送。MQ分发的内容需要通过中间消息表进行记录并分发。满足事务最终一致性原则。
如:一个分布式事务中包含A,B两个不同数据库服务器上的操作,A操作执行本地事务+消息表记录B操作,MQ发送消息表信息,如果B服务器接收到信息,返回消息接收成功。那么A本地事务就会执行结束并提交。至于B服务器是否执行成功,A服务器不再关心。B服务器接收到消息之后,会在B本地事务执行,如果执行失败,则会一直重试,直到执行成功,以保持事务最终一致性。
其中如果A发送队列失败,也会重试发送。