Java Web数据库篇之漫谈MySQL锁

Java Web系列文章汇总贴: Java Web知识总结汇总


1、锁分类

1.1、悲观锁

释义

就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

如何应用

需要使用数据库的锁机制,如行锁中的共享锁(S锁)、互斥锁(X锁),还有意向锁(IS锁、IX锁)

select * from user where name=”zc” for update

这条 sql 语句锁定了user 表中所有符合检索条件(name=”zc”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。

1.2、乐观锁

释义

就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

如何应用

  • 使用自增长的整数表示数据版本号。更新时检查版本号是否一致,比如数据库中数据版本为1,更新提交时version=1+1,使用该version值(=2)与数据库version+1(=2)作比较,如果相等,则可以更新,如果不等则有可能其他程序已更新该记录,所以返回错误。

  • 使用时间戳来实现.

1.3、悲观锁&乐观锁优缺点

不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

乐观锁相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整。

参考:
并发控制中的乐观锁与悲观锁

1.4、多版本并发控制(MVCC)

除了悲观、乐观并发控制,数据库系统引入了另一种并发控制机制 - 多版本并发控制(Multiversion Concurrency Control),每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回;在这时,读写操作之间的冲突就不再需要被关注,而管理和快速挑选数据的版本就成了 MVCC 需要解决的主要问题。

MVCC 并不是一个与乐观和悲观并发控制对立的东西,它能够与两者很好的结合以增加事务的并发量。

参考:
浅谈数据库并发控制 - 锁和 MVCC


2、MySQL锁机制

2.1、概述

MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

2.2、MyISAM表锁

MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。
对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM存储引擎的读锁和写锁是互斥的,MyISAM表的读操作与写操作之间,以及写操作之间是串行的

2.3、InnoDB锁

2.3.1、InnoDB锁类型概述

Java Web数据库篇之漫谈MySQL锁_第1张图片

图片来自:
MySQL InnoDB锁机制全面解析分享

2.3.2、InnoDB锁机制

  • InnoDB行锁是通过给索引上的索引项加锁来实现的。所以,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
  • 使用不同的索引列,不同的事务也可以锁定不同的行
  • 使用同一索引的不同事务,存在锁冲突。
  • 条件中使用了索引,但是MySQL认为全表扫描更高效,会放弃使用索引,此时也是表锁,具体需要看执行计划。

InnoDB通过索引添加共享锁、排他锁

  • 共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
  • 排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE

另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁(InnoDB 的意向锁有什么作用?)。

  • 意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
  • 意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

锁互斥如下图:
Java Web数据库篇之漫谈MySQL锁_第2张图片

三种锁锁定介绍:
Java Web数据库篇之漫谈MySQL锁_第3张图片

  • 间隙锁(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这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。

还要特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!

参考:
MySQL中的锁(表锁、行锁,共享锁,排它锁,间隙锁)
五分钟了解Mysql的行级锁——《深究Mysql锁》
MySQL行锁与表锁


2.4、MySQL锁

2.4.1、事务隔离级别与锁

在并发事务处理带来的问题中,“更新丢失”通常应该是完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。数据库实现事务隔离的方式,基本可以分为以下两种。

  • 一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。
  • 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。

在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL InnoDB为例:
1、快照读:简单的select操作,属于快照读,不加锁。(已经加了排他锁也可以这样查询记录)

select * from table where ?; 

2、当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
下面语句都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;

2.4.2、事务隔离级别与MVCC

MVCC实现简介

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存了行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系
统版本号(System version number),每开始一个新的事物,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

MVCC如何操作

下面来看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。

SELECT

InnoDB会根据一下两个条件检查每行记录:
1、InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样就可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或修改过的。
2、行的删除版本号要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除。
只有符合上述这两个条件的记录,才能返回查询结果。

INSERT

InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

DELETE

InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

UPDATE

InnoDB为插入一行新纪录保存当前系统版本号作为行版本号。同时保存当前系统版本号到原来的行作为行删除标识。

这样做的优缺点:
  • 保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。
  • 不足之处是每行记录都需要额外的存储空间,需要更多的行检查工作,以及一些额外的维护工作。

MVCC适用隔离级别

MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容。因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行;而SERIALIZABLE则会对所有读取的行加锁。

参考:
MySQL InnoDB锁机制全面解析分享

2.4.3、MySQL MVCC实现原理与Undo Redo日志

MySQL InnoDB引擎,写数据前,先把当前数据写到Undo日志,便于多版本控制或者回滚保证事务的原子性,之后把更新后的数据写入Redo日志,产生脏页,后面由主线程写入磁盘。写的时候为了避免redo日志写一半出现故障,进行了二次写的设计,先写入二次写缓存,然后在写入磁盘。如果写磁盘出现故障,也可以从二次写缓存中恢复过来

MVCC实现原理

MySql每行记录有三个隐藏字段,通过指向回滚版本的字段或删除版本标识,实现多版本的读取。
Java Web数据库篇之漫谈MySQL锁_第4张图片

MVCC多版本快照由innodb的rollback segment构照的,一个sql进行查找数据当查找到某一个数据需要到回滚段中查找数据时,就会根据当前页上行数据的一个指针到回滚段中查找对应数据,在innodb的表主键中都会存在三个隐藏的字段:

  • DB_TRX_ID:该字段存储最后一个修改该行数据的事务ID,占用6byte的空间,MySQL的delete操作是标记删除,所以对应行数据的该字段就为一个删除标记。
  • DB_ROLL_PTR:该字段就记录执行roll segment的指针信息,当事务需要rollback时就通过该字段寻找记录重新构照行数据,该字段占用7byte空间。
  • DB_ROW_ID:记录每个行ID,该ID值为单调递增型整数,在innodb表指定了主键之后DB_ROW_ID存在于主键索引上,如果无主键该值就不会存在,占用6byte空间。

在一个sql进行查询时,读取到一行数据的DB_TRX_ID值和自己事物ID的对比,假如隔离级别为MySQL的默认级别,就只读取该ID值小于本身事物ID的数据,其余数据就需要通过DB_ROLL_PTR的信息到回滚段中读取。MVCC是否起到相应的作用需取决于数据库隔离级别的配置。

参考:
InnoDB的多版本并发控制机制—— MVCC底层实现
mysql 之mvcc多版本控制
innodb的多版本控制

2.4.4、MySQL RR级别下怎么解决幻读

  • 在快照读(select * from table)读情况下,mysql通过mvcc来避免幻读。
  • 在当前读(update/insert等)读情况下,mysql通过next-key来避免幻读

参考:
mysql Innodb在RR级别如何避免幻读


3、死锁

3.1、什么是死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等的进程称为死锁进程.

3.2、死锁产生的四个必要条件

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

3.3、如何预防死锁

阻止死锁的途径就是避免满足死锁条件的情况发生,为此我们在开发的过程中需要遵循如下原则:

  • 尽量避免并发的执行涉及到修改数据的语句。
  • 要求每一个事务一次就将所有要使用到的数据全部加锁,否则就不允许执行。
  • 预先规定一个加锁顺序,所有的事务都必须按照这个顺序对数据执行封锁。如不同的过程在事务内部对对象的更新执行顺序应尽量保证一致。
  • 每个事务的执行时间不可太长,对程序段的事务可考虑将其分割为几个事务。在事务中不要求输入,应该在事务之前得到输入,然后快速执行事务。
  • 使用尽可能低的隔离级别。
  • 数据存储空间离散法。该方法是指采用各种手段,将逻辑上在一个表中的数据分散的若干离散的空间上去,以便改善对表的访问性能。主要通过将大表按行或者列分解为若干小表,或者按照不同的用户群两种方法实现。
  • 编写应用程序,让进程持有锁的时间尽可能短,这样其它进程就不必花太长的时间等待锁被释放。

3.4、解除死锁

解除死锁的两种方法:
(1)终止(或撤销)进程。终止(或撤销)系统中的一个或多个死锁进程,直至打破循环环路,使系统从死锁状态中解除出来。
(2)抢占资源。从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以打破死锁状态。

摘自:
MySQL产生死锁的根本原因及解决方法

3.5、出现死锁如何定位

解除正在死锁的状态有两种方法:
第一种:
1.查询是否锁表

show OPEN TABLES where In_use > 0;

2.查询进程(如果您有SUPER权限,您可以看到所有线程。否则,您只能看到您自己的线程)

show processlist

3.杀死进程id(就是上面命令的id列)

kill id

第二种:
1.查看下在锁的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

2.杀死进程id(就是上面命令的trx_mysql_thread_id列)

kill 线程ID

其它关于查看死锁的命令:
1:查看当前的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

2:查看当前锁定的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;

3:查看当前等锁的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

摘自:
mysql查看死锁和解除锁

参考:
死锁产生原因和解决办法


4、锁优化

  • 尽可能让所有的数据检索都通过索引来完成,从而避免Innodb因为无法通过索引键加锁而升级为表级锁定;
  • 合理设计索引,让Innodb在索引键上面加锁尽可能准确,尽可能的缩小锁定范围,避免造成不必要的锁定而影响其他Query的执行;
  • 尽可能减少基于范围的数据检索过滤条件,避免间隙锁带来的负面影响而锁定了不该锁定的记录;
  • 尽量控制事务的大小,减少锁定的资源量和锁定时间长度;
  • 尽量使用较低的隔离级别; 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
  • 选择合理的事务大小,小事务发生锁冲突的几率也更小;
  • 给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;
  • 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;
  • 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响; 不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
  • 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。

参考:
MySQL锁机制及优化

你可能感兴趣的:(Java,Web知识总结)