由于在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用之外,数据也是一种供须要用户共享的资源python
当并发事务同时访问一个共享的资源时,有可能致使数据不一致、数据无效等问题mysql
例如在上一篇介绍过的事务并发状况下出现的读现象: 脏读、不可重复读、幻读等算法
为了解决这些方法, 主流的数据库软件都提供了锁机制, 以及事务隔离级别的概念sql
而锁机制能够将并发的数据访问顺序化, 以保证数据库中数据的一致性和有效性数据库
ps : 锁冲突也是影响数据库并发性能的一个重要因素, 对锁对数据库很是重要, 但也更加复杂安全
按照按锁的粒度划分 : 可分为行级锁、表级锁、页级锁架构
按照级别划分 : 可分为共享锁、排他锁、意向锁、间隙锁(Next-Key)并发
按照使用方式分 : 可分为乐观锁、悲观锁高并发
按照加锁方式分 : 可分为自动锁、显式锁性能
按照操做划分 : DDL锁、DML锁
其余 : 死锁、MVCC
DML锁(data locks, 数据锁),用于保护数据的完整性, 其中包括行级锁(Row Locks (TX锁))、表级锁(table lock(TM锁))
DDL锁(dictionary locks,数据字典锁), 用于保护数据库对象的结构,如表、索引等的结构定义; 其中包排他DDL(Exclusive DDL lock)、共享DDL锁(Share DDL lock),可中断解析锁(Breakable parse locks)
在DBMS中, 能够按照锁的粒度把数据库锁分为行级锁(Innodb引擎默认使用)、表级锁(Myisam引擎默认使用)和页级锁(BDB引擎默认使用)
行级锁是Mysql中锁定粒度最细的一种锁, 表示只针对当前操做的行进行加锁; 行级锁能大大减小数据库操做的冲突; 其加锁粒度最小, 但加锁的开销也最大; 行级锁分为共享锁和排它锁
开销大, 加锁慢; 会出现死锁; 锁定粒度最小, 发生锁冲突的几率最低, 并发也最高
Innodb 引擎
"共享锁(s)" : select * from [表名] where [条件] lock in share mode; "排它锁(x)" : select * from [表名] where [条件] for update;
表级锁是MySQL中锁定粒度最大的一种锁, 表示对当前操做的整张表加锁, 它实现简单, 资源消耗较少, 被大部分MySQL引擎支持; 最常使用的Myisam与Innodb都支持表级锁定; 表级锁定分为表共享读锁(共享锁)和表独占写锁(排它锁)
开销小, 加锁快; 不会出现死锁; 锁定粒度大, 发出锁冲突的几率最高, 并发度最低
Myisam引擎、Memory引擎、Innodb引擎
"语法" : lock table [表名1] [resd|write],[表名2] [resd|write], ...; # 能够加读锁或者写做 lock table user read; # 将表 user 加上写锁 show open tables where in_user>=1; # 查看当前会话锁定一次以上的表 update user set name="song" where id=1; # 更新表数据(会提示表被锁定) unlock tables; # 释放当前会话持有的任何锁 update user set name="song" where id=1; # 再次更新能够成功
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁; 表级锁速度快, 但冲突多, 行级冲突少, 但速度慢; 因此取了折衷的页级, 一次锁定相邻的一组记录; BDB支持页级锁
开销和加锁时间界于表锁和行锁之间; 会出现死锁; 锁定粒度界于表锁和行锁之间, 并发度通常
BDB引擎
InnoDB行锁不是直接锁记录, 而是锁索引, 这一点MySQL与Oracle不一样, 后者是经过在数据块中对相应数据行加锁来实现的
InnoDB这种行锁实现特色意味着:只有经过索引条件检索数据, InnoDB才使用行级锁, 不然,InnoDB将锁住全部行, 实现的效果至关因而表锁
演示
create table t01(id int,name char(16)); # 建立表(而且不添加索引) insert t01 value(1,"aa"),(2,"bb"),(3,"cc"),(4,"dd"); # 插入记录 # 开启两个会话窗口, 分别手动开启事务 # 事务1对 id=2 进行锁行操做, 事务2对 id!=2 的行进行更新 # 发现阻塞, 一段时间后显示超时 : ERROR 1205 (HY000): Lock wait timeout exceeded;....
create index index_id on t01(id); # 为 id 字段建立索引 desc t01; # 查看表结构 # 再次重复上面开启事务的步骤
行锁锁的是索引, 索引又分为主键索引和非主键索引两种, 因此锁定的方式分为如下三种 :
在实际应用中, 要特别注意InnoDB行锁的这一特性, 不然可能致使大量的锁冲突, 从而影响并发性能 :
与对行处理有关的语句有 : insert
、update
、delete
、select
, 这四类语句在操做记录时, 均可觉得行加上锁, 但须要注意的是 :
# 手动开启事务1,对 id=1 的记录进行增删改操做, 而且为提交状态 # 开启事务2,也对 id=1 的记录进行增删改操做 # 发现阻塞在原地,一段时间后显示超时 : ERROR 1205 (HY000): Lock wait timeout exceeded ....
"共享锁(s)" : select * from [表名] where [条件] lock in share mode; "排它锁(x)" : select * from [表名] where [条件] for update;
共享锁又称为读锁, 简称S锁, 顾名思义, 共享锁就是多个事务对于同一数据能够共享一把锁, 获准共享锁的事务只能读数据, 不能修改数据直到已释放全部共享锁, 因此共享锁能够支持并发读
若是事务1对数据A加上共享锁后, 则其余事务只能对A再加共享锁或不加锁 (在其余事务里必定不能再加排他锁, 可是在事务1本身里面是能够加的), 反之亦然
select * from [表名] where [条件] lock in share mode;
在查询语句后面增长
lock in share mode
,Mysql会对查询结果中的每行都加共享锁,当没有其余线程对查询结果集中的任何一行使用排他锁时, 能够成功申请共享锁, 不然会被阻塞; 其余线程也能够读取使用了共享锁的表, 并且这些线程读取的是同一个版本的数据
排他锁又称为写锁, 简称X锁, 顾名思义, 排他锁就是不能与其余所并存, 如一个事务获取了一个数据行的排他锁, 其余事务就不能再对该行加任何类型的其余他锁 (共享锁和排他锁), 可是获取排他锁的事务是能够对数据就行读取和修改
select * from [表名] where [条件] for update;
在查询语句后面增长
for update
, Mysql会对查询结果中的每行都加排他锁, 当没有其余线程对查询结果集中的任何一行使用排他锁时, 能够成功申请排他锁, 不然会被阻塞
加过排他锁的数据行在其余事务种是不能修改数据的, 也不能经过
for update
和lock in share mode
锁的方式查询数据, 但能够直接经过select ...from...
查询数据, 由于普通select查询没有任何锁机制
创建了索引且命中的状况下:
意向锁是表级锁, 其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型
当一个事务在须要获取资源锁定的时候, 若是遇到本身须要的资源已经被排他锁占用的时候, 该事务能够须要锁定行的表上面添加一个合适的意向锁
若是本身须要一个共享锁, 那么就在表上面添加一个意向共享锁; 而若是本身须要的是某行(或者某些行)上面添加一个排他锁的话, 则先在表上面添加一个意向排他锁
- 意向共享锁(IS) : 事务打算给数据行共享锁; 事务在给一个数据行加共享锁前必须先取得该表的IS锁
- 意向排他锁(IX) : 事务打算给数据行加排他锁; 事务在给一个数据行加排他锁前必须先取得该表的IX锁
ps : 意向锁是InnoDB自动加的,不须要用户干预
绝大部分状况使用行锁, 但在个别特殊事务中, 也能够考虑使用表锁
若使用默认的行锁,不只该事务执行效率低(由于须要对较多行加锁,加锁是须要耗时的); 并且可能形成其余事务长时间锁等待和锁冲突; 这种状况下能够考虑使用表锁来提升该事务的执行速度
这种状况也能够考虑一次性锁定事务涉及的表, 从而避免死锁、减小数据库因事务回滚带来的开销固然, 应用中这两种事务不能太多, 不然, 就应该考虑使用Myisam
经过检查 InnoDB_row_lock 状态变量来分析系统上的行锁的争夺状况, 在着手根据状态量来分析改善
show status like "innodb_row_lock%"; # 查看行锁状态
Innodb 四种锁定模式的共存逻辑关系 :
共享锁(S) | 排他锁(X) | 意向共享锁(IS) | 意向排他锁(Ⅸ) | |
---|---|---|---|---|
共享锁(S) | 兼容 | 冲突 | 兼容 | 冲突 |
排他锁(X) | 冲突 | 冲突 | 冲突 | 冲突 |
意向共享锁(IS) | 兼容 | 冲突 | 兼容 | 兼容 |
意向排他锁(Ⅸ) | 冲突 | 冲突 | 兼容 | 兼容 |
若是一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,若是二者不兼容,该事务就要等待锁释放
select * from emp where id>=100 for update;
语句的时候, 它是一个范围条件检索, 而且命中了索引, InnoDB不只会对符合条件的emp_id值为100的记录加锁, 也会对em_pid大于100 (这些记录并不存在) 的"间隙"加锁ps : 对于行的查询, Innodb采用的都是Next-Key Lock, 主要目的是解决幻读的问题, 以知足相关隔离级别以及恢复和复制的须要
MyISAM中是不会产生死锁的,由于MyISAM老是一次性得到所需的所有锁,要么所有知足,要么所有等待。而在InnoDB中,锁是逐步得到的,就形成了死锁的可能
create index index_id on t01; # 删除以前的索引 alter table t01 modify id int primary ket; # 将id字段设置成主键 create index index_id on t01(id); # 建立汇集索引 create index index_name on t01(name); # 建立辅助索引(非汇集索引) desc t01; # 查看表结构
第二个死锁问题,只有多个事务同时运行的状况下才可能出现,但隐蔽性极强,虽然每一个Session都只有一条语句,仍旧会产生死锁。要分析这个死锁,首先必须用到本文前面提到的MySQL加锁的规则。针对Session 1,从name索引出发,读到的[hdc, 1],[hdc, 6]均知足条件,不只会加name索引上的记录X锁,并且会加聚簇索引上的记录X锁,加锁顺序为先[1,hdc,100],后[6,hdc,10]。而Session 2,从pubtime索引出发,[10,6],[100,1]均知足过滤条件,一样也会加聚簇索引上的记录X锁,加锁顺序为[6,hdc,10],后[1,hdc,100]。发现没有,跟Session 1的加锁顺序正好相反,若是两个Session刚好都持有了第一把锁,请求加第二把锁,死锁就发生了
- 在MySQL中, 行级锁并非直接锁记录, 而是锁索引; 索引分为主键索引和非主键索引两种
- 若是一条sql语句操做了主键索引, MySQL就会锁定这条主键索引
- 若是一条语句操做了非主键索引, MySQL会先锁定该非主键索引, 再锁定相关的主键索引
- 在update、delete操做时, MySQL不只锁定WHERE条件扫描过的全部索引记录, 并且会锁定相邻的键值, 即所谓的next-key locking
- 死锁的发生与否, 并不在于事务中有多少条SQL语句, 死锁的关键在于 : 两个(或以上)的Session加锁的顺序不一致
- 而使用上面提到的, 分析MySQL每条SQL语句的加锁规则, 分析出每条语句的加锁顺序
- 而后检查多个并发SQL间是否存在以相反的顺序加锁的状况, 就能够分析出各类潜在的死锁状况, 也能够分析出线上死锁发生的缘由
发生死锁后, InnoDB通常均可以检测到, 并使一个事务释放锁回退, 另外一个获取锁完成事务, 上面实验也证实了, 但也有多种方法能够避免死锁:
数据库管理系统 (DBMS) 中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性
乐观并发控制 (乐观锁) 和悲观并发控制 (悲观锁) 是并发控制主要采用的技术手段。
不管是悲观锁仍是乐观锁, 都是人们定义出来的概念, 能够认为是一种思想; 其实不只仅是关系型数据库系统中有乐观锁和悲观锁的概念, 像memcache、hibernate、tair等都有相似的概念
针对于不一样的业务场景, 应该选用不一样的并发控制方式; 因此, 不要把乐观并发控制和悲观并发控制狭义的理解为DBMS中的概念, 更不要把他们和数据中提供的锁机制 (行锁、表锁、排他锁、共享锁) 混为一谈; 其实, 在DBMS中, 悲观锁正是利用数据库自己提供的锁机制来实现的
悲观的认为操做数据库就是修改数据, 为了不数据库中的数据同时被修改, 直接对该操做加锁处理
当咱们要对一个数据库中的一条数据进行修改的时候, 为了不同时被其余人修改, 最好的办法就是直接对该数据进行加锁以防止并发
这种借助数据库锁机制在修改数据以前先锁定, 再修改的方式被称之为悲观并发控制 (又名“悲观锁”,Pessimistic Concurrency Control, 缩写“PCC”)
在关系数据库管理系统里,悲观并发控制 (又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”) 是一种并发控制的方法
它能够阻止一个事务以影响其余用户的方式来修改数据; 若是一个事务执行的操做都某行数据应用了锁,那只有当这个事务把锁释放, 其余事务才可以执行与该锁冲突的操做
悲观并发控制主要用于数据争用激烈的环境, 以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中
悲观锁, 正如其名, 它指的是对数据被外界 (包括本系统当前的其余事务, 以及来自外部系统的事务处理) 修改持保守态度(悲观), 所以, 在整个数据处理过程当中, 将数据处于锁定状态;
悲观锁的实现, 每每依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性, 不然, 即便在本系统中实现了加锁机制, 也没法保证外部系统不会修改数据) , 如今互联网高并发的架构中, 受到 fail-fast 思路的影响, 悲观锁已经很是少见了
在对任意记录进行修改前, 先尝试为该记录加上排他锁 (exclusive locking)
若是加锁失败, 说明该记录正在被修改, 那么当前查询可能要等待或者抛出异常; 具体响应方式由开发者根据实际须要决定
若是成功加锁, 那么就能够对记录作修改, 事务完成后就会解锁了
其间若是有其余对该记录作修改或加排他锁的操做, 都会等待咱们解锁或直接抛出异常
ps : 行锁、表锁、读锁、写锁都是在操做以前先上排他锁
悲观并发控制主要用于数据争用激烈的环境, 以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中
悲观并发控制其实是“先取锁再访问”的保守策略, 为数据处理的安全提供了保证
- 在效率方面, 处理加锁的机制会让数据库产生额外的开销, 还有增长产生死锁的机会
- 在只读型事务处理中因为不会产生冲突, 也不必使用锁, 这样作只能增长系统负载
- 会下降了并行性, 一个事务若是锁定了某行数据, 其余事务就必须等待该事务处理完才能够处理那行数
乐观的认为操做数据库不会形成冲突, 只有在对数据进行更新的时候才会进行校验. 若是冲突就返回错误让用户决定如何去作
- 在关系数据库管理系统里, 乐观并发控制 (又名“乐观锁”, Optimistic Concurrency Control, 缩写“OCC”) 是一种并发控制的方法
- 它假设多用户并发的事务在处理时不会彼此互相影响, 各事务可以在不产生锁的状况下处理各自影响的那部分数据
- 在提交数据更新以前, 每一个事务会先检查在该事务读取数据后, 有没有其余事务又修改了该数据
- 若是其余事务有更新的话, 正在提交的事务会进行回滚; 乐观事务控制最先是由孔祥重 (H.T.Kung) 教授提出
乐观锁 (Optimistic Locking) 相对悲观锁而言, 乐观锁假设认为数据通常状况下不会形成冲突, 因此在数据进行提交更新的时候, 才会正式对数据的冲突与否进行检测, 若是发现冲突了, 则让返回用户错误的信息, 让用户决定如何去作
相对于悲观锁, 在对数据库进行处理的时候, 乐观锁并不会使用数据库提供的锁机制; 通常的实现乐观锁的方式就是记录数据版本
数据版本 : 为数据增长的一个版本标识, 当读取数据时, 将版本标识的值一同读出, 数据每更新一次, 同时对版本标识进行更新
当咱们提交更新的时候, 判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对, 若是数据库表当前版本号与第一次取出来的版本标识值相等, 则予以更新, 不然认为是过时数据
每一行数据多一个字段version, 每次更新数据对应版本号+1,
原理 : 读出数据, 将版本号一同读出, 以后更新, 版本号+1, 提交数据版本号大于数据库当前版本号, 则予以更新, 不然认为是过时数据, 从新读取数据
每一行数据多一个字段 time
原理 : 读出数据, 将时间戳一同读出, 以后更新, 提交数据时间戳等于数据库当前时间戳, 则予以更新, 不然认为是过时数据, 从新读取数据
乐观并发控制相信事务之间的数据竞争(data race)的几率是比较小的, 所以尽量直接作下去, 直到提交的时候才去锁定, 因此不会产生任何锁和死锁
二者的区别于使用场景 :
随着互联网三高架构 (高并发、高性能、高可用) 的提出, 悲观锁已经愈来愈少的被使用到生产环境中了, 尤为是并发量比较大的业务场景