数据库概念
锁分类
锁(按照锁粒度分类)
表级锁:每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
-
行级锁:每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
InnoDB
的行级锁是针对索引加的锁,不针对数据记录
虽然行级索具有粒度小、并发度高等特点,但是表级锁有时候也是有其适用场景的:
- 事务更新大表中的大部分数据直接使用表级锁效率更高
- 事务比较复杂,使用行级索很可能引起死锁导致回滚
锁(按照读写分类)
表级锁和行级锁可以进一步划分为共享锁(S)和排他锁(X)
-
共享锁(S),即读锁
其他用户可以并发读取数据,但任何事务都不能获取数据上的排他锁,直到已释放所有共享锁。
-
排他锁(X),即写锁
若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。
读写锁有一定的缺憾,如遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定,所以就有了意向锁。
意向锁解释:
当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是在某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。
InnoDB
另外的两个表级锁:
- 意向共享锁(IS):表示事务准备给数据行记入共享锁,事务在一个数据行加共享锁前必须先取得该表的IS锁。
- 意向排他锁(IX):表示事务准备给数据行加入排他锁,事务在一个数据行加排他锁前必须先取得该表的IX锁。
意向锁是表级锁,表示的是一种意向,仅仅表示事务正在读或写某一行记录,在真正加行锁时才会判断是否冲突。意向锁是InnoDB
自动加的,不需要用户干预。
IX,IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S发生冲突。
综上,当一个事务请求的锁模式与当前的锁兼容,InnoDB
就将请求的锁授予该事务;反之如果请求不兼容,则该事物就等待锁释放。
页级锁
锁定粒度介于行级锁和表级锁中间的一种锁。
由于表级锁速度快,但冲突多,行级冲突少,但速度慢。而页级锁进行了折衷,一次锁定相邻的一组记录。开销和加锁时间界于表锁和行锁之间,会出现死锁。锁定粒度界于表锁和行锁之间,并发度一般。
死锁及其解决方法
由于InnoDB
的行级锁是针对索引加的锁,不针对数据记录,因此即使访问不同行的记录,如果使用了相同的索引键仍然会出现锁冲突,需要注意的是,在通过以下方式
SELECT ...LOCK IN SHARE MODE;
SELECT ...FOR UPDATE;
使用锁的时候,如果表没有定义任何索引,那么InnoDB
会创建一个隐藏的聚簇索引并使用这个索引来加记录锁。
此外,不同于MyISAM
总是一次性获得所需的全部锁,InnoDB
的锁是逐步获得的,当两个事务都需要获得对方持有的锁,导致双方都在等待,这就产生了死锁。发生死锁后,InnoDB
一般都可以检测到,并使一个事务释放锁回退,另一个则可以获取锁完成事务。当然,我们可以自己事先做一些考虑:
- 通过表级锁来减少死锁产生的概率
- 多个程序尽量约定以相同的顺序访问表(这也是解决并发理论中哲学家就餐问题的一种思路)
- 同一个事务尽可能做到一次锁定所需要的所有资源
锁结论
间隙锁主要还是用于防止幻读的情况,所以多个事务能够同时获取同一段间隙锁本身并没有问题,间隙锁能够阻塞插入意向锁也并没有问题。
而插入意向锁可以看成是一种特殊的间隙锁,是用于在同一个间隙,插入不同的数据,不会互相阻塞。它比普通间隙锁的数据一致性更低,但并发性能更好。
缓存查询
实例针对Mysql说明:
my.cnf加入以下配置,重启Mysql开机查询缓存
query_cache_type=1
query_cache_size=600000
Mysql执行以下命令也可以开启查询缓存
set global query_cache_type=1;
set global query_cache_size=600000;
如上,开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。
上面所述的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。因此任何两个在任何字符上有不同的查询都会导致缓存命不中。此外,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、Mysql
库中的系统表,其查询结果也不会被缓存。
当缓存建立后,Mysql
的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。
缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁,因此,开启缓存查询要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,还可以通过sql_cache
和sql_no_cache
来控制某个查询语句是否需要缓存:
select sql_no_cache count(*) from usr;
事务
事务特性
- 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性: 执行事务前后,数据保持一致;
- 隔离性: 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;
- 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
并发事务带来的问题
脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
-
丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
其中,不可重复读的重点是修改,幻读的重点在于新增或者删除.
事务隔离级别
-
READ_UNCOMMITTED
(未授权读取): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 -
READ_COMMITTED
(授权读取): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 -
REPEATABLE_READ
(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -
SERIALIZABLE
(串行): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
MySql
架构设计
存储引擎
以5.5为例
特点比较
MyISAM
- 不支持行锁(
MyISAM
只有表锁),读取时对需要读到的所有表加锁,写入时则对表加排他锁 - 不支持事务
- 不支持外键
- 不支持崩溃后的安全恢复
- 在表有读取查询的同时,支持往表中插入新纪录
- 支持
BLOB
和TEXT
的前500个字符索引,支持全文索引 - 支持延迟更新索引,极大地提升了写入性能
- 对于不会进行修改的表,支持压缩表,极大地减少了磁盘空间的占用
InnoDB
- 支持表级锁和行级锁,有可能死锁
- 支持事务,其默认的事务隔离级别是
REPEATABLE_READ
,Oracle
默认采用的READ_COMMITTED
隔离级别 - 支持外键
- 支持崩溃后的安全恢复
- 不支持全文索引(
innodb
已经在5.6.4
支持全文索引了)
InnoDB
行级锁分类:
-
Record Lock
:对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项; -
Gap Lock
:对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。 -
Next-key Lock
:锁定索引项本身和索引范围。即Record Lock
和Gap Lock
的结合。可解决幻读问题。
事务隔离机制的实现基于锁机制
和并发调度
。其中并发调度使用的是MVVC
(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性
综上:MyISAM
更适合读密集的表,而InnoDB
更适合写密集的的表。在数据库做主从分离的情况下,可以选择MyISAM
作为主库的存储引擎。
索引结构
MyISAM
和InnoDB
都使用B+树作为其索引结构
但二者也是有区别的:
- 在
InnoDB
中,表数据文件是按B+树
组织的索引结构,这棵树的叶节点data域保存了完整的数据记录(这种索引叫聚集索引),而MyISAM
索引文件和数据文件是分离的(非聚集的),索引文件仅保存数据记录的地址,当定位到具体的数据记录地址后,还需要一次IO才能找到具体的数据记录 -
InnoDB
辅助索引data域存储的是相应记录主键的值,即InnoDB
的所有辅助索引都引用主键作为其data域,而MyISAM
辅助索引data域存储的是相应记录的地址
综上可知,InnoDB
在根据主索引查找数据时,直接找到key所在的节点即可取出数据文件,在根据辅助索引查找数据时,首先需要在key所在的节点的数据文件中找到主键值,再通过主键走一遍主键索引才能找到对应的数据文件(即辅助索引搜索需要检索两遍索引)。当使用InnoDB
创建表时,尽量指明主键,当不包含主键时,存储引擎会生成默认的主键索引,其长度为6B。除此之外,不建议使用过长的字段作为主键,因为辅助索引是通过引用主索引找到其真正的数据文件的,过长的主索引会令辅助索引变得过大;也不建议用非单调的字段作为主键,由于InnoDB
数据文件是一颗B+树,非单调的主键会造成在插入新记录时数据文件为了维持B+树的特性而频繁的分裂调整,十分低效,建议使用自增值作为主键(这样可使随机insert变为顺序append,不会产生页分裂和碎片,提升写性能)。
事务处理机制
InnoDB
事务日志
InnoDB 使用undo
,redo log
来保证事务原子性、一致性及持久性,同时采用预写日志方式将随机写入变成顺序追加写入,提升事务性能。
- undo log:记录事务变更前的状态。操作数据之前,先将数据备份到
undo log
,然后进行数据修改,如果出现错误或用户执行了rollback
语句,则系统就可以利用undo log
中的历史版本恢复到事务开始之前的状态。 - redo log:记录事务将要变更后的状态。事务提交时,只要将
redo log
持久化即可,数据可在内存中变更。当系统崩溃时,虽然数据没有落盘,但是redo log
已持久化,系统可以根据redo log
的内容,将所有数据恢复到最新的状态。 - checkpoint:随着时间的积累,
redo log
会变的很大很大。如果每次都从第一条记录开始恢复,恢复的过程就会很慢。为了减少恢复的时间,就引入了checkpoint
机制。定期将databuffer
的内容刷新到磁盘datafile
内,然后清除checkpoint
之前的redo log
。 - 自动恢复:InnoDB通过加载最新快照,然后重放最近的
checkpoint
点之后所有redo log
事务(包括未提交和回滚了的),再通过undo log
回滚那些未提交的事务,来完成数据恢复。需要注意的地方是,undo log
其实也是行数据,对其写操作也会记录到redo log
内,即undo log
也是通过redo log
来保证持久化的。
下图为事务写操作执行的大致过程,整个过程中只有一次刷盘操作,即事务提交时的redo log
的写盘。
其实写盘并不一定会立马持久化到磁盘,要看数据库配置,默认情况下Innodb_flush_log_at_trx_commit=1
,即一次redo log
写盘操作会立即写到磁盘中,是最保险的方案。
InnoDB
中多个事务共享一个redo log buffer
, 写盘时,会将当前buffer
中的多个事务日志持久化,而不管事务有没有commit
,而且并不是只有事务commit
才会触发redo log
写盘,其它操作也会触发redo log
写盘操作如:
-
redo log buffer
空间不足 - 触发
checkpoint
- 实例
shutdown
-
binlog
切换时
MVCC
MVCC (Multi-Version Concurrency Control)
多版本并发控制协议,将读操作分成两类:快照读 与当前读 。读取的是记录的最新版本,会对返回的记录加上锁,确保其他事务不能并发修改。
-
快照读:简单的查询操作,属于快照读,不加锁
select * from table where ?;
-
当前读:特殊的读操作及插入/更新/删除操作,属于当前读,需要加锁
1 select * from table where ? lock in share mode; 2 select * from table where ? for update; 3 insert into table values (…); 4 update table set ? where ?; 5 delete from table where ?;
快照读是通过undo log
来实现多个版本的控制。如下图,每个数据行:row_id
为行id,trx_id
表示最近修改的事务id,db_roll_ptr
为指向undo segment
中undo log
的指针。快照读时,比较当前事务id与trx_id
的关系,如果trx_id
小于事务id,则该条数据对当前事务可见,反之不可见,不可见时再通过db_roll_ptr
查找历史版本记录,取出可见的最近的历史记录。undo log
的链路不会很深,后台purge
线程定期清除无用的历史版本(在没有活动事务依赖时,undo log
即可被删除)。
数据恢复机制
数据恢复主要分为以下几个步骤:
- 首先检查最近的Checkpoint,在RedoLog中将Checkpoint起始到目前的已提交状态的行数据提交到盘,并将回滚状态的通过在undolog日志中查找相应的行数据恢复
- 将未提交或者为回滚的则通过在undolog日志中查找相应的行数据恢复
表优化
当单表记录数过大时,数据库的CRUD
性能会明显下降,所以有以下方案来解决。
基础方案
- 查询时限定数据的范围
- 读写分离:M-S模式,主库负责写,从库负责读
- 缓存技术:使用数据库的缓存,还可以对重量级、更新少的数据通过应用级别的缓存方式解决
除了以上这些,还有下面两块比较关键的技术。
分表
分表是指数据表列的拆分,把一张列比较多的表拆分为多张表。
分表可以使行数据变小,在查询时减少读取的Block数,减少I/O次数;还可以可以简化表的结构,易于维护。
但是主键会出现冗余,需要管理冗余列,并会引起Join操作,当然可以通过在应用层进行Join来解决,并且会让事务变得更加复杂。
分库
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 分库(水平拆分)可以支撑非常大的数据量。
分库能够支持非常大的数据量存储,应用端改造也少,但分片事务难以解决,跨界点Join性能较差,逻辑复杂。
PS引用:
http://blog.codinglabs.org/articles/theory-of-mysql-index.html
https://segmentfault.com/a/1190000006158186
写的有点糙,欢迎大家批评指正>-<