数据库中的概念,按我我的理解:可以保证一组任务所有执行成功或者所有执行失败的这么个机制,叫事务html
事务是数据库中重要概念,若是没有这种保障机制,数据库中的数据就是不安全的(就是没法保证数据的正确性)mysql
在数据库中,一组任务,就是放在一块儿执行的多条sqlsql
因此如何才能保证数据安全呢?前人总结了以下四点数据库
1.原子性,一组任务所有执行成功或者所有执行失败.这是基础,由于咱们一组操做通常是有顺序的,有互相依赖关系的,要同步的,不能A表修改了,B表修改失败,这样数据就不一样步了,也就是不正确了安全
2.一致性,就是咱们的一组操做应该合辙(合乎逻辑,合乎道理),常见例子就是互相转帐的例子:A转给C 10块,B转给C 20块 ,C最后手里必定要多 30块,这样就是保证了一致性,这样数据才正确多线程
3.隔离性,隔离性在并发事务中才能体现做用,每一个单独的事务要保证数据正确性,那么同时并发的多个单独的事务最后的结果,也应该保证最后数据正确并发
4.持久性,就是咱们所作的操做必须被持久的保存下来,不能说事务正确的结束了,可是数据库没有被修改,这时数据也是不正确的.oracle
因此保证数据正确性,就要保证数据开始是正确的,过程当中每一步操做都是正确的(原子性,隔离性),最后结果是正确的(一致性),并且结果被正确的保留下来(持久性)运维
咱们给这些特性总结了一个名字,叫事务,因此不少不理解事务的童鞋,能够不要去理解事务是什么,而应该反过来理解什么是事务(长成这样的就是事务)工具
要保证事务,须要理解的知识点以下:
两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中的已经提交的数据。因此它的读取是非阻塞的)
一致性读参看 https://www.cnblogs.com/digdeep/p/4947694.html
若是咱们不显式指定事务的访问模式,那么该事务的访问模式就是读写模式
2.提交事务:一种命令
COMMIT [WORK] (COMMIT 等价于 COMMIT WORK , 因此work能够写能够不写)
3.回滚事务:一种命令
ROLLBACK [WORK] (ROLLBACK 等价于 ROLLBACK WORK , 因此work能够写能够不写)
4.回滚到savepoint
SAVEPOINT [自定义savepoint的name]; -- 设置保存点
ROLLBACK TO [自定义savepoint的name] -- 回滚到以前的某个保存点(只能在当前状态向前回滚,不能向后回滚)
手动提交事务使用commit,可是有些语句会自动提交当前事务,称之为隐式提交
1.头一个就是开启自动提交事务,当前sql的事务会隐式提交
2.DDL会隐式提交事务
最后一句话:不要在程序运行过程当中随便DDL,由于若是DDL会锁全表,若是表中数据量大,就会卡住别的事务,影响生产环境
但若是在生产环境中修改表结构怎么办
这里留两个博客,我本身没尝试过,以后再深研究
http://www.cnblogs.com/wangtao_20/p/3504395.html(灵感来源)
https://zhang.ge/5134.html (版本较新) 在线ddl工具(直接甩锅运维了[手动偷笑])(这个博客我的感受很详细,因此本身留了一份截图,以防以后404
(传送门: https://www.cnblogs.com/fast-bullet/p/10848561.html)
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日志:重作日志,事务中包括多条sql,一条sql可能会对数据库的基础结构(如:B+树,数据页,索引,隐藏列等)产生操做,咱们称之为MTR(mini transaction),每一个MTR都会记录成一条至多条redo日志,mtr至关于底层最小的一次原子性操做,因此咱们redo日志通常是多条才能表示一次原子操做
这样的话,多条redo日志组成一个MTR,多个MTR组成一条sql,多条sql组成一次事务,只要redo日志存在,就能将当前事务永久的保存下来(持久性)
redo日志会按顺序纪录当前事务操做,保证数据持久化,若是一个事务中只有一条操做,就只记录一条redo日志,若是一个事务有多条操做,就会纪录多条日志,咱们上面说了,可能多条日志才是一个原子性操做,可是程序怎么知道日志是从哪到哪算是一组呢?mysql中每条redo日志占用8个字节,其中7个字节用来存储日志内容,最左边的第八个位置用来作标记,若是是单条日志则为1,若是是多条日志则为0,这样扫描到1说明当前为一个原子操做,数据恢复便可,若是是多条,会在全部操做记录完以后再加一个类型为MLOG_MULTI_REC_END类型的redo日志,扫描到这个日志,就知道当前操做完成,才会恢复以前一组redo日志,不然直接丢弃,保证未提交数据不被恢复.
如今咱们了解了redo日志的做用,mysql的持久化依赖于redo日志,redo日志是否被记录到硬盘中是持久化实现的关键,咱们知道刷盘操做很慢,因此咱们的redo日志一开始会记录在一个叫log buffer的缓冲区中,那么何时会将缓冲中的日志刷到磁盘呢?
1.当内存不足
2.事务提交时会刷到硬盘中(这样就能保证持久化,mysql中提供了设置,也能够设置事务提交时不持久化)
3.mysql后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘保证数据持久化
这个redo日志会被存放在mysql的data下的ib_logfile[数字]文件下
可是咱们有时候可能须要回滚操做,好比事务中间出错,或者手动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 多版本并发控制.
不解决任何问题隔离级别就是RU,不解决问题因此不考虑,以后RC和RR都是对于读的并发优化,解决并发读的问题主要依靠MVCC机制和锁
其中由于锁机制是一种预防性的,读会阻塞写,写也会阻塞读,当锁定粒度较大,时间较长是并发性能就不会太好;而MVCC是一种后验性的,读不阻塞写,写也不阻塞读,等到提交的时候才检验是否有冲突,因为没有锁,因此读写不会相互阻塞,从而大大提高了并发性能。
因此MVCC性能更加优秀,MySQL在 read committed ,Repeatable Read 两个级别下都会使用到MVCC, 而且只在这两个级别下使用。
MVCC利用readview和undo日志来实现
这个MVCC和undo日志有什么关系呢,为啥要用undo实现?由于undo日志为了方便回滚,会按顺序记录数据内容和事务id,这样我以前的状态其实都在undo中,A事务操做数据会给undo加一条日志,B事务操做也会给undo加一条日志,对于数据库自己来讲,如今是什么状态数据库本身是明明白白的,可是因为A事务没有提交,因此B事务在读数据的时候,应该读取A事务提交以前的样式,好比A将张三改为李四,这时候B读数据的时候,应该读到张三,由于李四还未持久化,那么这个张三在哪记录着呢,就在undo日志上,因此想要读事务以前的数据,就得靠undo日志.对同一条数据所作的屡次操做会产生多条undo日志,而这些undo日志串起来的链条咱们成为版本链(这个链表头就是当前的数据,不是说只记录历史,还包括当前数据),你的事务只能读到以前版本的数据,未提交的数据就读不到了,这样就保证了重复读
对于使用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)。
题外话,正式因为有MVCC这种机制,因此删除操做执行的时候(还未提交事务),是不会删除掉数据的,而是打一个删除标记,这样MVCC才有机会读取到以前的readview,那何时真正删除这个数据呢?就须要用到purge工做线程了.相似于一个垃圾回收器.
网上不少帖子说防止幻读是使用了锁和MVCC,可是还有一部分人说MVCC不能防止幻读,这里给出一个订阅号文章,能够本身去看,关键点我复制到我这了
https://mp.weixin.qq.com/s/wSlNZcQkax-2KZCNEHOYLA
MVCC不能禁止幻读的原理
T1第一次执行普通的SELECT语句时生成了一个ReadView,以后T2向hero表中新插入了一条记录便提交了,ReadView并不能阻止T1执行UPDATE或者DELETE语句来对改动这个新插入的记录(由于T2已经提交,改动该记录并不会形成阻塞),可是这样一来这条新记录的trx_id隐藏列就变成了T1的事务id,以后T1中再使用普通的SELECT语句去查询这条记录时就能够看到这条记录了,也就把这条记录返回给客户端了。由于这个特殊现象的存在,你也能够认为InnoDB中的MVCC并不能完彻底全的禁止幻读。
参考文章 : 掘金小册中的<<MySql是怎样运行的:从根上理解MySql> >
做者 文章写的仍是很详实的,经过阅读他的这本小册,写了上边的这篇随笔,这本小册读完让我对mysql中的一些原理又略知了三四,可是本人对于小册中不少细节还未能吸取,因此上面的文字描述并非相似于教学同样教读者怎么作,权当本身的一篇随笔,记录一些关键知识点,简单串联了一下,若是有缘能看到这篇文章,建议能够去我提供的连接或者本身搜索其中知识点,已得到更加清晰的了解.
这篇文章能起到抛砖引玉的做用,足矣