当前位置: 首页 > news >正文

一文讲清:数据库事务、锁、隔离级别

事务是数据库执行的一组操作(如多条SQL)的逻辑单元,要么全部成功,要么全部失败,不存在中间状态。

一、数据库事务的4个基本特征

数据库事务具有以下4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID。

  • 原子性(Atomicity):事务里的操作是一个整体,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务前后,数据必须符合所有约束规则(如金额不能为负)。
  • 隔离性(Isolation):多个事务并发执行时,彼此像串行一样隔离,防止数据混乱。
  • 持久性(Duration):事务一旦提交,数据永久保存,即使断电也不丢失。

举例:银行转账(A给B转100元)

操作包含两步:① A账户扣100元;② B账户加100元。

  • 原子性:如果第②步服务器宕机,第①步的扣款会回滚,A的钱一分不少。

  • 一致性:转账前后,A+B的总金额始终不变(都是1000元),不会凭空多出或消失。

  • 隔离性:转账过程中(A已扣但B未加),C查总账看到的是转账前的旧状态,不会看到“钱少了还没到”的脏数据。

  • 持久性:转账成功后,银行系统提示“到账”,即使立刻停电,重启后B的账户里也永久多了这100元。

二、锁机制介绍

  • 乐观锁:假定每次操作都不会发生冲突,所以不加锁,只在提交更新时检查数据是否被修改过,若被改过则拒绝提交并重试。

  • 悲观锁:假定每次读写数据都会发生冲突,所以在操作前先加锁,阻塞其他事务,直到操作完成才释放。

乐观锁的常见实现(实际不加锁,靠版本控制):

  • 版本号机制(最常用):表中增加version字段,更新时SET version=version+1 WHERE id=? AND version=旧值,若影响行数为0则说明已被改过。
  • 时间戳机制:用last_updated时间戳代替版本号,判断是否被更新。
  • CAS(比较并交换)思想:在SQL中直接通过条件判断状态,如UPDATE stock SET num=num-1 WHERE id=1 AND num>0

悲观锁的常见实现(由数据库提供):

  • 排他锁(X锁/写锁):用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时对同一资源进行多重更新。锁定后其他事务无法修改或加锁。
-- NOWAIT:立即返回错误而不等待 -- SKIP LOCKED:跳过已被锁定的行 SELECT ... FOR UPDATE [NOWAIT | SKIP LOCKED]

扩展:MySQL-select ... for update语句详解

  • 共享锁(S锁/读锁):用于不更改或不更新数据的操作(只读操作),如 SELECT 语句,允许其他事务读取(加共享锁),但禁止修改。
SELECT ... LOCK IN SHARE MODE

扩展:锁定查询结果 LOCK IN SHARE MODE

按粒度划分表锁(锁整张表)和行锁(InnoDB 仅锁索引行),还有间隙锁(Gap Lock)防止幻读等。


补充:死锁是什么?

死锁是指两个或多个事务在并发执行时,互相持有对方需要的资源,并等待对方释放,导致所有事务永久阻塞、无法继续执行的局面。

经典例子(相互等待):事务A先锁住表1,试图去锁表2;事务B先锁住表2,试图去锁表1
A等B释放表2,B等A释放表1,两人谁也不松手,形成循环等待,永远卡死。


如何解决死锁?

解决思路分为预防超时检测回滚三个层面:

1. 预防(代码层面,最根本)

  • 固定访问顺序:约定所有事务都按相同顺序访问资源(如先更新表1,再更新表2),破坏“循环等待”条件。

  • 尽量缩短事务:将大事务拆小,减少持锁时间,降低冲突概率。

2. 超时机制(数据库设置)

  • 设置锁等待超时参数(如MySQL的innodb_lock_wait_timeout),事务等待超过指定时间(如50秒)则主动放弃并回滚,避免无限等待。

3. 死锁检测与自动回滚(数据库自带)

  • InnoDB等存储引擎会实时检测等待图,一旦发现环状死锁,立即选择回滚“代价较小”的事务(如undo记录少的事务),让其释放锁,让另一个事务继续执行。被回滚的事务会收到错误码,由应用程序捕获后重试。

注:什么是死锁和如何解决死锁

三、丢失更新

在多个事务同时操作数据库的情况下,会引发丢失更新的场景。比如,电商有一种商品,在疯狂抢购(如秒杀)中,会出现多个事务同时访问商品库存的场景,这样就会产生丢失更新。一般而言,存在两种类型的丢失更新,接下来让我们从一个例子中了解他们。

假设一种商品的库存数量还有100,每次抢购都只能抢购1件商品,那么在抢购中就可能出现下表中的情况。

时刻事务1事务2
T1初始库存100初始库存100
T2扣减库存,余99
T3扣减库存,余99
T4提交事务,库存变为99
T5回滚事务,库存100

从上表中可以看到,T5时刻事务1回滚,导致原本库存为99的变为了100,这导致事务2的提交结果丢失了。

类似地,对于这样一个事务回滚另一个事务提交而引发的数据不一致的情况,我们称之为第一类丢失更新。(既事务回滚导致的另一个事务的更新丢失)

对于第一类丢失更新,因为目前大部分数据库已经解决了,所以我们不对此进行深入讨论。接下来,我们来看看什么是第二类丢失更新。

时刻事务1事务2
T1初始库存100初始库存100
T2扣减库存,余99
T3扣减库存,余99
T4提交事务,库存变为99
T5提交事务,库存变为99

注意T5时刻提交的事务。因为在事务1中,无法感知事务2的操作,这样它就不知道事务2已经修改过了数据,因此它依旧认为只是发生了一笔业务,所以库存变成了99,这导致事务2提交的结果丢失。

像这样,多个事务都提交而引发的丢失更新称为第二类丢失更新。(即事务A覆盖事务B已经提交的数据,造成的丢失更新)

为了克服第二类丢失更新,数据库提出了事务之间的隔离级别的概念,下面,让我们详细看看事务的隔离级别。

四、事务的隔离级别

1、未提交读(READ_UNCOMMITTED)

未提交读是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。这是一种危险的隔离级别,所以一般在我们实际的开发中应用不广,但是它的优点在于并发能力高,适用于那些对数据一致性没有要求而追求高并发的场景,他的最大坏处是出现脏读(一个事务读取了另一个尚未提交(未Commit)的事务修改过的数据​​​​​​​

举例:卖家A在事务中把房价从500万改成400万(未提交)。买家B去查房价,看到400万,大喜过望开始筹钱。结果卖家A反悔,执行了回滚(Rollback),价格恢复成500万。
结果:B读到的400万是根本不存在的“脏数据”,纯属白高兴一场。

时刻事务1事务2备注
T0商品库存初始为2
T1读取库存为2
T2扣减库存此时库存为1
T3扣减库存此时库存为0,因为读取到事务1未提交的库存数据
T4提交事务库存保存为0
T5回滚事务因为第一类丢失更新已经解决,所以不会回滚为2。因此此时库存为0

上表的T3时刻,因为采用未提交读,所以事务2可以读取事务1未提交的库存数据(库存为1),这里当它扣减库存后提交,数据库将保存此数据为0。当事务1回滚时,结果仍然为0。很显然,这是错误的,发生了脏读现象。

脏读:A事务读取B事务尚未提交的更改数据,并在这个数据的基础上进行操作,这时候如果事务B回滚,那么A事务读到的数据是不被承认的。

为了克服脏读的问题,数据库隔离级别提供了读写提交(READ COMMITTED)

2、读写提交(READ COMMITTED)

读写提交隔离级别,是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据

时刻事务1事务2备注
T0商品库存初始为2
T1读取库存为2
T2扣减库存此时库存为1
T3扣减库存库存为1,因为是读写提交隔离级别,读取不到事务1未提交的库存数据
T4提交事务库存保存为1
T5回滚事务因为第一类丢失更新已经解决,所以不会回滚为2。因此此时库存为1,结果正确

在T3时刻,由于采用了读写提交的隔离级别,所以事务2不能读取到事务1中未提交的数据,因此解决了脏读问题。但是,读写提交也会导致下面的问题:

时刻事务1事务2备注
T0商品库存初始为1
T1读取库存为1
T2扣减库存事务未提交
T3读取库存为1认为可扣减
T4提交事务库存保存为0
T5扣减库存失败,因为此时库存为0,无法扣减

在T3时刻事务2读取库存时候,因为事务1未提交事务,所以读出的库存为1,于是事务2认为当前可扣减库存。当时当T4时刻,事务1提交事务后,在T5时刻,事务2会发现扣减失败。像这样的问题,叫做不可重复读

不可重复读:在一个事务内,两次读取同一行数据,由于中间被其他事务修改(UPDATE)并提交了,导致两次读到的值不一样。

举例:财务事务A在上午9点查询员工张三的工资,显示为5000元。事务进行中,老板事务B在9点05分给张三涨薪到6000元提交。9点10分,财务事务A再次查询张三工资,发现变成了6000元
结果:同一个事务里,同一行数据前后不一致,对账对不上。

为了克服这个不足,数据库的隔离级别进一步提出了可重复读的隔离级别。

3、可重复读(REPEATABLE READ)

可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能一些值的变化,影响当前事务的执行,如上诉的库存就是一个变化的值。接下来,让我们看看可重复读隔离级别是如何解决不可重复读的。

时刻事务1事务2备注
T0商品库存初始为1
T1读取库存为1
T2扣减库存事务未提交
T3尝试读取库存不允许读取,等待事务1提交
T4提交事务库存保存为0
T5读取库存库存为0,无法扣减

可以看到,事务2在T3时刻尝试读取库存,但是此时这个库存已经被事务1事先读取,锁住了(相当于加了个写锁,不允许其他事务读和写,实际上是使用行级锁,只锁住该行数据,不允许其他事务读写),所以这个时候数据库就阻塞了事务2的读取,直至事务1提交,事务2才能读取库存的值。此时,完美解决不可重复读问题。

但是这样也会引发新的问题——幻读

时刻事务1事务2备注
T0读取库存50件商品库存初始为100,现在已经销售50件,库存50件
T1查询交易记录,50笔
T2扣减库存事务未提交
T3插入一笔交易记录
T4提交事务库存保存为49件,交易记录为51笔
T5打印交易记录,51笔这里与查询的不一致,在事务2看来有一笔交易记录是虚幻的

这便是幻读现象。幻读:在一个事务内,两次执行同样的范围查询,由于中间被其他事务插入(INSERT)或删除(DELETE)并提交了,导致第二次查询多出(或少掉)几行“凭空出现”的记录。

举例:人事事务A查询“技术部总人数”,得到10人。事务进行中,新员工事务B插入了一条新记录“小王(技术部)”并提交。人事事务A再次查询“技术部总人数”,发现变成了11人
结果:多了个“幽灵”人员,就像出现了幻觉,且这多出的行无法用锁住“某一行”来阻止。

注意:幻读和不可重复读的区别

不可重复读是指读到了已经提交的事务的更改数据(修改或删除),幻读是指读到了其他已经提交事务的新增数据。对于这两种问题解决采用不同的办法。为了防止读到更改数据(解决不可重复读),只需对操作的数据添加行级锁,防止操作中的数据发生变化;而为了防止读到新增数据(解决幻读),往往需要添加表级锁,将整张表锁定,防止新增数据(oracle采用多版本数据的方式实现)。

4、串行化(SERIALIZABLE)

串行化是数据库中最高的隔离级别,它要求所有的事务排队顺序执行,即事务只能一个接一个地处理,不能并发。所以它能完全保证数据的一致性。

隔离级别和可能发生的现象总结如下:

隔离级别脏读不可重复读幻读第一类丢失更新第二类丢失更新
未提交读允许允许允许不允许允许
读写提交不允许允许允许不允许允许
可重复读不允许不允许允许不允许不允许
串行化不允许不允许不允许不允许不允许

注意:事务的隔离级别和数据库并发性是成反比的,隔离级别越高,并发性越低。所以应该根据实际情况选择事务的隔离级别。

对于隔离级别,不同的数据库对其的支持也是不一样的。例如,Oracle只能支持读写提交和串行化,而MySQL则能够全部支持。对于Oracle默认的隔离级别为读写提交,MySQL则是可重复读。

5. 总结

  • 读未提交(最低级):不加任何防护。事务没提交的改动,别人也能看到(脏数据满天飞),极不推荐。

  • 读已提交(Oracle默认):只能看到别人提交的数据。解决了脏读,但同一个查询查两次结果可能不同(不可重复读)。

  • 可重复读(MySQL默认):事务开始后,多次查询同一数据始终一致。解决了不可重复读,但查范围数据时可能多出“幽灵行”(幻读)。

  • 串行化(最高级):强制事务挨个排队执行,完全隔离,但并发性能极差,相当于单线程写数据库。

http://www.jsqmd.com/news/1277693/

相关文章:

  • 5分钟搭建企业级数据治理平台:DataHub终极指南
  • 保健按摩师考试高效备考:知云题库与刷题技巧
  • 2026年防锈漆厂家推荐:环氧防锈漆、醇酸防锈漆、水性防锈漆、钢结构防锈漆优质品牌深度解析 - 卓企推荐
  • 5分钟快速上手:基于Chihaya构建企业级P2P分发系统的完整实战指南
  • 3步改造Flipper Zero:从单调白屏到炫彩RGB背光的完整指南
  • Django与大数据构建短视频推荐系统实践
  • 2026元宝区女人街本地好口碑优质靠谱丹东女装店
  • Frida动态分析:spawn与attach模式对抗反调试机制实战
  • 基于开源技术栈构建企业级AI Agent:从知识库构建到私有化部署实践
  • N_m3u8DL-RE终极指南:三分钟掌握流媒体视频下载技巧
  • (2026最新)大理本地人必选的靠谱漏水检测维修推荐:正规防水补漏防水-卫生间/厨房/屋顶/阳台/外墙渗漏水精准测漏,本地人的信赖之选 - 安佳防水
  • Claude Code系统提示精简80%:AI编程助手如何实现少即是多
  • JAVA游戏下载神器!一键海量资源,安卓秒玩经典,管理超省心
  • 终极Minecraft离线启动器:无需账号快速畅玩的完整指南
  • 豆包即梦图片水印去除方法:2026关闭水印与规则解读 - 耶斯去水印
  • 中文AI大模型横向评测:性能差异与选型指南
  • ROFL-Player:3分钟学会的英雄联盟回放分析工具
  • Tiny11Builder终极指南:快速打造精简版Windows 11系统镜像
  • 深入解析TI TPIC7710EVM评估板:硬件设计、软件操作与汽车电子系统验证
  • 百度网盘提速与直链解析终极指南:告别KB级限速的5种高效加速方案
  • HarmonyOS应用开发实战:猫猫大作战-fileIo 的文本文件操作
  • 盘点五种最常见的业务逻辑漏洞挖掘案例,零基础学网络安全最快上手拿赏金的方法你一定要知道!
  • Graph API权限滥用实战分析:3个步骤实现从证书泄露到全局管理员权限提升
  • C++ 从凸包中删除点(Deleting points from Convex Hull)
  • League Akari:英雄联盟玩家的终极效率工具
  • 【通义千问私有化部署终极 checklist】:NVIDIA A10/A800/H20适配清单、国产信创环境兼容矩阵、安全审计必检项(含等保2.0合规对照表)
  • (2026最新)大同本地人必选的靠谱漏水检测维修推荐:正规防水补漏防水-卫生间/厨房/屋顶/阳台/外墙渗漏水精准测漏,本地人的信赖之选 - 安佳防水
  • 从零理解强化学习:核心概念、算法演进与PPO、DQN等实战入门
  • Claude模型选择指南:Opus、Sonnet、Haiku的成本效益与场景化应用
  • 星火应用商店:Linux桌面软件生态的革命性解决方案 [特殊字符]