Mysql事务原理与优化最佳实践

学习来源-图灵课堂

https://vip.tulingxueyuan.cn

说到MySQL的innodb的一大特性,就不得不说到事务。今天就学习事务。

事务

事务的定义:逻辑上的一组操作,要么一起成功,要么一起失败,中间绝对不会存在别的状态。逻辑上的一组操作,就是说这些操作都是有逻辑关系的。

我们通常说的事务是针对修改操作(新增,编辑,删除),但是查询操作也是可以加事务的。

事务有四大特性:ACID,就是所谓的原子性,一致性,隔离性,持久性。其中一致性是其它三个特性都满足之后才能实现的。

  1. 原子性(Atomicity) :当前事务的操作要么同时成功,要么同时失败,从效果上看一体的。原子性由undo log日志来实现。针对修改操作,undo log中会写入和其完全相反的一句SQL,便于rollback;rollback和commit是保证原子性的。
  2. 一致性(Consistent) :使用事务的最终目的,由其它3个特性以及业务代码正确逻辑来实现。
  3. 隔离性(Isolation) :在事务并发执行时,他们内部的操作不能互相干扰。隔离性由MySQL的各种锁以及MVCC机制来实现。
  4. 持久性(Durable) :一旦提交了事务,它对数据库的改变就应该是永久性的。持久性由redo log日志来实现。

并发事务带来的问题

  • 更新丢失(Lost Update)或脏写:当两个或多个事务选择同一行数据修改,有可能发生更新丢失问题,即最后的更新覆盖了由其他事务所做的更新。
  • 脏读(Dirty Reads):事务A读取到了事务B已经修改但尚未提交的数据,回滚之后数据无效。
  • 不可重读(Non-Repeatable Reads):事务A内部的相同查询语句在不同时刻读出的结果不一致。主要是针对查询单条数据,别的事务可能对这行数据进行了修改。
  • 幻读(Phantom Reads)或者是幻读:事务A读取到了事务B提交的新增数据。主要是读期间别的事务执行了插入操作。每次读取都能得到新的数据。

事务隔离级别

事务隔离级别是数据库本身提供的功能。都有其默认值,但是也可以进行修改的。可以根据业务需求去决定是否要修改默认的隔离级别。
隔离级别

脏读(Dirty Read)

不可重复读

幻读(Phantom Read)
读未提交(Read
uncommitted)
可能   可能  可能
读已提交(Read
committed)
不可能 可能 可能
可重复读
(Repeatable read) 
不可能 不可能 可能
可串行化
(Serializable) 
不可能 不可能 不可能
 
 
 
 
 
 
 
 
事务隔离级别越高,其安全性一致性就越好,但是相应的其执行效率就越低。
不同的业务场景针对事务隔离级别有要求,可以针对的进行设置。
MySQL默认的隔离级别是可重复读;Oracle默认的隔离级别是读已提交。
用Spring开发程序时,如果不设置隔离级别默认用数据库设置的隔离级别,如果Spring设置了就用已经设置的隔离级别。
 

事务隔离级别的具体实现

  • 读未提交,没有进行任何额外处理,其数据库执行效率是最高的,但是其数据库安全性是最低的, 要在业务侧保证事务的执行。
  • 读已提交,此时是读的当前,当前读就是时刻读取当前的最新已经提交的值。
  • 可重复读,引入了MVCC和锁机制去保证。其中MVCC就用到了快照读,就是读的历史版本;锁机制,提高间隙锁等。
  • 可串行化,是严格的加锁机制,读写互斥,写写互斥,读读不互斥。是类似Java的悲观锁。效率极其低下。

MySQL的读写锁

读锁(共享锁、S锁):select ... lock in share mode; 读锁是读读共享的,多个事务可以同时读取同一个资源,但不允许其他事务修改。

写锁(排它锁、X锁):select ... for update; 写锁是排他的,会阻塞其他的写锁和读锁,update、delete、insert都会加写锁。

一般情况下读是不会加锁的,可以通过上面两个SQL在查询时加锁。

串行化会在每次select之后加读锁。

读写可并行执行,那么就是读历史版本数据,写是最新的数据;否则无法谈读写并发。

MVCC(Multi-Version Concurrency Control)多版本并发控制

就可以做到读写不阻塞,且避免了类似脏读这样的问题,主要通过undo日志链来实现

select操作是快照读(历史版本)

insert、update和delete是当前读(当前版本)

read commit (读已提交) ,语句级快照

repeatable read (可重复读),事务级快照。

RR,就是每次事务开始时,针对全库都生成一个快照,只读这个快照及之前的版本;

RC是和commit标记相关的,会找到要查询数据的undo log版本链最后一个commit标记,并且读取这条记录的值为最新值。

 

表结构,每行数据的结构

除了我们手动设置的字段之外,还有两个隐藏字段,一个是trx_id,事务id;一个是roll_pointer,事务回滚指针。这两个字段是MySQL后台维护的。

回滚指针指向undo log的记录。根据回滚指针指向回滚语句。

undo log日志版本链,就是针对同一行数据来说的,首次修改写一条日志,然后后续的修改的回滚指针都指向上次修改的日志,就像一条链子一样。正在修改并且没有提交的日志也会加入到日志版本链中。

因为数据库真正的数据只会维护一份,否则基于空间考虑和一致性考虑都不行。

真实的数据只是会维护一份;不同线程的查询是通过整个版本链来查的。所有的查询修改共同构成这条数据的版本链。

每次数据的修改,都会有自己的一套版本链。查询的时候,就是通过可见性算法去定位到自己要查询的版本。

并不是多个快照,多个日志,多条真实数据,真实的数据一直就是一份;这样的话就太占用空间了。

查询操作方法需要使用事务吗?

正常来说查询是不需要使用事务的。但是这个也要看具体的事务隔离级别。

如果是读已提交,最好不用加事务,因为事务提交之后,别的事务就能立刻读取到最新的值;如果使用了事务,那么就无法获取到最新值,与设置的隔离级别相悖,所以对读已提交的隔离级别,最好不加事务。读已提交,如果两个或者多个查询之间的间隔时间较长,可能上一个查询获取的值已经是被别的事务修改了,已经是旧值了,如果拿这个旧址去指向业务逻辑操作,并不符合事务隔离级别,也不符合业务场景。

如果是可重复读,此时是可以加上事务的。因为这个可以保证是同一个时间维度的业务场景。针对这个时间点对整个库进行快照,保证此时数据都是能读取到的最新的,保证可重复读的隔离级别。此时可能不是最新的数据,但是是同一个时间点的数据。如果要最新的值,那么再去查询一次就可以了。

看具体的业务场景;如果是要高并发场景,那就是读已提交;如果是要保证数据的一致性,那么可以用可重复读。

事务的持久性

MySQL引入了redo log,Buffer Pool内存写完了,然后会写一份redo log,这份redo log记载着这次在某个页上做了什么修改。

即便MySQL在中途挂了,我们还可以根据redo log来对数据进行恢复。

redo log 是顺序写的,写入速度很快。并且它记录的是物理修改(xxxx页做了xxx修改),文件的体积很小,恢复速度也很快。

redo log是记录的物理修改,在磁盘哪里修改了什么数据,是二进制类型;binlog是逻辑记录,主要是记录SQL语句。
为什么不直接将修改的值直接刷盘,而是经过这几个日志文件?是为了提高效率。通过这些日志文件,可以保证最终刷盘的时候是顺序写,写到buffer 的时候就是有序追加写入,appened,顺序写。机械盘就是通过顺序写提升性能。

大事务的影响

  • 并发情况下,数据库连接池容易被撑爆:数据库的连接是有限并且珍贵的,如果被大事务占据太多,别的的请求无法获取到空闲的连接,会一直请求连接池,让连接池压力升高。
  • 锁定太多的数据,造成大量的阻塞和锁超时:在事务没有完成之前,锁定的数据别的事务都无法获取,可能会阻塞别的请求。
  • 执行时间长,容易造成主从延迟:集群数据同步会很慢,造成主库和从库压力都变大,对外提供的访问能力变差。
  • 回滚所需要的时间比较长:事务越大,回滚耗费的时间就越久,也会占用资源。
  • undo log膨胀:undo log也是有大小限制的,如果事务大,那么会一直追加写入log,让存储压力变大。
  • 容易导致死锁:别的请求无法获取到数据,长时间等待死锁等。

事务优化 

  •  将查询等数据准备操作放到事务外:事务尽量小,并且尽量是修改语句。
  • 事务中避免远程调用,远程调用要设置超时,防止事务等待时间太久:减少远程跨系统的调用次数,缩短调用链路。
  • 事务中避免一次性处理太多数据,可以拆分成多个事务分次处理:拆分事务,虽然总体执行时间变长,但是对系统来说压力变小了,系统更稳定,不会频繁宕机。
  • 更新等涉及加锁的操作尽可能放在事务靠后的位置:如果事务有插入有编辑,最好是先插入,后编辑,这样被数据被编辑之前还可以被读到。
  • 能异步处理的尽量异步处理:减少同步等待。
  • 应用侧(业务代码)保证数据一致性,非事务执行:业务侧保证事务,数据库不做任何处理。
 
 
 
 
 
 
 
 

 

posted @ 2024-04-28 23:29  圣辉  阅读(63)  评论(0编辑  收藏  举报