索引分类
-
B+树索引
B代表平衡的意思,B+树索引并不能找到一个给定键值的具体行,B+索引能找到的只是被查找数据行所在的页,然后数据库把页读入内存,再在内存中查找。
B+树是为磁盘或者其他直接存取辅助设备设计的一种平衡查找树,所有记录节点都是按照键值的大小顺序存放在同一层的叶子节点上,叶子节点通过指针连接。
- 聚集索引
InnoDB存储引擎表是索引组织表,即表中数据按照主键顺序存放。而聚集索引就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子节点称为数据页,聚集索引的这个特性决定了索引组织表中数据也是索引的一部分。同B+树数据结构一样,每个数据页都通过一个双向链表来进行链接。
由于实际的数据页只能按照一棵B+树进行排序,因此每张表只能拥有一个聚集索引。查询优化器倾向于采用聚集索引,因为聚集索引能够在B+树索引的叶子节点上直接找到数据。而且由于定义了数据的逻辑顺序,聚集索引能够很快地访问针对范围值的查询,查询优化器能够快速发现某一段范围的数据页需要扫描。
聚集索引存储并不是物理连续的,而是逻辑连续,页通过双向链表链接并且按照主键的顺序排序,每个页中记录也是通过双向链表维护。
聚集索引对于主键的排序查找和范围查找速度非常快,并且叶子节点数据就是用户要查询的数据。
聚集索引对于主键的排序查找和范围查找很快。
select... from t order by id desc limit 20 可以快速定位到最后一个数据又因为是B+树是双向链表,所以可以直接查询20条记录。
select... from t where id > 20 and id < 100 查询主键某一范围的数据,通过叶子节点的上层中间节点就可以得到页的范围,之后直接读取数据页即可。
Fast Index Creation(FIC)
MySQL 5.5(不含)对于索引的添加/删除这种DDL操作过程为:
- 创建临时表
- 把原有数据导入临时表
- 删除原表
- 把临时表重名为原表名
如果表比较大那么耗费的时间越长,如有大量事务访问正在修改的表意味着数据库不可用。InnoDB 1.0.x之后开始支持FIC,对于辅助索引,会有表加S锁,不需要重建表,因为是S锁因此在创建过程中只能对表进行读操作,如果事务进行写服务同样不可用。对于主键的创建和删除,FIC同样需要重建一张表。
MySQL 5.6开始支持Online DDL,允许创建辅助索引时允许其他诸如insert、update、delete这类DML操作,极大提高了在生产环境中的可用性。
Online DDL也支持其他DDL:改变自增长值;添加删除外键约束;列的重命名。
所以应该创建表时定义好索引,提前做好优化
- 辅助索引(Secondary Index,也称非聚集索引)
叶子节点不含记录数据,只含键值和一个书签(bookmark),因为InnoDB存储引擎表是索引组织表,所以书签就是相应行数据的聚集索引建(主键)。 通过主键索引来找一个完整的行记录。所以查询一个完整的行记录的逻辑IO次数=辅助索引树高度+聚集索引树高度。
如果字段的取值范围很广,几乎没有重复,即属于高选择性,使用B+树索引是最合适的,取值范围很小例如性别只有2种的情况下不适合用B+。
- 全文索引
select... from t where c like 'a%' 这种可以用B+树索引,但是这种select... from t where c like '%a%' 就可以使用全文索引。 - 哈希索引
- 聚簇索引
锁
- 开发多用户数据库驱动的应用时,最大的难点是:一方面要最大程度利用数据库的并发访问,另一方面还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁的机制,为了支持对共享资源的并发访问,提供数据的完整性和一致性。
- 人们认为行级锁总会增加开销,其实,只有当实现本身会增加开销时,行级锁也才增加开销,InnoDB存储引擎不需要锁升级,因为一个锁和多个锁的开销时相同的。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。
锁的类型
- 共享锁(S Lock), 允许事务读一行数据。
- 排他锁(X Lock), 允许事务删除或更新一行数据。
如果事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取没有改变行r的数据,称为锁兼容。如若事务T3想获取行r的排它锁,则必须等待事务T1、T2释放行r的共享锁--这种情况称锁不兼容。
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
X锁与其他锁都不兼容,S仅与S兼容。
意向锁
①在mysql中有表锁
LOCK TABLE my_tabl_name READ; 用读锁锁表,会阻塞其他事务修改表数据。
LOCK TABLE my_table_name WRITe; 用写锁锁表,会阻塞其他事务读和写。
②Innodb引擎又支持行锁,行锁分为共享锁,一个事务对一行的共享只读锁。排它锁,一个事务对一行的排他读写锁。
③这两中类型的锁共存的问题考虑这个例子:
事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。数据库要怎么判断这个冲突呢?
step1:判断表是否已被其他事务用表锁锁表
step2:判断表中的每一行是否已被行锁锁住。
注意step2,这样的判断方法效率实在不高,因为需要遍历整个表。于是就有了意向锁。
在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。在意向锁存在的情况下,上面的判断可以改成step1:不变step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要使用代码来申请。
总结一下就是:
IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突
行级别的X和S按照普通的共享、排他规则即可。
一致性非锁定读
一致性非锁定读是指InnoDB存储引擎通过行多版本控制的方式来读取当前执行时间数据库中行的数据。正常的select操作不需要等待行上X锁的释放,而去读行的一个快照数据。
快照数据是指该行的之前版本的数据,该实现是通过undo段来完成,而undo用来在事务中回滚数据,因此快照数据本身没有额外的开销。读快照不需要上锁,因为没有事务需要对历史数据进行修改操作。所以说非锁定读机制极大提高了数据库的并发性。
并不是每种事务隔离级别都采用一致性非锁定读,read committed和repeatable read(InnoDB默认事务隔离级别)是使用的非锁定的一致性读,然而对快照数据的定义却不同。
- read committed:总是读取被锁定行的最新一份快照数据。
- repeatable read:读取事务开始时的行数据版本。
一致性锁定读
某些应用场景下,需要读库的时候加锁来保证数据逻辑的一致性,一致性锁定读分为2种:
- select ... for update
- select ... lock in share mode
前者是对行加个X锁,后者是对行加S锁。遵循前边讲述的X、S互斥原则:X锁与其他锁都不兼容,S仅与S兼容。
注意:对于一致性非锁定读(即不带for update和 lock in share mode的select...)即使读到的行已被执行了select... for update或者select... lock in share mode,也是可以读取的(历史快照)。
在使用上述2条语句的时候,务必加上begin, start transaction或者set autocommit=0,因为上述2条语句只有在事务中才生效,当事务提交了锁也就释放了。
行锁的算法
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
- Next-key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身
Record Lock总是会锁住索引记录,如果表没有索引,会使用隐式的主键来进行锁定。
InnoDB对于行的查询都是采用Next-key Lock,假如一个索引有10,11,13,20四个值,那么该索引可能被Next-Key Locking的区间为:
(-∞,10]
(10,11]
(11,13]
(13,20]
(20,+∞)
采用Next-key Lock的技术成为Next-key Locking,Next-key Lock 设计的目的是为了解决Phantom Problem,后续会讲。相对的,还有Previous-key Locking技术,区别在于后者的开区间在前边,闭区间符号在右边。
当查询的索引是唯一索引时,InnoDB会对Next-key Lock优化,将其降级为Record Lock,即仅锁住索引本身而不是范围。但是若唯一索引有多个列组成(联合索引),而查询仅查询多个列中的一个(其实没有用到联合索引),那么查询其实是range类型查询,而不是point,依然使用next-key lock。
看个例子:
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;
若在回话A中执行下面的SQL:
select * from z where b = 3 for update
用了辅助索引b,因此使用next-key lock,并且由于有2个索引,其需要分别进行锁定。 对于聚集索引,对a列=5的索引加上Record lock。对于辅助索引,对列b 锁定的范围是(1,3),注意,还会对辅助索引下一个键值加上gap lock,即(3,6)。
若同时开启会话B,运行下面语句都会被阻塞:
select * from z where a=5 lock in share mode
因为会话A已经对聚集索引中列a=5的值加上了X锁。insert into z select 4,2
主键插入4没问题,但是2在辅助索引锁定的范围(1,3)中。insert into z select 6,5
主键没问题,5在(3,6)中。
下面的语句可以被执行:
insert into z select 8,6
insert into z select 2,0
Gap Lock 可以阻止多个事务将记录插入同一范围内,而这会导致幻读问题的产生。如果没锁定(3,6)则B会话可以插入b列为3的记录,那么A会话再查询就会返回不同的记录,就会产生幻读。
也可以关闭Gap Lock:
- 事务隔离级别设置为read committed
- innodb_locks_unsafe_for_binlog设置为1
上述2条设置下,除了外键约束和唯一性检查依然需要Gap Lock,其余情况仅使用Record lock进行锁定。但是上述设置破坏了事务隔离性。
解决幻读
repeatable read下,采用nexy-key locking机制避免幻读,并符合事务的隔离性。幻读是指在同一事务下,连续执行2次同样的sql可能会返回之前不存在的行。
如果事务1执行select.. from t where a > 3 for update;其他事务执行insert into t select 4 则会阻塞,因为事务1对(2,+无穷大)这个范围加了x锁。事务1就避免了幻读。
在read committed下,仅采用Record lock。
如果通过索引查询一个值,并对该行加个s lock,即使查询的值不存在其锁定也是一个范围,若没有返回任何行,再插入的数据肯定是唯一的。
看个base case:
时间 | 会话A | 会话B |
---|---|---|
1 | begin | |
2 | seelct... from z where b=4 lock in share mode | |
3 | select... from z where b=4 lock in share mode | |
4 | insert into z select 4,4 # 阻塞 | |
5 | insert into z select 4,4; ERROR:Deadlock found when trying to get lock; try resgtart transaction #抛出死锁异常 | |
6 | #insert插入成功 |
共享锁可以兼容,所以可以同时获取s锁;由于S和X不兼容,步骤4,会话A发现会话B持有S锁,所以等待X锁所以步骤4会阻塞;因为会话A也有s锁,所以步骤5会话去获取X会也会阻塞,导致会话A等待会话B释放S锁,而会话B等待会话A释放S锁,就导致死锁异常。
锁带来的问题
开发中,如果能防止以下三种问题的发生,那么将不会产生并发异常。
- 脏读
- 不可重复读
- 丢失更新
脏读
脏数据是指事务对缓冲池中行记录的修改并且没有commit。一个事务读到了另一个事务未提交的数据就是脏读,违背了数据库的隔离性。在read uncommitted的时候才会有脏读。
不可重复读(幻读)
一个事务内两次读到的数据不一样。read committed下会产生幻读。
InnoDB默认的Read repeatable使用next-key lock避免了幻读。
丢失更新
Time | 会话A | 会话B |
---|---|---|
1 | begin | |
2 | select x into@y from t where uid=1 for update | |
3 | select x into@y from t where uid=1 for update # 阻塞 | |
4 | update t set x = y-100 | |
5 | commit | |
6 | update t set x = y-1 | |
7 | commit |
一个事务的更新操作被另一个事务覆盖,其实是一个逻辑问题。只要使用select... for update 让操作串行化就可以避免丢失更新。
事务查询数据放入内存y
Time | 会话A | 会话B |
---|---|---|
1 | begin | |
2 | select x into@y from t where uid=1 for update | |
3 | select x into@y from t where uid=1 for update # 阻塞 | |
4 | update t set x = y-100 | |
5 | commit | |
6 | update t set x = y-1 | |
7 | commit |
阻塞
因为锁兼容问题,事务在等待别的事务才有的资源的过程就是阻塞。阻塞是为了确保事务可以并发且正常运行。
InnoDB中,参数innodb_lock_wait_timeout可以控制等待的时间(默认50s);innodb_rollback_on_timeout用来设置是否在等待超时后对事务进行回滚(默认OFF不回滚)。
innodb_lock_wait_timeout可以在运行时调整:
set @@innodb_lock_wait_timeout=60;
而innodb_rollback_on_timeout是不可以在运行时调整的。
当发生超时,mysql回抛出1205的错误:
Lock wait timeout exceeded; try restarting transaction
innodb_rollback_on_timeout 默认是关闭的,所以在等待超时后不会对事务回滚 ,看下面的case:
begin;
insert into t select 5;
insert into t select 3;
Lock wait timeout exceeded; try restarting transaction
在执行第2条insert语句时超时,再用select * from t where id=5查询发现已经插入了数据,这十分危险!所以业务应该捕获异常来进行判断是否需要rollback或者commit。
死锁
多个事务在等待资源时互相进行等待的现象就是死锁。解决死锁的简单办法是将互相等待转化为回滚,并且事务重新开始,但是这样确实并发性能下降。
AB-BA死锁:
Time | 会话A | 会话B |
---|---|---|
1 | begin | |
2 | select * from t where a=1 for update | begin |
3 | select * from t where a=2 for update | |
4 | select * from t where a=2 for update # 等待 | |
6 | select * from t where a=1 for update ERROR 1213(40001):Deadlock found when trying to get lock; try restarting transaction |
在会话B抛出死锁异常后,会话A马上得到了记录2的这个资源,这是因为会话B的事务进行了回滚,否则会话A的事务不可能得到2这个资源。
InnoDB不会回滚大部分异常(例如timeout 异常),但死锁除外。发现死锁后,InnoDB会马上回滚一个事务!如果在程序中捕获了1213这个错误,业务不需要对其显式得回滚。
Time | 会话A | 会话B |
---|---|---|
1 | begin | |
2 | begin | |
3 | select * from t where a=4 for update | |
4 | select * from t where a<=4 lock in share mode # 等待 | |
5 | insert into t values(3) # ERROR 1213(40001):Deadlock found when trying to get lock; try restarting transaction | |
6 | 获得锁,正常运行 |
另一种死锁:即当前事务持有了待插入记录的下一个记录的X锁,但是在等待队列中存在一个S锁的请求,则可能发生死锁。
Time | 会话A | 会话B |
---|---|---|
1 | begin | |
2 | begin | |
3 | select * from t where a=4 for update | |
4 | select * from t where a<=4 lock in share mode # 等待 | |
5 | insert into t values(3) # ERROR 1213(40001):Deadlock found when trying to get lock; try restarting transaction | |
6 | 获得锁,正常运行 |
如果能插入3,那么会话B在在获取记录4持有的S锁后,还需要向后获取记录3,这显然不合理。所以InnoDB选择了主动死锁,而回滚的是undo log记录大的事务,这与AB-BA处理方式不同。
锁升级是指将当前锁的粒度降低,例如数据库可以把1000个行锁升级为一个页锁,或页锁升级为表锁。InnoDB不存在锁升级的问题,因为其不是根据每个记录产生锁的,而是根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式,因为不管一个事务锁住页中几个还是多个记录开销通常是一样的。
事务
read committed不能满足隔离性(I),但是read repeatable完全满足了ACID。
事务分类
- 扁平事务
- 带有保存点的是扁平事务
- 链事务
- 嵌套事务
- 分布式事务
扁平事务
上述讲的所有事务都是扁平事务,是最简单和用的最频繁的事务,begin、commit、rollback这些。主要缺点是不能提交或者回滚事务的某一部分和分几个步骤提交。
带有保存点的是扁平事务
除了支持扁平事务外,允许事务回滚到较早的状态。有保存点(savepoint)的概念。其实扁平事务隐式设置了一个保存点,整个事务中只有一个保存点,只能回滚到事务开始的状态。
嵌套事务
由一个顶层事务控制各个层次的事务,顶层事务之下嵌套的事务被称为子事务。子事务可以是嵌套事务也可以是其他事务。子事务既可以提交也可以回滚,但是他的提交操作并不马上生效,除非其父事务已经提交,可以推出结论:任何子事务都在顶事务提交后才真正的提交。
任意一个事务的回滚会引起它的所有子事务一同回滚,故子事务仅保留ACI特性,不具有D。
事务控制语句
在mysql命令行的默认设置下,事务都是自动提交(auto commit),即执行sql后就会马上执行commit操作。要想显示开启一个事务需要使用命令begin、start transaction或者set autocommit=0禁用自动提交。
隐式提价的sql:DDL语句,管理语句..
show global status like 'com_commit'; 统计tps(必须是统计的显示的提交,如果是隐式或者autocommit=1不会计算在内)。
事务隔离级别
由低到高-> read uncommitted, read committed, repeatable read, serializable.
repeatable read使用next-key避免幻读产生,所以说,repeatable read能完全保证事务的隔离性要求,即达到SQL标准的serializable隔离级别。
有人证实过,repeatable read 并不会比read committed损失多少性能。
在serializable隔离下,InnoDB会对每个select语句自动加上lock in share mode即每个读操作加一个共享锁,所以对一致性非锁定读不再支持。
在read committed下,除了唯一性的约束检查以及外键约束的检查需要gap lock,innodb不会使用gap lock算法。
不好的事务习惯
- 在循环中反复提交
- 使用自动提交
自动提交不是一个好习惯,mysql默认是自动提交的。
可以set autocommit=0或者使用start transation,begin来显示开启一个事务。在默认设置下,显式开启事务后,mysql会自动执行set autocommit=0命令,并在commit或者rollback结束一个事务后执行set autocommit=1。
但是不同语言自动提交是不同的,例如c api默认是自动提交,python api则会自动执行set autocommit=0。
长事务
就是执行时间特别长的事务,例如update t set a = a + 1; 如果表中数据很多的话就可能执行很长时间。带来的问题就是发生硬件故障时,回滚时间和重新开始事务不可接受。可以通过批量处理小事务来完成大事务的逻辑。每次完成一个小事务可以把最大id存下来下次接着跑。
性能问题
InnoDB一般应用于OLTP(在线事务处理),这种应用的特点:
- 操作并发量大
- 事务处理的时间比较短
- 查询语句较为简单,一般走索引
- 复杂的查询少
所以OLTP属于IO密集型的操作。
B+树的优点:
非叶子节点不会带上ROWID,这样,一个块中可以容纳更多的索引项,一是可以降低树的高度。二是一个内部节点可以定位更多的叶子节点。
叶子节点之间通过指针来连接,范围扫描将十分简单
- B+-tree的磁盘读写代价更低 B+-tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。 举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+ 树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
- B+-tree的查询效率更加稳定 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
- “B+树还有一个最大的好处,方便扫库,B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了,B+树支持range-query非常方便,而B树不支持。这是数据库选用B+树的最主要原因
参考:https://www.jianshu.com/p/2879225ba243