MySQL--事务隔离级别及其原理详解

事务隔离级别

InnoDB引擎中,定义了四种隔离级别供我们使用,级别越高,事务隔离性越好,但性能就越低,而隔离性是由MySQL中的各种锁以及MVCC(多版本并发控制)机制来实现的

  • 读未提交(read uncommitted):有脏读问题

  • 读已提交(read committed):有不可重复读问题

  • 可重复读(repeatable read):有幻读问题

  • 串行化(serializable):上面问题都解决,但性能最低

 read uncommitted(读未提交)

 - 事物A和事物B,事物A未提交的数据,事物B可以读取到
 - 这里读取到的数据叫做“脏数据”
 - 这种隔离级别最低,这种级别一般是在理论上存在,数据库隔离级别一般都高于该级别

实例展示:

打开两个控制台:

set transaction isolation level  read uncommitted ; -- 设置隔离等级 读未提交
 start transaction ;
 update account set money = money + 1000 where id = 1; -- 执行后数据库改变,但还没有提交 
 rollback ;
 set transaction isolation level  read uncommitted ; -- 设置隔离等级
 start transaction ;
 select * from account where id = 1; -- 在rollback之前查询的数据为5500
 select * from account where id = 1; -- 在rollback之后查询的数据为4500
 commit;

原理:在事务中的读取操作,SQL底层并没有加上读锁

2、read committed(读已提交)

 - 事物A和事物B,事物A提交的数据,事物B才能读取到
 - 这种隔离级别高于读未提交
 - 换句话说,对方事物提交之后的数据,我当前事物才能读取到
 - 这种级别可以避免“脏数据”
 - 这种隔离级别会导致“不可重复读取”
 - Oracle默认隔离级别

代码展示:

依旧两个控制台:

 set transaction isolation level  read committed ; -- 设置隔离等级 读已提交
 start transaction ;
 update account set money = money + 1000 where id = 1; -- 执行后查询数据依旧不变 只有提交后数据库数据才改变
 commit ; -- 提交
 begin ;
 update account set money = money + 1000 where id = 1;-- 执行提交后数据改变
 commit ;

 set transaction isolation level  read committed ; -- 设置隔离等级
 start transaction ;
 select * from account where id = 1; -- 数据为4500
 select * from account where id = 1; -- 数据为5500
 select * from account where id = 1; -- 数据为6500 这就是不可重复读,你可能会在同一个事务中读到一个数据的不同值
 commit ; -- 提交

3、repeatable read(可重复读)

 - 事务A和事务B,事务A提交之后的数据,事务B读取不到
 - 事务B是可重复读取数据
 - 这种隔离级别高于读已提交
 - 换句话说,对方提交之后的数据,我还是读取不到
 - 这种隔离级别可以避免“不可重复读取”,达到可重复读取
 - 比如1点和2点读到数据是同一个
 - MySQL默认级别
 - 虽然可以达到可重复读取,但是会导致“幻读”

代码展示:

 set transaction isolation level  repeatable read ; -- 设置隔离等级 可重复读
 start transaction ;
 update account set money = money + 1000 where id = 1;-- 第一次提交,在另一个控制台读
 commit ; -- 提交
 begin ;
 update account set money = money + 1000 where id = 1;-- 第二次提交,在另一个控制台读
 commit ;
 begin ;
 update account set money = money + 1000 where id = 1;-- 第三次提交,在另一个控制台读
 commit ;
set transaction isolation level  repeatable read ; -- 设置隔离等级 可重复读
 start transaction ;
 select * from account where id = 1; -- 第一次读取的是5500  这就是可重复读,只要在这个事务中读取过一次数据,接下来的数据都会以这个数据为准,不会改变,直至事务结束
 ​
 select * from account where id = 1; -- 第二次读取的是5500
 ​
 select * from account where id = 1; -- 第三次读取的是5500
 commit ; -- 提交

细节解析:

在可重复读中,事务开始后,在第一次做查询操作的时候,数据库中所有的记录都会有一个类似的快照保存所有数据,在读表中的数据,都会以第一次查询时的数据库数据的快照数据为准。(其实就是MVCC机制)

缺点:这里事务中如果以快照数据来做增删改操作,就会导致脏写问题:导致该事务中算出的错误数据覆盖到原本的正确数据上

解决方案:

  • 乐观锁:CAS机制(比较并替换)(比较版本号是否一致,如果报错,寻找最新的版本号数据,再来更新。直至成功为止) ,在数据表里再加一个字段version(版本),每次更新数据的时候,version+1,以后再更改数据的时候,将版本号一同查询出来,如果拿到的数据是以前的数据,那么真正的数据的版本号一定和以前的数据版本号不一致,那么更改数据的语句就会报错,这就是乐观锁

  • 悲观锁:在操作语句中加入写锁,在MySQL底层,所有加锁的操作都不会读快照数据,而是去读取版本链中最新的数据更来操作

4、serializable(串行化)

 - 事务A和事务B,事务A在操作数据库时,事务B只能排队等待
 - 这种隔离级别很少使用,吞吐量太低,用户体验差
 - 这种级别可以避免“幻像读”,每一次读取的都是数据库中真实存在数据,事务A与事务B串行,而不并发

代码展示:

 set transaction isolation level  serializable ; -- 设置隔离等级  串行化(序列化)
 start transaction ;
 update account set money = money + 1000 where id = 1;-- 执行之后不提交去查询 
 commit ; -- 提交
 begin ;
 update account set money = money + 1000 where id = 1;
 commit ;
 begin ;
 update account set money = money + 1000 where id = 1;
 commit ;

 set transaction isolation level  serializable ; -- 设置隔离等级
 start transaction ;
 select * from account where id = 1; -- 在查询时会一直等待前一个事务的提交 直至事务提交之后才会查询数据
 ​
 select * from account where id = 1;
 ​
 select * from account where id = 1;
 commit ; -- 提交

细节解析:

串行化是隔离级别最高的,我们所有的问题都是因为并发事务导致的,串行化中所有的事务都是串行执行的,每个事务互不影响,隔离性最好,性能最差,效率太低,大多数不会去使用,现在使用最多的是 read committed repeatable commited

原理:在底层为我们的读取事务加了读锁 而修改事务底层本就存在写锁。读锁与写锁有互斥关系

我们可以在隔离权限为read uncommitted 的事务中实现串行化的效果

代码展示:

 set transaction isolation level read uncommitted ;
 begin ;
 update account set money = money + 1000 where id = 1; -- 在修改事务中,底层都会加锁
 -- 在执行之后不提交,去查询,查询事务会等待该事务提交之后再进行操作
 ​
 commit ; -- 提交

 set transaction isolation level  read uncommitted ; -- 设置隔离等级
 start transaction ;
 select * from account where id = 1 lock in share mode ;-- 在查询事务中添加一把读锁 查询之后不提交去另一个事务修改,
 -- 则另一个修改事务需要等该查询事务提交之后在执行
 commit ;

原理:锁与锁之间是互斥的,我们在查询事务中加了读锁,而在修改事务中底层原本就有写锁,写锁与读锁相互排斥,这样就实现了串行化效果

重点:MVCC(多版本并发控制)机制

MVCC(Multi-Version Concurrency Control):多版本并发控制 ,可以做到读写不堵塞,且避免了类似脏读这样的问题,主要通过undo日志链来实现

目的:就是为了在事务中读和写能够同时高并发的去执行,同时并不是使用串行化的形式,让效率提升

其实和copy on write机制相同,都是让你去读原先的旧数据

在事务执行的时候,每修改完一条数据,在MySQL后台会给你记录一条undo日志

什么叫做多版本并发控制机制?

在后台中的每条数据在数据版本链中是有多个版本的,根据事务隔离级别,会读取不同版本的数据。

实现原理:copy on write :读的是老数据,在复制老数据的副本上写,再将其变为新数据。

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

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

这两个事务隔离级别底层就是MVCC机制实现的。

CopyOnWrite 原理详解

CopyOnWrite 写入时复制 简称COW: 计算机程序设计领域的一种优化策略

本质的优化思想:读写数据分离:就是读和写的数据要分离才可以实现高并发,高性能,一份写,一份读,同时实现高并发。

如果读和写的数据是一份的话,要实现高并发就必须加锁,但加锁效率过慢,性能过低

拿Nacos举例说明:

很多注册中心底层的注册表在高并发实现的时候都有用到COW(Copy on Write)

Nacos就是以微服务架构的注册中心的一个中间件,注册中心底层有一个注册表,可能有很多微服务机器要把自己的IP,端口号包括一些服务信息等等要写入到注册表中。为了以后我们那些服务之间调用,从注册中心里面拿到这些服务的名称,从注册表中拿到我们之前这些机器的注册信息。

比如说,订单系统中要调用库存系统,库存系统可能有不少机器,这些机器之前都会把自己的信息注册,比如库存系统在注册中心里注册了两台机器,那当我们的订单系统调用库存服务的时候可以直接读取注册表里面的库存系统注册的信息,读到之后放到本地缓存,每次挑一台机器轮换去调用。

在上述的库存系统在将自己的IP,端口号服务注册到注册表的时候,注册表是需要更新的,将这些信息都更新过来,而在Nacos底层并不是直接在注册表上更新,他事先将注册表复制一份(类似于快照读),然后将要更新的数据更新到复制的这个注册表副本上,而我们订单系统在这个时间段读取注册表的时候,读取的是原始的旧数据,这就是Copy on Write ,更新数据的时候,更新的注册表的副本数据,当副本数据更新完后,直接替换真实的数据。

如果不使用Copy on Write,所有的数据只在那一个注册表上更新,这样做里面可能会出现一些问题:你去更新的时候,可能更新的操作有很多,比如Nacos在修改注册表的时候,实际上修改了很多数据,可能会有很多步操作,每一步操作都是涉及到修改注册表里面的部分信息,比如一些附属服务信息,端口号等等,在这些修改操作未完成的时候,如果这时候订单系统如果读到修改一半的信息,那这个信息就是脏数据,拿这种数据去操作绝对会出问题!

如何解决?

方法一:在更新这个注册表的时候添加一个锁,在更新结束之前锁一直存在,等锁释放之后才能去读,这样读到的就是完整的数据,但这样做的缺点就是将读,写串行化,性能会很低 而Nacos是底层读写高并发的,让他的读和写并发更高

方法二:也就是Nacos底层所使用的方法:将注册表复制一份,修改的时候修改这个副本,修改完了在替换回真实的注册中心,那读还是读取修改之前的数据(第一份注册表)

好处:读和写可以高并发,因为读和写的数据不是同一份,不会出现方法一的问题,

缺点:在注册已经完成,副本准备替换回真实的注册中心的时候,订单系统访问,这时候订单系统读的还是老的数据,读的是之前的旧数据,但不是脏数据,(类似于快照读),问题不是很大,在微服务注册中心可以容忍的,在为微服务架构中是要支撑它的读写高并发,其他方面是可以妥协的

在许多分布式系统的底层,为了提高系统的性能,都有用到copy on write 机制,尤其是高并发编程中有CopyonWriteArrayList,CopyOnwriteArraySet

undo回滚日志链

在我们执行insert语句时,在MySQL底层除了自己设置的字段,其实还有两条隐藏的字段:

trx_id(事务的唯一id)

roll_pointer(回滚指针)

而roll-pointer指向的是我们insert语句中的undo日志 (delete语句)即:如果现在回滚,通过回滚指针找到我们insert对应的undo日志,在底层会执行undo日志中的delete语句,完成回滚,

而在我们执行下一条操作语句(update语句)的时候,我们的回滚指针会指向上一次修改的那条数据,如果事务没有提交,回滚,那么回滚指针会找到上一条记录,让上一条记录还原数据库真实的记录数据,这是update语句的undo日志

如果继续执行第三条操作语句,则roll-pointer会继续指向上一条数据库记录,接下来也是如此

在执行那个最后一条语句也是一样,

在事务完成之后,数据库表面上只显示最后修改的记录,buffer pool(缓存池)中缓存page中的记录以及数据库磁盘文件里面也是这条最后修改的数据。

但在实际上在Mysql的后台,会有很长的一段undo回滚日志, 然后整个记录结合真实的记录以及undo日志,会形成一个记录版本链

所以在后端MySQL的存数据的存储是之前所有记录的版本链

如图所示:

read commited(读已提交)

原理:每次读取的都是记录版本链中最新提交的数据

repeatable read(可重复读)

原理:在一条操作语句执行提交后,另外一个事务来查询,查询到的是记录版本链中最新提交的数据,

然后,这个数据和这个查询事务底层有个算法,无论接下来的操作语句如何提交,在这个查询事务没有提交之前,查询到的所有数据都是这一条与事务绑定的数据,是在记录版本链中寻找历史数据。

拓展:

面试题:查询操作方法需要使用事务吗?

分业务场景,隔离级别

如果隔离级别为可重复读,则需要使用事务,因为需要使用事务来确保数据的一致性,如果不使用事务,那就无法确保可重复读的效果,就是不开启事务的话,你查询到的两条数据的时间维度可能不一致,这样会导致数据误差,而开启事务,就可以保证读到的数据是一个时间维度的快照数据

如果隔离级别为读已提交,则可以不使用事务

如果业务需要高并发事务的话,则使用read committed

如果业务需要性能较高的话,则使用 repeatable read

希望对大家有所帮助!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值