Mysql的锁
本文有些是黏贴的别人的见解,本人理解后稍做修改,文字叙述比较多,大家要耐心看完,如果有不对的请指出,如果对这块不了解的同学,看完你必定有所得,何以解忧,唯有学习!
首先如果在正常情况下我们对数据的操作都是同步的获取是顺序进行的(等一个操作完成再进行下一个操作)那么很少会有事务问题或者锁的问题。但是由于用户多,操作多,不可避免的进行并发操作,此时就会有并发操作相同表或者相同数据的问题,此时数据库就引入可锁的机制。本文有些是黏贴的别人的见解,本人理解后稍做修改,文字叙述比较多,大家要耐心看完。
Mysql的锁可以分为三类:
- 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
- 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
- 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
InnoDB是在索引之上对行进行加锁(行级锁),如果没有走索引那么InnoDB直接会使用表级所。Mysql默认使用Readreoeat (RR级,使用的InnoDB引擎),通常RR级会存在幻读问题,但是MySQL的RR级别中,是攻克了幻读的读问题的。
不可重复读和幻读
不可重复读是在隔离级别为读提交的情况发生的
幻读是在可重复读的隔离级别的情况下发生的
在InnoDB中,使用乐观锁思想,会在每行数据后添加两个额外的隐藏的值来实现MVCC(回滚段的快照),这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。 在可重读Repeatable reads事务隔离级别下:
- SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
- INSERT时,保存当前事务版本号为行的创建版本号
- DELETE时,保存当前事务版本号为行的删除版本号
- UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行
undo_log
Undo_log是一种逻辑日志,也就是旧的数据备份,他有两个作用,一个是为事务回滚提供回滚数据源,二是为MVCC提供老版本的数据
怎样解决重复读和幻读问题的
在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据。对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)
首先我们的SELECT查询分为快照读和实时读,快照读通过MVCC(并发多版本控制)来解决幻读问题,实时读通过行锁来解决幻读问题。
实时读:
update ... (更新操作)
delete ... (删除操作)
insert ... (插入操作)
select ... lock in share mode (共享读锁)
select ... for update (写锁)
当前读,读取的是最新版本,并且对读取的记录加锁,阻塞其他事务同时修改相同记录,避免出现安全问题
快照读:
单纯的select操作(不包括上面当前读的select ... lock in share mode,select ... for update)
Read Committed隔离级别:每次select都生成一个快照读
Read Repeatable隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读
这两个问题发生于读取阶段,在读取mysql数据的时候,不管在一个事务中读取多少次,都会先看当前读取数据是否已经加锁,如果加锁就转向查询当前记录回滚段中最近的快照,读快照不加锁,非常快;如果没有加锁,那说明本次的查询数据中没有被其他事务占用也就不存在读取数据时的事务问题。一般我们的查询基本都是快照读,只有一些特殊的查询比如:update更新时必须先查询再更新,那就肯定是实时读。
重复读
在每个事务中都会有ReadView,ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。说白了就是有个列表会记录当前事务,读写的时候会从这个列表中获取事务然后与数据库的事务版本做对比。
当隔离级别为RC的时候,在一个事务中的读取操作每次都会新建一个ReadView,由于是新建的所以活跃的事务列表也是最新的,那么与版本对比的事务也是最新,这就使得读取的数据也是最新提交的那个事务的版本,那么最新的数据也会被读取到,这样就不能重复读,其实RC下就是实时读。
当隔离级别为RR的时候,在当前事务中的每次查询都是使用历史的(如果是第一个就会是新建毫无疑问),这就使得每次读取的都是旧的事务,也就是每次对比的版本号都是同一个,那么,即使有新数据提交,按照旧的事务版本去读取的时候也不会读取到新提交的数据。
幻读问题
同样的也是版本号的对比,因为在RR级下是按旧版本去匹配数据,那么即使其他事务在当前事务期间有数据新增修改的提交依然不会被匹配到,这样就解决了幻读问题。
以上是用快照读的方式处理幻读等问题,那么实时读是怎么决绝的
实时读的幻读问题
当在RR级时(RC是不会解决幻读问题的,都是实时读),执行实时读的时候就会给行加锁,也就是说,如果有事务需要操作当前查询的数据那么必须等待当前的操作完成才能进行,反过来如果实时读的时候发现已经存在锁,那么也必须等待锁释放,然后当前事务获取锁之后进行下一步操作。
通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。在MySQL的RR级别中,是解决了幻读的读问题的。
共享锁:
共享锁及查询,任何情况下共享锁可以共存,但不能与排他锁共存,即:加了共享锁之后其他事务可以获取共享锁但是不能获取排他锁。
排它锁:
排它锁就是新增、更新、删除时获取的锁,排他锁不能和共享锁共存,更不能与排它锁共存,即:获取了排他锁之后,既不能获取共享锁(查询)也不能获取排它锁,只能等当前排他锁执行完毕才能获取其他锁。
间隙锁(Gap Lock):
锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间,其主要作用也就是锁定行防止其他事务进行插入,但是当实时查询的是唯一主键、或者是唯一索引的时候,其实是不会添加间隙锁的,没有必要,因为直接扫描行已经有了行锁。
Next-Key Lock:
行锁与间隙锁组合起来用就叫做Next-Key Lock。
举例说明: 假设表中的字段user_id存储的是1、2、3.。。。101
select * from user where user_id > 100 for update;
此条语句必须是实时查询不可以是快照查,原因前面已经说过,那么按索引检索出的就是101这条记录,此时,InnoDB会将101这条记录添加行锁,同时将对于user_id>101的记录(尽管这些记录不一定存在)添加Gap Lock(间隙锁),也就是说101之前的数据我可以不用关心,有其他事务对他们的操作不会影响我当前的事务,而因为使用的>101,也就是101之后的记录是对我当前事务有影响的,所以我得对其加锁,这样Gap Look和行锁一起合并为Next-Key Lock,此时这些数据都被上锁,如果有其他事务要对这些数据操作,那必须等当前事务完成,或者如果这些数据已经被其他事务占用,那么当前事务必须等待其他事务的锁被释放,这其实就是实时读如何解决幻读的问题的原理
死锁:
是指多个操作(事务)之间的锁产生了回路,这是一个资源竞争问题,如果没有一个事务主动放弃资源锁那么死锁永远会存在
锁冲突:
冲突指的是指锁不相容,比如共享锁和排他锁不同同时存在。
死锁问题
场景1:
一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了
解决方法:
这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进 行处理,尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。
场景2:
用户A查询一条纪录,然后修改该条纪录;这时用户B修改该条纪录,这时用户A的事务里锁的性质由查询的共享锁企图上升到独占锁,而用户B里的独占锁由于A 有共享锁存在所以必须等A释放掉共享锁,而A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁,于是出现了死锁。这种死锁比较隐蔽,但在稍大点的项 目中经常发生。如在某项目中,页面上的按钮点击后,没有使按钮立刻失效,使得用户会多次快速点击同一按钮,这样同一段代码对数据库同一条记录进行多次操 作,很容易就出现这种死锁的情况。
解决方法:
- 对于按钮等控件,点击后使其立刻失效,不让用户重复点击,避免对同时对同一条记录操作。
2、使用乐观锁进行控制。乐观锁大多是基于数据版本(Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是 通过为数据库表增加一个“version”字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数 据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。乐观锁机制避免了长事务中的数据 库加锁开销(用户A和用户B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。Hibernate 在其数据访问引擎中内置了乐观锁实现。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不受我们系统的控制,因此可能会造 成脏数据被更新到数据库中。
3、使用悲观锁进行控制。悲观锁大多数情况下依靠数据库的锁机制实现,如Oracle的Select … for update语句,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。如一个金融系统, 当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户账户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读 出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对成百上千个并发,这 样的情况将导致灾难性的后果。所以,采用悲观锁进行控制时一定要考虑清楚。
场景3:
如果在事务中执行了一条不满足条件的update语句,则执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。类似的情 况还有当表中的数据量非常庞大而索引建的过少或不合适的时候,使得经常发生全表扫描,最终应用系统会越来越慢,最终发生阻塞或死锁。
解决方法:
SQL语句中不要使用太复杂的关联多表的查询;使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,建立相应的索引进行优化。
总结:
以上就是对mysql的事务、锁、隔离级别的相关理解,对于mysql而言默认是RR级别,也就是可重复读,在通常情况下不需要去修改其隔离级别,而且mysql对锁的处理已经做了很多,如果在高并发的情况下出现锁或者一致性问题,大多需要自己去修改逻辑业务。