一、什么是事务
数据库中的概念,按我个人理解:能够保证一组任务全部执行成功或者全部执行失败的这么个机制,叫事务
事务是数据库中重要概念,如果没有这种保障机制,数据库中的数据就是不安全的(就是无法保证数据的正确性)
在数据库中,一组任务,就是放在一起执行的多条sql
二、ACID保证数据安全
所以如何才能保证数据安全呢?前人总结了如下四点
1.原子性,一组任务全部执行成功或者全部执行失败.这是基础,因为我们一组操作一般是有顺序的,有互相依赖关系的,要同步的,不能A表修改了,B表修改失败,这样数据就不同步了,也就是不正确了
2.一致性,就是我们的一组操作应该合辙(合乎逻辑,合乎道理),常见例子就是互相转账的例子:A转给C 10块,B转给C 20块 ,C最后手里一定要多 30块,这样就是保证了一致性,这样数据才正确
3.隔离性,隔离性在并发事务中才能体现作用,每个单独的事务要保证数据正确性,那么同时并发的多个单独的事务最后的结果,也应该保证最后数据正确
4.持久性,就是我们所做的操作必须被持久的保存下来,不能说事务正确的结束了,但是数据库没有被修改,这时数据也是不正确的.
原子性,隔离性),最后结果是正确的(一致性),而且结果被正确的保留下来(持久性)
我们给这些特性总结了一个名字,叫事务,所以很多不理解事务的童鞋,可以不要去理解事务是什么,而应该反过来理解什么是事务(长成这样的就是事务)
要保证事务,需要理解的知识点如下:
两do一点:redo日志,undo日志,checkpoint
MVCC(ReadView)
事务隔离级别
savepoint
锁
MDL锁
这里我只是给自己留的随笔,所以不会详细讲解,但是会留入口
三、事务流程
大致分这么三条线
redo日志进入硬盘,即可保证持久性)
undo日志回滚数据)
只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效(redo日志的作用),对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态(undo日志的作用)。
四、事务的开启与提交与回滚操作
前提: 数据库默认事务自动提交,所以默认每一条sql的执行都是一个单独的事务
涉及到的系统参数为autocommit ,ON和OFF
查看 show variables like 'autocommit'
修改 SET autocommit = OFF; -- 关闭自动提交事务
一般的,如果需要向数据库导入大量数据,由于sql执行默认单条sql为一次事务,如果有10条sql,就会分别开启事务提交事务10次,所以可以先关闭事务自动提交,自己手动开一次,执行sql,提交事务
1.开启事务:两种命令
BEGIN [WORK] (BEGIN 等价于 BEGIN WORK , 所以work可以写可以不写)
START TRANSACTION [READ ONLY | READ WRITE | WITH CONSISTENT SNAPSHOT];
两种语法区别:第二种可以指定事务是只读模式(事务中不能有写操作的sql),读写模式(事务中可读可写,默认的)和一致性读(一致性读,又称为快照读。使用的是MVCC机制读取undo中的已经提交的数据。所以它的读取是非阻塞的)
一致性读参看
如果我们不显式指定事务的访问模式,那么该事务的访问模式就是读写模式
2.提交事务:一种命令
COMMIT [WORK] (COMMIT 等价于 COMMIT WORK , 所以work可以写可以不写)
3.回滚事务:一种命令
ROLLBACK [WORK] (ROLLBACK 等价于 ROLLBACK WORK , 所以work可以写可以不写)
4.回滚到savepoint
SAVEPOINT
ROLLBACK TO
上面说的是手动提交事务,mysql中还有一些自动会提交事务的命令
手动提交事务使用commit,但是有些语句会自动提交当前事务,称之为隐式提交
1.头一个就是开启自动提交事务,当前sql的事务会隐式提交
2.DDL会隐式提交事务
- 如果A事务对T表有DML事务,未提交之前A事务执行了对T表表结构的修改,会自动提交A事务,然后执行DDL.
- 如果A事务对T表有DML事务,B事务修改非A表表结构,会将B事务自动提交,但不会影响到A事务,A事务依然没有提交,程序正常运行,新起一个C事务依然读不到A事务未提交的数据
- 如果A事务对T表有DML事务,B事务修改T表表结构,由于修改同一张表,按道理是要有锁保护的,否则A事务改字段B事务改表结构不就冲突了么,但mysql5.1之前确实是没有所以B事务可以直接执行DDL操作,并且导致A事务出错,但是在mysql5.5之后引入了一个MDL锁,这样就会让事务按顺序执行,我的mysql版本为8.0,所以我看到的效果是B事务的DDL一直等待(因为先开始A事务,而且A事务并未提交).直到A事务提交后,B事务的DDL才会执行.这个操作基于MDL锁
结论:DDL只会隐式提交自己当前的事务,不会操作其他事务,如果冲突,则等待,基于MDL锁(如果影响到了其他事务(其它事务中有当前DDL的表),会等待直到其他事务提交后才能执行DDL操作.)
最后一句话:不要在程序运行过程中随便DDL,因为如果DDL会锁全表,如果表中数据量大,就会卡住别的事务,影响生产环境
但如果在生产环境中修改表结构怎么办
我自己没尝试过,之后再深研究
https://zhang.ge/5134.html (版本较新) 在线ddl工具(直接甩锅运维了[手动偷笑])(这个博客个人感觉很详细,所以自己留了一份截图,以防之后404
3.BEGIN命令
开始事务的操作会隐式提交当前所在事务,然后另开一个事务
4.使用LOCK TABLES、UNLOCK TABLES等关于锁定的语句也会隐式的提交前边语句所属的事务
5.比如我们使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务。
6.关于MySQL复制的一些语句
使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等语句时也会隐式的提交前边语句所属的事务。
使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、 LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。
五、redo日志
为了将数据持久的保存下来,同时没保存之前还不能更改数据库,所以我们需要将我们所做的所有操作笔记一下,之后需要还原数据(这里的还原不等于回滚,应该是还原到他原本的样子,即操作完的样子),我们只需要按照笔记一步一步操作就行了.这个笔记就是redo日志(重做日志)
redo日志:重做日志,事务中包括多条sql,一条sql可能会对数据库的基础结构(如:B+树,数据页,索引,隐藏列等)产生操作,我们称之为MTR(mini transaction),每个MTR都会记录成一条至多条redo日志,mtr相当于底层最小的一次原子性操作,所以我们redo日志一般是多条才能表示一次原子操作
持久性)
redo日志会按顺序纪录当前事务操作,保证数据持久化,如果一个事务中只有一条操作,就只记录一条redo日志,如果一个事务有多条操作,就会纪录多条日志,我们上面说了,可能多条日志才是一个原子性操作,但是程序怎么知道日志是从哪到哪算是一组呢?mysql中每条redo日志占用8个字节,其中7个字节用来存储日志内容,最左边的第八个位置用来做标记,如果是单条日志则为1,如果是多条日志则为0,这样扫描到1说明当前为一个原子操作,数据恢复即可,如果是多条,会在所有操作记录完之后再加一个类型为MLOG_MULTI_REC_END类型的redo日志,扫描到这个日志,就知道当前操作完成,才会恢复之前一组redo日志,否则直接丢弃,保证未提交数据不被恢复.
mysql的持久化依赖于redo日志,redo日志是否被记录到硬盘中是持久化实现的关键,我们知道刷盘操作很慢,所以我们的redo日志一开始会记录在一个叫log buffer的缓冲区中,那么什么时候会将缓冲中的日志刷到磁盘呢?
1.当内存不足
2.事务提交时会刷到硬盘中(这样就能保证持久化,mysql中提供了设置,也可以设置事务提交时不持久化)
后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘保证数据持久化
这个redo日志会被存放在mysql的data下的ib_logfile[数字]文件下
六、undo日志
但是我们有时候可能需要回滚操作,比如事务中间出错,或者手动rollback,这个时候我们不能用redo日志来操作,否则我们一条一条redo要执行到哪年才算完啊,于是有了undo日志,这个日志用来备份,比如你把张三改成李四,undo日志会先将张三保存下来,然后再改成李四,这样你想要回滚的时候,我一下就知道李四之前是张三,因为我做了备份,直接还原即可,(可以说备份,也可以理解成当前数据快照,或者说会记录一条如果你要回滚,需要做什么样的操作)
你插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。
你删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
你修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。
这样事务一旦出问题,之间按照undo回滚即可
一般的,所有写库操作都会放在事务中执行,每个事务又有自己的一个id,undo日志会按顺序记录数据内容和事务id,方便日后回滚,因为不能因为A事务回滚而把B事务已提交的操作撤销,互相不能有影响(隔离性)
七、事务隔离性
多个事务在并发时,可能产生如下问题
1.脏写(丢失更新)
所谓脏写,就是A事务修改了一条数据还未提交,B事务也去修改这个数据,然后A事务提交了,发现不是自己想要的结果,自己改的数据没了,这就是丢失更新.
这个问题不需要考虑,因为不管数据库哪个隔离级别,当两个事务A和B尝试去更新同一条数据时,假定A先更新数据,会对更新的数据行记录加上排他锁(也叫写锁,悲观锁),除非事务A提交或终止从而释放排他锁,否则事务B都是无法更新数据的。加锁排队修改.避免脏写
2.脏读
所谓脏读,就是指事务A读到了事务B还没有提交的数据,比如数据库中有个数据100,事务A开启事务,此时切换到事务B,事务B开启事务将100-1,还未提交,此时切换回事务A,事务A读取到了99,因为事务B还未提交,就让A读到了,我们说A事务发生了脏读.为什么这是个问题呢,因为事务是可以回滚的,万一B回滚了事务,那么A读到的99其实是不存在的,就可能会出现问题
3.不可重复读(虚读)
所谓不可重复读,就是指在一个事务里面读取了两次某个数据,读出来的数据不一致。以银行取钱为例,事务A开启事务-->查出银行卡余额为1000元,此时切换到事务B事务B开启事务-->事务B取走100元-->提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。
4.幻读
所谓幻读,就是指在一个事务两次查询的操作中发现了多了或者少了数据。比如学生信息,事务A查询所有大于20岁的学生信息,此时切换到事务B,事务B开启事务-->事务B插入了一条学生数据,年龄25,此时切换回事务A,事务A再次查询的时候发现多了一条数据,这就是幻读,幻读出现的前提是并发的事务中有事务发生了插入、删除操作。
正是因为多线程的原因,当某一个正在写数据时,另一个线程读取的数据是不准确的,所以需要进行同步处理.
mysql利用锁和MVCC(多版本并发控制)来解决上面所说的问题,为了方便设置,数据库规定了如下四个隔离级别,不同的隔离级别解决不同的问题
这个图是随便找了一张,链接 https://www.erlo.vip/share/2/26041.html
一般生产中不太在意幻读的问题,所以生产环境一般设置隔离级别为不可重复读(RC),oracle默认就是RC,mysql默认级别为RR
那么mysql如何解决这些问题的呢,方案如下
八、MVCC的基础
MVCC 多版本并发控制.
不解决任何问题隔离级别就是RU,不解决问题所以不考虑,之后RC和RR都是对于读的并发优化,解决并发读的问题主要依靠MVCC机制和锁
其中因为锁机制是一种预防性的,读会阻塞写,写也会阻塞读,当锁定粒度较大,时间较长是并发性能就不会太好;而MVCC是一种后验性的,读不阻塞写,写也不阻塞读,等到提交的时候才检验是否有冲突,由于没有锁,所以读写不会相互阻塞,从而大大提升了并发性能。
所以MVCC性能更加优秀,MySQL在 read committed ,Repeatable Read 两个级别下都会使用到MVCC, 并且只在这两个级别下使用。
MVCC利用readview和undo日志来实现
undo
这个链表头就是当前的数据,不是说只记录历史,还包括当前数据),你的事务只能读到之前版本的数据,未提交的数据就读不到了,这样就保证了重复读
readview
对于使用RU隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;对于使用SERIALIZABLE隔离级别的事务来说,设计InnoDB的大叔规定使用加锁的方式来访问记录,所以也跟MVCC没关系;对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。为此提出了一个ReadView的概念.
readview其实就类似与一个查询快照
1.如果使用READ COMMITTED隔离级别的事务,在每次查询开始时都会生成一个独立的ReadView。假如A事务开启->A事务查询(此时有一个快照) , B事务开启->B事务修改内容(记录在了undo日志中) ->B事务提交, A事务继续第二次查询(由于又生出一个readview,所以这个readview长得和刚才那个不一样,是很正常的,这个readview能读到最新的数据,是因为B事务已经提交,A是可以直接读取undo日志上的新数据的)
2.如果使用REPEATABLE READ ,会在第一次读取数据时生成一个ReadView,之后每次查询都复用这个快照.假如A事务开启->A事务查询(此时有一个快照) , B事务开启->B事务修改内容(记录在了undo日志中) ->B事务提交, A事务继续第二次查询(查询刚才那个快照)
从上边的描述中我们可以看出来,所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
这样就保证了不可重复读的问题
小结
MVCC读不影响写,写不影响读,实现高效率的可重复读(参考链接 https://www.imooc.com/article/17289)
1.读不影响写:事务以排他锁的形式修改原始数据,读时不加锁,因为 MySQL 在事务隔离级别Read committed 、Repeatable Read下,InnoDB 存储引擎采用非锁定性一致读--即读取不占用和等待表上的锁。即采用的是MVCC中一致性非锁定读模式。因读时不加锁,所以不会阻塞其他事物在相同记录上加 X锁来更改这行记录。
2.写不影响读:事务以排他锁的形式修改原始数据,当读取的行正在执行 delete 或者 update 操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB 存储引擎会去读取行的一个快照数据(readview)。
purge工作线程
题外话,正式由于有MVCC这种机制,所以删除操作执行的时候(还未提交事务),是不会删除掉数据的,而是打一个删除标记,这样MVCC才有机会读取到之前的readview,那什么时候真正删除这个数据呢?就需要用到purge工作线程了.类似于一个垃圾回收器.
网上很多帖子说防止幻读是使用了锁和MVCC,但是还有一部分人说MVCC不能防止幻读,这里给出一个订阅号文章,可以自己去看,关键点我复制到我这了
MVCC不能禁止幻读的原理
T1第一次执行普通的SELECT语句时生成了一个ReadView,之后T2向hero表中新插入了一条记录便提交了,ReadView并不能阻止T1执行UPDATE或者DELETE语句来对改动这个新插入的记录(因为T2已经提交,改动该记录并不会造成阻塞),但是这样一来这条新记录的trx_id隐藏列就变成了T1的事务id,之后T1中再使用普通的SELECT语句去查询这条记录时就可以看到这条记录了,也就把这条记录返回给客户端了。因为这个特殊现象的存在,你也可以认为InnoDB中的MVCC并不能完完全全的禁止幻读。
九、锁
十、写在后面
参考文章 : 掘金小册中的<<MySql是怎样运行的:从根上理解MySql> >
作者 文章写的还是很详实的,通过阅读他的这本小册,写了上边的这篇随笔,这本小册读完让我对mysql中的一些原理又略知了三四,但是本人对于小册中很多细节还未能吸收,所以上面的文字描述并不是类似于教学一样教读者怎么做,权当自己的一篇随笔,记录一些关键知识点,简单串联了一下,如果有缘能看到这篇文章,建议可以去我提供的链接或者自己搜索其中知识点,已获得更加清晰的了解.
这篇文章能起到抛砖引玉的作用,足矣