深刻理解Mysql——锁、事务与并发控制

本文对锁、事务、并发控制作一个总结,看了网上不少文章,描述很是不许确。若有与您观点不一致,欢迎有理有据的拍砖!html

mysql服务器逻辑架构

这里写图片描述

每一个链接都会在mysql服务端产生一个线程(内部经过线程池管理线程),好比一个select语句进入,mysql首先会在查询缓存中查找是否缓存了这个select的结果集,若是没有则继续执行 解析、优化、执行的过程;不然会之间从缓存中获取结果集。mysql

mysql并发控制——共享锁、排他锁

共享锁

共享锁也称为读锁,读锁容许多个链接能够同一时刻并发的读取同一资源,互不干扰;算法

排他锁

排他锁也称为写锁,一个写锁会阻塞其余的写锁或读锁,保证同一时刻只有一个链接能够写入数据,同时防止其余用户对这个数据的读写。sql

锁策略

锁的开销是较为昂贵的,锁策略其实就是保证了线程安全的同时获取最大的性能之间的平衡策略。数据库

  • mysql锁策略:talbe lock(表锁)

表锁是mysql最基本的锁策略,也是开销最小的锁,它会锁定整个表;缓存

具体状况是:若一个用户正在执行写操做,会获取排他的“写锁”,这可能会锁定整个表,阻塞其余用户的读、写操做;安全

若一个用户正在执行读操做,会先获取共享锁“读锁”,这个锁运行其余读锁并发的对这个表进行读取,互不干扰。只要没有写锁的进入,读锁能够是并发读取统一资源的。服务器

一般发生在DDL语句\DML不走索引的语句中,好比这个DML update table set columnA=”A” where columnB=“B”. 
若是columnB字段不存在索引(或者不是组合索引前缀),会锁住全部记录也就是锁表。若是语句的执行可以执行一个columnB字段的索引,那么会锁住知足where的行(行锁)。session

  • mysql锁策略:row lock(行锁)

行锁能够最大限度的支持并发处理,固然也带来了最大开销,顾名思义,行锁的粒度实在每一条行数据。架构

事务

事务就是一组原子性的sql,或者说一个独立的工做单元。 
事务就是说,要么mysql引擎会所有执行这一组sql语句,要么所有都不执行(好比其中一条语句失败的话)。

好比,tim要给bill转帐100块钱: 
1.检查tim的帐户余额是否大于100块; 
2.tim的帐户减小100块; 
3.bill的帐户增长100块; 
这三个操做就是一个事务,必须打包执行,要么所有成功,要么所有不执行,其中任何一个操做的失败都会致使全部三个操做“不执行”——回滚。

CREATE DATABASE IF NOT EXISTS employees;
USE employees;

CREATE TABLE `employees`.`account` (
  `id` BIGINT (11) NOT NULL AUTO_INCREMENT,
  `p_name` VARCHAR (4),
  `p_money` DECIMAL (10, 2) NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`)
) ;
INSERT INTO `employees`.`account` (`id`, `p_name`, `p_money`) VALUES ('1', 'tim', '200'); 
INSERT INTO `employees`.`account` (`id`, `p_name`, `p_money`) VALUES ('2', 'bill', '200'); 

START TRANSACTION;
SELECT p_money FROM account WHERE p_name="tim";-- step1
UPDATE account SET p_money=p_money-100 WHERE p_name="tim";-- step2
UPDATE account SET p_money=p_money+100 WHERE p_name="bill";-- step3
COMMIT;

一个良好的事务系统,必须知足ACID特色:

事务的ACID

  • A:atomiciy原子性 
    一个事务必须保证其中的操做要么所有执行,要么所有回滚,不可能存在只执行了一部分这种状况出现。

  • C:consistency一致性 
    数据必须保证从一种一致性的状态转换为另外一种一致性状态。 
    好比上一个事务中执行了第二步时系统崩溃了,数据也不会出现bill的帐户少了100块,可是tim的帐户没变的状况。要么维持原装(所有回滚),要么bill少了100块同时tim多了100块,只有这两种一致性状态的

  • I:isolation隔离性 
    在一个事务未执行完毕时,一般会保证其余Session 没法看到这个事务的执行结果

  • D:durability持久性 
    事务一旦commit,则数据就会保存下来,即便提交完以后系统崩溃,数据也不会丢失。

隔离级别

这里写图片描述

查看系统隔离级别:
select @@global.tx_isolation;
查看当前会话隔离级别
select @@tx_isolation;
设置当前会话隔离级别
SET session TRANSACTION ISOLATION LEVEL serializable;
设置全局系统隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

READ UNCOMMITTED(未提交读,可脏读)

事务中的修改,即便没有提交,对其余会话也是可见的。 
能够读取未提交的数据——脏读。脏读会致使不少问题,通常不适用这个隔离级别。 
实例:

-- ------------------------- read-uncommitted实例 ------------------------------
-- 设置全局系统隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- Session A
START TRANSACTION;
SELECT * FROM USER;
UPDATE USER SET NAME="READ UNCOMMITTED";
-- commit;

-- Session B
SELECT * FROM USER;

//SessionB Console 能够看到Session A未提交的事物处理,在另外一个Session 中也看到了,这就是所谓的脏读
id  name
2   READ UNCOMMITTED
34  READ UNCOMMITTED

READ COMMITTED(提交读或不可重复读,幻读)

通常数据库都默认使用这个隔离级别(mysql不是),这个隔离级别保证了一个事务若是没有彻底成功(commit执行完),事务中的操做对其余会话是不可见的

-- ------------------------- read-cmmitted实例 ------------------------------
-- 设置全局系统隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ  COMMITTED;
-- Session A
START TRANSACTION;
SELECT * FROM USER;
UPDATE USER SET NAME="READ COMMITTED";
-- COMMIT;

-- Session B
SELECT * FROM USER;

//Console OUTPUT:
id  name
2   READ UNCOMMITTED
34  READ UNCOMMITTED


---------------------------------------------------
-- 当 Session  A执行了commit,Session B获得以下结果:
id  name
2   READ COMMITTED
34  READ COMMITTED

也就验证了read committed级别在事物未完成commit操做以前修改的数据对其余Session 不可见,执行了commit以后才会对其余Session 可见。 
咱们能够看到Session B两次查询获得了不一样的数据。

read committed隔离级别解决了脏读的问题,可是会对其余Session 产生两次不一致的读取结果(由于另外一个Session 执行了事务,一致性变化)。

REPEATABLE READ(可重复读)

一个事务中屡次执行统一读SQL,返回结果同样。 
这个隔离级别解决了脏读的问题,幻读问题。这里指的是innodb的rr级别,innodb中使用next-key锁对”当前读”进行加锁,锁住行以及可能产生幻读的插入位置,阻止新的数据插入产生幻行。 
下文中详细分析。

具体请参考mysql手册

https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html

SERIALIZABLE(可串行化)

最强的隔离级别,经过给事务中每次读取的行加锁,写加写锁,保证不产生幻读问题,可是会致使大量超时以及锁争用问题。

多版本并发控制-MVCC

MVCC(multiple-version-concurrency-control)是个行级锁的变种,它在普通读状况下避免了加锁操做,所以开销更低。 
虽然实现不一样,但一般都是实现非阻塞读,对于写操做只锁定必要的行

  • 一致性读 (就是读取快照) 
    select * from table ….;
  • 当前读(就是读取实际的持久化的数据) 
    特殊的读操做,插入/更新/删除操做,属于当前读,处理的都是当前的数据,须要加锁。 
    select * from table where ? lock in share mode; 
    select * from table where ? for update; 
    insert; 
    update ; 
    delete;

注意:select …… from where…… (没有额外加锁后缀)使用MVCC,保证了读快照(mysql称为consistent read),所谓一致性读或者读快照就是读取当前事务开始以前的数据快照,在这个事务开始以后的更新不会被读到。详细状况下文select的详述。

对于加锁读SELECT with FOR UPDATE(排他锁) or LOCK IN SHARE MODE(共享锁)、update、delete语句,要考虑是不是惟一索引的等值查询。

写锁-recordLock,gapLock,next key lock

对于使用到惟一索引 等值查询:好比,where columnA=”…” ,若是columnA上的索引被使用到, 
那么会在知足where的记录上加行锁(for update是排他锁,lock in shared 是共享锁,其余写操做加排他锁)。这里是行级锁,record lock。

对于范围查询(使用非惟一的索引): 
好比(作范围查询):where columnA between 10 and 30 ,会致使其余会话中10之后的数据都没法插入(next key lock),从而解决了幻读问题。

这里是next key lock 会包括涉及到的全部行。 
next key lock=recordLock+gapLock,不只锁住相关数据,并且锁住边界,从而完全避免幻读可点击查看这篇推荐文章

对于没有索引 
锁表 
一般发生在DDL语句\DML不走索引的语句中,好比这个DML update table set columnA=”A” where columnB=“B”. 
若是columnB字段不存在索引(或者不是组合索引前缀),会锁住全部记录也就是锁表。若是语句的执行可以执行一个columnB字段的索引,那么会锁住知足where的行(行锁)。

INNODB的MVCC一般是经过在每行数据后边保存两个隐藏的列来实现(实际上是三列,第三列是用于事务回滚,此处略去), 
一个保存了行的建立版本号,另外一个保存了行的更新版本号(上一次被更新数据的版本号) 
这个版本号是每一个事务的版本号,递增的。

这样保证了innodb对读操做不须要加锁也能保证正确读取数据。

MVCC select无锁操做 与 维护版本号

下边在mysql默认的Repeatable Read隔离级别下,具体看看MVCC操做:

  • Select(快照读,所谓读快照就是读取当前事务以前的数据。): 
    a.InnoDB只select查找版本号早于当前版本号的数据行,这样保证了读取的数据要么是在这个事务开始以前就已经commit了的(早于当前版本号),要么是在这个事务自身中执行建立操做的数据(等于当前版本号)。

    b.查找行的更新版本号要么未定义,要么大于当前的版本号(为了保证事务能够读到老数据),这样保证了事务读取到在当前事务开始以后未被更新的数据。 
    注意: 这里的select不能有for update、lock in share 语句。 
    总之要只返回知足如下条件的行数据,达到了快照读的效果:

(行建立版本号< =当前版本号 && (行更新版本号==null or 行更新版本号>当前版本号 ) )
  •  
  • Insert

    InnoDB为这个事务中新插入的行,保存当前事务版本号的行做为行的行建立版本号。

  • Delete 
    InnoDB为每个删除的行保存当前事务版本号,做为行的删除标记。

  • Update

    将存在两条数据,保持当前版本号做为更新后的数据的新增版本号,同时保存当前版本号做为老数据行的更新版本号。

当前版本号—写—>新数据行建立版本号 && 当前版本号—写—>老数据更新版本号();
  •  

脏读 vs 幻读 vs 不可重复读

脏读一事务未提交的中间状态的更新数据 被其余会话读取到。 当一个事务正在访问数据,而且对数据进行了修改,而这种修改尚未 提交到数据库中(commit未执行),这时,另外会话也访问这个数据,由于这个数据是尚未提交, 那么另一个会话读到的这个数据是脏数据,依据脏数据所作的操做也多是不正确的。

不可重复读简单来讲就是在一个事务中读取的数据可能产生变化,ReadCommitted也称为不可重复读

在同一事务中,屡次读取同一数据返回的结果有所不一样。换句话说就是,后续读取能够读到另外一会话事务已提交的更新数据。 相反,“可重复读”在同一事务中屡次读取数据时,可以保证所读数据同样,也就是,后续读取不能读到另外一会话事务已提交的更新数据。

幻读:会话T1事务中执行一次查询,而后会话T2新插入一行记录,这行记录刚好能够知足T1所使用的查询的条件。而后T1又使用相同 的查询再次对表进行检索,可是此时却看到了事务T2刚才插入的新行。这个新行就称为“幻像”,由于对T1来讲这一行就像忽然 出现的同样。 
innoDB的RR级别没法作到彻底避免幻读,下文详细分析。

----------------------------------前置准备----------------------------------------
prerequisite:
-- 建立表
mysql>
CREATE TABLE `t_bitfly` (
   `id` bigint(20) NOT NULL DEFAULT '0',
   `value` varchar(32) DEFAULT NULL,
   PRIMARY KEY (`id`)
 )

-- 确保当前隔离级别为默认的RR级别

mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.00 sec)
---------------------------------------开始--------------------------------------------- 


session A                                           |   session B
                                                    |
                                                    |
mysql> START TRANSACTION;                           |   mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)                |   Query OK, 0 rows affected (0.00 sec)                                        
                                                    |   
                                                    |
mysql> SELECT * FROM test.t_bitfly;                 |   mysql> SELECT * FROM test.t_bitfly; 
Empty set (0.00 sec)                                |   Empty set (0.00 sec)
                                                    |
                                                    |   mysql> INSERT INTO t_bitfly VALUES (1, 'test');
                                                    |   Query OK, 1 row affected (0.00 sec)
                                                    |
                                                    |
mysql> SELECT * FROM test.t_bitfly;                 |
Empty set (0.00 sec)                                |
                                                    |
                                                    |   mysql> commit;
                                                    |   Query OK, 0 rows affected (0.01 sec)                                                
mysql> SELECT * FROM test.t_bitfly;                 |
Empty set (0.00 sec)                                |
-- 能够看到虽然两次执行结果返回的数据一致,         |
-- 可是不能说明没有幻读。接着看:                   |
                                                    |
mysql> INSERT INTO t_bitfly VALUES (1, 'test');     |
ERROR 1062 (23000):                                 |
Duplicate entry '1' for key 'PRIMARY'               |
                                                    |
-- 明明为空的表,为何说主键重复?——幻读出现 !!!       |

如何保证rr级别绝对不产生幻读?

在使用的select …where语句中加入 for update(排他锁) 或者 lock in share mode(共享锁)语句来实现。其实就是锁住了可能形成幻读的数据,阻止数据的写入操做。

实际上是由于数据的写入操做(insert 、update)须要先获取写锁,因为可能产生幻读的部分,已经获取到了某种锁,因此要在另一个会话中获取写锁的前提是当前会话中释放全部因加锁语句产生的锁。

mysql死锁问题

死锁,就是产生了循环等待链条,我等待你的资源,你却等待个人资源,咱们都相互等待,谁也不释放本身占有的资源,致使无线等待下去。 
好比:

//Session A
START TRANSACTION;
UPDATE account SET p_money=p_money-100 WHERE p_name="tim";
UPDATE account SET p_money=p_money+100 WHERE p_name="bill";
COMMIT;
//Thread B
START TRANSACTION;
UPDATE account SET p_money=p_money+100 WHERE p_name="bill";
UPDATE account SET p_money=p_money-100 WHERE p_name="tim";
COMMIT;

当线程A执行到第一条语句UPDATE account SET p_money=p_money-100 WHERE p_name=”tim”;锁定了p_name=”tim”的行数据;而且试图获取p_name=”bill”的数据;

,此时,刚好,线程B也执行到第一条语句:UPDATE account SET p_money=p_money+100 WHERE p_name=”bill”;

锁定了 p_name=”bill”的数据,同时试图获取p_name=”tim”的数据; 
此时,两个线程就进入了死锁,谁也没法获取本身想要获取的资源,进入无线等待中,直到超时!

innodb_lock_wait_timeout 等待锁超时回滚事务: 
直观方法是在两个事务相互等待时,当一个等待时间超过设置的某一阀值时,对其中一个事务进行回滚,另外一个事务就能继续执行。这种方法简单有效,在innodb中,参数innodb_lock_wait_timeout用来设置超时时间。

wait-for graph算法来主动进行死锁检测: 
innodb还提供了wait-for graph算法来主动进行死锁检测,每当加锁请求没法当即知足须要并进入等待时,wait-for graph算法都会被触发。

如何尽量避免死锁

1)以固定的顺序访问表和行。好比两个更新数据的事务,事务A 更新数据的顺序 为1,2;事务B更新数据的顺序为2,1。这样更可能会形成死锁。

2)大事务拆小。大事务更倾向于死锁,若是业务容许,将大事务拆小。

3)在同一个事务中,尽量作到一次锁定所须要的全部资源,减小死锁几率。

4)下降隔离级别。若是业务容许,将隔离级别调低也是较好的选择,好比将隔离级别从RR调整为RC,能够避免掉不少由于gap锁形成的死锁。

5)为表添加合理的索引。能够看到若是不走索引将会为表的每一行记录添加上锁,死锁的几率大大增大。

显式锁 与 隐式锁 
隐式锁:咱们上文说的锁都属于不须要额外语句加锁的隐式锁。 
显示锁

SELECT ... LOCK IN SHARE MODE(加共享锁);
SELECT ... FOR UPDATE(加排他锁);

详情上文已经说过。

经过以下sql能够查看等待锁的状况

select * from information_schema.innodb_trx where trx_state="lock wait";
或
show engine innodb status;

mysql中的事务

show variables like "autocommit";

set autocommit=0; //0表示AutoCommit关闭
set autocommit=1; //1表示AutoCommit开启
  • 自动提交(AutoCommit,mysql默认)

mysql默认采用AutoCommit模式,也就是每一个sql都是一个事务,并不须要显示的执行事务。  若是autoCommit关闭,那么每一个sql都默认开启一个事务,只有显式的执行“commit”后这个事务才会被提交。

相关文章
相关标签/搜索