关于数据库事务和锁的一些分析

本文将针对以下几个问题给大家解答:

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发送队列失败,也会重试发送。

posted @ 2021-08-03 15:40  跳跃的键盘手  阅读(494)  评论(0编辑  收藏  举报