锁以及数据库事务

数据库锁:数据库锁出现的原因是为了处理并发问题。

 

InnoDB行锁和表锁都支持!

innodb默认是行锁,前提条件是建立在索引之上的。如果筛选条件没有建立索引,会降级到表锁。即如果where条件中的字段都加了索引,则加的是行锁;否则加的是表锁。

MyISAM只支持表锁!

 

表锁 是开销比较小的策略,会锁定整张表。MyISAM和InnoDB都支持表级锁定。用户对表进行写操作,需要先获得锁,并且会阻塞用户对该表的所有读操作。 数据库中对表进行修改,如alter table会使用到表锁,会锁定整张表,因此此类操作在数据库中应该谨慎使用。

行锁 行锁可以高效的支持并发,当然锁开销也是最大。MySQL的InnoDB引擎中实现了行锁,在用户写数据时,只锁定需要操作的数据行,相比于表锁并发度更好。

 

 

InnoDB实现了以下两种类型的行锁:

共享锁(S锁):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。也叫做读锁:读锁是共享的,多个客户可以同时读取同一个资源,但不允许其他客户修改

SELECT * FROM t_cms_promotion t WHERE t.pro_id = ?LOCK IN SHARE MODE

排他锁(X锁):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。也叫做写锁:写锁是排他的,写锁会阻塞其他的写锁和读锁

 SELECT * FROM t_cms_promotion t WHERE t.pro_id = ? FOR UPDATE

 

注:如果让以上SQL语句上行锁,查询条件pro_id需要建立索引

 

 

InnoDB支持事务:(MySQL里面的事务是不支持嵌套的,在开启一个事务的情况下,再开启一个事务,会隐式的提交上一个事务)

事务是数据库区别于文件系统的额重要特性之一。事务符合ACID特性

原子性:原子性是指数据库事务是不可分割的工作单位。只有数据库事务中的所有数据库操作执行都成功,才算整个事务成功。如果事务中的任何一个SQL语句执行失败,已经执行的SQL语句也必须撤销,数据库状态退回到执行事务前的状态。

一致性:事务执行之前和之后,数据库的完整性约束没有被破坏。

隔离性:一个事物的影响在它提交前对其他事务不可见(通过锁来实现)

持久性:事务一旦提交,其结果就是永久性的。

 

事务隔离的四种级别:

Read uncommitted:

   会出现脏读:事务T1更新了数据R,事务T2读取了更新后的数据R,事务T1由于某种原因被撤销,恢复数据R。这样T2读取的数据和数据库中内容不一样。

Read committed:主要针对的是语句级别的快照,每次读取的都是当前最新的版本。

  可以解决脏读的问题,必须等到前一个事务提交了以后才能再对他进行读取。但又会导致不可重复读的问题,就是一个事务可能对同一行数据读取两次,读完第一次以后,另外一个事务对他进行了修改并提交了,这样第二次读的时候就会和第一次不一样。

Repeatable read:主要针对的是事务级别的快照,保证每次读取的都是当前事务的版本。即时被修改了也只会读取当前事务版本的数据。

  可以解决不可重复读的问题,但不能解决幻读的问题。(幻读:主要是在一个事务里查询两次数据,但中间有其他的事务插入了一条数据,就会导致前后读的不一样就好像出现幻读一样)

Serializable:串行可以避免上面的所有情况。

 

 

MVCC(Multi-Version Concurrency Control)多版本并发控制,可以简单地认为:MVCC就是行级锁的一个变种(升级版)。它就是用来解决上面的问题的。

MVCC实现的读写不阻塞正如其名:多版本并发控制--->通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本。

InnoDB的MVCC,实际上是在每一行的记录后面加了两个隐藏的列来实现的。一列保存的是行的创建时间,一列保存的是行的过期时间,当然存储的是系统版本号,并不是实际的时间,每开始一个新的事务,系统版本号都会自动递增

InnoDB会根据两个条件来检查每一行记录:

1.只查询系统版本号早于(小于等于)当前事务版本的数据行,这样就可以事务读取的行要么在事务开始前已经存在,要么就是事务自身插入或者修改的。(这个地方也就是不能解决幻读的问题,其他事务插入一行也会被读到

2.行的删除版本号要么没有定义,要么大于当前事务版本(在这个事务时候删除的,我当前事务当然还可以查到啊),这样就可以保证事务读取的行在事务开始之前没有被删除,事务自身删除当然也会读不到了。

当然我们在Repeatable read隔离级别上好像也可以解决幻读的问题。

 MVCC原理:将历史数据存一份快照,所以其他事务增加与删除数据,对于当前事务来说是不可见的

 

快照读:读取的是记录数据的可见版本(可能是过期的数据),不用加锁

 

当前读:读取的是记录数据的最新版本,并且当前读返回的记录都会加上锁,保证其他事务不会再并发的修改这条记录

1、select快照读(照片)

  当你执行select *之后,在A与B事务中都会返回4条一样的数据,这是不用想的,当执行select的时候,innodb默认会执行快照读,相当于就是给你目前的状态找了一张照片,以后执行select 的时候就会返回当前照片里面的数据,当其他事务提交了也对你不造成影响,和你没关系,这就实现了可重复读了,那这个照片是什么时候生成的呢?不是开启事务的时候,是当你第一次执行select的时候,也就是说,当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据......之后无论再有其他事务commit都没有关系,因为照片已经生成了,而且不会再生成了,以后都会参考这张照片。

2、update、insert、delete 当前读

  当你执行这几个操作的时候默认会执行当前读,也就是会读取最新的记录,也就是别的事务提交的数据你也可以看到,这样很好理解啊,假设你要update一个记录,另一个事务已经delete这条数据并且commit了,这样不是会产生冲突吗,所以你update的时候肯定要知道最新的信息啊。

 

普通的select就是快照读  select * from T where number = 1;在快照读的情况下就通过mvcc机制来避免幻读(这个之前一直理解有问题)

当前读和快照读不一样:当前读通过以下两种锁来解决幻读的问题:

  • 记录锁(行锁)
  • 间隙锁

select * from t where a=1 lock in share mode;属于当前读,这个属于行锁

SELECT * FROM emp WHERE empid > 100 FOR UPDATE;这个属于间隙锁

 

间隙锁(Next-Key锁)

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制不是所谓的间隙锁(Next-Key锁)。
举例来说,假如emp表中只有101条记录,其empid的值分别是1,2,...,100,101,下面的SQL:
  SELECT * FROM emp WHERE empid > 100 FOR UPDATE
是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
     InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另一方面,是为了满足其恢复和复制的需要。有关其恢复和复制对机制的影响,以及不同隔离级别下InnoDB使用间隙锁的情况。
    很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。

 

在上面讨论的事务的2,3两种隔离机制实际上都是在解决读写冲突的问题。

那么对于写写冲突呢?

也就是并发事务中的另外一个问题:丢失更新:一个事务的更新覆盖了其它事务的更新结果。

当然用Serializable隔离机制可以解决这个问题。

这里我们采用另外一种办法:

乐观锁是一种思想,具体实现是,表中有一个版本字段,第一次读的时候,获取到这个字段。处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第一次的一样。如果一样更新,反之拒绝。之所以叫乐观,因为这个模式没有从数据库加锁,等到更新的时候再判断是否可以更新。

悲观锁是数据库层面加锁,都会阻塞去等待锁

 

死锁

死锁的必要条件:

  1. 互斥:指进程对所分配的资源进行排它性使用,即在一段时间内某资源只由一个进程占用,如果此时还有其他进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
  2. 请求和保持:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不释放。
  3. 不剥夺:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  4. 环路等待:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

两个用户分别锁定一个资源,之后双方又都等待双方释放所锁定的资源,就产生一个锁定请求环,从而出现死锁。死锁往往出现在行级锁中。

数据库中通常死锁出现的原因以及解决方法:

1.事务之间对资源访问顺序的交替

例如一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。

 主要是程序逻辑的bug,对于数据库的多表操作时,尽量按照相同的顺序进行处理,比如按先A后B的顺序来处理,尽量避免同时锁定两个资源。

2.并发的修改某一个记录

例如事务A开始只是读数据,所以获得共享锁,这时候事务B想去更新这一条数据,去获得排他锁,等待A释放共享锁,这时候A突然想更新了,也想去获得排他锁,但有B在前面,所以就会出现一个相互等待的情况。

乐观锁加版本号控制或者悲观锁控制。

3.索引不当导致全表扫描

如果在事务中执行了一条不满足条件的语句,执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。类似的情况还有当表中的数据量非常庞大而索引建的过少或不合适的时候,使得经常发生全表扫描,最终应用系统会越来越慢,最终发生阻塞或死锁。
SQL语句中不要使用太复杂的关联多表的查询;使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,建立相应的索引进行优化。
4.事务封锁范围大且相互等待,使用低隔离级别,可以适当使用脏读,这样就避免了锁。

 我们有时候可以设定死锁超时参数。

参考:

https://juejin.im/post/5b55b842f265da0f9e589e79#heading-19

posted @ 2018-09-02 15:22  LeeJuly  阅读(208)  评论(0)    收藏  举报