数据库事务隔离引发的关于锁机制的思考

本文深入解析数据库事务的四种隔离级别,包括未提交读、已提交读、可重复读和序列化读,以及各种锁机制如悲观锁、乐观锁、独占锁和共享锁的原理与应用场景,探讨分布式锁的实现与挑战。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

DB提供两种机制来保证事务的ACID(原子性,一致性,隔离性和持久性)特性,日志预写(write-ahead loging)和锁(lock),前者用于保证原子性、一致性,后者用于保证隔离性。
事务在没有提交前的一系列修改都不能持久化,因此这一系列的操作都是依赖两种log来实现,redo-log和undo-log;修改前的数据由undo-log记录,修改后的数据由redo-log记录,事务提交成功则执行redo-log,失败则执行undo-log。

DB提供的事务隔离级别如下:
#1 未提交读,READ UNCOMMITTED
执行读操作时不加任何锁,执行写操作时添加行级共享锁,直到事务结束。多个事务可以并行读取某数据,但是一旦某个事务执行了写操作后,其他事务仍旧可以读数据,但需要等待锁释放后才可以执行写操作 。
问题:不能解决脏读,一个事务可能读取到另外一个事务最终没有提交的数据。

#2 已提交读,READ COMMITTED
执行读操作时添加行级共享锁,读完之后就释放锁;执行写操作时添加行级排他锁,直到事务结束。多个事务可以并行读取某数据,但是一旦某个事务执行了写操作后,其他事务就不再允许进行任何读写操作,因此可以解决当前事务读取的数据都是最终提交的。
问题:不能解决可重复读,另一个事务数据更新操作可能让当前事务连续两次读取的数据不一致。

#3 可重复读,REPEATED READ
执行读操作时添加行级共享锁,直到事务结束;执行写操作时添加行级排他锁,直到事务结束。多个事务可以并行读取某数据,但事务仅当数据上没有任何锁的时候才能添加排他锁进行写操作,直到当前事务结束后,其他事务才能尝试添加拍他所进行写操作。因此一旦某数据被读取之后,其他事务不能对其进行修改。
问题:不能解决幻读,另一个事务的新数据插入操作可能让当前事务连续两次读取的数据不一致。

#4 序列化读,SERIALIZABLE
执行读操作时添加表级共享锁,直到事务结束;执行写操作时添加表级排他锁,直到事务结束。因此涉及到表内某行数据的读写都串行发生。
问题:所有事务都串行发生,访问性能极大降低。

Racing Process中关于锁的机制

就用途而言,锁包含悲观锁、乐观锁、独占锁、共享锁、(非)公平锁、分布式锁和自旋锁。

独占锁(Exclusive Lock):
也叫做写锁(X锁,排它锁)资源只允许当前事务读写,其他事务的任何操作都不允许,添加之后一直到事务结束时才释放;当资源上已经有其他锁的时候无法添加独占锁;插入、更新和删除操作自动添加独占锁。

 

共享锁(Shared Lock):

也叫做读锁(S锁)资源允许当前事务读写,允许其他事务的读操作,不允许其他事务的写操作,读操作结束后共享锁就立即释放;查询操作自动添加共享锁。

 

悲观锁(Pessimistic Lock):
事务认为数据处理过程中都需要锁定数据,防止其他racing process的修改,一般都是依靠数据库锁机制实现,包含表锁、行锁等,一旦触发肯定会引起线程的阻塞等待

 

乐观锁(Optimistic Lock):
事务认为数据处理过程中不需要锁定数据;一般通过数据库表的版本列实现,为数据库表添加一列version,读取数据的时候同时读取version数值;之后写入的时候对version + 1并与当前DB中的version数值比较,如果大于数据库表当前的版本号则进行更新,否则认为是过期数据而不进行更新,其实也就是多版本并发控制(Multi-Version Cuoncurrency Controll)。
在Mysql的Innodb引擎中,数据库表会额外添加一列version字段,仅在事务级别为未提交读和已提交读的时候会开启mvcc,在保证数据一致性的前提下提供最大程度的并发。每一个racing process在读取数据的时候会在原始数据的基础上生成一条新的临时数据,同时version +1,这条数据在事务提交前对其他事务都是不可见的;一旦事务结束提交的时候会对比临时数据跟原始数据的version版本号,如果临时数据的version小于原始数据的version,则说明临时数据已经过期,放弃或者重新读取数据执行当前的操作。

 

分布式锁(Distributed Lock):
同一个进程内的多个线程之间可以共享内存,因此基于共享内存的synchnorized或者lock机制都可以实现锁,对于进程之间或者多台server之间基于共享内存方式实现的锁就不再能满足要求。分布式环境的锁需要提供一个所有server都可以访问到的存储介质(DB或者内存),最原始的分布式锁设计源于DB,多个业务系统之间的同步通过向同一张DB table中插入一条具有唯一性键值的数据,成功插入数据的业务系统获得锁,处理完自己的业务数据之后将DB table中的数据删除,其他业务系统就可以继续插入数据从而获取锁;可以通过timer task解决死锁问题,可以在table中增加业务系统标识column解决锁重入的问题;在并发量不是特别大,并且能保证DB系统足够稳定的前提下,DB based的分布式锁可以解决分布式锁的问题。

随着并发业务量的上升和访问延迟要求的提高,缓存锁逐渐替代了数据库锁,以Redis RedLock和ZooKeeper Mutex为代表的基于内存数据操作架构可以保证多业务系统对同一项操作的并发执行,最终只会有一个业务系统操作成功,或者有一个先后顺序(竞争排队);RedLock的核心利用的是Redis的命令set NX EX,NX属性表示仅当某个key不存在的时候才能成功set这个key,这样racing process同时对同一个key执行setnx,最终只有一个process可以设置成功,也表示获取锁成功;EX表示key值的过期时间,防止死锁的情况。

分布式锁主要用于处理在分布式环境中多个racing process对同一个资源进行访问时候的数据一致性问题,因此分布式锁需要满足五个特性:
#1 保证同一个方法在在同一时间点只能被一台机器上的一个线程执行:

#2 保证锁可以正常的释放:避免因加锁的racing process服务下线造成锁不能释放的场景;DB based实现可以通过timer task删除过期的record,Redis based实现可以通过setnx上设置key过期时间自动删除键值,ZooKeeper based实现通过判定session connection断开自动删除创建的节点。

#3 保证锁的实现为阻塞锁:racing process需要等待获取锁之后才继续往下执行业务;DB based实现和Redis based实现都可以通过while(true)循环实现等待(自旋锁),但不够优雅,ZooKeeper based实现通过event-watcher机制自动通知racing process锁已经获取。

#4 保证锁是可重入的:已经获得了锁的racing process可以访问被加了同一把锁的其他资源,避免自身死锁;DB based实现可在record上增加一列lock holder info来辨识当前获取锁的racing process,Redis based实现可将value设置成lock holder info来辨识,ZooKeeper based实现可以在创建的node上记录lock holder info。

#5 保证锁的获取和释放服务具有高可用性能:避免单点故障问题,DB,Redis和ZooKeeper都可以集群的方式提供分布式锁服务。

 

自旋锁(Self-Spin Lock):
racing process通过互斥同步实现访问一致性的时候,挂起和恢复线程都需要转到系统内核模式完成,频繁的切换会给并发性能带来压力;同时,大多数共享数据的锁定状态都只需要很短的时间,业务的开销甚至比系统切换开销还低,所以racing process可以暂时不用切换状态,而只需要执行一段时间的忙循环(也就是继续占用CPU时间片),从而等待共享数据的锁被释放,这样的策略就是自旋锁。

转载于:https://www.cnblogs.com/leo-chen-2014/p/9739829.html

<think>好的,我现在需要回答用户关于数据库两阶段(2PL)的问题。首先,我得回忆一下两阶段的基本概念,确保自己理解正确。两阶段数据库并发控制的一种方法,用来保证事务隔离性,避免脏读、不可重复读等问题。 接下来,用户可能希望了解两阶段的定义、工作原理、阶段划分、优缺点以及应用场景。我需要组织这些内容,用清晰的结构呈现,同时遵循用户提供的格式要求,比如使用Markdown,分点说明,代码示例等。 首先,定义部分需要简明扼要地解释什么是两阶段协议。然后,详细说明两个阶段:增长阶段和收缩阶段,每个阶段的作用和规则。可能需要用例子来说明,比如事务在不同阶段如何加和解,以及这些操作如何影响其他事务。 还要考虑用户的潜在问题,比如两阶段如何解决并发冲突,与其他并发控制机制(如乐观、多版本并发控制)的区别。此外,优缺点部分需要客观分析,比如严格二阶段和强二阶段的不同,以及可能导致的死问题。 需要检查是否有常见的误解,比如两阶段与两阶段提交的区别,避免混淆。同时,是否需要举实际数据库系统的例子,比如MySQL、PostgreSQL如何实现两阶段,或者是否在分布式系统中应用。 在组织内容时,要确保符合用户之前的格式要求,比如使用加粗标题、代码块示例、表格对比等。同时,保持语言简洁,避免技术术语过于晦涩,必要时用比喻或实际场景帮助理解。 最后,总结部分要强调两阶段的重要性和适用场景,帮助用户在实际应用中做出合适的选择。还要确保回答中没有错误,比如阶段顺序是否正确,的类型是否准确,例子是否恰当等。 现在,我需要将这些思考整理成一个结构化的回答,确保每个部分逻辑连贯,覆盖用户可能关心的所有方面,同时保持内容的准确性和易读性。</think>--- ### **数据库两阶段(Two-Phase Locking, 2PL)** **定义** 两阶段数据库系统中**保证事务并发执行时数据一致性**的核心协议。其核心规则是: > 事务必须分两个阶段操作: > 1. **增长阶段(Growing Phase)**:只能申请,不能释放。 > 2. **收缩阶段(Shrinking Phase)**:只能释放,不能申请。 通过强制的申请和释放顺序,确保事务间的操作可串行化,避免脏读、不可重复读等问题。 --- ### **两阶段的流程** #### **1. 增长阶段(Growing Phase)** - 事务在访问数据前**必须获取对应**(如共享S、排他X)。 - 一旦释放,则进入收缩阶段,**不能再申请新**。 **示例**: 事务T1执行如下操作: 1. 申请X,修改数据A(增长阶段)。 2. 申请S,读取数据B(增长阶段)。 3. **释放B的S** → 进入收缩阶段。 4. 尝试申请X修改数据C → **违反2PL规则**(收缩阶段禁止申请)。 --- #### **2. 收缩阶段(Shrinking Phase)** - 事务逐步释放已持有的,但**不能申请新**。 - 释放后,其他事务可访问对应数据。 **关键限制**: - 若事务在收缩阶段释放部分后需要访问新数据,会因无法申请而失败。 - 因此,**严格两阶段(Strict 2PL)** 更常用:所有事务提交时一次性释放,避免脏读。 --- ### **的兼容性规则** | | **S(共享)** | **X(排他)** | |----------|------------------|------------------| | **S** | 兼容 | 不兼容 | | **X** | 不兼容 | 不兼容 | - **共享(S)**:允许多事务并发读同一数据。 - **排他(X)**:仅允许单事务读写数据,其他事务无法加。 --- ### **两阶段的变体** #### 1. **严格两阶段(Strict 2PL)** - **规则**:所有(包括S和X)在事务提交时**一次性释放**。 - **优点**:避免脏读(其他事务无法读取未提交的修改)。 - **缺点**:持有时间较长,可能增加阻塞概率。 #### 2. **强两阶段(Rigorous 2PL)** - **规则**:事务提交前**不释放任何**(包括读和写)。 - **优点**:最高隔离性(可串行化)。 - **缺点**:并发度最低,可能引发。 --- ### **两阶段的优缺点** | **优点** | **缺点** | |----------------------------------|----------------------------------| | 保证事务可串行化,避免数据不一致 | 粒度粗时易导致并发性能下降 | | 实现简单,易于集成到数据库系统 | 可能引发(需额外机制检测和解决) | | 支持灵活隔离级别(如RC、RR) | 严格2PL增加事务提交时的资源争用 | --- ### **实际应用场景** 1. **银行转账事务**: - T1从账户A转100元到账户B。 - 对A和B加X → 修改余额 → 提交时释放。 - 严格2PL确保其他事务无法读到中间状态(如A已扣款但B未到账)。 2. **库存扣减**: - 高并发场景下,对商品库存加X,防止超卖。 --- ### **两阶段 vs 乐观** | **对比维度** | **两阶段** | **乐观** | |------------------|----------------------------------|--------------------------------| | **冲突处理** | 悲观策略:提前加避免冲突 | 乐观策略:提交时检测冲突 | | **适用场景** | 高冲突场景(如频繁写操作) | 低冲突场景(如读多写少) | | **性能开销** | 管理开销大,可能阻塞事务 | 无竞争,但回滚成本高 | --- ### **代码示例(简化伪代码)** ```sql -- 事务T1(严格2PL) BEGIN TRANSACTION; SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 加X(增长阶段) UPDATE accounts SET balance = balance - 100 WHERE id = 1; SELECT * FROM accounts WHERE id = 2 FOR UPDATE; -- 加X(仍在增长阶段) UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; -- 提交时释放所有(收缩阶段) ``` --- ### **总结** 两阶段通过**的阶段性管理**,为数据库提供了强一致性保障,但需权衡并发性能。实际应用中: - 选择**严格2PL**可避免脏读,适合金融等高一致性场景。 - 结合**粒度优化**(如行代替表)可提升并发度。 - 死检测(超时或等待图算法)是必要补充机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值