事务特性
事务(Transaction)是一组逻辑操作单元,使数据从一种状态变换到另一种状态。
事务处理的原则:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态。
在 MySQL 命令行工具中有自动提交模式和手动提交模式:在自动提交模式中,每条命令都是一个独立事务,执行完自动提交;需要手动提交的话,用START TRANSACTION;开启一个事务,最后使用commit/rollback来提交或者回滚。
# ACID 特性
- 原子性(Atomicity) :原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency) :事务必须使数据库从一个一致性状态变换到另一个一致性状态。
- 隔离性(Isolation) :事务的隔离性是指,多个用户并发的访问数据库时,数据库为每一个用户开启的事务不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。(MySQL 默认的隔离级别是 REPEATABLE READ)
- 持久性(Durability) :持久性是指,一个事务一旦被提交,它对数据库中数据的改变就是永久性的,写入磁盘和日志文件,接下来即使数据库发生故障也不应该对其有任何影响。持久性是通过事务日志来保证的。日志包括了重做日志和回滚日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。
分别用以下技术保证了四个特性:
- 持久性是通过 redo log (重做日志)来保证的。
- 原子性是通过 undo log(回滚日志) 来保证的。
- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的。
- 一致性是通过持久性 + 原子性 + 隔离性来保证。
# 事务状态
有以下 5 种状态:
- Active(活动状态) 事务在开始执行后进入活动状态,此时事务正在执行操作,还没有提交或回滚。事务在这一阶段可以进行增删改查等操作。
- Partially Committed(部分提交状态) 当事务执行完所有操作,准备提交时,进入部分提交状态。此时事务已完成全部数据库操作,但还未将数据真正持久化(操作都在内存中执行,所造成的影响并没有刷新到磁盘)。因此,事务在这一阶段并不一定成功,需要等待提交完成。
- Failed(失败状态) 如果事务在执行过程中遇到错误或故障,进入失败状态。发生故障可能是由于系统崩溃、数据冲突或违反了约束条件等。此时事务无法继续进行,数据库系统会中止该事务并回滚已执行的操作。
- Aborted(中止状态) 当事务进入失败状态后,数据库系统执行回滚操作,将事务已完成的操作撤销,恢复到事务开始之前的状态。事务回滚完成后,进入中止状态。此时数据库可以选择重新启动事务,或放弃该事务。
- Committed(已提交状态) 当事务的所有操作成功执行且持久化后,进入已提交状态。已提交状态表示事务的操作已永久写入数据库(即从内存写入到磁盘),即使系统出现崩溃,也不会丢失数据。进入此状态后,事务就算完成了全部执行流程。
# 隔离级别
隔离级别解决的是并发事务导致的问题,有以下三个问题:
- 脏读:读到其他事务未提交的数据,例如一个事务读取到另一个事务未提交的数据,后者最终回滚,那么前一个事务读取的数据就是无效的“脏”数据。
- 不可重复读:同一事务内两次读同一条记录,结果不一致,重点在于修改
- 幻读:同一事务内两次查询,行数不一致,重点在于删除或新增
隔离级别有以下四种:
读未提交(read uncommitted):指一个事务还没提交时,它做的变更就能被其他事务看到。在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。
读已提交(read committed):指一个事务提交之后,它做的变更才能被其他事务看到。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。
可重复读(repeatable read):指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别。事务 A 在读到一条数据之后,此时事务 B 对该数据进行了修改并提交,那么事务 A 再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。
串行化(serializable ):会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
# 数据库的锁
数据操作类型分类:
共享锁(SharedLock,SLock) 和排他锁(ExclusiveLock,XLock),也叫读锁(readlock) 和 写锁(writelock)
- 读锁:用 S 表示,针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的,阻断其他写锁。使用场景:当事务需要读取某些数据,但不打算修改它时,使用读锁。
- 写锁 :用 X 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。使用场景:当事务需要修改数据时,使用写锁。
使用如下语句添加 S 锁:
SELECT ... LOCK IN SHARE MODE;
当前事务会给读取的记录加共享锁(S 锁),允许其他事务也能对该记录加共享锁(即其他事务可以继续读取该记录)。但其他事务无法加排他锁(X 锁),即它们无法修改该记录。如果有其他事务尝试修改该记录,它们会被阻塞,直到当前事务提交并释放锁。
使用如下语句添加 X 锁:
SELECT ... FOR UPDATE;
DELETE、UPDATE、INSERT
2
当前事务会为读取的记录加排他锁(X 锁),这会阻止其他事务对这些记录进行任何读写操作。其他事务无法加共享锁(S 锁)或排他锁(X 锁),它们会被阻塞,直到当前事务提交并释放锁。
从数据操作的粒度划分:
- 表级锁: 表级锁是最粗的锁粒度,它锁定的是整个表,任何对该表的操作(包括读和写操作)都会被阻塞,直到当前事务释放锁。
表级锁
├── 表锁(Table Lock)
│ ├── 读锁(READ Lock)- 共享锁
│ └── 写锁(WRITE Lock)- 排他锁
├── 元数据锁(MDL, Metadata Lock)
│ ├── MDL读锁
│ └── MDL写锁
├── 意向锁(Intention Lock)
│ ├── 意向共享锁(IS)
│ └── 意向排他锁(IX)
└── AUTO-INC锁(自增锁)
2
3
4
5
6
7
8
9
10
11
- 表锁: 最基本的表级锁,MyISAM 存储引擎默认使用
LOCK TABLES 语句:为表加上 S 锁或 X 锁。例如:
LOCK TABLES employees READ; # 对表 employees 加共享锁(S 锁),其他事务仍然可以读取该表,但不能修改。
LOCK TABLES employees WRITE; # 对表 employees 加排他锁(X 锁),其他事务既不能读取也不能修改该表。
2
- 元数据锁: 保护表结构等元数据,避免 DDL 和 DML 冲突。
元数据有表名、列名、列的数据类型、索引定义和约束(主键、外键等)等
| 锁类型 | 说明 | 操作 | 兼容性 |
|---|---|---|---|
| MDL 读锁 | 共享锁,允许读取表结构 | SELECT、INSERT、UPDATE、DELETE | 多个读锁兼容 |
| MDL 写锁 | 排他锁,修改表结构 | ALTER、DROP、RENAME、TRUNCATE | 与所有锁冲突 |
- 意向锁: 表明事务即将对行加锁,协调表锁和行锁。
意向锁分为 意向共享锁(IS) 和 意向排他锁(IX),用于在事务访问表级别时,向数据库系统传递该事务将对表中的某些行进行加锁的信息。
意向共享锁(Intention Shared Lock, IS):事务有意向对表中的某些行加共享锁(S 锁)。
意向排他锁(Intention Exclusive Lock, IX):事务有意向对表中的某些行加排他锁(X 锁)。
- AUTO-INC 锁: 控制自增列并发插入的表级锁。
自增锁通常是表级锁,它会锁定整个表,以避免多个事务同时生成相同的自增主键值,保证 AUTO_INCREMENT 属性
- 行级锁: 锁住表中的行,开销大,并发度高,InnoDB 中实现了行级锁。
行级锁(InnoDB)
├── 记录锁(Record Lock)
│ ├── 共享锁(S锁)
│ └── 排他锁(X锁)
├── 间隙锁(Gap Lock)
├── 临键锁(Next-Key Lock)
└── 插入意向锁(Insert Intention Lock)
2
3
4
5
6
7
- 记录锁: 锁住单条索引记录。
当一个事务对某条记录加上 S 型记录锁时,其他事务仍然可以对这条记录加上 S 型记录锁,即允许多个事务并发地读取这条记录。 当一个事务对某条记录加上 X 型记录锁时,其他事务 既不能获取 S 型锁,也不能获取 X 型锁,即该记录完全被锁定,其他事务不能读取或修改这条记录。
- 间隙锁 : 锁住索引记录之间的间隙,防止幻读。
间隙锁,锁定一个范围,但是不包含记录本身。间隙锁(Gap Locks)是 InnoDB 在 REPEATABLE READ 隔离级别下为了解决幻读问题而引入的机制。通过加锁区间,可以防止出现幻影记录。幻读问题指的是在事务执行期间,查询结果行数发生变化,特别是某些记录(幻影记录)在事务未提交时被另一个事务插入、更新或删除,导致数据的一致性和隔离性问题。
间隙锁的主要作用是防止在读取区间中的空隙处插入新的记录,也就是防止插入产生幻影记录。它并不会直接锁定某一行记录,而是锁定某个区间,防止其他事务在该区间内插入新的记录。
- 临键锁 : 记录锁和间隙锁的组合,锁住记录和之前的间隙。
- 插入意向锁 : 特殊的间隙锁,表示插入新记录的意图。
从对待数据的态度划分:
- 乐观锁
乐观锁的核心思想是“假设最好的情况”,即认为大多数情况下不会发生并发冲突,因此不需要每次操作数据时都加锁,而是在提交更新时检查数据是否已经被修改。乐观锁通过应用程序来实现数据的并发控制。在读取数据时不加锁,在更新数据时通过检查是否有其他事务修改过数据来决定是否提交。适用于并发冲突较少的场景,能够提高并发性能和吞吐量。不会发生死锁,因为不会长时间占用锁。因此适用于读操作多的场景。
- 悲观锁:“总有刁民想害朕”
悲观锁的核心思想是“假设最坏的情况”,即认为在数据操作过程中,其他事务可能会对数据进行修改,因此在每次操作数据之前都必须加锁,保证数据操作的排它性。悲观锁总是预防并发修改冲突,采用的锁机制可以是数据库层级的锁,比如行锁,表锁等,读锁,写锁等。它们都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。
# MVCC
MVCC (Multi-Version Concurrency Control,多版本并发控制) 是一种无锁并发控制技术,通过保存数据的多个版本,实现读不阻塞写,写不阻塞读,从而提高数据库并发性能。 在高并发场景下,传统的锁机制会导致大量的锁等待,严重影响性能,因此出现了 MVCC,与锁协同工作。
实现原理:
- 版本链: InnoDB 数据引擎在每行数据后添加隐藏数据(事务 ID ,自增 ID 和回滚指针),将同一行数据的不同版本连接起来形成版本链,旧版本数据存储在 Undo Log 中,Undo Log 中的一个个的历史版本就称为一个个的快照。
DB_TRX_ID:6 字节,创建或最近一次修改该记录的事务 ID。DB_ROW_ID:6 字节,隐含的自增 ID(隐藏主键)。DB_ROLL_PTR:7 字节,回滚指针,指向这条记录的上一个版本。
- 读视图 (Read View): 事务在进行快照读操作时会生成读视图 Read View,在该事务执行快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃的事务 ID。
Read View 维护了以下几个字段:
m_ids: 一张列表,记录 Read View 生成时刻,系统中活跃的事务 ID。m_up_limit_id: 记录 m_ids 列表中事务 ID 最小的 ID。m_low_limit_id: 记录 Read View 生成时刻,系统尚未分配的下一个事务 ID,最大 ID + 1m_creator_trx_id: 记录创建该 Read View 的事务的事务 ID。
- 可见性判断: 事务根据 Read View 和数据版本的事务 ID 来判断数据是否对它(也就是当前事务)可见,不可见则沿着版本链回溯查找旧版本。
事务ID轴:
0 -------- up_limit_id -------- low_limit_id -------- +∞
| | |
| | |
可见 需判断 不可见
(已提交) (在m_ids中?已提交) (未来事务)
2
3
4
5
6
读已提交和可重复读的区别:
- 在 RR 级别下,事务第一次进行快照读时会创建一个 Read View,将当前系统中活跃的事务记录下来,此后再进行快照读时就会直接使用这个 Read View 进行可见性判断,因此当前事务看不到第一次快照读之后其他事务所作的修改。
- 在 RC 级别下,事务每次进行快照读时都会创建一个 Read View,然后根据这个 Read View 进行可见性判断,因此每次快照读时都能读取到被提交了的最新的数据。
RR 级别下快照读只会创建一次 Read View,所以 RR 级别是可重复读的,而 RC 级别下每次快照读都会创建新的 Read View,所以 RC 级别是不可重复读的。