MySQL的四种事务隔离级别以及各种锁的问题 - Go语言中文社区

MySQL的四种事务隔离级别以及各种锁的问题


MySQL的四种事务隔离级别

事务的四大特性ACID

1.原子性(Atomicity):一个事务要么全部执行,要么全部不执行,如果执行过程中出现异常则回滚;犹如化学中的原子一样具有不可分割性(别跟我较真啊,虽然原子又可以分为质子、中子、电子等);
2.一致性(Consistency):事务的一致性则是指事务的开启前后,数据库的完整性约束并没有被破坏;即A向B转账,转账前后A和B的总金额还是不变的;
3.隔离性(ISOLocation):同一时间,只允许一个事务请求一个数据,不同的事务之间没有互相的干扰;这主要是针对于并发事务来讲,又并发事务所做的数据修改必须与其他并发事务隔离开进行;事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同;
隔离性设置成最高级别可串行化的时候,在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。
4.持久性( Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚;

事务的四种隔离级别以及产生的问题

在这里插入图片描述
1.读未提交(read Uncommited):在该隔离级别,所有的事务都可以读取到别的事务中未提交的数据,会产生脏读问题,在项目中基本不怎么用,安全性太差;
2.读已提交(read commited):这是大多数数据库默认的隔离级别,但是不是MySQL的默认隔离级别;这个隔离级别满足了简单的隔离要求:一个事务只能看见已经提交事务所做的改变,所以会避免脏读问题;
由于一个事务可以看到别的事务已经提交的数据,于是随之而来产生了不可重复读和虚读等问题(下面详细介绍这种问题,结合问题来理解隔离级别的含义);
3.可重复读(Repeatable read):这是MySQL的默认隔离级别,它确保了一个事务中多个实例在并发读取数据的时候会读取到一样的数据;不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
4.可串行化(serializable):事物的最高级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争,一般为了提升程序的吞吐量不会采用这个;


讲一下上面所说的由于各个隔离级别产生的数据读取问题:
1.脏读:一个事务A读取到另个事务B未提交的数据即为脏读;
试想一下,我给你转账,在转账数据过去后我并没有提交数据,这个时候告诉你钱还你了你查查,你看了钱到账了,看诉我收到了,这个时候我执行了刚刚的事务回滚,那个钱是不是又跑回我这来了呢??
2.不可重复读:在一个事务中两次查询的数据不一致(侧重于数据的修改);这么说吧,不一致本身没有危害,危害的是我们不知道它不一致,导致错误的数据处理;
试想一下,你在一个事务中用到两次A这个数据,第一次用完后,另个事务B对数据进行了修改,等到第二次用的时候数据就变了,跟你预期的要操作的那个A数据不一致了,这个时候你接下来的逻辑代码就是在操作一个你不知道的数据,可怕不可怕??
3.虚读:在一个事务中查询的行数不一样(侧重于数据的新增和删除);
试想这样,如果事务A对一个表中的所有name这一列的内容全部改成“迪迦奥特曼”,执行完后事务B新增了一个数据的name内容是“孙悟空”,等你在事务A中再查数据库会发现,唉我去,咋还有一行没改过来呢,这样是不是有点点蛋疼???


不可重复读侧重的是对数据的修改,而虚读侧重的是对数据的增加和删除;解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表



数据库的各种锁的问题

共享锁(s)和排他锁(x):
共享锁【S锁】又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。
排他锁【X锁】又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。
在这里插入图片描述
说明:
1)共享锁和排他锁都是行锁,意向锁都是表锁,应用中我们只会使用到共享锁和排他锁,意向锁是mysql内部使用的,不需要用户干预。

2)对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁,事务可以通过以下语句显示给记录集加共享锁或排他锁。
共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE。
排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE。
**对于锁定行记录后需要进行更新操作的应用,应该使用Select…For update 方式,获取排它锁。(用共享锁,在读了之后再写会阻塞,会导致死锁)
这里说说Myisam:MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁。

3)InnoDB行锁是通过给索引上的索引项加锁来实现的,因此InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!


乐观锁和悲观锁
我先说一种情况,现在有个公司要给一个员工A涨薪,工作人员1打开A的工资修改页面,这个时候离开了;工作人员2一看A的工资没涨,打开修改页面给涨了,提交到数据库;此时工作人员2回来了,又给A员工涨了一遍,你要是老板你是不是会赔死??这个时候就要用到悲观锁和乐观锁了。
先来解释一下悲观锁和乐观锁。悲观锁与乐观锁都是对被修改数据在并发情况下的一种保护机制;这里悲观与乐观的含义是指对于即将修改的数据被外界修改持一种悲或乐的态度;即:
悲观锁是指我认为当我要修改一个数据的时候,别人也在修改,所以我要对即将操作的数据进行全程加锁,以保证我的操作不会被别人所影响;
乐观锁是指当我要修改数据的时候,别人一般不会再修改,因此,我只在提交我的修改时再加锁,而不用全程加锁。由此可见:悲观锁的加锁时间更长。
结合上面的例子形象的解释下这俩的区别:悲观锁会持悲观的态度,当工作人员1取到员工A的数据的时候,会认为自己修改的过程中别人也会修改,所以将数据全程加锁,确保自己的使用过程中别人不会打扰;而采用乐观锁的时候,会比较乐观的认为在使用过程中不会有人改变,在提交到数据库的时候再来检查一遍数据是否改变,如果数据已经改变跟之前的不一样了,便会提醒用户数据已经改变,下面介绍两种乐观锁常用的方法:
1).使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
2).乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突;

总结:
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
  另外,高并发情况下个人认为乐观锁要好于悲观锁,因为悲观锁的机制使得各个线程等待时间过长,极其影响效率,乐观锁可以在一定程度上提高并发度。

线程和事务的关系

事务,是为了解决并发问题,多个用户同时访问数据库造成的并发问题;线程是为了让多段代码同时运行,解决了多部分同时运行的问题,让运行效率更高;
事务,简单理解为一个业务需求的最小单位;比如A给B转账500元,包括A减少500元,B增加500元这两个事务。而一个会话中可以包含多个事务,一个客户对应一个会话session;
当多线程共享一个MySQL连接的时候,这时要注意,多线程的运行有一定的不可控性,所以当多线程设计到事务的时候容易引发问题;尽量避免多线程使用同一个连接,此时尽量使用数据库连接池,为每一个线程建一个连接;
一个会话可以包含多个事务,会话其实就是开启和数据库的会话,进行相应的操作;会话其实也就是执行一个或者多个任务和操作(一个或者多个事务);而线程是操作系统的概念,是用来执行过程中提升效率,让很多代码块并发执行;


会话可以产生多个事务,一个事务只来源于一个会话;
一个事务可能会产生一个或者多个线程,同样一个会话可能包含多个线程(毕竟一个会话可以包含多个事务),比如RMAN备份,是可以创建多个线程可加快备份速度;一个线程可以开启多个事务,但是在同一时间只能执行一个事务,在没结束当前事务前是无法释放资源来进行下一个事务的;


上面这些有点绕,大家一定要静下心来好好想通,如果此时静不下心来,那OK,学点别的改天再来看;
在这里插入图片描述

死锁

死锁的第一种情况
一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。
解决方法:
这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进 行处理,尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。
死锁的第二种情况
用户A查询一条纪录,然后修改该条纪录;这时用户B修改该条纪录,这时用户A的事务里锁的性质由查询的共享锁企图上升到独占锁,而用户B里的独占锁由于A 有共享锁存在所以必须等A释放掉共享锁,而A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁,于是出现了死锁。这种死锁比较隐蔽,但在稍大点的项 目中经常发生。如在某项目中,页面上的按钮点击后,没有使按钮立刻失效,使得用户会多次快速点击同一按钮,这样同一段代码对数据库同一条记录进行多次操 作,很容易就出现这种死锁的情况。
解决方法:
1、对于按钮等控件,点击后使其立刻失效,不让用户重复点击,避免对同时对同一条记录操作。
2、使用乐观锁进行控制。乐观锁大多是基于数据版本(Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是 通过为数据库表增加一个“version”字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数 据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。乐观锁机制避免了长事务中的数据 库加锁开销(用户A和用户B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。Hibernate 在其数据访问引擎中内置了乐观锁实现。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不受我们系统的控制,因此可能会造 成脏数据被更新到数据库中。
3、使用悲观锁进行控制。悲观锁大多数情况下依靠数据库的锁机制实现,如Oracle的Select … for update语句,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。如一个金融系统, 当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户账户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读 出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对成百上千个并发,这 样的情况将导致灾难性的后果。所以,采用悲观锁进行控制时一定要考虑清楚。
死锁的第三种情况
如果在事务中执行了一条不满足条件的update语句,则执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。类似的情 况还有当表中的数据量非常庞大而索引建的过少或不合适的时候,使得经常发生全表扫描,最终应用系统会越来越慢,最终发生阻塞或死锁。
解决方法:
SQL语句中不要使用太复杂的关联多表的查询;使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,建立相应的索引进行优化。

总结

讲述了MySQL的特性和事务隔离级别,以及各个隔离级别存在的问题;还写了会话、事务、多线程间的关系;这篇文章思考性较强,好好理解;

在这里插入图片描述
/*************************************************************************
/*************************************************************************
此文章版权方是个人,目的是为自己记录学习历程的同时为大家提供一些参考;如果有不正确的地方,欢迎大家提出!

/*************************************************************************
/*************************************************************************

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_39495922/article/details/82886781
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢