使用事务
使用事务有两种方式,分别为 显式事务 和 隐式事务 。
显式事务
步骤一、START TRANSACTION 或者 BEGIN ,作用是显式开启一个事务。
BEGIN;
#或者
START TRANSACTION;
START TRANSACTION 语句相较于 BEGIN 特别之处在于,后边能跟随几个 修饰符 :
① READ ONLY :标识当前事务是一个 只读事务 ,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
② READ WRITE :标识当前事务是一个 读写事务 ,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
③ WITH CONSISTENT SNAPSHOT :启动一致性读。
步骤2:一系列事务中的操作(主要是DML,不含DDL)
# 提交事务。当提交事务后,对数据库的修改是永久性的。
COMMIT;
# 回滚事务。即撤销正在进行的所有没有提交的修改
ROLLBACK;
# 将事务回滚到某个保存点。
ROLLBACK TO [SAVEPOINT]
隐式事务
MySQL中有一个系统变量 autocommit
1 SHOW VARIABLES LIKE 'autocommit';
如果我们想关闭这种 自动提交 的功能,可以使用下边两种方法之一:
显式的的使用 START TRANSACTION 或者 BEGIN 语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。
把系统变量 autocommit 的值设置为 OFF ,就像这样:
1 SET autocommit = OFF;
2 #或
3 SET autocommit = 0;
隐式提交数据的情况
1、数据定义语言(Data definition language,缩写为:DDL)
2、隐式使用或修改mysql数据库中的表
3、事务控制或关于锁定的语句
① 当我们在一个事务还没提交或者回滚时就又使用 START TRANSACTION 或者 BEGIN 语句开启了另一个事务时,会 隐式的提交 上一个事务。即:
② 当前的 autocommit 系统变量的值为 OFF ,我们手动把它调为 ON 时,也会 隐式的提交 前边语句所属的事务。
③ 使用 LOCK TABLES 、 UNLOCK TABLES 等关于锁定的语句也会 隐式的提交 前边语句所属的事务。
4、加载数据的语句
5、关于MySQL复制的一些语句
6、其它的一些语句
当我们设置 autocommit=0 时,不论是否采用 START TRANSACTION 或者 BEGIN 的方式来开启事务,都需要用 COMMIT 进行提交,让事务生效,使用 ROLLBACK 对事务进行回滚。
当我们设置 autocommit=1 时,每条 SQL 语句都会自动进行提交。 不过这时,如果你采用 START TRANSACTION 或者 BEGIN 的方式来显式地开启事务,那么这个事务只有在 COMMIT 时才会生效,在 ROLLBACK 时才会回滚。
事务隔离级别
问题
访问相同数据的事务在 不保证串行执行 (也就是执行完一个再执行另一个)的情况下可能会出现哪些问题:
严重性来排序: 脏写 > 脏读 > 不可重复读 > 幻读
脏写( Dirty Write )
对于两个事务 Session A、Session B,如果事务Session A 修改了 另一个 未提交 事务Session B 修改过 的数据,那就意味着发生了 脏写
脏读( Dirty Read )
对于两个事务 Session A、Session B,Session A 读取 了已经被 Session B 更新 但还 没有被提交 的字段。之后若 Session B 回滚 ,Session A 读取 的内容就是 临时且无效 的。
Session A和Session B各开启了一个事务,Session B中的事务先将studentno列为1的记录的name列更新为'张三',然后Session A中的事务再去查询这条studentno为1的记录,如果读到列name的值为'张三',而Session B中的事务稍后进行了回滚,那么Session A中的事务相当于读到了一个不存在的数据,这种现象就称之为 脏读 。
不可重复读( Non-Repeatable Read )
对于两个事务Session A、Session B,Session A 读取 了一个字段,然后 Session B 更新 了该字段。 之后Session A 再次读取 同一个字段, 值就不同 了。那就意味着发生了不可重复读。
我们在Session B中提交了几个 隐式事务 (注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了studentno列为1的记录的列name的值,每次事务提交之后,如果Session A中的事务都可以查看到最新的值,这种现象也被称之为 不可重复读 。
幻读( Phantom )
对于两个事务Session A、Session B, Session A 从一个表中 读取 了一个字段, 然后 Session B 在该表中 插入 了一些新的行。 之后, 如果 Session A 再次读取 同一个表, 就会多出几行。那就意味着发生了幻读。
Session A中的事务先根据条件 studentno > 0这个条件查询表student,得到了name列值为'张三'的记录;之后Session B中提交了一个 隐式事务 ,该事务向表student中插入了一条新记录;之后Session A中的事务再根据相同的条件 studentno > 0查询表student,得到的结果集中包含Session B中的事务新插入的那条记录,这种现象也被称之为 幻读 。我们把新插入的那些记录称之为 幻影记录 。
SQL中的四种隔离级别
SQL标准 中设立了4个 隔离级别 :
READ UNCOMMITTED :读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。
READ COMMITTED :读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。
REPEATABLE READ :可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。
SERIALIZABLE :可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
隔离级别 |
脏读可能性 |
不可重复读可能性 |
幻读可能性 |
加锁读 |
READ UNCOMMITTED |
Y |
Y |
Y |
N |
READ COMMITTED |
N |
Y |
Y |
N |
REPEATABLE READ |
N |
N |
Y |
N |
SERIALIZABLE |
N |
N |
N |
Y |
脏写太严重了所有事务都解决了。读未提交没解决啥事一般不选。读已提交解决了脏读。可重复读解决了不可重复读问题。串行化解决了幻读问题。
查询当前默认隔离级别
SELECT @@transaction_isolation;
SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别'
#其中,隔离级别格式:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE
使用 GLOBAL 关键字(在全局范围影响)当前已经存在的会话无效,只对执行完该语句之后产生的会话起作用 。
使用 SESSION 关键字(在会话范围影响)对当前会话的所有后续的事务有效。如果在事务之间执行,则对后续的事务有效。该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务。
MySQL事务日志
事务的隔离性由 锁机制 实现。
而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。
REDO LOG 称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。
UNDO LOG 称为 回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
redo日志 (重做日志)
为什么需要REDO日志
一方面,缓冲池可以帮助我们消除CPU和磁盘之间的鸿沟,checkpoint机制可以保证数据的最终落盘,然而由于checkpoint 并不是每次变更的时候就触发 的,而是master线程隔一段时间去处理的。所以最坏的情况就是事务提交后,刚写完缓冲池,数据库宕机了,那么这段数据就是丢失的,无法恢复。
另一方面,事务包含 持久性 的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。
那么如何保证这个持久性呢? 一个简单的做法 :在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题
另一个解决的思路 :我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把 修改 了哪些东西 记录一下 就好。比如,某个事务将系统表空间中 第10号 页面中偏移量为 100 处的那个字节的值 1 改成 2 。我们只需要记录一下:将第0号表空间的10号页面的偏移量为100处的值更新为 2 。
好处、特点
1.好处
redo日志降低了刷盘频率
redo日志占用的空间非常小
2. 特点
redo日志是顺序写入磁盘的
事务执行过程中,redo log不断记录
redo的组成
重做日志的缓冲 (redo log buffer) ,保存在内存中,是易失的。 show variables like '%innodb_log_buffer_size%';
重做日志文件 (redo log file) ,保存在硬盘中,是持久的。
redo的整体流程
第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值。 事务一边执行,一边就在记录了
第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
第4步:定期将内存中修改的数据刷新到磁盘中
Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化。
redo log的刷盘策略
针对上面第三步。
redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后以 一定的频率 刷入到真正的redo log file 中。这里的一定频率怎么看待呢?这就是我们要说的刷盘策略。
注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存(page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit 参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:
设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)
设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 )
设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自己决定什么时候同步到磁盘文件。
0的情况下,有可能事务还没有提交,就被刷盘进磁盘了。也有可能丢失一秒钟的数据。
1:可靠,效率低
Undo日志 (回滚日志)
在事务中 更新数据 的 前置操作 其实是要先写入一个 undo log 。
理解Undo日志
事务需要保证 原子性 ,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:
情况一:事务执行过程中可能遇到各种错误,比如 服务器本身的错误 , 操作系统错误 ,甚至是突然 断电 导致的错误。
情况二:程序员可以在事务执行过程中手动输入 ROLLBACK 语句结束当前事务的执行。
以上情况出现,我们需要把数据改回原先的样子,这个过程称之为 回滚 ,这样就可以造成一个假象:这个事务看起来什么都没做,所以符合 原子性 要求。
当插入的时候记录主键方便回滚删除,修改记录旧值,删除记录整条记录方便回滚插入。
undo日志也有持久化的要求,也会记录redo日志。
Undo日志的作用
作用1:回滚数据
不是物理意义上的时间回溯到了事务开始之前,只是逻辑上数据回到开始之前。比如A给B一百块,回滚就是B把一百还给A,不能是时间回溯到没有发生这件事。
作用2:MVCC 多版本并发控制机制
当用户读取一行数据时,若该记录已经被其他事务占用,当前事务可以通过undo日志读取之前的行版本信息,以此实现非锁定读取。
锁
事务的隔离性由锁来实现。
对 并发操作进行控制 ,因此产生了 锁 。同时 锁机制 也为实现MySQL的各个隔离级别提供了保证。 锁冲突 也是影响数据库 并发访问性能 的一个重要因素。
事务并发访问
情况分类
读-读、读-写、写-写
读读没事,写写加锁串行,读写复杂。
当前事务是T1,没有处于等待状态。
小结几种说法:
不加锁
意思就是不需要在内存中生成对应的 锁结构 ,可以直接执行操作。
获取锁成功,或者加锁成功
意思就是在内存中生成了对应的 锁结构 ,而且锁结构的 is_waiting 属性为 false ,也就是事务可以继续执行操作。
获取锁失败,或者加锁失败,或者没有获取到锁
意思就是在内存中生成了对应的 锁结构 ,不过锁结构的 is_waiting 属性为 true ,也就是事务需要等待,不可以继续执行操作。
一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读 、 不可重复读 、 幻读 的问题。
各个数据库厂商对 SQL标准 的支持都可能不一样。比如MySQL在 REPEATABLE READ 隔离级别上就已经解决了 幻读 问题。
解决方案
怎么解决 脏读 、 不可重复读 、 幻读
方案一:读操作利用多版本并发控制( MVCC ,下章讲解),写操作进行 加锁 。
普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。
在 READ COMMITTED 隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了 事务不可以读取到未提交的事务所做的更改 ,也就是避免了脏读现象;
在 REPEATABLE READ 隔离级别下,一个事务在执行过程中只有 第一次执行SELECT操作 才会生成一个ReadView,之后的SELECT操作都 复用 这个ReadView,这样也就避免了不可重复读和幻读的问题。
方案二:读、写操作都采用 加锁 的方式。
小结对比发现:
采用 MVCC 方式的话, 读-写 操作彼此并不冲突, 性能更高 。
采用 加锁 方式的话, 读-写 操作彼此需要 排队执行 ,影响性能。
一般情况下我们当然愿意采用 MVCC 来解决 读-写 操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用 加锁 的方式执行。