关系数据库中的事务——SqlServer为例说明
一、关系数据库的事务简介
1.1、事务简介
简单的说事务是一个或多个SQL语句被看做是一个整体单元,要么都执行成功,要么只要有一个失败就全部失败【即事务确保一个事务内的所有语句都成功或都失败,不允许部分完成】。
1.2、事务的ACID特征
事务一般应用于联机事务处理 (OLTP)场景中:且事务具备ACID特征
序号 | 事务的ACID特征 | 说明 |
1 | 原子性(Atomicity) | 每个事务里面的内容都是一个不可分割的工作单位,要么事务中包含的各个操作全部都做,要么都不做,永远不会处于半完成状态。 (比如银行的资金转账功能从一个账户扣钱同时给另一个账户加钱是强相关不可分隔的,只要有一个操作完不成,另一个操作也会失败) |
2 | 一致性(Consistency) | 在事务在开始之前和结束之后,数据库的完整性没有被破坏,其他任何的故障都不应该对其造成影响【即事务只能将数据库中的数据从一种有效状态转换为另一种有效状态(执行成功)或回滚到原始的一致性状态(执行失败)】;会使用各种锁策略来支持事务的强一致性 (比如银行转账功能中,事务的完成状态必须反映出钱从一个账户转移到另一个账户) |
3 | 隔离性(Isolation) | 并发执行的各个事务之间不能相互干扰(每个事务内部使用和操作的数据对其他事务都是隔离的) (比如:银行的资金转账事务、余额查询事务;余额查询事务查询的余额结果要么是子资金转账前的数据不会变化,要么是在资金转账成功后的数据;资金转账的中间状态数据【可能会存在不一致】不应该暴露给其他事务) |
4 | 持久性(Durability) | 事务在完成提交后就保持“已提交”状态,即使发生系统崩溃后宕机等故障,也一定能够根据事务日志对持久化的数据重新进行操作,将其恢复到事务成功结束的状态【意味着事务在完成之后,该事务对数据库所做的更改便持久的保存在数据库中了,并不会因为系统故障而被回滚】 (许多数据库通过采用预写式日志机制【WAL(Write-Ahead Logging)】来保证事务持久性和数据完整性,减少IO操作) (比如:银行资金转账事务完成后,修改后的账户余额会持久保存,这样即使数据库崩溃或系统宕机,再次开启时也会反映已提交的事务) |
1.3、关系数据库的OLTP与OLAP两种使用场景
关系数据库使用场景一般分为【联机事务处理(OLTP)】【联机分析处理(OLAP)】:
序号 | 联机事务处理(OLTP)【写入密集型事务】 | 联机分析处理(OLAP)【读取密集型事务】 |
1 | 联机事务处理 (OLTP)指【使用计算机系统对事务数据进行管理,主要是跟踪和组织日常运营中发生的各种业务交互(这些交互通常有业务事务如订单的接收与交付、货款的收款与付款、货物的收发等;这些事务本身的事件通常会包含时间、数值等内容)】(OLTP的数据库针对单个记录条目进行了优化。 这些数据库保存有价值的信息) OLTP 系统设计用于高效地处理和存储事务,以及查询事务数据 | 联机分析处理(OLAP)指【组织大型业务数据库用来执行复杂计算和趋势分析的技术(此方法支持复杂的查询且不会中断事务;针对大量读取和低写入任务进行了优化,经过建模和清理,可以有效地从数据中提取商业智能,OLAP数据库通常会保留历史数据用来供时序分析)】 语义建模提供数据库架构的抽象级别,以便用户不需要知道基础数据结构。 最终用户无需对基础架构执行聚合和联接即可轻松查询数据。 列通常重命名为更易用的名称,以使数据的上下文和含义更加明显【语义建模主要适用于读取密集型方案】; 【语义层放置在数据仓库之上】语义层通常具有以下特征: 1、设置了聚合行为,以便报告工具可以正确显示它们。 2、定义了业务逻辑和计算。 3、包括了面向时间的计算。 4、数据通常是从多个源集成的。 5、支持实时分析。 |
2 | 事务数据的典型特征: | 语义建模的典型特征: |
3 | 何时选择联机事务处理(OLTP)? 2、当处理过程中任何明显的延迟都会对业务的日常运营造成负面影响时; | 何时选择联机分析处理(OLAP)? 1、需要快速运行复杂的分析和按需查询,而不会对 OLTP 系统产生负面影响。 2、你想要为业务用户提供一种从数据生成报表的简单方法。 3、你希望提供多个聚合,使用户能够快速、一致地获得结果 OLAP 对于针对大量数据应用聚合计算特别有用。 OLAP 系统针对读取密集型方案进行了优化 |
4 | 使用联机事务处理(OLTP)面临的挑战 1、OLTP 系统并非非常适合用于处理对大量数据的聚合; 2、在对高度规范化的数据进行分析和报告时,查询通常比较复杂,因为大多数查询需要使用联接来使数据非规范化; 3、无限期地存储事务历史记录以及在任何一个表中存储太多数据都会导致查询性能变慢,具体取决于所存储的事务数【常见的解决方案是在 OLTP 系统中维护一个相关时间范围(例如当前会计年度)并将历史数据卸载到其他系统】 | 使用联机分析处理(OLAP)面临的挑战 1、从各种源流入的事务会不断更新 OLTP 系统中的数据。 OLAP 数据存储通常以较慢的时间间隔刷新,具体取决于业务需求。 OLAP 系统适合战略业务决策,而不是立即响应更改。 还必须规划一定级别的数据清理和业务流程来使 OLAP 数据存储保持最新。 2、与 OLTP 系统中的传统规范化关系表不同,OLAP 数据模型往往具有多维性。 因此,很难或不可能将它们直接映射到实体关系或面向对象的模型,其中每个属性对应于一列。 相反,OLAP 系统通常使用星型或雪花型架构,而不是传统的规范化。 |
事务使用示例
二、事务的使用示例
2.1、事务的创建和异常回滚处理
1、事务的创建
--1、显式启动事务可使用全称【BEGIN TRANSACTION】或简称【BEGIN TRAN】
BEGIN TRANSACTION;--2、需要执行的多条sql语句--3、事务启动后,必须以提交【COMMIT TRANSACTION】或回滚结【ROLLBACK TRANSACTION】结束
COMMIT TRANSACTION;
ROLLBACK TRANSACTION;
2、事务的异常捕获回滚处理
BEGIN TRYBEGIN TRANSACTION;--需执行的多条sql语句COMMIT TRANSACTION;
END TRY
BEGIN CATCH
--捕获异常并回滚事务SELECT ERROR_NUMBER() AS ErrNum, ERROR_MESSAGE() AS ErrMsg;ROLLBACK TRANSACTION;
END CATCH;
序号 | XACT_STATE()函数值 | 说明 |
1 | 1 | 当前请求具有活动的可提交用户事务 |
2 | 0 | 无活动事务 |
3 | -1 | 当前请求具有活动用户事务,但发生了错误,导致事务被归类为不可提交事务 |
可以在 ROLLBACK 命令之前使用XACT_STATE【用来检查事务是否处于活动状态】
--实现事务只有在存在活动用户事务时才会被回滚
BEGIN TRYBEGIN TRANSACTION;--需要执行的多条sql语句COMMIT TRANSACTION;
END TRY
BEGIN CATCHSELECT ERROR_NUMBER() AS ErrNum, ERROR_MESSAGE() AS ErrMsg;IF (XACT_STATE()) <> 0BEGINROLLBACK TRANSACTION;END;END CATCH;
注意:
1、事务启动后必须进行在最后进行提交,如果连接在事务中途断开,整个事务将回滚 ;
2、事务是可以嵌套的,在这种情况下,如果外部事务回滚,内部事务将回滚;
3、如果事务中发生错误,请使用回滚命令(回滚命令则撤消在事务期间对数据所做的任何修改,使其处于事务启动前的状态);
4、存储过程中当 SET XACT_ABORT 为 ON 时,如果 SQL Server 引发错误,则会回滚整个事务。 当 SET XACT_ABORT 为 OFF 时,如果错误的严重性较低,则只回滚引发错误的语句;
5、由于并不总是清楚事务是提交还是回滚,因此向事务添加错误处理至关重要。
2.2、执行sql是否事务示例
--创建订单表
CREATE TABLE Test.dbo.Orders (custid int NOT NULL,empid int NOT NULL,orderdate datetime NOT NULL
);--创建订单详情表
CREATE TABLE Test.dbo.OrderDetails (orderid int NOT NULL,productid int NOT NULL,unitprice money NOT NULL,qty int NOT NULL
);
--模拟程序使用sql语句批量插入数据(故意给第二条数据的值设置错误)
BEGIN TRYINSERT INTO dbo.Orders(custid, empid, orderdate) VALUES (66, 6, GETDATE());INSERT INTO dbo.Orders(custid, empid, orderdate) VALUES (77, 'test', GETDATE());INSERT INTO dbo.OrderDetails(orderid,productid,unitprice,qty) VALUES (6, 666, 16.66, 66);INSERT INTO dbo.OrderDetails(orderid,productid,unitprice,qty) VALUES (7, 777, 27.77, 77);
END TRY
BEGIN CATCHSELECT ERROR_NUMBER() AS ErrNum, ERROR_MESSAGE() AS ErrMsg;
END CATCH;
如上《模拟程序使用sql语句批量插入数据(故意给第二条数据的值设置错误)》我们在批量执行多条sql语句插入数据的时候,只要不是第一条sql报错,那么第一条数据的内容就会被插入到表中,但是当后面的第二条sql报错后,第二条的数据是没有插入到表中,但是第一条插入的数据也并没有被回滚,导致数据库处于不一致的状态(即:该结果我们期望的要么数据都同时插入到表中,要么都不插入到表中的期望不符);
--模拟程序使用事务执行sql插入语句(这里也故意给第二天数据的值设置错误[执行后数据都没有插入到表中符合预期])
BEGIN TRYBEGIN TRANSACTION;INSERT INTO dbo.Orders(custid, empid, orderdate) VALUES (66, 6, GETDATE());INSERT INTO dbo.Orders(custid, empid, orderdate) VALUES (77, 'test', GETDATE());INSERT INTO dbo.OrderDetails(orderid,productid,unitprice,qty) VALUES (6, 666, 16.66, 66);INSERT INTO dbo.OrderDetails(orderid,productid,unitprice,qty) VALUES (7, 777, 27.77, 77);COMMIT TRANSACTION;
END TRY
BEGIN CATCHSELECT ERROR_NUMBER() AS ErrNum, ERROR_MESSAGE() AS ErrMsg;ROLLBACK TRANSACTION;
END CATCH;--模拟程序使用事务执行sql插入语句【修改为正常的内容后就可以全部插入表中也是符合预期】
BEGIN TRYBEGIN TRANSACTION;INSERT INTO dbo.Orders(custid, empid, orderdate) VALUES (66, 6, GETDATE());INSERT INTO dbo.Orders(custid, empid, orderdate) VALUES (77, 7, GETDATE());INSERT INTO dbo.OrderDetails(orderid,productid,unitprice,qty) VALUES (6, 666, 16.66, 66);INSERT INTO dbo.OrderDetails(orderid,productid,unitprice,qty) VALUES (7, 777, 27.77, 77);COMMIT TRANSACTION;
END TRY
BEGIN CATCHSELECT ERROR_NUMBER() AS ErrNum, ERROR_MESSAGE() AS ErrMsg;ROLLBACK TRANSACTION;
END CATCH;
如上《模拟程序使用事务执行sql插入语句》我们故意给第二条数据的值设置错误时则没有一条数据插入到表中符合预期;接下来我们使用都正确的事务sql语句时可以正常将数据插入表中也也符合预期;这样数据库就能永远处于一致的状态了。
2.3、并发
多用户数据库的核心功能是并发。 并发通过使用锁定和阻塞来使数据在许多用户同时更新和读取时保持一致。
注意:并发是在事务级别上发生的。 写入事务可能会阻止其他事务更新,甚至读取相同的数据。 同样,读取事务可以阻止其他读者,甚至是某些写入程序。 因此,必须避免不必要冗长的事务或涉及过多数据的事务。
关于锁的详细内容可以查看:事务锁定和行版本控制指南 - SQL Server | Microsoft Learn
关于阻塞的详细内容可查看:了解并解决阻止问题 - SQL Server | Microsoft Learn
序号 | 乐观并发 | 悲观并发 |
1 | 【使用乐观锁定时的假设是很少发生冲突的更新】在事务开始时,记录数据的初始状态。 在提交事务之前,当前状态将与初始状态进行比较。 如果状态相同,则事务已完成。 如果状态不同,则回滚事务 | 【使用悲观锁定时的假设是数据同时发生许多更新】通过使用锁,只能同时发生一个更新,并且更新发生时会阻止读取数据。 这可以防止出现如前面示例所示的大规模回滚,但可能导致不必要地阻止查询 |
2 | 示例:你有一个包含去年车间产线产品的表。 此数据很少更新,但会经常运行报告。 通过使用乐观锁定,事务不会相互阻止,因此系统运行效率更高。 遗憾的是,在过去几年中发现了错误,需要进行数据和更新。 当一个事务更新每一行时,另一个事务同时对单个行进行轻微编辑。 由于数据的状态已在初始事务运行时发生变化,因此将回滚整个事务 | |
并发影响 | Microsoft Learn |
数据库引擎中的隔离级别 | Microsoft Learnhttps://learn.microsoft.com/zh-cn/previous-versions/sql/sql-server-2008-r2/ms189122(v=sql.105)
Snapshot Isolation in SQL Server(SQL Server 中的快照隔离) - ADO.NET | Microsoft Learnhttps://learn.microsoft.com/zh-cn/dotnet/framework/data/adonet/sql/snapshot-isolation-in-sql-server SQL Server 中有五个不同的隔离级别,如上图所示:
序号 | 快照隔离说明 |
1 | 如果查询使用的是【读取已提交】事务隔离级别,则 READ_COMMITTED_SNAPSHOT_OFF 将在事务结束之前保留对受影响行的锁定。 尽管某些更新可能会发生,例如创建新行,但这可以防止对正在读取或更新的数据进行大多数冲突的更改。 这是悲观并发 |
2 | READ_COMMITTED_SNAPSHOT_ON 可获取数据的快照。 然后,在该快照上完成更新,允许其他连接查询原始数据。 在事务结束时,数据的当前状态与快照进行比较。 如果数据相同,则提交事务。 如果数据不同,则回滚事务 |
3 | |
READ_COMMITTED_SNAPSHOT_OFF 是 SQL Server 的默认隔离级别【即默认是悲观锁】 |
三、其他资料
使用 Transact-SQL 实现事务 - Training | Microsoft Learnhttps://learn.microsoft.com/zh-cn/training/modules/implement-transactions-transact-sql/?source=recommendations事务处理 - .NET Framework | Microsoft Learn
https://learn.microsoft.com/zh-cn/dotnet/framework/data/transactions/CLR 集成和事务 - SQL Server | Microsoft Learn
https://learn.microsoft.com/zh-cn/sql/relational-databases/clr-integration-data-access-transactions/clr-integration-and-transactions?view=sql-server-ver17使用 Transact SQL 实现事务 | dp-080-transact-SQL
https://microsoftlearning.github.io/dp-080-Transact-SQL/Instructions/Labs/13-implement-transitions-in-tsql.html浏览所有课程、学习路径和模块 - Training | Microsoft Learn
https://learn.microsoft.com/zh-cn/training/browse/?products=azure-sql-database%2Cazure-sql-managed-instance%2Cazure-sql-virtual-machines%2Cazure-sqlserver-vm%2Csql-server&levels=advanced&roles=data-analyst%2Cdata-engineer%2Cdata-scientist%2Cdatabase-administrator%2Cdeveloper%2Csolution-architect%2CstudentSQL - 事务:初学者指南 - 高级SQL - W3schools
https://w3schools.tech/zh-cn/tutorial/sql/sql-transactions
事务、事务的四大特性(ACID)、三大并发问题、四种锁、事务的隔离级别https://www.cnblogs.com/jimoyu/p/12251282.html深入学习MySQL事务:ACID特性的实现原理 - 编程迷思 - 博客园
https://www.cnblogs.com/kismetv/p/10331633.htmlsql server中的事务模式(隐式事务,显式事务,自动提交事务)
https://www.cnblogs.com/gered/p/10437395.html
数据库历史数据如何处理 https://docs.pingcode.com/baike/2077206