2018-01-29

96
吥破
2018.01.31 18:36* 字数 16377

                        记一次数据库死锁Deadlock

线上一次死锁分析


1、日志分析

SQL产生锁的类型:

sql加锁思考点:

mysql执行计划详解:

innodb 死锁预防策略:

死锁(Deadlock)

MySQL锁详解

1.表级锁定(table-level)

2.行级锁定(row-level)

3.页级锁定(page-level)

1.InnoDB锁定模式及实现机制

事务隔离级别 Isolation Level

Read Uncommited

Read Committed (RC)

Repeatable Read (RR)

Serializable

我们锁发生的场景 id唯一索引+RC

2.InnoDB行锁实现方式

3.间隙锁(Next-Key锁)

通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。下面就通过实例来介绍几种避免死锁的常用方法:

innodb索引分类:

聚簇索引(clustered index)

辅助索引(secondary index)

聚簇索引:

全面搞清楚死锁的来龙去脉需要的知识点:

表结构

CREATE TABLE `balance` (

`balance_id` varchar(30) NOT NULL,

`uid` varchar(30) NOT NULL ,

`mid` varchar(30) NOT NULL,

`create_time` datetime NOT NULL COMMENT '创建时间',

`update_time` datetime NOT NULL COMMENT '更新时间',

`balance` decimal(16,4) DEFAULT '0.0000',

`frozen` decimal(16,4) DEFAULT '0.0000' ,

`available` varchar(30) ,

PRIMARY KEY (`balance_id`),

UNIQUE KEY `merchant_id` (`mid`,`uid`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户余额表';

1、日志分析

------------------------

LATEST DETECTED DEADLOCK

------------------------

2018-01-27 20:18:25 0x7f6d0eb3a700

*** (1) TRANSACTION:

TRANSACTION 59443956, ACTIVE 20 sec starting index read

mysql tables in use 1, locked 1

LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)  主键索引 唯一索引  持有三把锁  两把行锁 一把表级意向锁 行锁处于等待状态

MySQL thread id 194314, OS thread handle 140106119108352, query id 1374079622 id='201710101857160000000005295019' and mid='1001'

select * from balance  where uid='5019' and mid='1001'

for update

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:等待下面的锁

RECORD LOCKS space id 53 page no 12n bits 184 index PRIMARY of table`phoenix_dev`.`balance` trx id 59443956 lock_mode X locks rec but not gapwaiting 排他锁 无间隙  页面号12 共184bits可以锁184行

索引主键聚簇索引  锁数据行的时候会锁相关的索引记录 

Record lock, heap no 48 PHYSICAL RECORD: n_fields 10; compact format; info bits 0

0: len 30; hex 323031373130313031383537313730303030303030303038323935303230; asc 201710101857170000000008295020;;

1: len 6; hex 0000038b0af2; asc      ;;

2: len 7; hex 3e000003e30ca4; asc >      ;;

3: len 30; hex 323031373130313031383537313630303030303030303035323935303139; asc 201710101857160000000005295019;;

4: len 24; hex 323031373039323131383135323730303030303131303031; asc 201709211815270000011001;;

5: len 5; hex 999dd52e51; asc    .Q;;

6: len 5; hex 999ef7448a; asc    D ;;

7: len 8; hex 800000011e9206eb; asc        ;;

8: len 8; hex 8000000268652327; asc    he#';;

9: len 1; hex 31; asc 1;;

*** (2) TRANSACTION:

TRANSACTION 59443955, ACTIVE 23 sec starting index read

mysql tables in use 1, locked 1

8 lock struct(s), heap size 1136, 2 row lock(s) 8把锁 2个行锁

MySQL thread id 194307, OS thread handle 140106374817536,  updating

update        balance        set        balance=(balance-10),  frozen=(frozen+10), 

      update_time=now()        where  uid='5019' and mid='1001'

*** (2)HOLDS THE LOCK(S):持有的锁  主键索引相关  记录锁 已经加上

RECORD LOCKS space id 53 page no 12n bits 184 index PRIMARY of table `balance` trx id 59443955 lock_mode X locks rec but not gap

Record lock, heap no 48 PHYSICAL RECORD: n_fields 10; compact format; info bits 0

0: len 30; hex 323031373130313031383537313730303030303030303038323935303230; asc 201710101857170000000008295020;;

1: len 6; hex 0000038b0af2; asc      ;;

2: len 7; hex 3e000003e30ca4; asc >      ;;

3: len 30; hex 323031373130313031383537313630303030303030303035323935303139; asc 201710101857160000000005295019;;

4: len 24; hex 323031373039323131383135323730303030303131303031; asc 201709211815270000011001;;

5: len 5; hex 999dd52e51; asc    .Q;;

6: len 5; hex 999ef7448a; asc    D ;;

7: len 8; hex 800000011e9206eb; asc        ;;

8: len 8; hex 8000000268652327; asc    he#';;

9: len 1; hex 31; asc 1;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:等待下面正处于等待中的X锁 也即事务1持有的等待锁  行索引唯一索引

RECORD LOCKS space id 53 page no 7n bits 248index merchant_id of table `balance` trx id 59443955lock_mode X locks rec but not gap waiting

Record lock, heap no 51 PHYSICAL RECORD: n_fields 3; compact format; info bits 0

0: len 24; hex 323031373039323131383135323730303030303131303031; asc 201709211815270000011001;;

1: len 30; hex 323031373130313031383537313630303030303030303035323935303139; asc 201710101857160000000005295019;;

2: len 30; hex 323031373130313031383537313730303030303030303038323935303230; asc 201710101857170000000008295020;;

***WE ROLL BACK TRANSACTION (1)

session2 :  select * from balance where  uid='5019' and mid='1001' for update ;  获取S锁-> 持有S锁 ->获取X锁  ->持有X锁

session1:select * from balance where  uid='5019' and mid='1001'

for update ; 尝试获取S锁 ->持有S锁  尝试获取X锁 发现已被持有 释放S锁 等待获取行锁

session3:select * from balance where  uid='5019' and mid='1001'

for update ;尝试获取S锁 ->持有S锁  尝试获取X锁 发现已被持有 释放S锁 等待获取行锁

session2 : update        balance        set        balance=(balance-10),  frozen=(frozen+10), 

      update_time=now()        where  uid='5019' and mid='1001';

已经持有同类X锁 页面锁也没释放 所有加锁判断通过

session2 commit; 事务提交

session1或者session3中一个获得X锁

获得该锁的事务会话进行update操作  又去获取到X锁具有不确定性 姑且别名为事务a和事务b a得到X锁

update        balance        set        balance=(balance-10),  frozen=(frozen+10), 

      update_time=now()        where  uid='5019' and mid='1001';

发生原因总结:

    由于a获取X锁之前在等待过程s锁被释放过 因此在此去获取S锁 此时S锁如果没被其他事务获取过 

直接完成加锁

如果发现此页面共享锁被其他事务获取过 则等待其他事务完成(这个机制的存在是因为同一页面行数据很多 页面锁可以共享以提高效率 提高并发度 只要多个事务不是争抢同一个行锁资源很少会发生死锁)

我们这里最坏的情况发生了,就是事务b刚好在等待同一个数据的行锁 这样造成循环等待  死锁发生

数据库根据事务权重判断舍弃权重较低的事务将其回滚 undo log

报错就是deadlock

SQL产生锁的类型:

快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)

select * from table where ?;

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。

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 ?;

所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

sql加锁思考点:

前提一:筛选列是不是主键?

前提二:当前系统的隔离级别是什么?(不同隔离级别 锁类型不同)

前提三:筛选列如果不是主键,那么筛选列上有索引吗?

前提四:筛选列上如果有二级索引,那么这个索引是唯一索引吗?(唯一索引只锁一行)

前提五:两个SQL的执行计划是什么?索引扫描?全表扫描?(走行锁还是表锁或者间隙锁)

mysql执行计划详解:

1)、id列数字越大越先执行,如果说数字一样大,那么就从上往下依次执行,id列为null的就表是这是一个结果集,不需要使用它来进行查询。


2)、select_type列常见的有:

A:simple:表示不需要union操作或者不包含子查询的简单select查询。有连接查询时,外层的查询为simple,且只有一个

B:primary:一个需要union操作或者含有子查询的select,位于最外层的单位查询的select_type即为primary。且只有一个

C:union:union连接的两个select查询,第一个查询是dervied派生表,除了第一个表外,第二个以后的表select_type都是union

D:dependent union:与union一样,出现在union 或union all语句中,但是这个查询要受到外部查询的影响

E:union result:包含union的结果集,在union和union all语句中,因为它不需要参与查询,所以id字段为null

F:subquery:除了from字句中包含的子查询外,其他地方出现的子查询都可能是subquery

G:dependent subquery:与dependent union类似,表示这个subquery的查询要受到外部表查询的影响

H:derived:from字句中出现的子查询,也叫做派生表,其他数据库中可能叫做内联视图或嵌套select


3)、table

显示的查询表名,如果查询使用了别名,那么这里显示的是别名,如果不涉及对数据表的操作,那么这显示为null,如果显示为尖括号括起来的就表示这个是临时表,后边的N就是执行计划中的id,表示结果来自于这个查询产生。如果是尖括号括起来的,与类似,也是一个临时表,表示这个结果来自于union查询的id为M,N的结果集。


4)、type

依次从好到差:system,const,eq_ref,ref,fulltext,ref_or_null,unique_subquery,index_subquery,range,index_merge,index,ALL,除了all之外,其他的type都可以使用到索引,除了index_merge之外,其他的type只可以用到一个索引

A:system:表中只有一行数据或者是空表,且只能用于myisam和memory表。如果是Innodb引擎表,type列在这个情况通常都是all或者index

B:const:使用唯一索引或者主键,返回记录一定是1行记录的等值where条件时,通常type是const。其他数据库也叫做唯一索引扫描

C:eq_ref:出现在要连接过个表的查询计划中,驱动表只返回一行数据,且这行数据是第二个表的主键或者唯一索引,且必须为not null,唯一索引和主键是多列时,只有所有的列都用作比较时才会出现eq_ref

D:ref:不像eq_ref那样要求连接顺序,也没有主键和唯一索引的要求,只要使用相等条件检索时就可能出现,常见与辅助索引的等值查找。或者多列主键、唯一索引中,使用第一个列之外的列作为等值查找也会出现,总之,返回数据不唯一的等值查找就可能出现。

E:fulltext:全文索引检索,要注意,全文索引的优先级很高,若全文索引和普通索引同时存在时,mysql不管代价,优先选择使用全文索引

F:ref_or_null:与ref方法类似,只是增加了null值的比较。实际用的不多。

G:unique_subquery:用于where中的in形式子查询,子查询返回不重复值唯一值

H:index_subquery:用于in形式子查询使用到了辅助索引或者in常数列表,子查询可能返回重复值,可以使用索引将子查询去重。

I:range:索引范围扫描,常见于使用>,<,is null,between ,in ,like等运算符的查询中。

J:index_merge:表示查询使用了两个以上的索引,最后取交集或者并集,常见and ,or的条件使用了不同的索引,官方排序这个在ref_or_null之后,但是实际上由于要读取所个索引,性能可能大部分时间都不如range

K:index:索引全表扫描,把索引从头到尾扫一遍,常见于使用索引列就可以处理不需要读取数据文件的查询、可以使用索引排序或者分组的查询。

L:all:这个就是全表扫描数据文件,然后再在server层进行过滤返回符合要求的记录。


5)、possible_keys

查询可能使用到的索引都会在这里列出来


6)、key

查询真正使用到的索引,select_type为index_merge时,这里可能出现两个以上的索引,其他的select_type这里只会出现一个。


7)、key_len

用于处理查询的索引长度,如果是单列索引,那就整个索引长度算进去,如果是多列索引,那么查询不一定都能使用到所有的列,具体使用到了多少个列的索引,这里就会计算进去,没有使用到的列,这里不会计算进去。留意下这个列的值,算一下你的多列索引总长度就知道有没有使用到所有的列了。要注意,mysql的ICP特性使用到的索引不会计入其中。另外,key_len只计算where条件用到的索引长度,而排序和分组就算用到了索引,也不会计算到key_len中。


8)、ref

如果是使用的常数等值查询,这里会显示const,如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段,如果是条件使用了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示为func


9)、rows

这里是执行计划中估算的扫描行数,不是精确值


10)、extra

这个列可以显示的信息非常多,有几十种,常用的有

A:distinct:在select部分使用了distinc关键字

B:no tables used:不带from字句的查询或者From dual查询

C:使用not in()形式子查询或not exists运算符的连接查询,这种叫做反连接。即,一般连接查询是先查询内表,再查询外表,反连接就是先查询外表,再查询内表。

D:using filesort:排序时无法使用到索引时,就会出现这个。常见于order by和group by语句中

E:using index:查询时不需要回表查询,直接通过索引就可以获取查询的数据。

F:using join buffer(block nested loop),using join buffer(batched key accss):5.6.x之后的版本优化关联查询的BNL,BKA特性。主要是减少内表的循环数量以及比较顺序地扫描查询。

G:using sort_union,using_union,using intersect,using sort_intersection:

using intersect:表示使用and的各个索引的条件时,该信息表示是从处理结果获取交集

using union:表示使用or连接各个使用索引的条件时,该信息表示从处理结果获取并集

using sort_union和using sort_intersection:与前面两个对应的类似,只是他们是出现在用and和or查询信息量大时,先查询主键,然后进行排序合并后,才能读取记录并返回。

H:using temporary:表示使用了临时表存储中间结果。临时表可以是内存临时表和磁盘临时表,执行计划中看不出来,需要查看status变量,used_tmp_table,used_tmp_disk_table才能看出来。

I:using where:表示存储引擎返回的记录并不是所有的都满足查询条件,需要在server层进行过滤。查询条件中分为限制条件和检查条件,5.6之前,存储引擎只能根据限制条件扫描数据并返回,然后server层根据检查条件进行过滤再返回真正符合查询的数据。5.6.x之后支持ICP特性,可以把检查条件也下推到存储引擎层,不符合检查条件和限制条件的数据,直接不读取,这样就大大减少了存储引擎扫描的记录数量。extra列显示using index condition

J:firstmatch(tb_name):5.6.x开始引入的优化子查询的新特性之一,常见于where字句含有in()类型的子查询。如果内表的数据量比较大,就可能出现这个

K:loosescan(m..n):5.6.x之后引入的优化子查询的新特性之一,在in()类型的子查询中,子查询返回的可能有重复记录时,就可能出现这个


除了这些之外,还有很多查询数据字典库,执行计划过程中就发现不可能存在结果的一些提示信息


11)、filtered

使用explain extended时会出现这个列,5.7之后的版本默认就有这个字段,不需要使用explain extended了。这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体记录数。

innodb 死锁预防策略:

相对于事务锁,页面锁是一个短期持有的锁,而事务锁(行锁、表锁)是长期持有的锁。因此,为了防止页面锁与事务锁之间产生死锁。InnoDB做了死锁预防的策略:持有事务锁(行锁、表锁),可以等待获取页面锁;但反之,持有页面锁,不能等待持有事务锁。

根据死锁预防策略,在持有页面锁,加行锁的时候,如果行锁需要等待。则释放页面锁,然后等待行锁。此时,行锁获取没有任何锁保护,因此加上行锁之后,记录可能已经被并发修改。因此,此时要重新加回页面锁,重新判断记录的状态,重新在页面锁的保护下,对记录加锁。如果此时记录未被并发修改,那么第二次加锁能够很快完成,因为已经持有了相同模式的锁。但是,如果记录已经被并发修改,那么,就有可能导致本文前面提到的死锁问题。


死锁(Deadlock)

所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。 一种情形,此时执行程序中两个或多个线程发生永久堵塞(等待),每个线程都在等待被其他线程占用并堵塞了的资源。例如,如果线程A锁住了记录1并等待记录2,而线程B锁住了记录2并等待记录1,这样两个线程就发生了死锁现象。计算机系统中,如果系统的资源分配策略不当,更常见的可能是程序员写的程序有错误等,则会导致进程因竞争资源不当而产生死锁的现象。锁有多种实现方式,比如意向锁,共享-排他锁,锁表,树形协议,时间戳协议等等。锁还有多种粒度,比如可以在表上加锁,也可以在记录上加锁。

产生死锁的原因主要是:

(1)系统资源不足。

(2) 进程运行推进的顺序不合适。

(3)资源分配不当等。

如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

产生死锁的四个必要条件:

(1) 互斥条件:一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

MySQL锁详解

一、概述

数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。对于任何一种数据库来说都需要有相应的锁定机制,所以MySQL自然也不能例外。MySQL数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。MySQL各存储引擎使用了三种类型(级别)的锁定机制:表级锁定,行级锁定和页级锁定。

1.表级锁定(table-level)

表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。

当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。

使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。

2.行级锁定(row-level)

行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。

虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。

使用行级锁定的主要是InnoDB存储引擎。

3.页级锁定(page-level)

页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。

在数据库实现资源锁定的过程中,随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗的内存数量是越来越多的,实现算法也会越来越复杂。不过,随着锁定资源颗粒度的减小,应用程序的访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也随之提升。

使用页级锁定的主要是BerkeleyDB存储引擎。

总的来说,MySQL这3种锁的特性可大致归纳如下:

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;

行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;

页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

适用:从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

二、表级锁定

由于MyISAM存储引擎使用的锁定机制完全是由MySQL提供的表级锁定实现,所以下面我们将以MyISAM存储引擎作为示例存储引擎。

1.MySQL表级锁的锁模式

MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。锁模式的兼容性:

对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;

对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;

MyISAM表的读操作与写操作之间,以及写操作之间是串行的。当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。

2.如何加表锁

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。

3.MyISAM表锁优化建议

对于MyISAM存储引擎,虽然使用表级锁定在锁定实现的过程中比实现行级锁定或者页级锁所带来的附加成本都要小,锁定本身所消耗的资源也是最少。但是由于锁定的颗粒度比较到,所以造成锁定资源的争用情况也会比其他的锁定级别都要多,从而在较大程度上会降低并发处理能力。所以,在优化MyISAM存储引擎锁定问题的时候,最关键的就是如何让其提高并发度。由于锁定级别是不可能改变的了,所以我们首先需要尽可能让锁定的时间变短,然后就是让可能并发进行的操作尽可能的并发。

(1)查询表级锁争用情况

MySQL内部有两组专门的状态变量记录系统内部锁资源争用情况:


mysql> show status like 'table%';+----------------------------+---------+| Variable_name              | Value  |+----------------------------+---------+| Table_locks_immediate      | 100    || Table_locks_waited        | 11      |+----------------------------+---------+

这里有两个状态变量记录MySQL内部表级锁定的情况,两个变量说明如下:

Table_locks_immediate:产生表级锁定的次数;

Table_locks_waited:出现表级锁定争用而发生等待的次数;

两个状态值都是从系统启动后开始记录,出现一次对应的事件则数量加1。如果这里的Table_locks_waited状态值比较高,那么说明系统中表级锁定争用现象比较严重,就需要进一步分析为什么会有较多的锁定资源争用了。

(2)缩短锁定时间

如何让锁定时间尽可能的短呢?唯一的办法就是让我们的Query执行时间尽可能的短。

a)尽两减少大的复杂Query,将复杂Query分拆成几个小的Query分布进行;

b)尽可能的建立足够高效的索引,让数据检索更迅速;

c)尽量让MyISAM存储引擎的表只存放必要的信息,控制字段类型;

d)利用合适的机会优化MyISAM表数据文件。

(3)分离能并行的操作

说到MyISAM的表锁,而且是读写互相阻塞的表锁,可能有些人会认为在MyISAM存储引擎的表上就只能是完全的串行化,没办法再并行了。大家不要忘记了,MyISAM的存储引擎还有一个非常有用的特性,那就是ConcurrentInsert(并发插入)的特性。

MyISAM存储引擎有一个控制是否打开Concurrent Insert功能的参数选项:concurrent_insert,可以设置为0,1或者2。三个值的具体说明如下:

concurrent_insert=2,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录;

concurrent_insert=1,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置;

concurrent_insert=0,不允许并发插入。

可以利用MyISAM存储引擎的并发插入特性,来解决应用中对同一表查询和插入的锁争用。例如,将concurrent_insert系统变量设为2,总是允许并发插入;同时,通过定期在系统空闲时段执行OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录而产生的中间空洞。

(4)合理利用读写优先级

MyISAM存储引擎的是读写互相阻塞的,那么,一个进程请求某个MyISAM表的读锁,同时另一个进程也请求同一表的写锁,MySQL如何处理呢?

答案是写进程先获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前。

这是因为MySQL的表级锁定对于读和写是有不同优先级设定的,默认情况下是写优先级要大于读优先级。

所以,如果我们可以根据各自系统环境的差异决定读与写的优先级:

通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接读比写的优先级高。如果我们的系统是一个以读为主,可以设置此参数,如果以写为主,则不用设置;

通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。

虽然上面方法都是要么更新优先,要么查询优先的方法,但还是可以用其来解决查询相对重要的应用(如用户登录系统)中,读锁等待严重的问题。

另外,MySQL也提供了一种折中的办法来调节读写冲突,即给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。

这里还要强调一点:一些需要长时间运行的查询操作,也会使写进程“饿死”,因此,应用中应尽量避免出现长时间运行的查询操作,不要总想用一条SELECT语句来解决问题,因为这种看似巧妙的SQL语句,往往比较复杂,执行时间较长,在可能的情况下可以通过使用中间表等措施对SQL语句做一定的“分解”,使每一步查询都能在较短时间完成,从而减少锁冲突。如果复杂查询不可避免,应尽量安排在数据库空闲时段执行,比如一些定期统计可以安排在夜间执行。

三、行级锁定

行级锁定不是MySQL自己实现的锁定方式,而是由其他存储引擎自己所实现的,如广为大家所知的InnoDB存储引擎,以及MySQL的分布式存储引擎NDBCluster等都是实现了行级锁定。考虑到行级锁定君由各个存储引擎自行实现,而且具体实现也各有差别,而InnoDB是目前事务型存储引擎中使用最为广泛的存储引擎,所以这里我们就主要分析一下InnoDB的锁定特性。

1.InnoDB锁定模式及实现机制

考虑到行级锁定君由各个存储引擎自行实现,而且具体实现也各有差别,而InnoDB是目前事务型存储引擎中使用最为广泛的存储引擎,所以这里我们就主要分析一下InnoDB的锁定特性。

总的来说,InnoDB的锁定机制和Oracle数据库有不少相似之处。InnoDB的行级锁定同样分为两种类型,共享锁和排他锁,而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,InnoDB也同样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种。

当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。所以,可以说InnoDB的锁定模式实际上可以分为四种:共享锁(S),排他锁(X),意向共享锁(IS)和意向排他锁(IX),我们可以通过以下表格来总结上面这四种所的共存逻辑关系:

如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。

意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁(事务级别注意);事务可以通过以下语句显示给记录集加共享锁或排他锁。

共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE

用SELECT ... IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。

但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT... FOR UPDATE方式获得排他锁。

算法:

next KeyLocks锁,同时锁住记录(数据),并且锁住记录前面的Gap    

Gap锁,不锁记录,仅仅记录前面的Gap

Recordlock锁(锁数据,不锁Gap)

所以其实 Next-KeyLocks=Gap锁+ Recordlock锁

sql查询锁举例

select * from t1 where id = 10 for update; 

找到满足条件的记录,并且记录有效,则对记录加X锁,No Gap锁(lock_mode X locks rec but not gap);

找到满足条件的记录,但是记录无效(标识为删除的记录),则对记录加next key锁(同时锁住记录本身,以及记录之前的Gap:lock_mode X);

未找到满足条件的记录,则对第一个不满足条件的记录加Gap锁,保证没有满足条件的记录插入(locks gap before rec);

RR事务级别下才有gap锁一说 为了防止幻读的发生

事务隔离级别 Isolation Level


隔离级别:

Isolation Level,也是RDBMS的一个关键特性。相信对数据库有所了解的朋友,对于4种隔离级别:Read Uncommited,Read Committed,Repeatable Read,Serializable,都有了深入的认识。本文不打算讨论数据库理论中,是如何定义这4种隔离级别的含义的,而是跟大家介绍一下MySQL/InnoDB是如何定义这4种隔离级别的。


MySQL/InnoDB定义的4种隔离级别:

Read Uncommited

可以读取未提交记录。此隔离级别,不会使用,忽略。

Read Committed (RC)

快照读忽略,本文不考虑。

针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。

Repeatable Read (RR)

快照读忽略,本文不考虑。

针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。

Serializable

从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。

Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。

我们锁发生的场景 id唯一索引+RC:


这个组合,id不是主键,而是一个Unique的二级索引键值。那么在RC隔离级别下,delete from t1 where id = 10; 需要加什么锁呢?见下图:

此组合中,id是unique索引,而主键是name列。此时,加锁的情况由于组合一有所不同。由于id是unique索引,因此delete语句会选择走id列的索引进行where条件的过滤,在找到id=10的记录后,首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name = ‘d’ 对应的主键索引项加X锁。为什么聚簇索引上的记录也要加锁?试想一下,如果并发的一个SQL,是通过主键索引来更新:update t1 set id = 100 where name = ‘d’; 此时,如果delete语句没有将主键索引上的记录加锁,那么并发的update就会感知不到delete语句的存在,违背了同一记录上的更新/删除需要串行执行的约束。


结论:若id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的[name=’d’,id=10]的记录。

2.InnoDB行锁实现方式

InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁

在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。

(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。

(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。

(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。

(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

3.间隙锁(Next-Key锁)

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;

对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。

例:

假如emp表中只有101条记录,其empid的值分别是 1,2,...,100,101,下面的SQL:

mysql> select * from emp where empid > 100 for update;

是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。

InnoDB使用间隙锁的目的:

(1)防止幻读,以满足相关隔离级别的要求(RR)。对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;

(2)为了满足其恢复和复制的需要。

很显然,在使用范围条件检索并锁定记录时,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。

除了间隙锁给InnoDB带来性能的负面影响之外,通过索引实现锁定的方式还存在其他几个较大的性能隐患:

(1)当Query无法利用索引的时候,InnoDB会放弃使用行级别锁定而改用表级别的锁定,造成并发性能的降低;

(2)当Query使用的索引并不包含所有过滤条件的时候,数据检索使用到的索引键所只想的数据可能有部分并不属于该Query的结果集的行列,但是也会被锁定,因为间隙锁锁定的是一个范围,而不是具体的索引键;

(3)当Query在使用索引定位数据的时候,如果使用的索引键一样但访问的数据行不同的时候(索引只是过滤条件的一部分),一样会被锁定。

因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。

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

4.死锁

上文讲过,MyISAM表锁是deadlock free的,这是因为MyISAM总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,当两个事务都需要获得对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。

在InnoDB的事务管理和锁定机制中,有专门检测死锁的机制,会在系统中产生死锁之后的很短时间内就检测到该死锁的存在。当InnoDB检测到系统中产生了死锁之后,InnoDB会通过相应的判断来选这产生死锁的两个事务中较小的事务来回滚,而让另外一个较大的事务成功完成。

那InnoDB是以什么来为标准判定事务的大小的呢?MySQL官方手册中也提到了这个问题,实际上在InnoDB发现死锁之后,会计算出两个事务各自插入、更新或者删除的数据量来判定两个事务的大小。也就是说哪个事务所改变的记录条数越多,在死锁中就越不会被回滚掉。

但是有一点需要注意的就是,当产生死锁的场景中涉及到不止InnoDB存储引擎的时候,InnoDB是没办法检测到该死锁的,这时候就只能通过锁定超时限制参数InnoDB_lock_wait_timeout来解决。

需要说明的是,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。

避免死锁常见方法:

通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。下面就通过实例来介绍几种避免死锁的常用方法:

(1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。

(2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。

(3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。

(4)在REPEATABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...FOR UPDATE加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题。

(5)当隔离级别为READ COMMITTED时,如果两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁。这时如果有第3个线程又来申请排他锁,也会出现死锁。对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK释放获得的排他锁。

5.什么时候使用表锁

对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁:

(1)事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。

(2)事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。

当然,应用中这两种事务不能太多,否则,就应该考虑使用MyISAM表了。

在InnoDB下,使用表锁要注意以下两点。

(1)使用LOCK TABLES虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层──MySQL Server负责的,仅当autocommit=0、InnoDB_table_locks=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server也才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁,否则,InnoDB将无法自动检测并处理这种死锁。

(2)在用 LOCK TABLES对InnoDB表加锁时要注意,要将AUTOCOMMIT设为0,否则MySQL不会给表加锁;事务结束前,不要用UNLOCK TABLES释放表锁,因为UNLOCK TABLES会隐含地提交事务;COMMIT或ROLLBACK并不能释放用LOCK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁。正确的方式见如下语句:

例如,如果需要写表t1并从表t读,可以按如下做:

SET AUTOCOMMIT=0;

LOCK TABLES t1 WRITE, t2 READ, ...;[do something with tables t1 and t2 here];COMMIT;

UNLOCK TABLES;

6.InnoDB行锁优化建议

InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势了。但是,InnoDB的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。

(1)要想合理利用InnoDB的行级锁定,做到扬长避短,我们必须做好以下工作:

a)尽可能让所有的数据检索都通过索引来完成,从而避免InnoDB因为无法通过索引键加锁而升级为表级锁定;

b)合理设计索引,让InnoDB在索引键上面加锁的时候尽可能准确,尽可能的缩小锁定范围,避免造成不必要的锁定而影响其他Query的执行;

c)尽可能减少基于范围的数据检索过滤条件,避免因为间隙锁带来的负面影响而锁定了不该锁定的记录;

d)尽量控制事务的大小,减少锁定的资源量和锁定时间长度;

e)在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少MySQL因为实现事务隔离级别所带来的附加成本。

(2)由于InnoDB的行级锁定和事务性,所以肯定会产生死锁,下面是一些比较常用的减少死锁产生概率的小建议:

a)类似业务模块中,尽可能按照相同的访问顺序来访问,防止产生死锁;

b)在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;

c)对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。

(3)可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:

mysql> show status like 'InnoDB_row_lock%';+-------------------------------+-------+| Variable_name                | Value |+-------------------------------+-------+| InnoDB_row_lock_current_waits | 0    || InnoDB_row_lock_time          | 0    || InnoDB_row_lock_time_avg      | 0    || InnoDB_row_lock_time_max      | 0    || InnoDB_row_lock_waits        | 0    |+-------------------------------+-------+

InnoDB 的行级锁定状态变量不仅记录了锁定等待次数,还记录了锁定总时长,每次平均时长,以及最大时长,此外还有一个非累积状态量显示了当前正在等待锁定的等待数量。对各个状态量的说明如下:

InnoDB_row_lock_current_waits:当前正在等待锁定的数量;

InnoDB_row_lock_time:从系统启动到现在锁定总时间长度;

InnoDB_row_lock_time_avg:每次等待所花平均时间;

InnoDB_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;

InnoDB_row_lock_waits:系统启动后到现在总共等待的次数;

对于这5个状态变量,比较重要的主要是InnoDB_row_lock_time_avg(等待平均时长),InnoDB_row_lock_waits(等待总次数)以及InnoDB_row_lock_time(等待总时长)这三项。尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。

如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors 来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。

锁冲突的表、数据行等,并分析锁争用的原因。具体方法如下:

mysql> create table InnoDB_monitor(a INT) engine=InnoDB;

然后就可以用下面的语句来进行查看:

mysql> show engine InnoDB status;

监视器可以通过发出下列语句来停止查看:

mysql> drop table InnoDB_monitor;

设置监视器后,会有详细的当前锁等待的信息,包括表名、锁类型、锁定记录的情况等,便于进行进一步的分析和问题的确定。可能会有读者朋友问为什么要先创建一个叫InnoDB_monitor的表呢?因为创建该表实际上就是告诉InnoDB我们开始要监控他的细节状态了,然后InnoDB就会将比较详细的事务以及锁定信息记录进入MySQL的errorlog中,以便我们后面做进一步分析使用。打开监视器以后,默认情况下每15秒会向日志中记录监控的内容,如果长时间打开会导致.err文件变得非常的巨大,所以用户在确认问题原因之后,要记得删除监控表以关闭监视器,或者通过使用“--console”选项来启动服务器以关闭写日志文件。

查看更多:

innodb索引分类:

聚簇索引(clustered index)

    1)  有主键时,根据主键创建聚簇索引

    2)  没有主键时,会用一个唯一且不为空的索引列做为主键,成为此表的聚簇索引

    3) 如果以上两个都不满足那innodb自己创建一个虚拟的聚集索引

辅助索引(secondary index)

   非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引


myisam索引:因为myisam的索引和数据是分开存储存储的,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索

                         索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因  


innodb索引:innodb的数据和索引放在一起,当找到索引也就找到了数据


自适应哈希索引:innodb会监控表上的索引使用情况,如果观察到建立哈希索引可以带来速度的提升,那就建立哈希索引,自 适应哈希索引通过缓冲池的B+树构造而来,

                               因此建立的速度很快,不需要将整个表都建哈希索引,InnoDB 存储引擎会自动根据访问的频率和模式来为某些页建立哈希索引。自适应哈希索引不需要

                               存储磁盘的,当停库内容会丢失,数据库起来会自己创建,慢慢维护索引。    


聚簇索引:

MySQL InnoDB一定会建立聚簇索引,把实际数据行和相关的键值保存在一块,这也决定了一个表只能有一个聚簇索引,即MySQL不会一次把数据行保存在二个地方。

     1)  InnoDB通常根据主键值(primary key)进行聚簇

     2) 如果没有创建主键,则会用一个唯一且不为空的索引列做为主键,成为此表的聚簇索引

     3) 上面二个条件都不满足,InnoDB会自己创建一个虚拟的聚集索引

聚簇索引的叶节点就是数据节点,而非聚簇索引的叶节点仍然是索引节点,并保留一个链接指向对应数据块

聚簇索引主键的插入速度要比非聚簇索引主键的插入速度慢很多。相比之下,聚簇索引适合排序,非聚簇索引(也叫二级索引)不适合用在排序的场合。

因为聚簇索引本身已经是按照物理顺序放置的,排序很快(好比住在一起的人和分散不同地方的人 聚在一起并排个高低)。非聚簇索引则没有按序存放,需要额外消耗资源来排序。

当你需要取出一定范围内的数据时,用聚簇索引也比用非聚簇索引好。

另外,二级索引需要两次索引查找,而不是一次才能取到数据,因为存储引擎第一次需要通过二级索引找到索引的叶子节点,从而找到数据的主键,然后在聚簇索引中用主键再次查找索引,再找到数据。(InnoDB的的二级索引的叶子节点存放的是KEY字段加主键值。因此,通过二级索引查询首先查到是主键值,然后InnoDB再根据查到的主键值通过主键索引找到相应的数据块)


优点:

聚簇索引的优点,就是提高数据访问性能。聚簇索引把索引和数据都保存到同一棵B+树数据结构中,并且同时将索引列与相关数据行保存在一起。这意味着,当你访问同一数据页不同行记录时,已经把页加载到了Buffer中,再次访问的时候,会在内存中完成访问,不必访问磁盘。不同于MyISAM引擎,它将索引和数据没有放在一块,放在不同的物理文件中,索引文件是缓存在key_buffer中,索引对应的是磁盘位置,不得不通过磁盘位置访问磁盘数据。


缺点:

1) 维护索引很昂贵,特别是插入新行或者主键被更新导至要分页(page split)的时候。建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZE TABLE优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片

2) 表因为使用UUId作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面更慢,所以建议使用int的auto_increment作为主键 

3) 如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值;过长的主键值,会导致非叶子节点占用占用更多的物理空间 

全面搞清楚死锁的来龙去脉需要的知识点:

了解数据库的一些基本理论知识:数据的存储格式 (堆组织表 vs 聚簇索引表);并发控制协议 (MVCC vs  Lock-Based CC);Two-Phase Locking;数据库的隔离级别定义 (Isolation Level);

了解SQL本身的执行计划 (主键扫描 vs 唯一键扫描 vs 范围扫描 vs 全表扫描);

了解数据库本身的一些实现细节 (过滤条件提取;Index Condition Pushdown;Semi-Consistent Read);

了解死锁产生的原因及分析的方法 (加锁顺序不一致;分析每个SQL的加锁顺序)

java解决方案:

1、捕捉死锁异常重试机制

https://www.cnblogs.com/EasonJim/p/7684649.html

2、代码优化 避免事务顺序错乱 分布式锁保证同一时间只有一个更新 其他等待重试

超时返回连接超时信息



参考文章:

http://hedengcheng.com/?p=844 何登成 阿里数据库专家 博士权威

http://hedengcheng.com/?p=771#_Toc374698315

https://www.cnblogs.com/zejin2008/p/5262751.html

技术随笔
Web note ad 1