数据库之事务
事务应该是数据库中的重点问题。面试的时候也被经常问到。一般我们只局限于事务的四个特性,却不太清楚他的底层实现,比如说日志啊锁啊等等。了解了底层实现之后有助于加深我们对事务的理解。
题外话:innodb支持事务,而myisam不支持事务。
事务
-
事务: 事务是一个序列操作,事务里的操作要么执行,要么不执行。是维护数据库一致性的单位。
四个属性:ACID,原子性,一致性,隔离性,持久性- 原子性:一个事务中的所有操作要么执行,要么不执行,如果中途某个操作执行失败了,就会回滚到事务开始的状态
- 一致性:在事务的开始和结束后,数据库的完整性没有被破坏。
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- 持久性:处理成功事务后,对数据库的修改是持久的。
锁:在所有的 DBMS 中,锁是实现事务的关键,锁可以保证事务的完整性和并发性。与现实生活中锁一样,它可以使某些数据的拥有者,在某段时间内不能使用某些数据或数据结构。当然锁还分级别的。
底层实现
原子性
要保证原子性,就需要在异常发生的时候能够对已经执行的操作进行回滚,在mysql中通过 undo log(回滚日志) 实现,所有的事务进行的修改都会放在这个回滚日志中,然后对数据库中对应行进行写入。它还能够在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。回滚日志并不能将数据库物理地恢复到执行语句或者事务之前的样子。它只会按照日志逻辑地将数据库中的修改撤销掉看,可以理解为,我们在事务中使用的每一条 INSERT 都对应了一条 DELETE,每一条 UPDATE 也都对应一条相反的 UPDATE 语句。
注意:外部的输出是无法回滚的。
事务的状态由三种,active正在执行,commited已经提交,failed失败。可能会有中间状态,比如partially commited最后一条语句执行之后, aborted事务被回滚后并且数据库恢复到事务进行之前的状态之后
并行性的原子性实现:如果tranction2依赖于transaction1,而transaction3也依赖于transction1,而transaction1由于执行出现了问题需要回滚,那么transaction2和transaction3也要进行回滚,叫做级联回滚。
持久性
事务的持久性就体现在,一旦事务被提交,那么数据一定会被写入到数据库中并持久存储起来。事务的持久性是通过日志来实现,mysql用的是redo log(重做日志)来实现持久性。由两个部分组成,一个是内存中的重做日志缓存,因为重做日志缓存区在内存里面,是易丢失的;另一个是在磁盘上的重做日志文件,它是持久的。
当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL 会将重做日志缓存中的内容刷新到重做日志文件,再将内存中的数据更新到磁盘上
隔离性
事务的隔离级别:
read uncommited:查询的时候不加锁,可能会读到未提交的行(dirty read)
read commited:只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read);
repeateable read:多次读取同一范围的数据会返回第一次查询的快照,不会返回不同的数据行,但是可能发生幻读(Phantom Read)【mysql作为默认设置】
serializable:InnoDB 隐式地将全部的查询语句加上共享锁,解决了幻读的问题;
随着隔离的级别越来越严格,数据库对于并发执行事务的性能也逐渐下降。
隔离的实现是通过锁、时间戳来实现的。不会锁住整个数据库,而是锁住那些要访问的数据项。锁的话分为两种:共享锁(读锁)和互斥锁(写锁)
一致性
如果一个事务原子地在一个一致地数据库中独立运行,那么在它执行之后,数据库的状态一定是一致的。对事务的要求不止包含对数据完整性以及合法性的检查,还包含应用层面逻辑的正确。
CAP 定理中的数据一致性,其实是说分布式系统中的各个节点中对于同一数据的拷贝有着相同的值。
一些相关的概念
脏读
一个事务读取了其他事务未提交的数据不可重复读
在一个事务中,同一行记录被访问了两次却得到了不同的结果。
不可重复读的原因就是,在 READ COMMITED 的隔离级别下,存储引擎不会在查询记录时添加行锁,锁定 id = 3 这条记录。幻读
在一个事务中,同一个范围内的记录被读取时,其他事务向这个范围添加了新的记录
在标准的事务隔离级别中,幻读是由更高的隔离级别 SERIALIZABLE 解决的,但是它也可以通过 MySQL 提供的 Next-Key 锁解决:
REPEATABLE READ 和 READ UNCOMMITED 其实是矛盾的,如果保证了前者就看不到已经提交的事务,如果保证了后者,就会导致两次查询的结果不同,MySQL 为我们提供了一种折中的方式,能够在 REPEATABLE READ 模式下加锁访问已经提交的数据,其本身并不能解决幻读的问题,而是通过文章前面提到的 Next-Key 锁来解决。事务日志
分为两种:回滚日志(undo log,用于原子性)与重做日志(redo log,用于持久性)
发生错误或者需要回滚的事务能够成功回滚(原子性);
在事务提交后,数据没来得及写会磁盘就宕机时,在下次重新启动后能够成功恢复数据(持久性);
MYSQL尝试
如何修改数据库中的隔离级别、各种隔离性的实现
查看隔离的级别:(可以看到默认是repeatable read)
这里修改事务权限的语句是:
set [ global | session ] transaction isolation level Read uncommitted | Read committed | Repeatable | Serializable;
如果选择global,意思是此语句将应用于之后的所有session,而当前已经存在的session不受影响。
如果选择session,意思是此语句将应用于当前session内之后的所有事务。
如果什么都不写,意思是此语句将应用于当前session内的下一个还未开始的事务。