1 事务概念
MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你既需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务。
1.1 事务特点
数据库通过原子性(A)、隔离性(I)、持久性(D)来保证一致性(C):
- Atomicity(原子性):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作
- Consistency(一致性):数据库总是从一个一致性状态转换到另一个一致状态。下面的银行列子会说到
- Isolation(隔离性):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。注意这里的“通常来说”,后面的事务隔离级级别会说到
- Durability(持久性):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。(持久性的安全性与刷新日志级别也存在一定关系,不同的级别对应不同的数据安全级别)
其中一致性是目的,原子性、隔离性、持久性是手段。因此数据库必须实现AID三大特性才有可能实现一致性。下表描述A、C、I、D的关系及实现手段:
目标 | Consistency | ||
手段 | Atomicity | Isolation | Durability |
实现 | undo log | 锁+mvcc | redo log |
1.2 事务并发带来的读异常
- 脏读:一个事务读到了另一个未提交事务修改过的数据
- 不可重复读:一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。(不可重复读在读未提交和读已提交隔离级别都可能会出现)
- 幻读:一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。(幻读在读未提交、读已提交、可重复读隔离级别都可能会出现)
1.3 不可重复读和幻读的区别
- 不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(因为中间有其他事务提交了修改)
- 幻读的重点在于新增或者删除:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(因为中间有其他事务提交了插入/删除)
1.4 解决方案
1.4.1 加锁
在读取数据前,对其加锁,阻止其他事务对数据进行修改
-
共享锁与排他锁
-
共享锁(读锁):其他事务可以读,但不能写
-
排他锁(写锁) :其他事务不能读取,也不能写
-
-
乐观锁与悲观锁
-
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 乐观锁不能解决脏读的问题。
-
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
-
-
粒度锁
-
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
-
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
-
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
-
1.4.2 多版本并发控制(MVCC)
不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。
MVCC只在读已提交和可重复读两个隔离级别下工作,MVCC不能解决幻读,幻读还是得用锁。 MVCC主要解决几个问题:
a. 读写之间阻塞的问题:通过 MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
b. 降低了死锁的概率:因为 InnoDB 的 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
c. 解决一致性读的问题:一致性读也被称为快照读,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。
1.4.3 幻读解决方案
MVCC+间隙锁
2 事务隔离
SQL标准定义了4类隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
2.1 隔离级别
- 读未提交
- 所有事务都可以看到其他未提交事务的执行结果
- 本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少
- 引发问题——脏读(Dirty Read):读取到了未提交的数据
- 读已提交
- 大多数数据库系统的默认隔离级别,不是MySQL默认的
- 满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
- 引发问题——不可重复读(Nonrepeatable Read)。导致这种情况的原因可能有:
- 有一个交叉的事务有新的commit,导致了数据的改变
- 一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit
- 可重复读
- MySQL的默认事务隔离级别
- 它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
- 引发问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行
- InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决幻读问题;InnoDB还通过间隙锁解决幻读问题
- 可串行化
- 最高的隔离级别,通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁
- 可能导致大量的超时现象和读写锁竞争
innoDB是mysql默认的存储引擎,默认的隔离级别是RR(Repeatable Read),并且在RR的隔离级别下更进一步,通过多版本并发控制(MVCC)解决不可重复读问题,加上间隙锁(也就是并发控制)解决幻读问题。因此innoDB的RR隔离级别其实实现了串行化级别的效果,而且保留了比较好的并发性能。
2.2 隔离级别对比
读未提交 | 读已提交 | 可重复读 | 可串行读 | |
脏读 | 可能 | 不会 | 不会 | 不会 |
不可 重复读 | 可能 | 可能 | 不会 | 不会 |
幻读 | 可能 | 可能 | 可能 | 不会 |
3 事务日志
mysql的日志文件有六种:
- 重做日志(redo log)
- 回滚日志(undo log)
- 二进制日志(binlog)
- 错误日志(errorlog)
- 慢查询日志(slow query log)
- 一般查询日志(general log)
- 中继日志(relay log)
其中重做日志和回滚日志与事务操作息息相关,二进制日志也与事务操作有一定的关系,这三种日志,对理解MySQL中的事务操作有着重要的意义。
3.1 redo日志(持久性)
防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
3.2 undo日志(原子性)
保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。
3.3 binlog日志
binlog日志主要作用:
- 在主从复制中,从库利用主库上的binlog进行重播,实现主从同步
- 用于数据库的基于时间点的还原
binlog
日志有三种格式:
-
STATMENT
: 基于SQL
语句的复制,每一条会修改数据的sql语句会记录到binlog
中-
优点: 不需要记录每一行的变化,减少了binlog日志量,节约了IO, 从而提高了性能
- 缺点: 在某些情况下会导致主从数据不一致,比如执行sysdate() 、 slepp() 等
-
-
ROW
: 基于行的复制,不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了- 优点: 不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题
- 缺点: 会产生大量的日志,尤其是
alter table
的时候会让日志暴
MIXED
: 基于STATMENT
和ROW
两种模式的混合复制(mixed-based replication, MBR
),一般的复制使用STATEMENT
模式保存binlog
,对于STATEMENT
模式无法复制的操作使用ROW
模式保存binlog
3.4 redo log与binlog区别
redo log | binlog | |
文件大小 | redo log 的大小是固定的 | binlog 可通过配置参数 max_binlog_size 设置每个binlog 文件的大小 |
实现方式 | redo log 是InnoDB 引擎层实现的,并不是所有引擎都有 | binlog 是 Server 层实现的,所有引擎都可以使用binlog 日志 |
记录方式 | redo log 采用循环写的方式记录,当写到结尾时,会回到开头循环写日志 | binlog通过追加的方式记录,当文件大小大于给定值后,后续的日志会记录到新的文件上 |
适用场景 | redo log 适用于崩溃恢复(crash-safe) | binlog 适用于主从复制和数据恢复 |