Mysql 基础知识(上)
1.4. Mysql的索引实现
1.4.1. 常见的索引
常见的索引有:普通索引、唯一索引、主键索引、组合索引、全文索引、
1.4.2. MyISAM 索引实现
MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址。下图是 MyISAM 索引的原理图:
这里设表一共有三列,假设我们以 Col1 为主键,则图 8 是一个 MyISAM 表的主索引(Primary key)示意。可以看出 MyISAM 的索引文件仅仅保存数据记录的地址。在 MyISAM 中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复。如果我们在 Col2 上建立一个辅助索引,则此索引的结构如下图所示:
同样也是一颗 B+Tree,data 域保存数据记录的地址。因此,MyISAM 中索引检索的算法为首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址,读取相应数据记录。
MyISAM 的索引方式也叫做“非聚集”的,之所以这么称呼是为了与 InnoDB 的聚集索引区分。
1.4.3. InnoDB 索引实现
虽然 InnoDB 也使用 B+Tree 作为索引结构,但具体实现方式却与 MyISAM 截然不同。
第一个重大区别是 InnoDB 的数据文件本身就是索引文件。从上文知道,MyISAM 索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这棵树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。
图 10 是 InnoDB 主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为 InnoDB 的数据文件本身要按主键聚集,所以 InnoDB 要求表必须有主键(MyISAM 可以没有),如果没有显式指定,则 MySQL 系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则 MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,这个字段长度为 6 个字节,类型为长整形。
第二个与 MyISAM 索引的不同是 InnoDB 的辅助索引 data 域存储相应记录主键的值而不是地址。换句话说,InnoDB 的所有辅助索引都引用主键作为 data 域。例如,图 11 为定义在 Col3 上的一个辅助索引:
1.4.4. 聚集索引和非聚集索引解释
聚集(clustered)索引,也叫聚簇索引。
定义:数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,一个表中只能拥有一个聚集索引。
非聚集(unclustered)索引。
定义:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。
非聚集索引查询过程:
1.4.5. Innodb 的聚集索引
Innodb 的存储索引是基于 B+tree,理所当然,聚集索引也是基于 B+tree。与非聚集索引的区别则是,聚集索引既存储了索引,也存储了行值。当一个表有一个聚集索引,它的数据是存储在索引的叶子页(leaf pages)。因此 innodb 也能理解为基于索引的表。
1.4.6. Innodb 如何选择一个聚集索引
对于 Innodb,主键毫无疑问是一个聚集索引。但是当一个表没有主键,或者没有一个索引,Innodb 会如何处理呢。请看如下规则
如果一个主键被定义了,那么这个主键就是作为聚集索引
如果没有主键被定义,那么该表的第一个唯一非空索引被作为聚集索引
如果没有主键也没有合适的唯一索引,那么 innodb 内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个 6 个字节的列,改列的值会随着数据的插入自增。
还有一个需要注意的是:
次级索引的叶子节点并不存储行数据的物理地址。而是存储的该行的主键值。
所以:一次级索引包含了两次查找。一次是查找次级索引自身。然后查找主键(聚集索引)
1.4.7. 建立自增主键的原因
Innodb 中的每张表都会有一个聚集索引,而聚集索引又是以物理磁盘顺序来存储的,自增主键会把数据自动向后插入,避免了插入过程中的聚集索引排序问题。聚集索引的排序,必然会带来大范围的数据的物理移动,这里面带来的磁盘 IO 性能损耗是非常大的。
而如果聚集索引上的值可以改动的话,那么也会触发物理磁盘上的移动,于是就可能出现 page 分裂,表碎片横生。
解读中的第二点相信看了上面关于聚集索引的解释后就很清楚了。
1.4.8. 索引的缺点
- 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行 insert、update 和 delete。因为更新表时,不仅要保存数据,还要保存一下索引文件。
- 建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会增长很快。
- 索引只是提高效率的一个因素,如果有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询语句。
1.4.9. 注意事项
使用索引时,有以下一些技巧和注意事项:
-
索引不会包含有 null 值的列
只要列中包含有 null 值都将不会被包含在索引中,复合索引中只要有一列含有 null 值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为 null。
-
使用短索引
对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个 char(255)的列,如果在前 10 个或 20 个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和 I/O 操作。
-
索引列排序
查询只使用一个索引,因此如果 where 子句中已经使用了索引的话,那么 order by 中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。
-
like 语句操作
一般情况下不推荐使用 like 操作,如果非使用不可,如何使用也是一个问题。like “%aaa%” 不会使用索引而 like “aaa%”可以使用索引。
-
不要在列上进行运算
这将导致索引失效而进行全表扫描,例如
SELECT * FROM table_name WHERE YEAR(column_name)<2017;
不使用 not in 和<>操作
1.4.10. 小结
MyISAM 索引和数据是分开的,MyISAM叶子节点数据是指向数据的地址。MyISAM主索引和辅助索引存储的结构是一样的。InnoDB的索引和数据是存一个文件(独享表空间和非独享表有区别,可以看#1.3.3),InnoDB的主键索引是聚集索引,主键索引非叶子结点只存键值,非叶子结点存的是数据。InnoDB其他索引都是非聚集索引,非聚集索引的叶子结点存的是主键的键值。
1.4.11. 参考
更多可以查看 Mysql 索引那些事
1.5. Mysql 锁
1.5.1. Mysql常见的几种锁
相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
- 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
- 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
- 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
1.5.2. MyISAM的锁
MyISAM存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型。
1.5.2.1. 查询表级锁争用情况
可以通过检查table_locks_waited
和table_locks_immediate
状态变量来分析系统上的表锁定争夺:mysql> show status like 'table%';
1.5.2.2. MySQL表级锁的锁模式
MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。锁模式的兼容性如表20-1所示。
请求锁模式 是否兼容 当前锁模式 | None | 读锁 | 写锁 |
---|---|---|---|
读锁 | 是 | 是 | 否 |
写锁 | 是 | 否 | 否 |
可见,对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的!根据如表20-2所示的例子可以知道,当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。
1.5.2.3. 如何加表锁
MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。下面示例中,显式加锁基本上都是为了方便而已,并非必须如此。
例如,有一个订单表orders,其中记录有各订单的总金额total,同时还有一个订单明细表order_detail,其中记录有各订单每一产品的金额小计 subtotal,假设我们需要检查这两个表的金额合计是否相符,可能就需要执行如下两条SQL:
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
这时,如果不先给两个表加锁,就可能产生错误的结果,因为第一条语句执行过程中,order_detail表可能已经发生了改变。因此,正确的方法应该是:
Lock tables orders read local, order_detail read local;
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
Unlock tables;
- 上面的例子在LOCK TABLES时加了“local”选项,其作用就是在满足MyISAM表并发插入条件的情况下,允许其他用户在表尾并发插入记录,有关MyISAM表的并发插入问题,在后面的章节中还会进一步介绍。
- 在用LOCK TABLES给表显式加表锁时,必须同时取得所有涉及到表的锁,并且MySQL不支持锁升级。也就是说,在执行LOCK TABLES后,只能访问显式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。其实,在自动加锁的情况下也基本如此,MyISAM总是一次获得SQL语句所需要的全部锁。这也正是MyISAM表不会出现死锁(Deadlock Free)的原因。
1.5.2.4. 并发插入(Concurrent Inserts)
MyISAM存储引擎有一个系统变量concurrent_insert
,专门用以控制其并发插入的行为,其值分别可以为0、1或2。
- 当
concurrent_insert
设置为0时,不允许并发插入。 - 当
concurrent_insert
设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置。 - 当
concurrent_insert
设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。
可以利用MyISAM存储引擎的并发插入特性,来解决应用中对同一表查询和插入的锁争用。例如,将concurrent_insert系统变量设为2,总是允许并发插入;同时,通过定期在系统空闲时段执行 OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录而产生的中间空洞。
1.5.2.5. MyISAM的锁调度
前面讲过,MyISAM存储引擎的读锁和写锁是互斥的,读写操作是串行的。那么,一个进程请求某个 MyISAM表的读锁,同时另一个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!这是因为MySQL认为写请求一般比读请求要重要。这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。这种情况有时可能会变得非常糟糕!幸好我们可以通过一些设置来调节MyISAM 的调度行为。
- 通过指定启动参数
low-priority-updates
,使MyISAM引擎默认给予读请求以优先的权利。 - 通过执行命令
SET LOW_PRIORITY_UPDATES=1
,使该连接发出的更新请求优先级降低。 - 通过指定
INSERT、UPDATE、DELETE
语句的LOW_PRIORITY
属性,降低该语句的优先级。
另外,MySQL也提供了一种折中的办法来调节读写冲突,即给系统参数max_write_lock_count
设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。
上面已经讨论了写优先调度机制带来的问题和解决办法。这里还要强调一点:一些需要长时间运行的查询操作,也会使写进程“饿死”!因此,应用中应尽量避免出现长时间运行的查询操作,不要总想用一条SELECT语句来解决问题,因为这种看似巧妙的SQL语句,往往比较复杂,执行时间较长,在可能的情况下可以通过使用中间表等措施对SQL语句做一定的“分解”,使每一步查询都能在较短时间完成,从而减少锁冲突。如果复杂查询不可避免,应尽量安排在数据库空闲时段执行,比如一些定期统计可以安排在夜间执行。
1.5.3. InnoDB的锁
1.5.3.1. 背景知识
- 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
- 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
- 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
- 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
1.5.3.2. 并发事务处理带来的问题
更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。
脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。
不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
1.5.3.3. 事务隔离级别
更新丢失
通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
脏读
、不可重复读
和幻读
,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。数据库实现事务隔离的方式,基本上可分为以下两种。
- 一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。
- 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。
读数据一致性及并发副作用 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
未提交读(read uncommitted) | 最低级别,不读物理上顺坏的数据 | 是 | 是 | 是 |
已提交读(read committed) | 语句级 | 否 | 是 | 是 |
可重复读(Repeatable read) | 事务级 | 否 | 否 | 是 |
可序列化(Serializable) | 最高级别,事务级 | 否 | 否 | 否 |
最后要说明的是:各具体数据库并不一定完全实现了上述4个隔离级别,例如,Oracle只提供Read committed和Serializable两个标准隔离级别,另外还提供自己定义的Read only隔离级别;SQL Server除支持上述ISO/ANSI SQL92定义的4个隔离级别外,还支持一个叫做“快照”的隔离级别,但严格来说它是一个用MVCC实现的Serializable隔离级别。MySQL 支持全部4个隔离级别,但在具体实现时,有一些特点,比如在一些隔离级别下是采用MVCC一致性读,但某些情况下又不是,这些内容在后面的章节中将会做进一步介绍。
1.5.3.4. mysql默认的事务隔离级别
InnoDB默认是可重复读的(REPEATABLE READ)
修改全局默认的事务级别,在my.inf文件的[mysqld]节里类似如下设置该选项(不推荐):
transaction-isolation = {READ-UNCOMMITTED | READ-COMMITTED | REPEATABLE-READ | SERIALIZABLE}
改变单个会话或者所有新进连接的隔离级别(推荐使用)
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
查询全局和会话事务隔离级别方法
#查询全局的事务隔离级别
`SELECT @@global.tx_isolation;`
#查询当前会话的事务级别
SELECT @@session.tx_isolation;
1.5.3.5. 获取InnoDB行锁争用情况
可以通过检查InnoDB_row_lock
状态变量来分析系统上的行锁的争夺情况:
mysql> show status like 'innodb_row_lock%';
如果发现锁争用比较严重,如InnoDB_row_lock_waits
和InnoDB_row_lock_time_avg
的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。
mysql> CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB;
1.5.3.6. InnoDB的行锁模式及加锁方法
InnoDB实现了以下两种类型的行锁。
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
- 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
- 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁
InnoDB行锁模式兼容性列表
请求锁模式 是否兼容 当前锁模式 | X | IX | S | IS |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果一个事务请求的锁模式与当前的锁兼容,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方式获得排他锁。
1.5.3.7. InnoDB行锁实现方式
- InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
- 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。
- 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
- 即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
1.5.3.8. 恢复和复制的需要,对InnoDB锁机制的影响
MySQL通过BINLOG录执行成功的INSERT、UPDATE、DELETE等更新数据的SQL语句,并由此实现MySQL数据库的恢复和主从复制(可以参见本书“管理篇”的介绍)。MySQL的恢复机制(复制其实就是在Slave Mysql不断做基于BINLOG的恢复)有以下特点。
- 一是MySQL的恢复是SQL语句级的,也就是重新执行BINLOG中的SQL语句。这与Oracle数据库不同,Oracle是基于数据库文件块的。
- 二是MySQL的Binlog是按照事务提交的先后顺序记录的,恢复也是按这个顺序进行的。这点也与Oralce不同,Oracle是按照系统更新号(System Change Number,SCN)来恢复数据的,每个事务开始时,Oracle都会分配一个全局唯一的SCN,SCN的顺序与事务开始的时间顺序是一致的。
从上面两点可知,MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读,这已经超过了ISO/ANSI SQL92“可重复读”隔离级别的要求,实际上是要求事务要串行化。这也是许多情况下,InnoDB要用到间隙锁的原因,比如在用范围条件更新记录时,无论在Read Commited或是Repeatable Read隔离级别下,InnoDB都要使用间隙锁,但这并不是隔离级别要求的。
可以发现,在BINLOG中,更新操作的位置在INSERT...SELECT之前,如果使用这个BINLOG进行数据库恢复,恢复的结果与实际的应用逻辑不符;如果进行复制,就会导致主从数据库不一致!
通过上面的例子,我们就不难理解为什么MySQL在处理“Insert into target_tab select * from source_tab where ...
”和“create table new_tab ...select ... From source_tab where ...
”时要给source_tab加锁,而不是使用对并发影响最小的多版本数据来实现一致性读。还要特别说明的是,如果上述语句的SELECT是范围条件,InnoDB还会给源表加间隙锁(Next-Lock)。
因此,INSERT...SELECT...
和 CREATE TABLE...SELECT...
语句,可能会阻止对源表的并发更新,造成对源表锁的等待。如果查询比较复杂的话,会造成严重的性能问题,我们在应用中应尽量避免使用。实际上,MySQL将这种SQL叫作不确定(non-deterministic)的SQL,不推荐使用。
如果应用中一定要用这种SQL来实现业务逻辑,又不希望对源表的并发更新产生影响,可以采取以下两种措施:
- 一是采取上面示例中的做法,将
innodb_locks_unsafe_for_binlog
的值设置为“on”,强制MySQL使用多版本数据一致性读。但付出的代价是可能无法用binlog正确地恢复或复制数据,因此,不推荐使用这种方式。 - 二是通过使用“
select * from source_tab ... Into outfile
”和“load data infile ...
”语句组合来间接实现,采用这种方式MySQL不会给source_tab
加锁。
1.5.3.9. 什么时候使用表锁
对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁。
- 第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
- 第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
当然,应用中这两种事务不能太多,否则,就应该考虑使用MyISAM表了。
在InnoDB下,使用表锁要注意以下两点。
使用LOCK TABLES虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层──MySQL Server负责的,仅当autocommit=0、innodb_table_locks=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server也才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁;否则,InnoDB将无法自动检测并处理这种死锁。有关死锁,下一小节还会继续讨论。
-
在用 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;
1.5.3.10. 关于死锁
上文讲过,MyISAM表锁是deadlock free的,这是因为MyISAM总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,这就决定了在InnoDB中发生死锁是可能的。
session_1:
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from table_1 where where id=1 for update;
session_2:
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from table_2 where id=1 for update;
session_1:
select * from table_2 where id =1 for update;
因session_2已取得排他锁,等待
session_2:
做一些其他处理...
session_2:
mysql> select * from table_1 where where id=1 for update;
死锁。session_1等待session_2释放table1-id1的锁,session_2等待session_1释放table2-id1的锁
在上面的例子中,两个事务都需要获得对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。
发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数 innodb_lock_wait_timeout来解决。需要说明的是,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。下面就通过实例来介绍几种避免死锁的常用方法。
- 在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。在下面的例子中,由于两个session访问两个表的顺序不同,发生死锁的机会就非常高!但如果以相同的顺序来访问,死锁就可以避免。
- 在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。
- 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。具体演示可参见20.3.3小节中的例子。
- 前面讲过,在REPEATABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...FOR UPDATE加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题。
- 当隔离级别为READ COMMITTED时,如果两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第3个线程又来申请排他锁,也会出现死锁。
1.5.3.11. InnoDB使用的七种锁
自增锁
自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
关于MySQL自增主键的几点问题(上)
共享/排他锁
共享/排它锁是标准的行级锁(row-level locking)
- 事务拿到某一行记录的共享S锁,才可以读取这一行;
- 事务拿到某一行记录的排它X锁,才可以修改或者删除这一行;
- 多个事务可以拿到一把S锁,读读可以并行;
- 而只有一个事务可以拿到X锁,写写/读写必须互斥;
共享/排它锁的潜在问题是,不能充分的并行,解决思路是数据多版本
意向锁
意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。 意向锁有这样一些特点:
- 首先,意向锁,是一个表级别的锁(table-level locking);
- 意向锁分为:
- 意向共享锁(intention shared lock, IS),它预示着,事务有意向对表中的某些行加共享S锁
- 意向排它锁(intention exclusive lock, IX),它预示着,事务有意向对表中的某些行加排它X锁
举个例子:
select ... lock in share mode,要设置IS锁;
select ... for update,要设置IX锁;
- 意向锁协议(intention locking protocol)并不复杂:
- 事务要获得某些行的S锁,必须先获得表的IS锁
- 事务要获得某些行的X锁,必须先获得表的IX锁
由于意向锁仅仅表明意向,它其实是比较弱的锁,意向锁之间并不相互互斥,而是可以并行的。
排它锁是很强的锁,不与其他类型的锁兼容。这也很好理解,修改和删除某一行的时候,必须获得强锁,禁止这一行上的其他并发,以保障数据的一致性。
插入意向锁
插入意向锁,是间隙锁(Gap Locks)的一种(所以,也是实施在索引上的),它是专门针对insert操作的。
- 多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。
- 思路总结
- InnoDB使用共享锁,可以提高读读并发;
- 排他锁,为了保证数据强一致,InnoDB使用强互斥锁,保证同一行记录修改与删除的串行性;
- InnoDB使用插入意向锁,可以提高插入并发;
记录锁
记录锁,它封锁索引记录,例如: select * from t where id=1 for update;
它会在id=1的索引记录上加锁,以阻止其他事务插入,更新,删除id=1的这一行。
说明: select * from t where id=1; 则是快照读(SnapShot Read),它并不加锁。
间隙锁
它封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。
select * from t
where id between 8 and 15
for update;
这个SQL语句会封锁区间,以阻止其他事务id=10的记录插入。
- 间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”
- 如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。
临键锁
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
更具体的,临键锁会封锁索引记录本身,以及索引记录之前的区间。
- 临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
1.5.3.12. 小结
- 记录锁锁定索引记录;
- 间隙锁锁定间隔,防止间隔中被其他事务插入;
- 临键锁锁定索引记录+间隔,防止幻读;
1.5.4 InnoDB存储引擎MVCC实现原理
InnoDB存储引擎MVCC实现原理
1.6 数据库Sharding
1.6.1 什么是Sharding?
Sharding的基本思想就要把一个数据库切分成多个部分放到不同的数据库(server)上,从而缓解单一数据库的性能问题。不太严格的讲,对于海量数据的数据库,如果是因为表多而数据多,这时候适合使用垂直切分,即把关系紧密(比如同一模块)的表切分出来放在一个server上。如果表并不多,但每张表的数据非常多,这时候适合水平切分,即把表的数据按某种规则(比如按ID散列)切分到多个数据库(server)上。当然,现实中更多是这两种情况混杂在一起,这时候需要根据实际情况做出选择,也可能会综合使用垂直与水平切分,从而将原有数据库切分成类似矩阵一样可以无限扩充的数据库(server)阵列。
1.6.2 常见Sharding方案
基于功能切分 (垂直切分)
将功能不同的表放在不同的数据库中。垂直切分的最大特点就是规则简单,实施也更为方便,尤其适合各业务之间的耦合度非常低,相互影响很小,业务逻辑非常清晰的系统。在这种系统中,可以很容易做到将不同业务模块所使用的表分拆到不同的数据库中。根据不同的表来进行拆分,对应用程序的影响也更小,拆分规则也会比较简单清晰。(这也就是所谓的”share nothing”)。
基于区间范围切分
将数据根据数值区间sharding到不同分片上,例如将uid在[1, 10000000]区间的数据放到shard1上,uid在[10000001, 20000000]区间的数据放到shard2上,以此类推......
扩展方案
- 数据扩容
当数据量增长,超过预留的最大数值时就需要进行数据扩容。比如预留的最大uid为30000000,当该值快要达到时就需要进行扩容,将最大uid扩容到40000000。
-
数据迁移
在设计初期,由于没有足够的数据作为sharding的参考,简单的方式就是把数据平均分到不同shard上,如每个shard均存放10000000用户的数据。系统上线后发现,如果用户id采用自增方式分配,即越早注册用户uid越小。通常情况下,系统早期用户都是比较活跃的用户。由于shard1上的用户更活跃,那么它的负载高于其他shard,当其性能出现瓶颈时就需要进行数据迁移。![1-2.png](https://upload-images.jianshu.io/upload_images/12321605-6769233d24c02806.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
于是将uid在区间[5000001, 10000000]上的用户数据从shard1迁移到shard2,此时数据落在shard2上的用户区间为[5000001, 20000000]。不幸的是当我们把数据从shard1迁移到shard2上后,shard2由于数据量增加也出现性能瓶颈,这时我们又需要把shard2上的数据往shard3上迁移,这就造成了恶性的连锁迁移。
可以采用如下两种方案解决连锁迁移问题:
-
多区间sharding 即一个shard存储多个区间的数据。针对上述连锁迁移问题,先扩容出一个shard4,其负责的新用户范围为[30000001, 35000000],并将[1, 5000000]范围内的用户数据迁移到shard4上。mongodb就是使用多区间sharding的方案进行扩展。
-
2. VIP sharding 另一种解决连锁迁移问题的方案是把特殊用户(VIP)sharding到独立的shard上,预先规划好热点用户。例如,QQ使用QQ号登陆,位数少的QQ号、有特殊含义的靓号容易记忆,也就更有价值。拥有这些QQ号的用户不是早期用户就是花了大价钱选号的用户,他们通常更活跃也更在乎用户体验,可将他们分配到专属的shard上。
![1-4.png](https://upload-images.jianshu.io/upload_images/12321605-0516fd99ca05a768.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
基于hash切分
基于区间范围切分非常适合处理有区间查询需求的场景,但是各个shard上负载的均衡很难保证。hash切分能够较均匀地分配数据,一开始便要确定切分的shard个数,通过hash取模来决定使用哪个 shard。但是随着数据量增大,需要进行扩展的时候,就比较麻烦了。每增加一个shard,就需要对hash算法重新运算,数据需要重新切分。
如上图所示,当shard个数从2扩展到3时,shard1上的数据4需要迁移到shard2上,shard2上的数据3需要迁移到shard1上,shard1、shard2上也都有数据要迁移到新的shard3上。这种每个shard都会接收其他shard迁移过来的数据,每个shard也都有数据要迁移到其他众多shard,这种混乱不堪的迁移简直就是灾难。导致这种混乱不堪的迁移的原因是hash取模算法不具备单调性,下面我们就说明何为hash算法的单调性。
hash算法单调性 hash算法的一个重要衡量指标就是单调性,其定义如下:
单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。为了让扩展更方便,我们需要保证hash算法具有单调性,下文介绍两种hash扩展方案。
扩展方案
- 一致性hash
著名的分布式缓存系统memcached就是采用一致性hash将数据sharding到不同节点上,算法如下: 首先求出memcached服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上; 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上; 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台memcached服务器上。
当需要增加一个新的节点node5时,如果是增加在node2和node4之间,那么只有node2和node4之间部分数据会迁移到node5上,其他数据均不需要迁移。
![1-7.png](https://upload-images.jianshu.io/upload_images/12321605-6b3d61fb154301fc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
-
二叉树扩展
二叉树扩展也能够很好地解决迁移混乱问题,redis在rehash渐进式迁移时就采用的这种方案。二叉树扩展要求shard的个数为2n个,只要满足此要求即可进行二叉树扩展。下面我们图解数据库如何进行二叉树扩展 :![1-8.png](https://upload-images.jianshu.io/upload_images/12321605-23e621cc351c9e8a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
假设我们数据的范围为[1, 2, 3, 4, 5, 6, 7, 8],一开始我们有2个shard,数据为[2, 4, 6, 8]和[1, 3, 5, 7],现在发现性能出现瓶颈,需要对其进行扩展; 对shard1和shard2分别建一个从库,主从同步完成后将其作为新的分片shard3和shard4,同时将取模算法从mod 2改为mod 4; 此时每个shard上包含了不用迁移出去的数据,而需要迁移出去的数据其他shard上已经存在,不需要进行迁移,只需将其删除即可。下图中标红的数据就是需要删除的数据。
基于路由表切分
前面的几种方式都是跟据应用的数据来决定操作的shard,基于路由表的切分是一种更加松散的方法。它单独维护一张路由表,根据用户的某一属性来查找路由表决定使用哪个shard,这种方式是一种更加通用的方案。因为每次数据操作的时候都需要进行路由的查找,所以将这些内容存储到一台独立cache上是一个非常好的方式,譬如memcached。这种切分的方式同时也带来了另一个好处,当需要增加shard的时候,可以在不影响在线应用的情况下来执行。
1.6.3. Sharding优点
- 提高了数据库的可扩展性。可以随着应用的增长来增加更多的服务器,只需要将新增加的数据以及负载放到新加的服务器上即可。
- 提高了数据库的可用性。其中几个shard服务器down掉之后,并不会使整个系统瘫痪,只会影响到需要访问这几个shard服务器上的数据的用户。
- 数据量小的数据库的负载较小,查询更快,性能更好。
- 系统有更好的可维护性。对系统的升级和配置可以按照shard一个一个来做,并不会对服务产生大的影响。
1.6.4. 常见Sharding方案对比
- 基于功能切分是纯粹业务上的隔离,更多的是业务层面的考量。
- 基于区间范围切分非常适合处理有区间查询需求的场景,但是各个shard上负载的均衡很难保证。
- 基于hash切分能够较均匀地分配数据,但是扩展比较麻烦,需要保证hash算法单调性。
- 基于路由表切分,最灵活、最通用、最容易扩展和迁移,但是需要额外维护路由数据。
1.6.4. 参考
数据库Sharding技术及扩展的方案