前言

上回在谈到 Concurrency 并发控制 时, 有提到过事务的概念. 这篇就补上它具体的实现.

以前写过相关的文章:

sql server 学习笔记 (nested transaction 嵌套事务)

Asp.net core 学习笔记 ( ef core transaction scope & change level )

Begin Trans, Commit, Rollback

begin tran; -- 开启事务
commit; -- 提交事务
rollback; -- 回滚事务

最基本的 3 个操作方式

实战版

begin tran; -- 开启事务

begin try
insert into Product ([Name], Price) values ('mk100', 500); -- 插入数据
if(RAND() > 0.5) -- 随机 commit or throw error 模拟报错
commit; -- 提交事务
else
throw 50000, 'custom error',1; --报错
end try
begin catch
rollback; -- 捕获错误并且回滚
end catch

事务通常会搭配 try catch 一起用.

Isolation Level 隔离级别

事务通常会搭配 Isolation Level 来管理. Level 越高锁的范围就越大. 锁是双刃剑.

锁越多就越安全, 但性能也越差. 所以我们需要按需求来设定锁的范围, 这样才能达到平衡.

一共有 6 个 level. snapshot 其实有分 2 种, 但我不太熟, 这里就跳过它. 它的特点是利用了一些缓存 tempdb, 减少了锁. 算是, 有好有坏吧, 有兴趣的可以参考这篇这篇

Read Uncommitted < Read Committed < Repeatable Read < Serializable

Serializable 等级最高, 锁也最多.

Dirty Read and Read Committed Level 脏读

你想读一个数据, 但这个数据却被另一个事务修改了, 而那个事务还没有完成提交. 这时数据是不稳定的. 因为事务最后可能被回滚.

这个就叫脏读, 数据正在被修改, 它是脏的.

如果我们不允许这种事情发生, 那么可以 set Isolation Level 为 Read Committed 或者更高 Level.

设定后, 遇到读脏数据它就会先等待. 等到对方事务提交后才会读取可靠的值.

注: SQL 默认的 Level 就是 Read Committed. 所以通常我们不需要设置这个的, 反之如果我们不希望等待则可以设定 Level 为 Read Uncommitted, 顾名思义它就是 uncommitted (未提交) 也照读出来用.

我方角度: 等别人写完, 我才读

Non-repeatable Read and Repeatable Read Level 重复读

你读了一个数据, 并拿这个数据做逻辑判断来完成后续的操作. 在你还没有完成这个事务的时候, 另一个事务修改了这个数据.

这时你的逻辑判断是 "旧" 的. 如果你提交事务, 可能业务逻辑就错了.

这就叫重复读. 如果我们想避开这个问题就需要升级 Isolation Level 到 Repeatable Read.

当我们读出数据以后, 这个数据就被锁上了. 其它事务无法写入. 它们会一直等到我们提交事务.

我方角度: 读了不许别人写

Phantom and Serializable Level 幻读

Repeatable Read 的锁是有漏洞的. 它防 update 但不防 insert.

举例

select * from table where id = 1;

这时 id 1 这条 row 被锁了, 无法 update. 成功实现了 repeatable read.

select * from table where name like '%abc%';

这时但凡 name 里有 'abc' 字符的 row 都被锁了. 无法 update. 但是依然可以 insert

insert into table (name) values ('x abc y'); 这句是可以跑的.

如果我依赖 row count 来做逻辑判断, 那么就不安全了. 这就叫幻读.

解决方法就是再提升 Level 到 Serializable.

切记, Isolation Level 越高 > 锁越多 > 等待越多 > 死锁机率越高

Serializable 几乎是锁整个表了 (题外话: 它可以通过 index 做到一些优化哦, 不是每次都需要锁整张表)

Set Isolation Level

set transaction isolation level read uncommitted;
set transaction isolation level read committed;
set transaction isolation level repeatable read;
set transaction isolation level serializable;

Changing Isolation Level 后, 之前的锁, 怎办?

参考: Docs – SET TRANSACTION ISOLATION LEVEL Remark

Isolation 是可以来来回回切换的.

当 Level 改变后, 往后的 query 都会依据这个 Level 的限制度去加锁. 而那些在改变 Level 以前就已经加好的锁则会一直保留着.

当 Isolation 遇上 Trigger 和 Stored Procedures

参考: Docs – SET TRANSACTION ISOLATION LEVEL Remark

在 Trigger 和 SP 中修改 Isolation Level, 它只会在作用域内有效. 当执行完退出 Trigger 或 SP 后. Isolation Level 会自动被设置回之前 (before Trigger/SP) 的 Level.

Transaction + Isolation

Repeatable Read

begin tran;

set transaction isolation level repeatable read; -- 设置 Isolation Level

declare @value nvarchar(50);
select @value = [Value] from Table where Id = 1; -- 这时 row id 1 被锁了. 其它人不允许修改
insert into Table2 ([value]) values (@value); -- 依赖刚 select 的 value 做一些事儿 commit; -- 提交

Serializable

begin tran;

set transaction isolation level serializable; -- 设置 Isolation Level

declare @count nvarchar(50);

-- 所有 row value contains 'abc' 都被锁, 同时表也不允许插入, 也无法修改其它 row value 到 contains 'abc'
-- 总之就是确保 count 是绝对稳定的.
select @count = count(*) from Table where value like '%abc%'; insert into Table2 ([count]) values (@count); -- 依赖 count 做一些事儿 commit; -- 提交

Serializable 会大范围的锁表或 row, 以确保 count 是稳定的

事务嵌套 (nested transaction)

上面我们说, 一个事务由多个操作语句组合而成. 要嘛全部成功, 要嘛全部失败.

这个适用于简单的场景. 但当业务需求变得复杂以后, 我们需要更灵活的管理能力.

比如, 事务嵌套

一个事务可以由多个事务组成. 封装事务可以通过 Stored Proceduces.

同时, 我们要有能力选择性 rollback 单个小事务, 而不是每一次都强制 rollback 所有事务.

遗憾的是, SQL Server 并不直接支持事务嵌套 (Oracle 是支持的). 但是它有一些规则可以共我们使用, 从而达模拟出类似嵌套事务的效果.

trans, commit, rollbak, @@TRANCOUNT 和 save transaction 的玩法

参考: SQL Server嵌套事务探讨

begin tran 和 @@TRANCOUNT

每一次 begin trans 后 @@TRANCOUNT 会累加 1

begin tran;
select @@TRANCOUNT; -- 1
begin tran;
select @@TRANCOUNT; -- 2

commit 和 @@TRANCOUNT

每一次 commit 后 @@TRANCOUNT 会递减 1, 同时如果递减前 @@TRANCOUNT 是 1 那么还会持久化所有的数据.

在简单场景中过程是这样的.

1. begin tran 一开始 count++ 变成 1

2. do something...

3. commit, 这时 count = 1 于是持久化数据, 然后 count-- 回到 0

rollback 和 @@TRANCOUNT

rollback 会立刻回滚所有数据 (从 begin tran 之后的所有的数据操作都会被还原). 同时 @@TRANCOUNT 被设定成 0 (无论它当前是多少)

在简单场景中过程是这样的.

1. begin tran 一开始 count++ 变成 1

2. do something...

3. rollback, 还原数据, 然后 count = 0

save transaction (Save Point) and rollback transaction

在 begin tran 后, 任何一个时刻我们都可以做一个 save point (记入点)

save transaction save_point_name;

它是搭配 rollback transaction 一起用的.

rollback; 表示完全回滚到 begin tran

rollback transaction save_point_name 表示只回滚到 save point 那一刻 (而不是全部)

rollback transaction 和 @@TRANCOUNT

上面提到 rollback 会把 @@TRANCOUNT 变成 0.

但 rollback transaction 则不会, 它完全不会修改 @@TRANCOUNT (不会 set to 0, 也不会递减, 完全不动就是了)

模拟事务嵌套

参考:

Docs – SAVE TRANSACTION (Transact-SQL)

Stack Overflow – SAVE TRANSACTION vs BEGIN TRANSACTION (SQL Server) how to nest transactions nicely

假设我们用 Stored Procedures 来封装小事务.

那在 Stored Procedures 里面会是这样控制的.

1. 先查看当前是否有正在运行的 transaction. 通过 @@TRANCOUNT <> 0 来判断.

2. 如果没有, 那 SP 负责 begin tran. 如果已经有了那 SP 只做一个 save point 就好.

3. 接着就是各种数据操作

4. 当数据操作全部都成功后, 如果是 SP 负责 begin tran 那这时需要 commit, 如果 SP 只是负责 save point, 那不需要做任何 commit 把职责留给外面.

5. 当遇到 error 的时候, 如果是 SP 负责 begin tran 那么就直接 rollback, 如果 SP 只是负责 save point 那就 rollback transaction save point 就好了. 最后 throw error 到外面

Why it work? 我们看看它是如何实现模拟事务嵌套的.

1. SP 判断是否已存在 transaction. 这一步就确保了全场只有一个 begin tran. @@TRANCOUNT 绝对不会超过 1

2. 当 SP 要 commit 的时候交由外部去 commit. 因为如果外部已有 transaction 那么肯定外面 (最上层) 至少会有一个 commit. (如果内部 commit @@TRANCOUNT 就会 -1 变成 0 那就会立刻持久化, 这不是想要的)

3. 当出现 error 的时候, 只 rollback SP 内的执行, 其余的交由外部. 外部捕获 error 后可以选择是否要忽略 SP 的问题 (skip 掉 SP), 或者连外部一柄 rollback.

在 SP 内 rollback save point 是不会修改 @@TRANCOUNT 的, 也相等于整个 SP 对外部完全没有产生任何副作用.

oh yeah, 至此 SQL Server 利用了许多规则终于实现了事务嵌套.

SP 大概长这样

create procedure SP_DoSomething
AS
declare @startCount int;
set @startCount = @@TRANCOUNT;
if @startCount = 0
begin tran;
else
save transaction SavePoint1; begin try
-- do anything your want
if @startCount = 0
commit;
end try
begin catch
if @startCount = 0
rollback;
else
IF XACT_STATE() <> -1
rollback transaction SavePoint1;
throw;
end catch

补充说明:

XACT_STATE() 会返回一个数 (1, 0 或者 -1)

1 表示 transaction 中没有任何 query 报错. 那进入到 catch 的原因可能是其它的逻辑问题 (比如 convert 失败等等)

0 表示 transaction 中没有任何 side effect 的 query. 没有 insert, update, 都是 select 操作

-1 表示 transaction 中有执行失败的 query. 在这个时候我们就必须执行 rollback 了.

SQL Server – Transaction & Isolation 事务与隔离的更多相关文章

  1. 转载 SQL Server 2008 R2 事务与隔离级别实例讲解

    原文:http://blog.itpub.net/13651903/viewspace-1082730/ 一.事务简介 SQL Server的6个隔离级别中有5个是用于隔离事务的,它们因而被称作事务隔 ...

  2. SQL Server中的事务与其隔离级别之脏读, 未提交读,不可重复读和幻读

    原本打算写有关 SSIS Package 中的事务控制过程的,但是发现很多基本的概念还是需要有 SQL Server 事务和事务的隔离级别做基础铺垫.所以花了点时间,把 SQL Server 数据库中 ...

  3. (转)深入sql server中的事务

    原文地址:http://www.cnblogs.com/chnking/archive/2007/05/27/761209.html 参考文章:http://www.cnblogs.com/zhuif ...

  4. SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因

    原本打算写有关 SSIS Package 中的事务控制过程的,但是发现很多基本的概念还是需要有 SQL Server 事务和事务的隔离级别做基础铺垫.所以花了点时间,把 SQL Server 数据库中 ...

  5. Sql Server中的事务隔离级别

    数据库中的事物有ACID(原子性,一致性,隔离性,持久性)四个特性.其中隔离性是用来处理并发执行的事务之间的数据访问控制.SqlServer中提供了几种不同级别的隔离类型. 概念 Read UnCom ...

  6. Microsoft SQL Server中的事务与并发详解

    本篇索引: 1.事务 2.锁定和阻塞 3.隔离级别 4.死锁 一.事务 1.1 事务的概念 事务是作为单个工作单元而执行的一系列操作,比如查询和修改数据等. 事务是数据库并发控制的基本单位,一条或者一 ...

  7. 【转】SQL Server中的事务与锁

    SQL Server中的事务与锁   了解事务和锁 事务:保持逻辑数据一致性与可恢复性,必不可少的利器. 锁:多用户访问同一数据库资源时,对访问的先后次序权限管理的一种机制,没有他事务或许将会一塌糊涂 ...

  8. SQL Server与Oracle中的隔离级别

    在SQL92标准中,事务隔离级别分为四种,分别为:Read Uncommitted.Read Committed.Read Repeatable.Serializable 其中Read Uncommi ...

  9. sql server 锁与事务拨云见日(中)

    一.事务的概述 上一章节里,重点讲到了锁,以及锁与事务的关系.离上篇发布时间好几天了,每天利用一点空闲时间还真是要坚持.听<明朝那些事儿>中讲到"人与人最小的差距是聪明,人与人最 ...

  10. Sql Server并发和事务

    锁的作用范围通常在事务中,事务是建立在并发模式下. 从SQL Server 2005开始,加入了一种新的并发模式-----乐观并发.不管使用哪种并发模式,如果多个会话同时修改相同的数据,都会产生资源争 ...

随机推荐

  1. 日常工作中需要避免的9个React坏习惯

    前言 React是前端开发领域中最受欢迎的JavaScript库之一,但有时候在编写React应用程序时,可能陷入一些不佳的习惯和错误做法.这些不佳的习惯可能导致性能下降.代码难以维护,以及其他问题. ...

  2. 实用!一键生成数据库文档的神器,支持MySQL/SqlServer/Oracle多种数据库

    Screw(螺丝钉)是一款简洁好用的数据库表结构文档生成工具,它的特点是:简洁.轻量.设计良好.多数据库支持.多种格式文档.灵活扩展以及支持自定义模板,对于有经常要进行数据库设计.评审.文档整理等需求 ...

  3. TokenObtainPairSerialize

    TokenObtainPairSerializer是Django REST framework的SimpleJWT库提供的序列化器.它用于对用户凭据(如用户名和密码)进行序列化和验证,并在成功的身份验 ...

  4. 几个适合Java开发者的免费IDEA插件

    今天,给大家推荐几个好用且免费的IntelliJ IDEA插件.如果你还没有用过,可以尝试一下,也许对你的日常工作会有一定的效率提升噢! RestFulTool 如果你是一个RESTful服务的开发者 ...

  5. CF1468N 题解

    洛谷链接&CF 链接 题目简述 共有 \(T\) 组数据,对于每组数据: 有三个桶,五种垃圾,每个桶有固定的容量. 前三种垃圾分别放入三种桶中,第四种垃圾可以放进 \(1,3\) 桶中,第五种 ...

  6. Java 基于Hutool实现DES加解密

    POM.XML配置 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="h ...

  7. DASCTF 2023六月挑战赛|二进制专项 PWN (上)

    DASCTF 2023六月挑战赛|二进制专项 PWN (上) 1.easynote edit函数对长度没有检查 free函数存在UAF漏洞 思路:1.通过堆溢出,UAF,修改size位达到堆块重叠,使 ...

  8. Linux服务器PBS任务队列作业提交脚本的使用方法

      本文介绍在Linux服务器中,通过PBS(Portable Batch System)作业管理系统脚本的方式,提交任务到服务器队列,并执行任务的方法.   最近,需要在学校公用的超算中执行代码任务 ...

  9. mysql执行步骤口诀:发连缓分析,优化执行器

    mysql执行步骤口诀:发连缓分析,优化执行器 mysql执行步骤: 1.我们在客户端发起一个SQL的查询: 2.连接器判断用户登录以及用户权限: 3.缓存命中,走缓存(mysql缓存不是redis缓 ...

  10. 【PostgreSQL】01 环境搭建

    [PostgreSQL数据库安装] 数据库本体就没下本机了,直接挂服务器的Docker上面跑 docker pull postgres:9.4 创建容器并运行: docker run --name p ...