1. 引言并发控制的重要性在现代数据库管理系统DBMS中多个用户或应用程序可能同时访问和修改同一份数据。这种并发访问能够显著提高系统的吞吐量和资源利用率但同时也带来了数据不一致的风险。例如两个用户同时尝试购买最后一张机票或者同时从同一个银行账户转账如果没有适当的控制机制就可能导致数据错误、丢失更新等问题。并发控制是数据库管理系统确保事务在并发执行时仍能保持数据一致性和完整性的机制。它通过协调多个事务对共享数据的访问防止它们互相干扰从而维护数据库的正确状态。锁机制是实现并发控制最核心的技术之一此外还有多版本并发控制MVCC、时间戳排序等。本章将全面深入地探讨数据库中的各种锁机制共享锁、排他锁、互斥锁、行锁、表锁、页面锁、悲观锁、乐观锁以及典型的并发问题丢失修改、读脏数据、不可重复读帮助读者建立完整的并发控制知识体系。2. 事务与ACID特性在深入锁机制之前我们需要先理解事务的概念。事务是数据库操作的基本执行单元它由一组逻辑相关的操作组成这些操作要么全部成功要么全部失败回滚。事务必须满足四个关键特性即ACID原子性Atomicity事务中的所有操作要么全部完成要么全部不执行。如果事务在执行过程中发生错误系统会将其回滚到开始前的状态。一致性Consistency事务执行前后数据库都必须处于一致状态。例如转账事务前后两个账户的总金额保持不变。隔离性Isolation并发执行的事务之间互不干扰一个事务的中间状态对其他事务是不可见的。隔离性正是通过并发控制机制如锁来实现的。持久性Durability一旦事务提交其对数据库的修改就应该是永久性的即使系统崩溃也不会丢失。其中隔离性直接关系到并发控制。不同的隔离级别允许不同程度的并发干扰从而在性能和数据一致性之间做出权衡。本章后续将详细讨论隔离级别与锁的关系。3. 并发操作引发的问题当多个事务同时操作同一数据时可能会产生以下几类典型的数据一致性问题本章后续将逐一深入解析丢失修改Lost Update两个事务同时读取同一数据并修改后提交的事务覆盖了先提交的事务的修改导致先提交的更新丢失。读脏数据Dirty Read一个事务读取了另一个未提交事务写入的数据。如果那个未提交的事务之后回滚则读取到的数据就是无效的“脏”的。不可重复读Non-Repeatable Read一个事务内两次读取同一数据但由于其他事务在期间修改并提交了该数据导致两次读取结果不一致。幻读Phantom Read一个事务内两次执行相同的查询第二次查询返回了第一次没有出现的行通常是因为其他事务插入了新数据就好像出现了“幻影”行。幻读可以视为不可重复读的一种特殊形式但通常针对的是插入操作。这些问题严重违反了事务的隔离性和一致性必须通过并发控制机制来解决。锁正是最基础的解决方案。4. 锁的基本概念与分类锁是一种用于管理对共享资源并发访问的同步机制。在数据库中锁被用来控制多个事务对数据项如表、行、页等的访问权限。一个事务在访问某数据项之前必须先请求并获得相应的锁操作完成后再释放锁。锁可以从不同维度进行分类按锁的模式权限分为共享锁S锁、排他锁X锁、意向锁IS锁、IX锁等、更新锁U锁等。按锁的粒度分为行级锁、页级锁、表级锁、库级锁等。按锁的实现策略分为悲观锁依赖数据库锁机制和乐观锁依赖版本控制。按锁的持有策略分为两阶段锁2PL事务开始后逐渐加锁到释放阶段不再加锁等。本章将重点讨论共享锁、排他锁、互斥锁以及各种锁粒度并深入介绍悲观锁与乐观锁这两种设计思想。5. 共享锁与排他锁互斥锁5.1 定义共享锁Shared Lock简称S锁又称为读锁。若事务T对数据对象A加上共享锁则事务T可以读A但不能修改A。其他事务只能再对A加共享锁而不能加排他锁直到T释放A上的共享锁。共享锁保证了在读取期间数据不会被其他事务修改但允许多个事务同时读取同一数据。排他锁Exclusive Lock简称X锁又称为写锁。若事务T对数据对象A加上排他锁则事务T既可以读A也可以修改A。其他事务不能再对A加任何类型的锁直到T释放A上的排他锁。排他锁保证了事务对数据的独占访问防止其他事务并发读写。互斥锁Mutex通常与排他锁是同义词强调其互斥特性即一次只能有一个事务持有该锁。在数据库领域我们通常称排他锁为互斥锁。5.2 锁的兼容性锁的兼容性是指在同一数据对象上多个事务同时申请不同类型的锁是否允许。共享锁和排他锁的兼容性矩阵如下已持有锁请求锁兼容性无锁S锁是无锁X锁是S锁S锁是S锁X锁否X锁S锁否X锁X锁否S锁与S锁兼容多个事务可以同时读取同一数据不会相互干扰。S锁与X锁互斥如果有事务正在读取持有S锁则不允许其他事务写入请求X锁反之亦然。这确保了读操作不会读到未提交的写数据写操作不会破坏读的一致性。5.3 加锁与释放锁的时机两阶段锁协议为了保证事务的隔离性数据库通常采用两阶段锁协议2PL来管理锁的申请和释放。两阶段锁协议将事务分为两个阶段第一阶段扩展阶段Growing Phase事务可以申请新的锁但不能释放任何锁。第二阶段收缩阶段Shrinking Phase事务可以释放锁但不能申请新的锁。两阶段锁协议保证了事务的冲突可串行化即并发执行的结果与某种串行执行的结果等价从而避免了并发问题。但严格的两阶段锁可能导致死锁数据库需要有死锁检测和处理机制。5.4 示例说明假设有两个并发事务T1和T2操作数据项A初始值为100。场景1使用共享锁进行读取T1请求对A加共享锁S锁成功。T1读取A100。T2请求对A加共享锁S锁兼容成功。T2也读取A100。T1完成读取释放S锁。T2也完成读取释放S锁。结果两个事务同时读取互不影响。场景2读取与写入冲突T1请求对A加共享锁S锁成功。T1读取A100。T2请求对A加排他锁X锁以修改A但T1持有S锁不兼容因此T2必须等待。T1完成读取释放S锁。T2立即获得X锁修改A为200并提交释放X锁。结果T2的修改在T1读取之后进行T1没有读到未提交的修改避免了脏读。场景3两个写入冲突T1请求对A加排他锁X锁成功。T1修改A为150但未提交。T2请求对A加排他锁X锁以修改A但T1持有X锁不兼容T2必须等待。T1提交或回滚后释放X锁T2获得X锁继续操作。结果保证了T1和T2对A的修改是串行的避免了丢失修改。5.5 升级与意向锁在实际的多粒度锁环境中如表锁和行锁共存需要引入意向锁来表示事务想要在更细粒度上加锁的意图。意向锁可以看作是一种提前声明避免每次检查锁冲突时都要遍历所有细粒度锁。常见的意向锁有意向共享锁IS锁事务想要对一个数据对象的某些子对象加共享锁。意向排他锁IX锁事务想要对一个数据对象的某些子对象加排他锁。意向锁遵循的兼容规则是意向锁之间以及与S/X锁之间可能存在一定的兼容关系例如IS锁与IS锁兼容IS锁与IX锁兼容但IX锁与S锁不兼容因为S锁要求整个表不能被修改而IX锁表示某些行可能被修改。意向锁使得加表锁时能够快速判断是否存在行级锁冲突无需扫描整个表的行锁列表。5.6 共享锁与排他锁的实现差异不同数据库不同数据库对共享锁和排他锁的实现细节有所不同。例如MySQL InnoDB实现了标准的S/X锁同时支持多粒度锁行锁表锁和意向锁。InnoDB的S锁和X锁既可以加在行上也可以加在表上通过意向锁间接实现表锁。Oracle默认使用多版本并发控制MVCC实现读不阻塞写、写不阻塞读因此普通的查询SELECT不加任何锁除了SELECT ... FOR UPDATE会加锁。其锁机制主要针对写操作通过TX锁事务锁类似于排他锁和TM锁表级锁来实现。SQL Server支持多种锁粒度并提供丰富的锁升级机制。其共享锁和排他锁行为与标准类似但可以通过锁提示如WITH (HOLDLOCK)来改变锁的持有方式。6. 锁的粒度行锁、表锁、页面锁锁的粒度指的是锁作用的数据范围大小。粒度越细并发度越高但锁管理的开销也越大粒度越粗并发度越低但开销小。数据库系统需要在并发性和开销之间取得平衡因此提供了多种锁粒度供选择。6.1 行级锁Row Lock定义锁定数据库表中的某一行记录。这是最细粒度的锁只影响被操作的行。优点并发度高不同事务可以同时操作同一表中的不同行互不干扰。特别适合OLTP在线事务处理系统如订单处理、银行交易等。缺点锁管理开销大需要维护大量的锁结构可能占用较多内存。加锁和解锁操作比粗粒度锁慢。此外如果事务涉及大量行锁数量过多可能导致锁开销甚至锁升级。实现方式通常通过索引项来实现行锁。例如InnoDB的行锁是基于索引记录的锁如果查询没有使用索引则会退化为表锁实际是锁住所有行但形式上仍是行锁不过效果等同于表锁。示例sql-- 事务T1 BEGIN; SELECT * FROM account WHERE id 1 FOR UPDATE; -- 对id1的行加行级排他锁 UPDATE account SET balance balance - 100 WHERE id 1; -- 事务T2可以同时操作id2的行而不被阻塞 UPDATE account SET balance balance 100 WHERE id 2; -- 成功 COMMIT;6.2 表级锁Table Lock定义锁定整个表。一旦事务获得表的锁其他事务对该表的任何访问取决于锁模式都会被阻塞。优点开销小管理简单适合以表为主要操作单位的场景如数据仓库中的批量加载、DDL操作ALTER TABLE等。也可以避免死锁因为锁范围大冲突容易预测。缺点并发度极低任何需要访问该表的事务都可能被阻塞严重影响系统吞吐量。兼容性表锁也分为共享表锁表S锁和排他表锁表X锁。表S锁允许其他事务读表但不允许写表表X锁不允许其他事务任何访问。示例sql-- MySQL中使用LOCK TABLES显式加表锁 LOCK TABLES account WRITE; -- 对account表加排他表锁 UPDATE account SET balance balance - 100 WHERE id 1; UPDATE account SET balance balance 100 WHERE id 2; UNLOCK TABLES; -- 释放锁6.3 页面锁Page Lock定义锁定数据库存储中的一页Page页是存储引擎管理的最小I/O单位通常大小为4KB、8KB或16KB包含多行数据。优点介于行锁和表锁之间。相比于行锁页面锁管理开销较小相比于表锁并发度更高。适用于某些特定的存储引擎如SQL Server默认使用页锁但会根据情况升级。缺点可能造成不必要的阻塞因为即使事务只修改页中的一行也会锁定整个页导致其他事务无法访问同一页的其他行从而引发“相邻行阻塞”问题。示例在SQL Server中当执行DML操作时默认会先尝试获得页锁或行锁如果扫描大量页可能升级为表锁。6.4 锁粒度的选择策略数据库系统通常采用多粒度锁机制允许事务根据操作范围选择合适的锁粒度。例如在MySQL InnoDB中默认使用行锁但当事务需要访问大量行时可能会自动升级为表锁通过意向锁来实现。InnoDB还支持手动指定锁粒度如LOCK TABLES命令强制表锁。在实际应用中应根据业务场景选择合适的锁粒度高并发、精确操作选择行锁如电商交易、在线预订。批量操作、低并发选择表锁如数据清洗、报表生成。混合场景依赖数据库的自动锁粒度管理同时通过索引优化使行锁有效。6.5 意向锁在多粒度锁中的作用前面提到的意向锁是多粒度锁机制的关键。当存在行锁时如果要给表加锁就必须先检查表中是否存在行级锁冲突。如果没有意向锁就需要遍历所有行的锁信息效率极低。意向锁通过在表级别记录“有事务正在持有或等待某些行的锁”这一信息使得加表锁时只需检查表的意向锁即可快速判断是否冲突。意向锁的兼容规则通常如下以MySQL InnoDB为例IS锁表级意向共享锁表示事务准备在某些行上加S锁。IX锁表级意向排他锁表示事务准备在某些行上加X锁。兼容矩阵表级别已持有请求ISIXSXIS是是是否IX是是否否S是否是否X否否否否IS和IX之间兼容因为它们只是“意图”实际行锁的冲突会在行级别检查。S锁与IX锁不兼容因为S锁要求整个表不能被修改而IX锁意味着某些行可能被修改。X锁与任何锁都不兼容包括IS和IX。通过意向锁数据库能够高效地支持多粒度锁共存。7. 悲观锁与乐观锁锁机制不仅可以从模式、粒度上分类还可以从并发控制策略上分为悲观锁和乐观锁。这两种策略代表了不同的设计哲学分别适用于不同的并发场景。7.1 悲观锁Pessimistic Locking核心思想认为并发冲突是常态因此在事务访问数据之前就采取“先加锁、后操作”的策略防止其他事务同时修改数据。悲观锁通常依赖于数据库提供的锁机制如共享锁、排他锁来实现。工作原理事务在读取或修改数据前先请求锁如SELECT ... FOR UPDATE加排他锁。如果锁被其他事务持有则当前事务等待。事务持有锁直到提交或回滚才释放遵循两阶段锁协议。其他事务必须等待锁释放后才能操作同一数据。优点能够严格保证数据一致性避免并发问题。适合写操作频繁、冲突概率高的场景。缺点并发度较低锁的持有可能导致大量事务等待降低系统吞吐量。此外容易引发死锁需要死锁检测和处理机制。实现方式数据库层使用SELECT ... FOR UPDATE或LOCK IN SHARE MODE显式加锁。应用层可以通过分布式锁如Redis锁模拟悲观锁但不在本文讨论范围。示例sql-- 事务T1悲观锁方式扣减库存 BEGIN; SELECT stock FROM product WHERE id 1 FOR UPDATE; -- 加行级排他锁 -- 假设stock10 UPDATE product SET stock stock - 1 WHERE id 1; COMMIT; -- 提交后释放锁 -- 事务T2同时尝试购买同一商品必须等待T1释放锁后才能执行SELECT FOR UPDATE BEGIN; SELECT stock FROM product WHERE id 1 FOR UPDATE; -- 阻塞直到T1提交 -- 然后执行后续操作7.2 乐观锁Optimistic Locking核心思想认为并发冲突是少数情况因此事务在操作数据时不加锁只在提交时检查是否与其他事务发生冲突。如果检测到冲突则回滚并重试。乐观锁通常通过版本号或时间戳来实现不依赖数据库锁。工作原理读取数据时同时获取其版本号或时间戳、校验和。进行业务计算准备更新。提交更新时检查当前数据库中数据的版本号是否与之前读取的版本号一致。如果一致说明没有其他事务修改过则执行更新并将版本号加1如果不一致说明数据已被修改当前事务更新失败需要回滚并重试。优点并发度高因为没有加锁开销不会阻塞其他事务。适合读多写少、冲突概率低的场景。缺点如果冲突频繁会导致大量事务重试反而降低性能。另外乐观锁不适用于需要强一致性且冲突频繁的场景。实现方式在表中增加版本字段如version int。更新时使用类似UPDATE table SET column new_value, version version 1 WHERE id ? AND version old_version的语句。检查受影响行数若为0则说明版本不匹配更新失败。示例sql-- 假设表product有字段id, stock, version -- 事务T1 SELECT stock, version FROM product WHERE id 1; -- 得到stock10, version1 -- 业务计算新库存9 UPDATE product SET stock 9, version 2 WHERE id 1 AND version 1; -- 检查受影响行数若为1则成功若为0则说明version已被其他事务改为2需要重试 -- 事务T2并发执行类似操作如果T2先于T1提交则T1的更新会失败。7.3 悲观锁与乐观锁的对比特性悲观锁乐观锁并发冲突态度认为冲突是常态提前加锁认为冲突是少数提交时检查锁的使用使用数据库锁S锁、X锁等不使用锁依靠版本号或CAS并发度较低锁导致等待较高无锁等待适用场景写操作频繁冲突率高读操作多写操作少冲突率低实现复杂度依赖数据库相对简单需要在应用层或数据库层实现版本检查死锁风险有死锁风险无死锁风险重试机制无需重试等待锁即可需要检测冲突并重试7.4 悲观锁与共享锁/排他锁的关系悲观锁是一个策略而共享锁/排他锁是实现该策略的具体工具。通常悲观锁会使用排他锁来锁定数据防止其他事务修改。但有时也可以只使用共享锁进行读取避免脏读而更新时再申请排他锁可能导致死锁。在实际应用中悲观锁常体现为“先锁定再操作”典型的SQL就是SELECT ... FOR UPDATE加排他锁或SELECT ... LOCK IN SHARE MODE加共享锁。乐观锁则完全脱离数据库锁依赖于应用层的并发控制逻辑。但有些数据库也提供了类似乐观锁的内置支持如SQL Server的快照隔离级别实际上是一种乐观并发控制。8. 深入解析并发问题丢失修改、读脏数据、不可重复读并发操作可能引起的数据不一致问题主要有三种有时加上幻读成为四种。本节将详细剖析每个问题的定义、产生原因、示例以及如何通过锁机制和隔离级别来防止。8.1 丢失修改Lost Update定义两个事务T1和T2读取同一数据并修改T2提交的修改覆盖了T1提交的修改导致T1的修改丢失。丢失修改是写-写冲突的典型结果。产生原因事务之间缺乏足够的隔离允许两个事务同时修改同一数据且没有强制串行化。示例text初始账户A余额100元。 时间点 1. T1读取A余额100。 2. T2读取A余额100。 3. T1将余额修改为10050150并提交。 4. T2将余额修改为100-3070并提交。 最终余额为70T1的50操作丢失。如果T2是基于自己读取的旧值100计算而忽略了T1的修改就造成了丢失修改。解决方案加锁使用排他锁保证T1和T2不能同时修改。例如T1先获得X锁T2等待T1提交后T2再读取新值150进行计算得到120最终正确。乐观锁通过版本号检测T2提交时会发现版本已变回滚重试。可重复读隔离级别RR在可重复读下通过锁机制可以防止丢失修改但不同数据库实现不同MySQL RR默认通过Next-Key Lock防止丢失修改和幻读但普通的快照读可能无法避免需要加锁读。8.2 读脏数据Dirty Read定义一个事务T1读取了另一个未提交事务T2修改的数据。如果T2后来回滚则T1读取到的数据就是无效的、脏的。产生原因允许事务读取未提交的数据即隔离级别为READ UNCOMMITTED。示例text初始账户A余额100元。 时间点 1. T1将A余额修改为150未提交。 2. T2读取A余额得到150脏数据。 3. T1因某种原因回滚A余额恢复为100。 4. T2基于150进行后续操作如打印报表导致数据错误。解决方案提高隔离级别至少使用READ COMMITTED级别该级别禁止读取未提交的数据。使用共享锁如果T2读取数据前加共享锁而T1持有排他锁未提交则T2会被阻塞直到T1提交或回滚。但很多数据库实现中普通的SELECT在RC及以上级别不加锁而是通过MVCC读取已提交的快照从而避免脏读。8.3 不可重复读Non-Repeatable Read定义在一个事务内两次读取同一数据得到不同的结果。这是因为在两次读取之间另一个事务修改并提交了该数据。不可重复读是针对同一条记录的修改update操作。产生原因允许事务读取已提交的修改即READ COMMITTED隔离级别但无法保证在事务内多次读取的一致性。示例text初始账户A余额100元。 时间点 1. T1读取A余额100。 2. T2修改A余额为150并提交。 3. T1再次读取A余额得到150。 此时T1内两次读取不一致破坏了事务的隔离性。解决方案提高隔离级别使用REPEATABLE READ级别该级别保证事务内多次读取同一数据结果一致。使用锁在T1第一次读取时对数据加共享锁并持续到事务结束如使用SELECT ... LOCK IN SHARE MODE或HOLDLOCK这样T2就无法修改该数据直到T1结束。多版本并发控制MVCC在REPEATABLE READ下通过快照读事务总是读取事务开始时的数据版本从而避免不可重复读。8.4 幻读Phantom Read定义在一个事务内两次执行相同的范围查询第二次查询返回了第一次没有出现的行通常是其他事务插入的新行。幻读针对的是插入insert操作。与不可重复读的区别不可重复读针对同一记录的修改幻读针对新增记录。示例text假设表account中满足余额100的有两条记录。 时间点 1. T1查询余额100的记录返回两条。 2. T2插入一条新记录余额200并提交。 3. T1再次执行相同查询返回三条记录出现了幻影行。解决方案最高隔离级别SERIALIZABLE强制事务串行执行避免幻读。使用范围锁Next-Key Lock如InnoDB在REPEATABLE READ级别下通过Next-Key Lock记录锁间隙锁锁定范围防止其他事务插入新记录从而避免幻读。但并非所有数据库的RR都避免幻读例如Oracle的RR其实就是快照隔离允许幻读。谓词锁理论上的锁锁定查询条件范围实际实现复杂。8.5 并发问题的总结对比问题涉及操作类型产生原因防止方法丢失修改写-写冲突多个事务基于旧值更新排他锁、乐观锁、可重复读加锁读读脏数据读-写冲突读取未提交数据READ COMMITTED及以上隔离级别不可重复读读-写冲突两次读取间数据被修改并提交REPEATABLE READ及以上隔离级别或加共享锁持续持有幻读读-写冲突插入两次范围查询间有新记录插入并提交SERIALIZABLE隔离级别或使用间隙锁/Next-Key Lock需要注意的是不同数据库对隔离级别的实现有差异例如MySQL InnoDB的REPEATABLE READ通过Next-Key Lock解决了幻读实际上达到了部分SERIALIZABLE的效果而Oracle的REPEATABLE READ即快照隔离不能防止幻读。因此理解具体数据库的锁机制至关重要。9. 数据库隔离级别与锁的关系SQL标准定义了四种事务隔离级别每个级别通过限制并发事务之间的干扰来解决不同的问题。隔离级别从低到高依次为READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。下面我们将结合锁机制详细说明每个级别。9.1 READ UNCOMMITTED读未提交特点允许事务读取其他未提交事务的修改。这是最低的隔离级别几乎不提供隔离性。并发问题可能发生脏读、不可重复读、幻读实际上所有问题都可能发生除了可能避免丢失修改丢失修改仍可能发生。锁机制通常不加锁或者只加极少的锁如修改时加排他锁但读取不加锁导致脏读。写操作仍然需要排他锁但读操作不加锁所以可以读到未提交的数据。使用场景几乎不用除非对数据一致性要求极低追求极致性能。9.2 READ COMMITTED读已提交特点一个事务只能读取到其他事务已经提交的数据避免了脏读。并发问题可能出现不可重复读和幻读丢失修改可能通过其他机制避免如使用乐观锁。锁机制实现方式有两种常见方案基于锁的RC事务在读取数据时加共享锁读完立即释放而不是事务结束。这样其他事务可以在之后修改数据导致不可重复读。写操作加排他锁直到事务结束。基于MVCC的RC大多数现代数据库Oracle、PostgreSQL、MySQL InnoDB的RC使用快照读每条查询看到的是查询开始时的已提交数据版本避免了脏读但不同查询可能看到不同快照因为其他事务提交导致不可重复读。示例MySQL InnoDB RC普通SELECT是快照读不加锁。对于UPDATE、DELETE使用当前读加锁读取最新已提交数据然后加锁。9.3 REPEATABLE READ可重复读特点保证在同一个事务内多次读取同一数据的结果是一致的即避免了不可重复读但可能发生幻读取决于实现。并发问题在标准定义下RR允许幻读但某些数据库如MySQL InnoDB通过间隙锁在RR级别就避免了幻读。锁机制基于锁的RR事务在第一次读取数据时加共享锁并持续到事务结束这样其他事务不能修改这些数据从而保证可重复读。但范围锁可能无法防止插入所以幻读可能发生。基于MVCC的RR事务开始时会创建一个一致性快照整个事务期间的所有查询都基于该快照因此不仅避免了不可重复读也避免了幻读因为新插入的行在快照中不可见。但这是快照隔离实际上并不是标准RR。MySQL InnoDB的RR正是这种机制同时配合间隙锁来处理当前读的幻读。示例MySQL InnoDB RR普通SELECT是快照读基于事务开始时的快照。加锁的SELECT如FOR UPDATE或UPDATE/DELETE会使用当前读并加上Next-Key Lock防止其他事务在范围内插入新行从而避免幻读。9.4 SERIALIZABLE可串行化特点最高的隔离级别强制事务串行执行避免了所有并发问题脏读、不可重复读、幻读、丢失修改。实际上它通过锁机制使得并发事务的执行结果与某个串行执行结果等价。并发问题无。锁机制基于锁的SERIALIZABLE所有读操作隐式加共享锁或更严格的锁所有写操作加排他锁并且锁都持有到事务结束。同时使用范围锁或谓词锁防止幻读。这会导致极低的并发性容易产生锁等待和死锁。基于MVCC的SERIALIZABLE如PostgreSQL的SERIALIZABLE快照隔离SSI通过检测读写冲突来实现可串行化比锁开销小但实现复杂。使用场景对数据一致性要求极高且并发量不大的场景如金融核心交易但通常也通过应用层控制。9.5 隔离级别与并发问题的关系表隔离级别脏读不可重复读幻读丢失修改注READ UNCOMMITTED可能可能可能可能READ COMMITTED不可能可能可能可能但可通过乐观锁避免REPEATABLE READ不可能不可能可能某些数据库不可能可能取决于实现SERIALIZABLE不可能不可能不可能不可能注丢失修改的防止依赖于具体实现。在RC级别如果使用乐观锁或加锁读也可以避免丢失修改但标准RC不保证。10. 多版本并发控制MVCC与锁的协同现代数据库普遍采用多版本并发控制MVCC来提升并发性能同时避免锁带来的阻塞。MVCC的核心思想是为每个数据行维护多个历史版本读操作读取某个历史版本快照写操作创建新版本从而实现了读不阻塞写、写不阻塞读。10.1 MVCC如何工作以InnoDB为例每一行记录实际上有两个隐藏列trx_id最后修改该行的事务ID和roll_pointer指向回滚段中的旧版本。事务在开始时会获取一个事务ID并创建一个读视图Read View其中包含了当前活跃的事务ID列表。当执行快照读普通SELECT时根据可见性规则决定读取哪个版本的数据。10.2 MVCC与隔离级别的结合READ COMMITTED每次查询都重新创建Read View因此可以读取到其他事务已提交的最新版本导致不可重复读。REPEATABLE READ只在事务第一次查询时创建Read View后续查询复用该视图保证多次读取结果一致从而避免不可重复读和幻读对于快照读而言。SERIALIZABLE如果使用MVCC则可能退化为快照隔离需要额外的冲突检测才能保证可串行化。10.3 MVCC与锁的关系虽然MVCC减少了锁的竞争但写操作仍需要加锁。例如当执行UPDATE或DELETE时InnoDB会对当前行加排他锁X锁防止其他事务同时修改。同时为了保证可重复读和防止幻读InnoDB还引入了Next-Key Lock它是行锁和间隙锁的结合。记录锁Record Lock锁定单个行记录。间隙锁Gap Lock锁定一个范围但不包含记录本身防止其他事务在间隙中插入新行。Next-Key Lock记录锁间隙锁锁定一个范围并且包含记录本身。例如对索引值在(10,20]之间的行加锁既锁定记录20也锁定10到20之间的间隙。在RR级别下InnoDB使用Next-Key Lock来防止幻读当使用当前读如SELECT ... FOR UPDATE扫描一个范围时会对扫描到的行加Next-Key Lock这样其他事务就无法在这个范围内插入新行。因此MVCC和锁机制是相辅相成的MVCC提供了高并发的快照读而锁保证了写操作的冲突可串行化和一致性。11. 综合案例转账业务中的并发控制为了更好地理解上述概念我们设计一个经典的银行转账场景。假设有两个账户A和B初始余额各为1000元。现在有两个并发事务事务T1从A转账100元到B。事务T2查询A和B的总余额用于报表。我们将演示在不同隔离级别和锁策略下可能出现的问题及解决方案。11.1 最低隔离级别 READ UNCOMMITTED操作序列T1开始读取A余额1000计算新余额900未提交。T1更新A余额为900未提交。T2开始读取A余额900脏读读取B余额1000计算总余额1900。T1继续读取B余额1000计算新余额1100更新B余额1100提交。T2基于90010001900生成报表但实际提交后的正确总余额应为90011002000。T2读取了脏数据报表错误。结论脏读导致不一致。11.2 READ COMMITTED 级别操作序列T1开始读取A余额1000计算新余额900。T1更新A余额900未提交。T2开始读取A余额由于RC此时T2只能看到已提交数据T1未提交所以读取到的A仍然是1000读取B余额1000总余额2000。T1继续读取B余额1000更新B1100提交。此时T2如果再次读取A和B会得到A900B1100总余额2000。但T2已经生成了报表基于第一次读取导致报表数据过时实际上T2在第一次读取时总余额是2000与最终一致但T2内两次读取可能不一致不可重复读。说明RC避免了脏读但可能产生不可重复读。如果T2在事务内多次查询余额可能会看到不同结果。11.3 REPEATABLE READ 级别MySQL InnoDB实现假设采用MVCC快照读。操作序列T1开始读取A余额1000计算新余额900当前读这里使用快照读。注意如果T1的读取是快照读则它看到的是事务开始时的快照即A1000, B1000。然后T1执行更新需要当前读会读取最新已提交数据可能被其他事务修改但这里没有其他事务。T1更新A900加X锁更新B1100加X锁提交。T2开始假设T2在T1提交之后开始或者之前开始为了演示可重复读假设T2在T1开始前开始。如果T2在T1之前开始那么T2的快照是T2开始时的数据A1000, B1000。即使T1提交了T2后续的快照读仍看到100010002000保证了可重复读总余额始终是2000与最终一致因为转账只是内部转移。这样T2的报表正确。如果T2在T1提交之后开始则快照包含更新后的数据也是正确的。结论在RR级别下通过快照读T2的总余额查询不会受到T1影响始终看到事务开始时的快照避免了不可重复读。而且因为转账操作本身不会产生不一致总余额不变所以快照读返回2000也是正确的。但是如果业务要求T2必须读取最新数据则可能需要使用加锁读如SELECT ... FOR UPDATE或SERIALIZABLE级别。11.4 使用悲观锁防止丢失修改假设有两个转账事务同时从A转出资金。操作序列T1读取A余额1000准备转出100。T2读取A余额1000准备转出50。若不加锁T1更新A900T2更新A950后提交的覆盖先提交的导致丢失修改最终950正确应为900实际上应该先扣100再扣50得到850但这里T2基于旧值1000扣50得到950T1的扣100丢失。使用悲观锁T1执行SELECT balance FROM account WHERE idA FOR UPDATE;获得X锁读取1000。T2执行同样的FOR UPDATE被阻塞等待T1释放锁。T1计算新余额900执行UPDATE提交释放锁。T2获得锁此时读取到最新余额900计算新余额850执行UPDATE提交。最终余额850正确。11.5 使用乐观锁防止丢失修改假设使用版本号字段。操作序列T1读取A余额1000版本号1。T2读取A余额1000版本号1。T1计算新余额900执行UPDATE account SET balance900, version2 WHERE idA AND version1;成功影响1行提交。T2计算新余额950执行UPDATE account SET balance950, version2 WHERE idA AND version1;影响0行更新失败。T2检测到冲突回滚并重试重新读取新数据版本号2余额900再计算850更新成功。最终余额850正确但T2经历了重试。11.6 SERIALIZABLE 级别在SERIALIZABLE下所有读操作隐式加锁如共享锁并且锁持有到事务结束。操作序列转账与查询并发T1开始执行SELECT balance FROM account WHERE idA FOR UPDATE?实际上在SERIALIZABLE下普通的SELECT也会被转换为SELECT ... LOCK IN SHARE MODE取决于数据库即加共享锁。T2开始尝试执行SELECT SUM(balance) FROM account由于T1持有A和B的共享锁假设T1已读取T2的查询也需要共享锁兼容所以T2可以执行得到当前快照不在基于锁的SERIALIZABLE中共享锁之间兼容所以T2可以读到数据但T1和T2可能发生死锁需要具体分析。实际上SERIALIZABLE通常通过两阶段锁保证冲突可串行化。如果T1先给A和B加了共享锁T2也需要共享锁兼容两者可以同时读。但T1后续要修改A和B需要将共享锁升级为排他锁这可能导致T1等待T2释放共享锁如果T2还没读完可能产生死锁或锁等待。在SERIALIZABLE下数据库会自动处理锁的升级和死锁检测但并发度极低。12. 总结与展望本文详细阐述了数据库并发控制的核心概念包括锁机制共享锁、排他锁、行锁、表锁、页面锁、并发控制策略悲观锁、乐观锁以及典型的并发问题丢失修改、读脏数据、不可重复读。我们深入探讨了这些概念之间的内在联系以及它们如何通过隔离级别和MVCC协同工作保证数据的一致性和隔离性。12.1 核心要点回顾共享锁和排他锁是锁的基础模式通过兼容性矩阵控制并发访问。锁粒度影响并发度和开销行锁适合高并发表锁适合批量操作。悲观锁和乐观锁是两种应对冲突的策略悲观锁提前加锁乐观锁提交检测。并发问题包括丢失修改、脏读、不可重复读、幻读它们由不同程度的读写冲突引起。隔离级别提供了不同层次的保护从最低的READ UNCOMMITTED到最高的SERIALIZABLE。MVCC是现代数据库提升并发性能的关键技术与锁机制相辅相成。12.2 实际应用中的选择在实际开发中应根据业务需求和数据一致性要求选择合适的隔离级别和锁策略对于金融交易等强一致性场景通常使用REPEATABLE READ或SERIALIZABLE并配合悲观锁如FOR UPDATE或乐观锁版本号来防止丢失修改。对于报表查询等只读操作可以使用REPEATABLE READ下的快照读避免锁开销同时保证一致性视图。对于高并发、读多写少的系统如电商商品详情乐观锁往往更合适。需要充分了解所用数据库的锁实现细节例如MySQL InnoDB的Next-Key Lock在RR下解决了幻读而Oracle的RR则不能。12.3 未来趋势随着分布式数据库和NewSQL的兴起并发控制面临新的挑战。传统的集中式锁机制在分布式环境下难以扩展因此涌现出许多新的并发控制协议如分布式锁基于ZooKeeper、etcd、乐观并发控制OCC、时间戳排序T/O、可串行化快照隔离SSI等。但无论技术如何演进共享锁、排他锁、悲观乐观等基本思想仍然贯穿其中是理解更复杂系统的基础。