MySQL事务

MySQL 事务机制主要用于处理操作量大、复杂度高的数据
MySQL 中只有使用了 Innodb 数据库引擎的数据表和数据库才支持事务
事务处理可以用来维护数据的完整性,保证多条 SQL 语句要么全部执行,要么全部不执行
事务用于管理 insert update delete 之类的 DML 语句, [select 语句 ] ,其它类型的 SQL 语句没有事
务的概念
概述事务
事务必须满足 ACID4 个条件: A 原子性、 C 一致性、 I 隔离性、 D 持久性
原子性:一个事务中的所有操作要么全部完成、要不一个都不做,不会结束在中间某个环节
一致性:事务执行结束后数据库的完整性没有破坏
隔离性:数据库允许多个并发事务对数据库中的数据进行读写操作,隔离性可以防止多个事务并发
执行时导致的数据不一致性。事务根据隔离等级可以分为 4 级:读未提交、读已提交、可重复读和
串行化
持久性:事务执行完成后对数据的修改就是永久的
事务与数据库底层数据
事务的进行过程中,在未结束之前, DML 语句并不会直接更改底层数据,只是将历史操作记录一下,在内存中完成记录。只有在事务结束时,而且应该是成功结束时,才会修改底层硬盘文件中的数据
        事务的原子性是通过undo log 来实现
        事务的持久性是通过redo log 来实现
        事务的隔离性是通过【读写锁+MVCC 多版本并发控制】来实现的
        事务的一致性是通过原子性、持久性和隔离性来实现的
事务控制语句
        begin或者 start transaction 可以显式的开启一个事务,结束事务有提交和回滚 2 种方式
        commit提交事务,并使已执行的对数据库的所有修改成为永久性修改
        rollback回滚结束事务,撤销已经执行的未提交的修改操作
        savepoint 标识名称 用于在事务过程种创建一个保存点,从而支持部分回滚。一个事务中可以添加多个保存点
        release savepoint 标识名 用于删除一个事务的保存点,如果对应名称的保存时不存在则抛出异常
        rollback to 标识名 将事务回滚到指定的保存点,执行名称的保存点到当前位置的所有操作撤销,但是保存点之前的操作仍旧保留,等待事务结束
        set transaction isolation level用于设置事务的隔离性, innodb 存储引擎提供的隔离性有读未提交read uncommitted、读已提交 read committed 、可重复读 repeatable read serializable串行化,系统默认隔离等级为可重复读
事务处理
begin 或者 start transaction 开启事务 rollback 事务回滚 commit 事务提交
还可以使用 set 改变 MySQL 的自动提交模式
set autocommit=0 禁止自动提交
set autocommit=1 开启自动提交
基本测试
navicat 或者命令行中开启两个窗口模拟两个并发修改数据库的进程
1 、创建数据库 create database test1 ;
2 、切换当前库 use test1 ;
3 、创建测试使用的表 create table t1(id int primary key,name varchar(32))
engine=innodb ;
4 、开启事务: begin ;
5 、插入操作: insert into t1 values(1,'zhangsan') ; ,在另外一个窗口中执行查询,则看不到插
入的数据,因为事务的默认隔离等级为可重复读
6 、提交事务 commit ; 修改生效,另外一个窗口中则能够查询到数据
7 、如果没有提交还可以使用 rollback 撤消修改操作
多点回滚
begin;
update t1 set name='modify1' where id=1;
select * from t1;
savepoint s1;
delete from t1;
rollback to s1;
select * from t1;
commit;

相关日志问题

事务的原子性是通过 undo log 来实现
事务的持久性是通过 redo log 来实现
redo log
如果每次读写数据都需要磁盘的 IO ,效率会很低。 innodb 提供了缓存 buffer pool 作为访问数据库的缓存,读取和修改操作都会涉及到缓存的操作,缓存会定期刷新到磁盘中,但是写入缓存的数据在系统宕机时会丢失,事务的持久性则无法保证。每次读写硬盘数据的IO 成本太高,为了解决这个问题,引入了redo log来提升更新数据的执行效率。
当事务提交时,先将 redo log buffer 写入 redo log 文件进行持久化,待事务 commit 操作完成时才算完成。这种作为成为预先日志持久化write-ahead log 。在持久化一个数据页之前,先将内存中相应的日志页持久化。当有一条数据需要更新时,innodb 会将记录写入到 redo log 中,并更新内存,这时更新就算完成。innodb 会在适当的时候,例如系统空闲时,才真正将操作记录更新到磁盘。如果在数据落盘之前系统宕机,数据库重启后,可以通过日志来保证数据的完整性
undo log
undo log 提供了两个作用:提供回滚和多版本控制 MVCC
在数据修改时不仅记录 redo ,还记录了相对应的 undo undo log 主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的所有操作。
undo 日志用于将数据库逻辑的恢复到原来的样子,所以实际上记录的时相反的工作。例如 insert 对应的是delete undo 日志用于事务的回滚操作,进而保证了事务的原子性
隔离级别
数据库重要的功能就是实现数据共享,对于同时运行的多个事务,当多个事务同时访问数据库中相同的数据时,如果没有采取必要的隔离机制,则会导致出现各种并发问题。
问题的本质就是共享数据的线程安全问题
常见问题
1 、第一类丢失更新: A 事务回滚时把已经提交的 B 事务更新的数据覆盖了。解决方案是锁机制
2 、脏读: A 事务读取到 B 事务更新但是还没有提供的数据,如果 B 回滚撤销,则 A 读取的数据就是临时而且无效的数据。
3 、不可重复读: A 事务读取到了一个字段值,但是 B 更新并提交了该字段的修改, A 再次读取同一个字段值,但是两次读取到的内容不一致
4 、幻读: A 事务从一个表中读取了多行数据,但是 B 事务插入或者删除了一些新的行,如果 A 再次读取,则发现数据会有多出来或者少掉的行
5 、第二类丢失更新: A 事务修改记录,同时 B 事务修改记录, B 提交数据后使用 B 的修改结果覆盖了事务A的修改结果
事务隔离性
数据库系统必须具有隔离并发各个事务的能力,使其相互不会影响,避免各种并发问题。一个事务和其它事务隔离的程度就成为隔离等级。数据库中规定了多种事务隔离级别。不同的隔离级别对应不同的干扰程度,隔离级别越到,数据的一致性就越号,但是并发性越差
MySQL 数据库支持 4 种隔离级别,默认可重复读
隔离级别的范围
隔离级别的作用范围可以分为全局级和会话级两种。全局级对所有的会话有效,会话级只对当前会话有效
设置全局隔离等级 set global transaction isolation level read committed ;
设置会话级隔离等级 set session transaction isolation level read uncommitted ;
总结隔离等级
MySQL事务_第1张图片

 

在具体应用开发中,一般建议使用数据库管理系统默认的隔离等级,同时在编程中引入乐观锁
样例:两个事务同时操作 tb_users 表中的 age
读未提交
MySQL 数据库中事务的隔离实际上是依靠锁机制来实现的,但是加锁会带来性能的损失。读未提交隔离等级是不加锁的,所以性能最好,但是由于基本没有什么限制,所以脏读问题都无法解决。
读已提交
解决脏读问题的方法就是只允许读取别的事务已经提交的数据,其它事务未提交数据当前事务不能读取。例如oracle 默认的事务隔离级别就是读已提交。由于只能读取已经提交的数据,所以可能出现两次读取的数据不一致
可重复读
针对不可重复读的问题提出了可重复读的隔离等级,针对查询采用了 MVCC 多版本并发控制引入快照机制,每个事务都有自己的数据快照,即使其它事务提交数据,也不影响当前事务相关行的数据快照。幻读仍旧会出现
为了解决不可重复读的问题, MySQL 采用了 MVCC 多版本并发控制的方式。数据库中的一行记录实际上有多个版本,每个版本除了有数据之外,还有一个标识版本的字段row trx_id ,这个字段就是产生的对应事务的id ,在事务开始的时候向事务系统申请,按照时间先后顺序递增
MySQL事务_第2张图片

 一行记录现在有3个不同的版本,每个版本中都记录了使其产生的事务id,每个数据存储的备份就是快照 【一致性视图】,可重读读就是在事务开始的时候生成一个当前事务的全局性的快照,但是读提交则是每次执行语句时都会重新生成一次快照。

读取快照数据的规则:
版本未提交不能读取
版本已经提交,但是却是在快照创建后提交的,不能读取
版本已经提交,且是在快照创建前提交的,则可以读取
可重复读和读已提交两者主要的区别在于快照的创建上,可重复读仅仅在事务开始时创建一次,而读已提交每次执行sql 语句时都要创建一次
串行化
隔离等级最高,隔离效果最好,可以解决脏读、不可重复度和幻读问题,当时并发性最差。将事务的并发执行转换为顺序执行,后一个事务的执行必须等待前一个事务结束
并发写问题
事务 A 执行 update 操作, update 时要对所修改的数据行进行加锁,这个行所在事务提交后才能释放,而在事务A 提交之前,如果事务 B 也希望修改这行数据,必须先进行行锁的申请,但是由于 A 已经占用了行锁,所以B 申请不到,此时 B 一直会处于等待状态,直到 A 提交释放锁后, B 才能执行
update tb_users set age= 10 where id= 1 ;

id就是主键PK,是有索引的,那么MySQL在索引树种查找到这行数据,然后加上行锁 

update tb_users set age= 20 where age= 10 ;
假设表种并没有针对 age 设置索引,所以 MySQL 无法直接定位这行数据。 MySQL 会给这个表种的所有行加锁,但是添加行锁后,MySQL 会再次执行一次过滤,发现不满足条件的行就释放锁,最后只留下符合条件的行。但是一次锁定一次解锁的过程对性能影响比较大,如果是大表的化,还是建议合理设计索引
幻读问题
解决并发问题的方案就是行锁,解决幻读也是依赖于锁机制实现,使用间隙锁。 MySQL 把行锁和间隙锁合并在一起,就可以解决并发写和缓读问题,这个锁叫做next-key
例如: select * from tb_student 可以获取 age=10 age=30 的数据,针对索引数据库会创建维护一
B+ 树,树可以用来快速定位行记录
针对具体的行数据,例如 age=10 age=30 的数据,添加一个行锁,根据 age=10 age=30 可以将整个区间划分为3 部分,(负无穷大 ,10] (10,30) [30, 正无穷大 ) 三个部分,在这 3 个区间上可以添加间隙锁
MySQL事务_第3张图片

 

在事务 A 提交之前,事务 B 的插入操作只能等待,这实际上就是间隙锁生效。
如果表中有索引,实际上直接可以使用行锁,如果不是索引列,那么数据库会为整个表加上间隙
锁。
MySQL innodb 引擎才能支持事务,其中可重复读是 MySQL 默认的隔离级别
读未提交和串行化基本上不需要考虑隔离级别,因为读未提交不加锁限制;串行化相当于单线程执
行,效率太差
读已提交解决了脏读问题,行锁解决了并发更新问题,并且 MySQL 在可重复读时引入行锁 + 间隙锁
的组合可以实现可重复读

 

你可能感兴趣的:(mysql,数据库,sql)