服务化-mysql-03 mysql事务、锁机制及MVCC并发控制

本文深入探讨MySQL中的事务概念及其实现机制,包括事务的ACID特性、隔离级别、锁机制、MVCC并发控制等内容,并详细解释了不同隔离级别下的问题及解决方案。

1 事务的概念

简单的说,事务就是保证一组数据库操作,要么全部成功,要么全部失败。

MySQL中,事务是由引擎层来实现的。但并不是所有的引擎都支持事务,如原生的MyISAM引擎不支持事务,导致InnoDB逐渐取代MyISAM。

由于数据库一般都是并发执行多个事务,当多个事务并发的增删查改同一批数据时,就会出现脏写、脏读、不可重复读、幻读等问题。这些问题的本质都是数据库的多事务并发造成的,为了解决多事务并发,Mysql数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制等等。

多事务并发操作,不考虑隔离性会产生哪些问题:

  • 脏写或更新丢失(Lost Update)

    解析:A、B事物同时对一行数据基于原始数据进行更新,事物B的更新对事物A的更新造成了覆盖。带来的问题:数据不准确。

  • 脏读.

    解析:A事物读到B(其它)事物已修改但尚未提交的数据。带来的问题:

    1. 给用户带来数据混乱的感觉

    2. 让用户看到根本不存在的数据

  • 不可重复读

    解析:事物A查询某个数据后,再次进行查询得到不同的结果。带来的问题:

    1. 基于查询到的数据直接操作,结果不准确

  • 幻读

    解析:A事物内的前后2次查询时,后次查询读取到其他事物新增的数据。带来的问题:

    1. 读取到其他事物新增的事物

2 隔离性与隔离级别

事务的特性:ACID(AtomicityConsistencyIsolationDurability,即原子性、一致性、隔离性、持久性)

  • Atomicity 原子性。事务是一个原子操作单元,其对数据的修改要么全部执行,要么全不执行。(重点落在操作上)
  • Consistency 一致性。在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据都必须
  • Isolation 隔离性。数据库提供隔离机制。事务间可以保证相对独立。
  • Durability 持久性。事务完成后,它对数据的修改时永久性的,即使系统故障也能够恢复。

2.1 mysql各种锁的关系

当事务的隔离级别越严格,mysql存储引擎的执行效率就越低。因此我们在设置隔离级别时,需要在效率与隔离级别间寻找平衡点。乐观锁、悲观锁、表锁、行锁、写锁、读锁等。

  • 读锁(所有session都能查,但所有事务都不能有其他操作)
  • 写锁(只有当前session能操作数据,其他session都不能操作)

 2.2 各隔离级别下操作与锁粒度关系

InnoDB的隔离界界别可以分为4类:

  • 读未提交(read uncommitted)

A事务还没提交时,它对数据操作的影响就能被B(其他)事务看到。

  • 读已提交(read committed)

A事务提交后,对数据操作的影响结果才能被B(其他)事务看到

  • 可重复读(repeatable read)

A事务在执行过程中读到的数据,从获取到视图都结束始终一致。(MVCC机制控制)

  • 串行化(serialzable)

锁机制最严格,事务只能串行进行。即使是读数据也会加行锁。

 2.3 设置隔离级别

set [ global | session ] transaction isolation level Read uncommitted | Read committed | Repeatable | Serializable;

golbal级别的事务级别无法在session中设置,会报错没有权限。需要在配置项中修改。session中可以设置会话的隔离级别,如下。

#查看目前的事务隔离级别
show variables like '%isolation%';
#设置为读未提交
set session transaction isolation level read uncommitted;
#设置为读已提交
set session transaction isolation level read committed;
#设置为可重复度
set session transaction isolation level repeatable read;
#设置为串行化
set session transaction isolation level serializable ;

#查询所有数据
select * from test;
#查询单行数据
select * from test where id='1';
#插入单行数据
insert into test (id,name) values (5,'zhangsan');
#删除单行数据
delete from test where id='5';
#修改单行数据
update test set name='' where id='1';

结论:各事务隔离级别下,增删改查的锁情况如下。具体说,只有select时(除串行化)不加锁,其余操作都加行锁。

  2.4 各隔离级别下操作数据产生的问题

  2.5 间隙锁(Gap lock)

mysql的innodb引擎下,默认使用RR(Repeatable-Read)隔离级别。看上去可重复读的数据也会发生幻读。当其他事务新增数据后,在本事务中新增数据,再次查询所有数据,就能查询到幻读数据。为了解决幻读问题,mysql又设计了间隙锁,用于解决RR下的幻读。何谓间隙锁?

如上图,当间隙锁锁住id={3.14]间的数据时,其他事务创建id={3,14]间的数据,会被阻塞。

但我们在实际生产中也会发现这种问题。在对表中不存在的数据进行删除,此时无疑会删除失败,但同时也会带来数据前后范围的间隙锁。来看下面的案例:

库中共计4条数据,开启A、B两个事务,A事务删除id=10的数据,此时无法找到数据,加锁范围扩大为间隙锁。锁住{9,+∞]的数据。

上述描述的是主键索引时的间隙锁,此时库中有该数据的操作加行锁,无该数据的操作加间隙锁。

当操作的数据列不是索引时,即使是select查询也会加入间隙锁。

 2.5 临建锁(Next-key-lock)

临键锁是行锁与间隙锁的组合。mysql锁是落在索引上的。当我们对非索引数据进行更新,会导致表锁。(个人理解:mysql无法通过索引对某行进行加行锁找到对应数据,只能加表锁以确定找到该值。)

InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁

3 MVCC

在RR级别下,mysql可以做到其他事务提交完成后,我们取到的数据依然不变,这说明无疑我们读取的数据是带有版本的。这其实就是通过undo log的版本链来实现的MVCC(Multi-Version Concurrency Control)

3.1 分配事务trx_id

mysql在为了并发控制版本,当事务进行增删改查时会为当前事务分配trx_id。我们通过下面实例佐证:

1)我们通过mysql的客户端打开2个session,使用test表并开始事务。此时INNODB_TRX表中空空如也。

 2)我们分别通过查询语句、更新语句(增、删语句同理)来查询数据,并且不要提交事务。发现INNODB_TRX表生成了事务的信息,最重要的是生成了事务号xtr_id。

 3)当提交事务后,INNODB_TRX表中的数据也将被删除。

 3.2 控制视图readview

表中的数据就像长江中滚滚的流水,只要有事务在不断操作数据,大河中的水就在不停流动。那作为A事务,需要精准获取此时河水中的到底有多少鱼怎么办?我先拍张照,看看有多少渔民正在捕鱼,排出掉他们的干扰,剩下的数据就精准了。

read-view数据:

1)当前尚未提交事务的最小值和最大值组成[100,200](数据从本文3.1节中获取)

2)当前已创建的中的最大trx_id,300(为了更好的了解版本比对过程,忽略SessionD的事务trx_id:400)

  3.3 回滚日志查询记录过程

  • 获取read-view视图      ---[100,200],300
  • 查看回滚日志记录

  • 比对回滚日志版本链

1)如果回滚日志日志中row的trx_id<trx_active_min_id (简言之:行事务id<活跃事务id的最小值)

当前行的事务已经提交,该行数据可以读到。

 2)回滚日志日志中row的trx_id>trx_max_id (简言之:行事务id>活跃事务id的最大值)

当前行的事务尚未开始。

3)前面2条都不符合,那么row的范围区间trx_active_min_id <= trx_id <= trx_max_id 。此时:

a)trx_id在read-view数组[100,150,200]中(假设活跃事务中还有个事务trx_id150,帮助理解)

那么当前事务未提交,不可见。

b)trx_id不在read-view数组[100,150,200]中,那么说明这个事务已经提交,可见。

分析:当读取回滚日志时,首先拿到第一行id=1,name=zwx,trx_id=300的row比对视图,此时属于3.b的范围(100 <= 300<= 300),且trx_id=300,不在read-view数组中[100,150,200],那么此事务已完成,数据可见。读到的结果是该行

  3.4 RR级别下的可重复度实现

 对上述案例中的事务A添加2个update操作,再次读取数据。由于视图再当前事务的

 分析:

读第一行:trx_id=100,属于trx_active_min_id <= trx_id <= trx_max_id,并且trx_id=100,在[100,150,200]数组中,说明这是尚未提交的事务。读下一行。

读第二行:同理。读下一行。

读第三行:trx_id,属于trx_active_min_id <= trx_id <= trx_max_id,并且trx_id=300,不在[100,150,200]数组中,说明事务已经提交。读取到name=zwx。

4 innodb引擎与Buffer pool缓存机制

  1. 执行器调用InnoDB引擎接口(增、删、改、查)
  2. 查看buffer pool中是否有对应的row信息
  3. 存在在跳至第5步,不存在进行第4步
  4. 从磁盘文件中,加载包含row信息的数据页
  5. 增删改操作写回滚日志,查操作读回滚日志并结束。
  6. 更新Buffer pool内存中的值。
  7. 记账模式写入redo log Buffer
  8. 写入redolog日志,并将该行状态修改为prepare(第一段提交)
  9. 准备写入Bin log落盘。
  10. binlog写入成功,提交redolog状态为commit(第二段提交)

 

<think>首先,用户的问题是:“我想了解Java锁机制的相关信息 请问Java锁机制介绍”。所以,我需要提供一个清晰的解释Java锁机制的介绍。 从系统级指令: - 所有行内数学表达式必须使用$...$格式。但在这个上下文中,Java锁机制是关于编程的,可能没有数学表达式。如果有变量或概念,我需要处理。例如,提到CAS时,可能有数学概念,但可能不需要数学格式。指令说“包括变量、不等式、方程等”,但在Java锁机制中,常见的是变量如锁对象或线程概念。如果需要,我可以使用$...$,但尽量不要过度应用。 - 独立公式使用$$...$$并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题:在回答末尾生成相关问题,用§§相关问题§§分隔。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。引用来自用户提供的引用:[^1], [^2], [^3], [^4]。这些引用是关于Java锁机制的,所以我应该在回答中提到这些概念时引用它们。 用户提供的引用: - [^1]: 提到锁的分类:公平锁/非公平锁等。 - [^2]: 详细目录结构,涉及锁的分类、ReentrantReadWriteLock、synchronized等。 - [^3]: 关于可重入锁的例子。 - [^4]: 解释可重入锁的定义和优点。 我的回答需要引用这些内容,自然地添加引用标识。 回答结构: - 结构清晰,帮助用户逐步解决问题。 - 尽量保证真实可靠。 - 使用中文。 主题:Java锁机制介绍。 核心内容包括: - 锁的基本概念:锁是用于线程同步的机制,防止多个线程同时访问共享资源。 - 分类:参考引用,列出常见的锁类型: - 公平锁 vs 非公平锁 - 可重入锁 vs 不可重入锁 - 独占锁 vs 共享锁 - 互斥锁 vs 读写锁 - 乐观锁 vs 悲观锁 - 分段锁 - 偏向锁/轻量级锁/重量级锁 - 自旋锁 - Java中的具体实现:如synchronized关键字、ReentrantLock类等。 - 锁的升级和优化:如从偏向锁到重量级锁的升级。 - 优点和用例:避免死锁、提高性能。 逐步介绍: 1. 引言:什么是Java锁机制?它是用于多线程编程中确保线程安全的工具。 2. 锁的分类:简要介绍主要类别。 3. 详细解释关键锁类型,如可重入锁(参考[^4]),使用例子。 4. Java中的实现:介绍synchronized和ReentrantLock。 5. 锁的状态升级:偏向锁、轻量级锁、重量级锁(参考[^2])。 6. 总结锁的重要性。 生成相关问题:在回答结束后,添加相关问题部分,标记为§§相关问题§§。 引用添加:当提到具体概念时,自然地添加引用标识。例如: - “可重入锁允许同一个线程多次获取同一把锁,避免死锁”[^4]。 - 确保引用是自然的。 避免数学表达式除非必要。在Java锁机制中,可能涉及CAS操作,如果提到,可以写成$CAS$ if needed, but it's not mathematical. CAS是Compare and Swap,可以用$...$包裹变量。 回答结构: - 开头:介绍Java锁机制- 主体:分小节解释关键点。 - 结尾:总结。 - 然后相关问题。 确保回答真实可靠:基于提供的引用和一般知识。 最终输出: - 用中文。 - 末尾有相关问题。 </think>以下是关于 Java 锁机制的详细介绍。Java 锁机制是多线程编程中用于实现线程同步的关键工具,它可以防止多个线程同时访问共享资源,从而避免数据不一致和并发问题。锁机制并非 Java 独有,但 Java 提供了丰富的内置实现(如 `synchronized` 关键字和 `java.util.concurrent.locks` 包)。接下来,我将从基础概念、常见分类、Java 核心实现等方面逐步解释,帮助您全面理解。本文内容基于您提供的引用进行整理,确保真实可靠。 ### 1. Java 锁机制的基本概念 Java 锁机制主要用于解决多线程环境下的资源竞争问题。当多个线程访问共享数据时,锁可以确保只有一个线程在特定时间内执行关键代码(临界区),从而实现线程安全。锁的核心作用是实现同步,防止并发错误如死锁或数据竞争。锁机制Java 中广泛应用于数据库操作、缓存管理等高并发场景[^1]。 ### 2. Java 锁的主要分类 Java 锁可以根据不同特性进行分类。以下是常见分类(基于引用内容整理),每个类别都有其适用场景: - **公平锁 vs 非公平锁**: - 公平锁:线程获取锁的顺序遵循先来先服务原则(FIFO),确保公平性,但可能导致性能开销。例如,`ReentrantLock` 可以通过构造函数设置为公平模式。 - 非公平锁:线程获取锁的顺序不保证公平性,可能导致线程“插队”,但通常性能更高(默认模式)。 - **可重入锁 vs 不可重入锁**: - 可重入锁:同一个线程可以多次获取同一把锁,而不会造成死锁。这在递归调用或嵌套方法中尤为重要。Java 中的 `synchronized` 和 `ReentrantLock` 都是可重入锁。例如,在方法调用链中,线程获取锁后进入内层方法会自动持有锁[^4]。 ```java public class Test { public void a() { ReentrantLock lock = new ReentrantLock(true); // 创建一个公平的可重入锁 lock.lock(); // 第一次获取锁 lock.lock(); // 第二次获取锁不会阻塞,因为可重入 // ... 执行代码 lock.unlock(); // 需要对应解锁 lock.unlock(); } } ``` - 不可重入锁:如果线程重复获取同一把锁,会引发死锁,因为锁无法被再次获取。 - **独占锁(写锁) vs 共享锁(读锁)**: - 独占锁:仅允许一个线程持有锁,用于写操作,防止其他线程读或写。例如,`ReentrantLock` 默认是独占锁。 - 共享锁:允许多个线程同时持有锁,用于读操作,提高并发性能。`ReentrantReadWriteLock` 实现了读写分离,读锁是共享的,写锁是独占的[^2]。 - **乐观锁 vs 悲观锁**: - 乐观锁:假设并发冲突较少,不使用锁,仅在提交时检测冲突(如使用 CAS 操作)。适合读多写少场景,例如 `java.util.concurrent.atomic` 包中的原子类。 - 悲观锁:假设并发冲突频繁,始终使用锁保护资源。例如 `synchronized` 关键字。 - **其他分类**: - **互斥锁 vs 读写锁**:互斥锁是独占锁的特例;读写锁结合了共享和独占特性(如 `ReentrantReadWriteLock`)。 - **分段锁**:将数据分成多个段,每个段独立加锁,减少锁竞争。例如 `ConcurrentHashMap` 的实现。 - **偏向锁/轻量级锁/重量级锁**:这是锁状态升级的过程(详见第 4 节)。 - **自旋锁**:线程在获取锁失败时,不立即阻塞,而是循环尝试(自旋),减少上下文切换开销。常用于短时操作[^2]。 ### 3. Java 中的核心锁实现 Java 提供了多种锁实现,主要分为两类: - **内置锁(synchronized)**:使用 `synchronized` 关键字实现,是一种可重入的互斥锁。它自动管理锁状态升级(从偏向锁到重量级锁),简化编程但灵活性较低。 ```java public synchronized void method() { // 临界区代码 } ``` - **显式锁(java.util.concurrent.locks)**:通过 `Lock` 接口及其实现类提供更丰富的功能。 - `ReentrantLock`:可重入锁,支持公平性和不公平性设置,提供 `tryLock` 等高级方法。 - `ReentrantReadWriteLock`:实现读写分离,提高读操作的并发性。 - `StampedLock`(JDK 8+):邮戳锁,结合乐观锁机制,性能更高,适合读多写少场景[^2]。 - `Condition`:条件变量,允许线程在特定条件下等待或唤醒,常用于生产者-消费者模式。 ### 4. 锁的状态升级和优化 Java 锁(尤其是 `synchronized`)会根据线程竞争情况动态升级状态,以平衡性能和安全性: - **无锁**:初始状态,没有线程竞争。使用 CAS(Compare and Swap)操作实现原子更新(例如 $CAS(\text{expectedValue}, \text{newValue})$ 比较并交换变量)。 - **偏向锁**:当只有一个线程访问时,锁被“偏向”该线程,减少开销。实现原理是标记线程 ID 在对象头中[^2][^4]。 - **轻量级锁**:当多个线程轻度竞争时,使用自旋等待。线程通过 CAS 尝试获取锁,避免阻塞。 - **重量级锁**:当竞争激烈时,锁升级为重量级锁,线程进入阻塞状态,依赖操作系统互斥量。这会增加开销但保证公平性。 锁升级的过程是不可逆的,旨在优化并发性能,避免不必要的系统调用[^2]。 ### 5. 锁机制的优势和注意事项 - **优势**:可重入锁能预防死锁(如递归调用),共享锁提升读取并发性,乐观锁减少锁争用。整体上,锁机制确保了线程安全和数据一致性。 - **注意事项**: - 过度使用锁可能导致性能瓶颈或死锁(例如线程相互等待)。 - 优先选择高级锁(如 `StampedLock`)来优化性能。 - 使用工具如 `CountDownLatch` 或 `Semaphore` 辅助线程协调[^2]。 Java 锁机制是多线程编程的基石,理解其分类和实现可以帮助您设计高效的并发应用。建议通过实际代码(如上述示例)加深理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旧梦昂志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值