MySQL知识梳理图,一图看完整篇文章: mysql
MySQL系列文章:算法
定义bash
对比session
限制条件并发
行锁和表锁,在不一样引擎还有所区别,MyISAM只有表锁,没有行锁,不支持事务。 InnoDB 有行锁和表锁,支持事务。高并发
InnoDB 存储引擎实现了两种标准的行锁,就是共享锁,也称叫S锁,容许事务读一行数据。排他锁,也称叫X锁,容许事务删除或更新一行数据。post
特性
加锁方式
select语句 在查询语句中,能够经过在SQL语句中主动加锁。
共享锁:
select * from table where 索引限制 lock in share mode
记住,行锁查询是须要具有索引条件。好比执行: select * from user where id=1 lock in share mode. 其中 id 是主键。
排他锁: select * from table where 索引限制 for update
好比执行: select name from user where id=1 for update.其中 id 是主键
insert or update or delete 语句。 InnoDB中对修改数据相关类SQL中,会自动给涉及到的数据加上排他锁。
如何释放锁
查看当前锁的状态 能够经过SQL语句 : show engine innodb status\G;
查看。
一致性的非锁定读是指InnoDB存储引擎经过行多版本控制的方式来读取当前执行时间数据库中行的数据。 若是读取的行的时候有正在执行的 Delete 或者 Update 操做,这时读取操做不会等待行上锁的释放,而是InnoDB引擎会去读取行的一个快照数据。
图片来自于《MySQL技术内幕第2版》
能够得知一致性非锁定读机制大大提高了数据库的并发性,这也是InnoDB默认的读取方式,即读取不会占用和等待表上的锁。但不一样事务隔离级别下,读取的方式不一样,对快照的定义也不一样,一个行记录可能有多个快照数据,通常称这种技术为行多版本技术,由此带来的并发控制,称之为多版本并发控制(MVCC)
事务隔离级别 READ-COMMITTED vs REPEATABLE-READ
REPEATABLE-READ 是InnoDB默认的事务隔离级别,REPEATABLE-READ 对于快照数据,非一致性读老是读取事务开始时的行数据版本。
READ-COMMITTED 事务隔离级别下,对于快照数据,非一致性读老是读取被锁定行的最新一份快照数据.
咱们来举例看看,开启2个终端,能够经过下面命令开始事务会话:
`start transaction;` or `begin;` or `set autocommit=0`
复制代码
经过 select @@tx_isolation\G;
能够查看事务隔离级别,先来看看 REPEATABLE-READ 的状况, 在SessionA 和 SessionB中,总共执行了6步,先执行1和2,都能查到id=2的内容,而后再SessionA中执行update操做,将id=2改成3,若是不执行commit操做,不管是REPEATABLE-READ or READ-COMMITTED ,都是能查到id=2的内容,但若是commit以后,REPEATABLE-READ仍是能够继续查看id=2的内容,演示数据以下 1-2-3-4-5-6 顺序。
Session A:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation\G;
*************************** 1. row ***************************
@@tx_isolation: REPEATABLE-READ
mysql> select * from user where id=2; # 1
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 2 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.01 sec)
mysql> update user set id=3 where id=2; # 3
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit; # 5
Query OK, 0 rows affected (0.00 sec)
复制代码
Session B:
mysql> select * from user where id=2; # 2
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 2 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> select * from user where id=2; # 4
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 2 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> select * from user where id=2; # 6
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 2 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
复制代码
再来事务隔离级别为READ-COMMITTED的状况:
能够经过命令 set session transaction isolation level read committed;
修改会话级的事务隔离级别。
以下面顺序 1-2-3-4-5-6,能够看出READ-COMMITTED下,SessionA commit以后,SessionB就更改了。
Session A
mysql> select @@tx_isolation\G;
*************************** 1. row ***************************
@@tx_isolation: READ-COMMITTED
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id=4; # 1
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 4 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.01 sec)
mysql> update user set id=3 where id=4; # 3
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit; # 5
Query OK, 0 rows affected (0.00 sec)
复制代码
Session B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation\G;
*************************** 1. row ***************************
@@tx_isolation: READ-COMMITTED
mysql> select * from user where id=4; # 2
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 4 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> select * from user where id=4; # 4
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 4 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> select * from user where id=4; # 6
Empty set (0.00 sec)
复制代码
默认状况下,InnoDB是一致性非锁定读,若是有些业务场景须要显式的对数据库读取操做进行加锁以保证数据逻辑的一致性。这就须要进行加锁了,加锁方式上面描述共享锁和排他锁的时候已经提到过,这里再也不重复。
select ... for update
和 select ... lock in share mode
下面演示一下: 顺序是 1-2-3-4,加锁的前提是必须在一个事务中,因此开始一个事务,而后进行加共享锁,若是未进行commit, SessionB执行update操做则会等待,等待的时候默认是50s,能够查看相关mysql配置,若是再超时以前,SessionA执行了commit操做,则SessionB会立刻执行成功。
Session A:
ysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id=3 lock in share mode; # 1
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 3 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> commit; # 3
Query OK, 0 rows affected (0.00 sec)
复制代码
Session B:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update user set id=4 where id=3; # 2
# 等待
ysql> update user set id=4 where id=3; # 4
Query OK, 1 row affected (18.18 sec)
Rows matched: 1 Changed: 1 Warnings: 0
复制代码
举例,索引有10,11,13,20这四个值。
select * from user where id=3
则只会锁定id=3这一行,即降级为Record Rock算法。 若是是辅助索引,则状况会有所不一样,举例解释一下,这里有点绕。
CREATE TABLE z (a INT, b INT, PRIMARY KEY(a), KEY(b));
INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;
复制代码
执行上面语句,会建立一个z表,同时数据库里有以下数据
mysql> select * from z;
+----+------+
| a | b |
+----+------+
| 1 | 1 |
| 3 | 1 |
| 5 | 3 |
| 7 | 6 |
| 10 | 8 |
+----+------+
5 rows in set (0.00 sec)
复制代码
如今开启一个会话A和B。
Session A:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from z where b=3 for update; # 1 给辅助索引b=3加上X锁
+---+------+
| a | b |
+---+------+
| 5 | 3 |
+---+------+
复制代码
给辅助索引b=3加上X锁以后,因为使用的Next-Key Lock算法,而且有涉及到a=5的主键索引,会首先对 a=5 进行Record Lock锁定,而后对b=3进行Next-Key Lock锁定,即锁定(1, 3]。须要特别注意的是,InnoDB还会对辅助索引的下一个键(6)加上Gap Lock锁,即锁定(3, 6)。
因此若是再SessionB中执行下面语句会是等待吗?
Session B:
select * from z where a=5 lock in share mode; #2
insert into z select 4, 2; #3
insert into z select 6, 5; #4
insert into z select 8, 6; #5
复制代码
执行2,发现须要等待,缘由是a=5索引已经被加上了X锁。 执行3,主键写入4没有问题,但辅助索引2是在锁定的范围(1,3)中。 执行4,主键写入6没有问题,但辅助索引5是在锁定的范围(3,6)中。 执行5,主键8和辅助索引6均没有问题,能够写入。
说了这么多,接下来讲一下如何关闭Gap Lock。
因此将隔离级别设置为READ-COMMITTED要谨慎。关闭GapLock以后,除了外键约束和惟一性检查还须要GapLoc,其他状况仅使用RecordLock进行锁定。这样设置会破坏事务的隔离性。下面来讲一下这个问题。
先来认识一个名词: Phantom Problem,幻像问题。Innodb存储引擎采用Next-Key Lock算法就是为了不Phantom Problem。
Phantom Problem是指同一个事务下,连续执行两次一样的SQL语句可能致使不一样的结果,第二次的SQL语句可能返回以前不存在的行。
举例分别在SessionA 和 SessionB中按顺序1-2-3-4执行。
Session A:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation\G;
*************************** 1. row ***************************
@@tx_isolation: READ-COMMITTED
mysql> select * from z where a>2 for update; # 1
+----+------+
| a | b |
+----+------+
| 3 | 1 |
| 5 | 3 |
| 7 | 6 |
| 10 | 8 |
+----+------+
4 rows in set (0.00 sec)
mysql> select * from z where a>2 for update; # 4
+----+------+
| a | b |
+----+------+
| 3 | 1 |
| 4 | 0 |
| 5 | 3 |
| 7 | 6 |
| 10 | 8 |
+----+------+
5 rows in set (0.00 sec)
复制代码
Session B:
mysql> insert into z select 4, 0; # 2
Query OK, 1 row affected (0.01 sec)
Records: 1 Duplicates: 0 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec) # 3
复制代码
结果发现SessionA中,事务还没结束,执行1和4返回的结果不同,这样就是违法了事务的隔离性。
若是使用事务隔离级别为: REPEATABLE-READ。会使用Next-Key Lock算法,则上面执行1则会锁定(2,+∞),从而第2步会等待,进而避免了Phantom Problem问题。
说了这么多,总结几点InnoDB默认下的几种状况:
锁机制虽然能够实现事务的隔离性要求,使得事务能够并发的工做,不过也会带来几个潜在的问题。
脏读是指不一样事务下, 当前事务能够读到另外事务未提交的数据。这个通常生产环境不多遇到,且只会发生在事务隔离级别为READ-UNCOMMITTED的状况下,这种事务隔离设置不多见。具体演示,感兴趣的能够试一下。
不可重复读是指一个事务内屡次读取同一数据集合,获得数据结果不同。与脏读的区别是,脏读读取到未提交的数据,而不可重复读读取到了已经提交的数据,可是违反了数据库事务一致性的要求,当前事务未结束,先后两次相同查询获得了不同的结果。
这种状况上面已经有演示过,当事务隔离级别是READ-COMMITTED,则会发生这种状况。
丢失更新就是一个事务的更新操做会被另一个事务的更新操做所覆盖,从而致使数据的不一致。 好比:
上面举例理论上在MySQL的事务隔离级别,都不会发生丢失更新,由于对行进行更新操做,都会对行继续加锁,因此第2步并不会执行成功,而是会阻塞,等待事务T1提交。
但丢失更新在生产环境是会发生的,出如今下面的状况:
致使这个问题,并非由于数据库自己的问题,而是在多用户系统环境下,高并发读取信息都有可能会产生这个问题。好比容易发生在帐单金额方面的场景。 要避免此类丢失更新发送,则须要事务在这种状况下的操做变成串行化,而不是并行操做。须要再1)中用户读取的记录加上一个排他锁(X锁),这样2)则读的时候须要等待1)3)事务结束才能够读到。从而避免了丢失更新的问题。
接下来,来看看死锁问题。 死锁是指两个或两个以上的事务在执行过程当中,因争夺锁资源而形成的一种互相等待的现象。
通常比较简单的解决死锁的问题是超时,当两个事务互相等待时,当一个等待时间超过设置的阀值时,则该事务进行回滚,另外一个等待的事务则继续进行。能够经过innodb_lock_wait_timeout来设置超时的时间。
除了超时机制,数据库普通采用等待图(wait-for graph)的方式来进行死锁检测,Innodb采用的是这种方式来进行死锁检测。
wait-for graph须要2个信息:
举例:
图中有t1,t2,t3,t4 4个事务,事务t1须要等待t2中row1的资源,则wait-for graph有节点t1指向t2。事务t2又须要等待t1,t4的资源,事务t3须要等待t1,t4,t2的资源,从而构成如下wait-for graph
能够看见t1和t2之间造成回路,从而存在死锁。
死锁实际举例:
A | B |
---|---|
select * from user where id=2 for update; | begin |
select * from user where id=8 for update; | |
select * from user where id=8 for update; (等待) | |
select * from user where id=2 for update; \ ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
如上表,SessionA 先给id=2行加上X锁,SessionB则给id=8加上X锁,SessionA也准备想给id=8加上X锁,从而处于等待中,须要等待SessionB是否id=8的锁,SessionB在未是否id=8的锁以前,又想给id=2加上X锁,从而使SessionA和SessionB互相等待,出现死锁。
上面讲了那么一大堆锁相关的知识,接下来来看看了解这些知识有什么用。 常见的一个场景,秒杀系统。双11或者电商抢购的时候,常常是多用户抢购一个商品,库存确定是颇有限的,如何控制库存不让出现超买超卖,以防止形成没必要要的损失。
仔细想一想,其实跟上面锁知识中描述的丢失更新相似,假设库存只剩下一个,若是查询的时候不加任何锁,也不开启事务。同时a、b、c三个用户读到了这一个库存,而后程序也均经过了,a、b、c用户付款后,依次更新数据库的库存,这时候发现库存出现负值,形成商家的损失。
如何避免了?
若是显式的给查询的时候加上S锁(共享锁),有用吗?显然根据上面的锁知识得知,仍是会出现,由于共享锁跟共享锁是兼容了,能够都读取,只是不能写入。这样a、b、c仍是会都读到最后一个库存。
因此只能使用排他锁了(X锁)。
总结以下:
若是不开启事务,读取结束后就会是否锁,因此必定要先开启事务。
固然这样加锁,高并发的状况,实际生产环境不会这么作,大量的数据库读写对性能和DB都有很大的压力。实际过程当中,均会引入缓存、队列等来协助实现秒杀系统。这只是单纯从数据库层面进行分析。
这一篇文章就到这里,下一篇继续对MySQL事务继续分析了解。
更多精彩文章,请关注公众号『天澄技术杂谈』