锁是计算机协调多个进程或者多个线程之间并发访问同一资源的机制。在数据库系统中,除了传统的计算机资源(CPU、RAM、I/O)的争用之外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性和有效性是全部数据库系统须要考虑的问题。锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对于数据库尤为重要,也更加复杂。
对于mysql来讲,不一样的引擎锁的实现方式不同,因此须要根据不一样数据库引擎来进行讨论。mysql
MyISAM 引擎的表锁有2种模式,读锁和写锁。sql
MyISAM在执行查询语句(SELECT)前,会自动给涉及的全部表加读锁,在执行更新操做(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不须要用户干预,所以用户通常不须要直接用LOCK TABLE命令给MyISAM表显式加锁。数据库
MyISAM存储引擎的读和写锁是互斥,读操做是串行的。那么,一个进程请求某个MyISAM表的读锁,同时另外一个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先得到锁。不只如此,即便读进程先请求先到锁等待队列,写请求后到,写锁也会插到读请求以前!这是由于MySQL认为写请求通常比读请求重要。这也正是MyISAM表不太适合于有大量更新操做和查询操做应用的缘由,由于,大量的更新操做会形成查询操做很难得到读锁,从而可能永远阻塞。这种状况有时可能会变得很是糟糕!幸亏咱们能够经过一些设置来调节MyISAM的调度行为。经过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。session
InnoDB与MyISAM的最大不一样有两点:一是支持事务(TRANSACTION);二是采用了行级锁。行级锁和表级锁原本就有许多不一样之处,另外,事务的引入也带来了一些新问题。数据结构
事务是由一组SQL语句组成的逻辑处理单元,事务具备4属性,一般称为事务的ACID属性。并发
相对于串行处理来讲,并发事务处理能大大增长数据库资源的利用率,提升数据库系统的事务吞吐量,从而能够支持能够支持更多的用户。但并发事务处理也会带来一些问题,主要包括如下几种状况。性能
在并发事务处理带来的问题中,“更新丢失”一般应该是彻底避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,须要应用程序对要更新的数据加必要的锁来解决,所以,防止更新丢失应该是应用的责任。优化
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供必定的事务隔离机制来解决。数据库实现事务隔离的方式,基本能够分为如下两种。线程
一种是在读取数据前,对其加锁,阻止其余事务对数据进行修改。设计
另外一种是不用加任何锁,经过必定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供必定级别(语句级或事务级)的一致性读取。从用户的角度,好像是数据库能够提供同一数据的多个版本,所以,这种技术叫作数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也常常称为多版本数据库。
数据库的事务隔离级别越严格,并发反作用越小,但付出的代价也就越大,由于事务隔离实质上就是使事务在必定程度上“串行化”进行,这显然与“并发”是矛盾的,同时,不一样的应用对读一致性和事务隔离程度的要求也是不一样的,好比许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。
为了解决“隔离”与“并发”的矛盾,ISO/ANSI SQL92定义了4个事务隔离级别,每一个级别的隔离程度不一样,容许出现的反作用也不一样,应用能够根据本身业务逻辑要求,经过选择不一样的隔离级别来平衡"隔离"与"并发"的矛盾
隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
未提交读(Read uncommitted) | 最低级别,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
已提交度(Read committed) | 语句级 | 否 | 是 | 是 |
可重复读(Repeatable read) | 事务级 | 否 | 否 | 是 |
可序列化(Serializable) | 最高级别,事务级 | 否 | 否 | 否 |
InnoDB锁包括了共享锁和排他锁,同时为了容许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
当前锁模式/是否兼容 | X | IX | S | IS |
---|---|---|---|---|
X(排他锁) | 冲突 | 冲突 | 冲突 | 冲突 |
S(共享锁) | 冲突 | 冲突 | 兼容 | 兼容 |
IX(意向排他锁) | 冲突 | 兼容 | 冲突 | 兼容 |
IS(意向共享锁) | 冲突 | 兼容 | 兼容 | 兼容 |
键值在条件范围内但并不存在的记录,叫作“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。举例来讲,假若有以下数据表:
Id | Name | Age |
---|---|---|
1 | p1 | 10 |
4 | p2 | 18 |
8 | p3 | 12 |
这时候执行以下的sql语句:
select * from demo where id>1 for update;
这时候在数据库中会对id=4,8的记录添加行锁。同时会添加以下间隙锁:(2,4], (4,8],(8, +suprenum],这种状况下若是要在数据库中insert id在间隙锁之间的记录是会被阻塞的。
当咱们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁。
举例来讲有表demo:
Id | Name | Age |
---|---|---|
1 | p1 | 10 |
2 | p2 | 18 |
3 | p3 | 12 |
开启第一个命令执行窗口执行for update操做:
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 秒) mysql> select * from demo where id>2 for update; +----+------+------+ | id | name | age | +----+------+------+ | 3 | p3 | 12 | | 4 | p4 | 11 | +----+------+------+ 2 行于数据集 (0.02 秒) mysql>
这是一个范围条件的检索,InnoDB不只会对符合条件的empid值为3,4的记录加锁,也会对id大于3(这些记录并不存在)的“间隙”加锁。
开启第二个命令窗口执行插入数据操做:
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 秒) mysql> insert into demo(name,age) values('p5', 22); Lock wait timeout exceeded; try restarting transaction
能够看到这条sql执行会被阻塞,由于大于3的不存在的记录也被加上了间隙锁了,因此在执行插入时就执行不下去了,由于新增记录的id是大于3的。
除了范围查询会使用间隙锁以外,对于等值查询而不存在的记录也会使用间隙锁,一样打开第一个命令窗口,去更新一条不存在的记录。
假如当前数据库记录以下:
Id | Name | Age |
---|---|---|
1 | p1 | 10 |
2 | p2 | 18 |
6 | p3 | 12 |
开启第一个命令执行窗口执行for update操做:
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 秒) mysql> update demo set name='pp' where id=3; Query OK, 0 rows affected (0.00 秒)
这时候去另外一个命令窗口新增一条记录,能够看到这条记录一直阻塞到超时也没有执行,这是由于若是一个等值的记录不存在的状况下,mysql会使用间隙锁锁住它临近的前面和后面2条记录之间的全部记录。对于id=3的记录来讲不存在,因此他会锁住id(2-6]的全部记录。
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 秒) mysql> insert into demo(id,name, age) values(4,'p5', 88); Lock wait timeout exceeded; try restarting transaction
InnoDB使用间隙锁的目的,一方面是为了防止幻读,以知足相关隔离级别的要求,对于上面的例子,要是不使 用间隙锁,若是其余事务插入了empid大于100的任何记录,那么本事务若是再次执行上述语句,就会发生幻读;另一方面,是为了知足其恢复和复制的须要。有关其恢复和复制对锁机制的影响,以及不一样隔离级别下InnoDB使用间隙锁的状况,在后续的章节中会作进一步介绍。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这每每会形成严重的锁等待。所以,在实际应用开发中,尤为是并发插入比较多的应用,咱们要尽可能优化业务逻辑,尽可能使用相等条件来访问更新数据,避免使用范围条件,对于删除。
在MVCC并发控制中,读操做能够分为2类。快照读和当前读,其中快照读读取的是可见版本,不加锁;而当前读读取的是最新版本,而且当前读返回的记录都会加锁,保证其余并发事务不能修改该记录。那么MVCC中那些语句会加锁,那些不加锁呢?
快照读:简单的select属于快照读,不须要加锁。
当前读:特殊的select操做(select for update),insert/update/delete属于当前读,都须要加锁。
对于InnoDB表,在绝大部分状况下都应该使用行级锁,由于事务和行锁每每是咱们之因此选择InnoDB表的理由。但在个另特殊事务中,也能够考虑使用表级锁。
固然,应用中这两种事务不能太多,不然,就应该考虑使用MyISAM表。
在InnoDB下 ,使用表锁要注意如下两点。
(1)使用LOCK TALBES虽然能够给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层MySQL Server负责的,仅当autocommit=0、innodb_table_lock=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server才能感知InnoDB加的行锁,这种状况下,InnoDB才能自动识别涉及表级锁的死锁;不然,InnoDB将没法自动检测并处理这种死锁。
(2)在用LOCAK TABLES对InnoDB锁时要注意,要将AUTOCOMMIT设为0,不然MySQL不会给表加锁;事务结束前,不要用UNLOCAK TABLES释放表锁,由于UNLOCK TABLES会隐含地提交事务;COMMIT或ROLLBACK产不能释放用LOCAK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁,正确的方式见以下语句。
InnoDB 行锁是经过给索引上的索引项加锁来实现的,只有经过索引条件检索数据,InnoDB 才使用行级锁,不然,InnoDB将使用表锁。
因为 MySQL 行锁是针对索引加的锁,不是针对记录加的锁,因此虽然是访问不一样行的记录,可是若是是使用相同的索引键,是会出现锁冲突的,设计的时候要注意这一点
当表有多个索引的时候,不一样的事务可使用不一样的索引锁定不一样的行,另外,不管是使用主键索引、惟一索引或普通索引,InnoDB都会使用行锁来对数据加锁
即使在条件中使用了索引字段,可是否使用索引来检索数据是由 MySQL 经过判断不一样执行计划的代价来决定的,若是 MySQL 认为全表扫描效率更高,好比对一些很小的表,它就不会使用索引,这种状况下 InnoDB 将使用表锁,而不是行锁。所以,在分析锁冲突时,别忘了检查 SQL 的执行计划,以确认是否真正使用了索引
检索值的数据类型与索引字段不一样,虽然 MySQL 可以进行数据类型转换,但却不会使用索引,从而致使 InnoDB 使用表锁。经过用explain 检查两条SQL的执行计划
所谓死锁是2个或者2个以上的并发进程在执行过程当中因争夺资源而相互等待的现象,若无外力做用它们都将没法继续推动下去,此时称系统处于死锁状态或系统产生了死锁。这些永远相互等待的进程称为死锁进程。表级锁不会产生死锁,因此解决死锁主要仍是针对于InnoDB。
死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。
那么对应的解决死锁问题的关键就是:让不一样的session加锁有次序
这种状况很好理解,事务A和事务B操做两张表,但出现循环等待锁状况。
事务A | 事务B |
---|---|
begin | begin |
delete from t1 where id=1 | |
update t2 set name='test' where id=2 | |
update t2 set name='test' where id=2 | |
delete from t1 where id=1 |
这种状况比较常见,以前遇到两个job在执行数据批量更新时,jobA处理的的id列表为[1,2,3,4],而job处理的id列表为[8,9,10,4,2],这样就形成了死锁。
事务A | 事务B |
---|---|
begin | begin |
update t2 set name='test' where id=1 | |
update t2 set name='test' where id=2 | |
update t2 set name='test' where id=2 | |
update t2 set name='test' where id=1 |
这种状况比较隐晦,事务A在执行时,除了在二级索引加锁外,还会在聚簇索引上加锁,在聚簇索引上加锁的顺序是[1,4,2,3,5],而事务B执行时,只在聚簇索引上加锁,加锁顺序是[2,3,4,5],这样就形成了死锁的可能性。
事务A | 事务B |
---|---|
update msg set message='订单' where token>'abc' | delete from msg where id>1 |
innodb在RR级别下,以下的状况也会产生死锁,比较隐晦。不清楚的同窗能够自行根据上节的gap锁原理分析下。
事务A | 事务B |
---|---|
begin | begin |
update msg set message='订单' where token='asd' | |
update msg set message='订单' where token='aaa' | |
insert into msg values(null, 'aad', 'hello'); | |
commit | insert into msg values(null, 'bsa', 'hello'); |
commit |
(1)共享读锁(S)之间是兼容的,但共享读锁(S)和排他写锁(X)之间,以及排他写锁之间(X)是互斥的,也就是说读和写是串行的。
(2)在必定条件下,MyISAM容许查询和插入并发执行,咱们能够利用这一点来解决应用中对同一表和插入的锁争用问题。
(3)MyISAM默认的锁调度机制是写优先,这并不必定适合全部应用,用户能够经过设置LOW_PRIPORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。
(4)因为表锁的锁定粒度大,读写之间又是串行的,所以,若是更新操做较多,MyISAM表可能会出现严重的锁等待,能够考虑采用InnoDB表来减小锁冲突。
(1)InnoDB的行销是基于索引实现的,若是不经过索引访问数据,InnoDB会使用表锁。
(2)InnoDB间隙锁机制,以及InnoDB使用间隙锁的缘由。
(3)在不一样的隔离级别下,InnoDB的锁机制和一致性读策略不一样。
(4)MySQL的恢复和复制对InnoDB锁机制和一致性读策略也有较大影响。
(5)锁冲突甚至死锁很难彻底避免。
在了解InnoDB的锁特性后,用户能够经过设计和SQL调整等措施减小锁冲突和死锁,包括:
间隙锁是针对insert而致使阻塞的锁,实际开发中应该尽可能避免间隙锁的发生,对于范围查询以及等值查询的修改,尽可能先去查询数据是否存在,存在再去执行更新和删除操做,不然可能会产生死锁。
锁是解决并发的机制,事务是对一组sql处理单元的统称,ACID是事务的4个属性,隔离级别是对事务之间隔离程度的一种描述。