目录
1.事务的ACID原则
2. 事务的隔离级别
2.1 数据库的脏读问题
2.2 数据库不可重复读问题
2.3 数据库幻读问题
2.4 数据库脏写问题
3.Mysql的锁
3.1 以锁粒度的维度划分
3.2 以互斥性的维度划分:
3.3 以操作类型的维度划分:
3.4 以加锁方式的维度划分:
3.5 以思想的维度划分:
4. 多版本并发控制
4.1回滚日志(Undolog)
4.2 重做日志(redolog)
4.3 MVCC
4.4 ReadView读视图
1.事务的ACID原则
原子性(Atomicity):当前事务的操作要么同时成功,要么同时失败。原子性由undo log 日志实现。
一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。简言之:一个事务中的所有操作,要么一起改变数据库中的数据,要么都不改变,对于其他事务而言,数据的变化是一致的。
隔离性(Isolation):多个用户的并发事务访问同一个数据库时,一个用户的事务不应该被其他用户的事务干扰,多个并发事务之间要相互隔离。
持久性(Durability):持久性就是指如果事务一旦被提交,数据库中数据的改变就是永久性的。
2. 事务的隔离级别
①Read uncommitted/RU(读未提交):一个事务可以读取到另一个事务尚未提交的数据修改。
存在脏读问题、不可重复读问题、幻读问题
②Read committed/RC:(读已提交):每次读取数据时都能看到其他事务已提交的最新数据。oracle默认
存在不可重复读问题、幻读问题
在读已提交级别中,一个事务中每次查询数据时,都会创建一个新的ReadView,然后读取最近已提交的事务数据,因此就会造成不可重复读的问题。
③Repeatable read/RR:(可重复读):事务开始后,所有的读操作都看到的是事务开始时的数据快照。即事务在其开始时就创建一个快照(或视图),确保在整个事务期间,所有读操作都只能看到这个快照中的数据。mysql默认:存在幻读问题
写操作加排他锁,对读操作依旧采用MVCC
机制,但RR
级别中,一个事务中只有首次select
会生成ReadView
快照。
④Serializable:(序列化/串行化):并发执行的多个事务之间,数据库系统能够以某种方式序列化(或顺序化)这些事务的执行,从而避免可能导致数据不一致性的情况发生。
存在 不存在问题
越靠后并发控制度越高,也就是在多线程并发操作的情况下,出现问题的几率越小,但对应的也性能越差,MySQL
的事务隔离级别,默认为第三级别:Repeatable read
可重复读。
SELECT @@transaction_isolation;
set transaction isolation level read uncommitted;
begin;
commit;
rollback;
savepoint point_name:添加一个事务回滚点
rollback to point_name:回滚到指定的事务回滚点
2.1 数据库的脏读问题
脏读:是指一个事务读到了其他事务还未提交的数据,也就是当前事务读到的数据,由于还未提交,因此有可能会回滚,如下:
2.2 数据库不可重复读问题
不可重复读:是指在一个事务中,多次读取同一数据,先后读取到的数据不一致。
在一个事务中,对同一数据的多次读取结果不一致,这是由于其他事务在第一次读取之后对该数据进行了更新。如下:
假设有一张账户表(accounts):
账户ID | 余额 |
1 | 1000 |
2.3 数据库幻读问题
幻读:在一个事务中,对同一范围的查询结果不一致,这是由于其他事务在第一次查询之后在该范围内插入了新记录或删除了原有记录。
幻读问题:指同一个事务内多次查询返回的结果集不一样。比如同一个事务A
,在第一次查询表的数据行数时,发现表中有n
条行记录,但是第二次以同等条件查询时,却发现有n+1
条记录,这就好像产生了幻觉。
假设有一张订单表(orders):
订单ID | 金额 |
1 | 100 |
2 | 200 |
幻读问题,发生幻读问题的原因是在于:另外一个事务在第一个事务要处理的目标数据范围之内新增了数据,然后先于第一个事务提交造成的问题。
2.4 数据库脏写问题
脏写的问题,也就是多个事务一起操作同一条数据,例如两个事务同时向表中添加一条ID=88
的数据,此时就会造成数据覆盖,或者主键冲突的问题,这个问题也被称之为更新丢失问题。
3.Mysql的锁
3.1 以锁粒度的维度划分
①表锁:
全局锁:加上全局锁之后,整个数据库只能允许读,不允许做任何写操作。
自增锁 / AUTO-INC锁:这个是为了提升自增ID的并发插入性能而设计的。
②行锁:
记录锁 / Record
锁:也就是行锁,一条记录和一行数据是同一个意思。
间隙锁 / Gap
锁:InnoDB
中解决幻读问题的一种锁机制。
3.2 以互斥性的维度划分:
共享锁 / S锁:不同事务之间不会相互排斥、可以同时获取的锁。
排他锁 / X锁:不同事务之间会相互排斥、同时只能允许一个事务获取的锁。
3.3 以操作类型的维度划分:
读锁:允许多个事务同时读取同一行数据,而不会相互阻塞。获取共享锁的事务只能读取数据,不能修改数据。
写锁:执行插入、删除、修改、DDL语句时使用的锁。获取排它锁的事务可以读取和修改数据,但会阻塞其他事务获取任何类型的锁(包括共享锁和排它锁)。
3.4 以加锁方式的维度划分:
显示锁:编写SQL语句时,手动指定加锁的粒度。
隐式锁:执行SQL语句时,根据隔离级别自动为SQL操作加锁。
3.5 以思想的维度划分:
乐观锁:每次执行前认为自己会成功,因此先尝试执行,失败时再获取锁。
悲观锁:每次执行前都认为自己无法成功,因此会先获取锁,然后再执行。
总归说来说去其实就共享锁、排他锁两种,只是加的方式不同,加的地方不同,因此就演化出了这么多锁的称呼。
4. 多版本并发控制
4.1回滚日志(Undolog)
Undo Log:
数据库事务开始之前,会将要修改的记录放到Undo日志里,当事务回滚时或者数据库崩溃时,可以利用UndoLog撤销未提交事务对数据库产生的影响。
Undo Log是事务原子性的保证。在事务中更新数据的前置操作其实是要先写入一个Undo Log
Undo Log日志里面不仅存放着数据更新前的记录,还记录着RowID
、事务ID
、回滚指针
。
那么如果当一个事务提交时,Undo的旧记录会不会立马被删除呢?因为事务都提交了,不需要再回滚改动过的数据,似乎用不上Undo旧记录了,对吗?确实如此,但不会立马删除Undo记录,对于旧记录的删除工作,InnoDB中会有专门的purger线程负责,purger线程内部会维护一个ReadView,它会以此作为判断依据,来决定何时移除Undo记录。
4.2 重做日志(redolog)
当一条写SQL
执行时,不会直接去往磁盘中的xx.ibdata
文件写数据,而是会写在undo_log_buffer
缓冲区中,因为工作线程直接去写磁盘太影响效率了,写进缓冲区后会由后台线程去刷写磁盘。
Redo-log日志到磁盘,后台线程再根据Redo-log日志把数据落盘,这个动作似乎看起来有些多余对吧?但实际上这样做好处很大:
-
①日志比数据先落入磁盘,因此就算MySQL崩溃也可以通过日志恢复数据。
-
②写日志时是以追加形式写到末尾,而写数据时则是计算数据位置,随机插入。
4.3 MVCC
MySQL
提供的锁机制确实能解决并发事务带来的一系列问题,但由于加锁后会让一部分事务串行化,而MySQL
本身就是基于磁盘实现的,性能无法跟内存型数据库娉美,因此并发事务串行化会使其效率更低。MVCC通过保存数据的历史版本。可以通过比较版本号决定数据是否显示出来。读取数据的时候不需要加锁可以保证事务的隔离效果。
1.事务版本号
每开启一个日志,都会从数据库中获得一个事务ID(也称为事务版本号),这个事务 ID 是自增的,通过 ID 大小,可以判断事务的时间顺序。
2.隐藏字段
隐藏主键 - ROW_ID,最近更新的事务ID - TRX_ID,回滚指针 - ROLL_PTR
4.4 ReadView
读视图
ReadView是“快照读”SQL执行时MVCC提取数据的依据。
快照读:就是最普通的Select查询SQL语句
当前读:指代执行下列语句时进行数据读取的方式
Insert、Update、Delete、 Select...for update、 Select...lock in share mode
Read committed和Repeatable read隔离级别的事务来说,都必须保证读到已经提交的事务修改过的记录。也就是说假如另一个事务已经修改了记录但是尚未提交,则不能直接读取最新版本的记录。
Read committed在每一次进行普通select操作前都会生成一个readview;Repeatable read只在第一次进行普通select操作前生成一个readview,之后的查询操作都重复使用这个readview。【特例:当两次快照读之间存在当前读,ReadView会重新生成,导致产生幻读】
如果一个事务要查询行记录,需要读取哪个版本的行记录呢? Read View 就是来解决这个问题的。Read View 可以帮助我们解决可见性问题。 Read View 保存了当前事务开启时所有活跃的事务列表。换个角度,可以理解为: Read View 保存了不应该让这个事务看到的其他事务 ID 列表。
例如:如果T2事务要查询一条行数据,此时这条行数据正在被T1事务写,那也就代表着这条数据可能存在多个旧版本数据,T2事务在查询时,应该读这条数据的哪个版本呢?此时就需要用到ReadView,用它来做多版本的并发控制,根据查询的时机来选择一个当前事务可见的旧版本数据读取。
可见性算法:
1.判断 当前事务id等于creator_trx_id(4)吗?成立说明数据就是自己这个事务改的,可以访问
2.判断 trx_id < min_trx_id(2)?成立说明数据已经提交了,可以访问
3.判断 trx_id > max_trx_id(5)?成立说明该事务是在ReadView生成以后才开启,不允许访问
4.判断 min_trx_id(2) <= trx_id <= max_trx_id(5),成立在m_ids数据中对比,不存在数据的则代表数据是已提交的,可以访问。