一、数据库的事务
1.什么是事务
数据库的事务( Transaction **)**是一种机制、一个操作序列,包含了一组数据库操作命令。事务把所有的命令作为一个整体一起向系统提交或撤销操作请求,即这一组数据库命令要么都执行,要么都不执行,因此事务是一个不可分割的工作逻辑单元。
在数据库系统上执行并发操作时,事务是作为最小的控制单元来使用的,特别适用于多用户同时操作的数据库系统。
2.事务的四大特性
事务具有 4 个特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),这 4 个特性通常简称为 ACID。
1. 原子性(通过undo log实现回滚)
事务是一个完整的操作。事务的各元素是不可分的(原子的)。事务中的所有元素必须作为一个整体提交或回滚。如果事务中的任何元素失败,则整个事务将失败。
以银行转账事务为例,如果该事务提交了,则这两个账户的数据将会更新。如果由于某种原因,事务在成功更新这两个账户之前终止了,则不会更新这两个账户的余额,并且会撤销对任何账户余额的修改,事务不能部分提交。
2. 一致性
当事务完成时,数据必须处于一致状态。也就是说,在事务开始之前,数据库中存储的数据处于一致状态。在正在进行的事务中. 数据可能处于不一致的状态,如数据可能有部分被修改。然而,当事务成功完成时,数据必须再次回到已知的一致状态。通过事务对数据所做的修改不能损坏数据,或者说事务不能使数据存储处于不稳定的状态。
以银行转账事务事务为例。在事务开始之前,所有账户余额的总额处于一致状态。在事务进行的过程中,一个账户余额减少了,而另一个账户余额尚未修改。因此,所有账户余额的总额处于不一致状态。事务完成以后,账户余额的总额再次恢复到一致状态。
3. 隔离性
对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。修改数据的事务可以在另一个使用相同数据的事务开始之前访问这些数据,或者在另一个使用相同数据的事务结束之后访问这些数据。
另外,当事务修改数据时,如果任何其他进程正在同时使用相同的数据,则直到该事务成功提交之后,对数据的修改才能生效。张三和李四之间的转账与王五和赵二之间的转账,永远是相互独立的。
4. 持久性(通过redo log实现)
事务的持久性指不管系统是否发生了故障,事务处理的结果都是永久的。
一个事务成功完成之后,它对数据库所作的改变是永久性的,即使系统出现故障也是如此。也就是说,一旦事务被提交,事务对数据所做的任何变动都会被永久地保留在数据库中。
事务的 ACID 原则保证了一个事务或者成功提交,或者失败回滚,二者必居其一。因此,它对事务的修改具有可恢复性。即当事务失败时,它对数据的修改都会恢复到该事务执行前的状态。
3.事务开启和结束的方式
MySQL 默认开启 autocommit = ON(自动提交),这是理解两种提交方式的基础:
- 当
autocommit = ON时,每一条 DML 语句(INSERT/UPDATE/DELETE)执行后会自动提交事务,相当于每条语句都是一个独立事务; - 当
autocommit = OFF时,需要手动触发提交 / 回滚,语句不会自动生效。
sql
begin;
UPDATE t_person set username = "张三666666" where id = 1
commit;
rollback;
4.事务的隔离性
4.1读一致性问题
脏读:一个事务读取到了另一个事务未提交的数据。

不可重复读:一个事务读取到了另一个事务已提交的数据,在同一个事务内,不管进行多少次查询,查询到的结果都是相同的。(强调修改与删除)

虚读(幻读):一个事务读取了另一个事务提交的新增数据。(强调新增)

4.2隔离级别解决的读一致性问题
- Read Uncommitted (读未提交),未解决任何并发问题,事务未提交的数据对其他事务也是可见的,会出现脏读。
- Read Committed (已提交读),解决脏读问题,一个事务开始之后,只能看到已提交的事务所做的修改,会出现不可重复读。
- Repeatable Read (可重复读),解决不可重复读问题,在同一个事务中多次读取同样的数据结果是一样的, 这种隔离级别未定义解决幻读的问题。
- Serializable (串行化),解决所有问题,最高的隔离级别,通过强制事务的串行执行。

可重复读:因为InnoDB会对区间加锁,是不会插入数据的,所以不会出现幻读
4.3思考:如果要解决读一致性的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,应该怎么做?
**第一种解决方式:**在读取数据前,对其加锁,阻止其他事务对数据进行修改
(LBCC) Lock Based Concurrency Control
**第二种解决方式:**生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取
(MVCC) Multi Version Concurrency Control。MVCC只在RC RR中使用。
二、MVCC解决读一致性的问题的原理
1.概念
MVCC就是通过同一份数据保留多版本的方式,实现并发控制。
如果有人从数据库中读数据的同时,有另外的人写入数据,有可能读数据的人会看到『半写』或者不一致的数据。有很多种方法来解决这个问题,叫做并发控制方法。
最简单的方法,通过加锁,让所有的读者等待写者工作完成,但是这样效率会很差。
MVCC 使用了一种不同的手段,每个连接到数据库的读者,在某个瞬间看到的是数据库的一个快照,写者写操作造成的变化在写操作完成之前(或者数据库事务提交之前)对于其他的读者来说是不可见的。
当一个 MVCC 数据库需要更新一个一条数据记录的时候,它不会直接用新数据覆盖旧数据,而是将旧数据标记为过时(obsolete)并在别处增加新版本的数据。这样就会有存储多个版本的数据,但是只有一个是最新的。
2.具体实现
2.1数据结构:版本链和read-view
Mysql会在表中添加2个隐藏的字段,DB_TRX_ID(插入或者更新行的最后一个事务的ID) 和 DB_ROLL_PTR (回滚指针)。
每次执行update会产生一个事务id
当执行查询sq|时会生成一致性视图read-view, 它由执行查询时所有未提交事务id数组(数组里最小的id为min id)和已创建的最大事务id (max id)组成,查询的数据结果需要跟read-view做比对从而得到快照结果。
Read-view是一个数组加一个已创建的事务最大id:[100,200]300,数组里面的是未提交的事务,数组外面是已经创建的最大事务id。这个时候min id是100,max id是300.
2.2版本链比对规则:

1.如果落在绿色部分( trx jid<min. id),表示这个版本是已提交的事务 生成的,这个数据是可见的;
2.如果落在红色部分( trx id>max. id),表示这个版本是由将来启动的事务生成的,是肯定不可见的;
3.如果落在黄色部分(min. id <=tnx id<= max_ id),那就包括两种情况
a.若row的trx_ id在数组中 ,表示这个版本是由还没提交 的事务生成的,不可见 ,当前自己的事务是可见的;
b.若row的trx. id不在数组中 ,表示这个版本是已经提交了的事务生成的,可见。
对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx. id修改成删除操作的trx_ id,同时在该条记录的头信息(record header)里的(deleted_ flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录。如果delete flag标记位为true, 意味看记录已被删除,则不返回数据。
2.3不同隔离级别的版本
读已提交:只能读有commit的最新数据
可重复读:第一次读到的已提交的数据,会和事务有一个绑定关系,之后再次查询,还是读这个绑定的数据。写的时候加一个排它锁,并且这个时候是当前读,会拿到最新的数据。
解决可重复读的脏写:
- 使用乐观锁,加一个version。
- 使用悲观锁,执行INSERT、UPDATE、DELETE 时,InnoDB不会直接修改 / 删除数据 ,而是先执行一次隐式的当前读 (读取数据最新版本),并自动为匹配的行加排他锁(X 锁)
2.4怎么选择不同的隔离级别
如果要实现可重复读,需要事务,保证多条查询语句查询到的是同一时间点的。
读已提交:不需要事务,所以性能更高,适合高并发场景
可重复读:保证查询的数据是同一时间点的,适合报表
三、锁
1.概念
锁是计算机协调多个进程或线程并发访问某一资源的机制。
MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。
MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking) ;
InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
**数据库的锁,锁住的是索引,**没有对应的索引时,InnoDB 不能用索引定位并加上期望的行级锁,而是进行全表的扫描,所以会进行全表加锁。
2.Innodb支持的锁
2.1全局锁
读锁:可以一起读,但是不能写
写锁:阻止读和写
使用场景:全库备份、全库导出
sql
FLUSH TABLES WITH READ LOCK
UNLOCK TABLES
2.2表锁
表共享锁
表独占锁
在MyISAM的读操作自动加读锁,InnoDB不会自动加锁
使用场景:读多写少,数据量不大,全表的更新和删除
MySQL哪些命令会发生表级锁
- **ALTER TABLE:**这个命令用于更改表的结构,如添加列、删除列、改变列的类型等。执行 这个命令的时候,MySQL需要锁定整个表以防止在更改过程中有新的数据写入。
- **DROP TABLE 和 TRUNCATE TABLE:**这两个命令都会导致表级锁。DROP TABLE命令会删除整个表,而TRUNCATE TABLE命令会删除表中的所有数据。在执行这些命令的时候 MySQL需要锁定整个表以防止在删除过程中有新的数据写入。
- LOCK TABLES: 这个命令可以显式地为一个或多个表加上读锁或写锁。LOCK TABLES命令后面可以跟上一系列的表名和锁模式,用来指定需要锁定哪些表,以及使用什么样的锁模 式。例如,**LOCK TABLES t1 WRITE,t2 READ;**命令会给表t1加上写锁,给表t2加上读锁。
- 全表扫描或大范围扫描:对于MyISAM存储引擎,全表扫描或大范围扫描会触发表级锁。
- FLUSH TABLES WITH READ LOCK(FTWRL):这个命令可以给所有表加上全局读锁,其他会话在此期间不能对数据进行修改。
请注意,InnoDB存储引擎主要使用行级锁,并在一些情况下使用表级锁,比如在执行某些 ALTER TABLE命令或者LOCK TABLES命令时。MyISAM存储引擎只支持表级锁。
2.3行锁的共享锁与排它锁
(1)共享锁,又称为读锁,简称S锁。
共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
sql
select * from t_person_lock where id=1 LOCK IN SHARE MODE;
释放锁:事务结束后会释放锁。
(2)排他锁又称为写锁,简称X锁。
排他锁不能与其他锁并存,如一个事务获取了个数据行的排他锁,其他事务就不能再获取该行的锁(共享锁、排他锁),只有该获取了排他锁的事务是可以对数据行进行读取和修改。
加锁释锁方式:
自动: delete / update / insert默认加上X锁;
手动: select * from t_person_lock where id=1 FOR UPDATE;
释放锁:事务结束后会释放锁。
2.4表锁之意向共享锁和排它锁
意向共享锁是表级别的锁,在给任意一条数据添加排它锁或者共享锁的时候 ,都需要先给表添加意向排它锁或者意向共享锁。
作用:是给这张表添加标志,表示该表中的一条数据已经被其他事务锁定了,那么再给这张表添加表锁的时候肯定会失败,而且不需要去遍历表中的数据查看是否添加了行锁,效率很高。
|--------------|-----------|-----------|
| 我想加的锁 | 表上已有 IS 锁 | 表上已有 IX 锁 |
| 共享表锁 (S) | 兼容 | 冲突 |
| 排他表锁 (X) | 冲突 | 冲突 |
2.5排它锁的实现------record记录锁
条件:唯一性索引(唯一/主键)等值查询 ,精准匹配,数据库有匹配的记录,只会对这一条记录锁住。
例如:select * from t4 where id = 4 for update;

2.4排它锁的实现------Gap Lock 间隙锁,作用:用来阻塞 insert,解决幻读
条件:查找的记录不存在,在包含这个值的区间进行加锁
select * from t2 where id > 20 for update;
锁住:(10, +∞)
select * from t where id =6 for update;
锁住范围是 (4,7)之间

2.5排它锁的实现------Next-key Loack 临建锁,锁定范围+记录。
条件:范围查询,包含记录和区间。能够查到记录,同时又是范围查询。
会锁住查到的值的前后间隙,左开右闭
例如:select * from t4 where id >5 and id <9 for update;
锁住:id >5 and id <9有一个记录7,InnoDB 会对与 7 相关的两个区间加锁:(4,7] (7,10] 的范围。

|------------------|---------------|---------------------|
| 场景 | 锁定区间类型 | 区间示例 (数据 1, 5, 10) |
| 查询不存在的值 id=3 | 纯间隙锁 (Gap) | (1,5) |
| 唯一索引查询存在的值 id=5 | 记录锁 (Record) | 仅锁定 5 |
| 普通索引查询存在的值 age=5 | Next-Key Lock | (1,5] (5,10)锁定前后区间 |
| 范围查询 id > 10 | Next-Key Lock | (10,+∞) |