在本文将讨论数据库原理和 MySQL 核心知识,MySQL 性能优化等,包含 MySQL基础 和 高性能MySQL实践 两部分。
参考资料:
学习资料:
这里先有个整体的MySQL Server的整体概念,详情转向:MySQL的多存储引擎架构
事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
MySQL 默认 自动提交模式。也就是说,如果不显式使用 START TRANSACTION 语句来开始一个事务,那么每个查询都会被当做一个事务自动提交
原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。比如在同一个事务中的SQL语句,要么全部执行成功,要么全部执行失败。
回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。以转账为例子,A向B转账,假设转账之前这两个用户的钱加起来总共是2000,那么A向B转账之后,不管这两个账户怎么转,A用户的钱和B用户的钱加起来的总额还是2000,这个就是事务的一致性。
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务 T1 和 T2,在事务 T1 看来,T2 要么在 T1 开始之前就已经结束,要么在 T1 结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。
事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:
满足最低要求的范式是第一范式(1NF)。在第一范式的基础上进一步满足更多规范要求的称为第二范式(2NF),其余范式以次类推。一般说来,数据库只需满足第三范式 (3NF)就行了。
范式的包含关系。一个数据库设计如果符合第二范式,一定也符合第一范式。如果符合第三范式,一定也符合第二范式…
范式理论是为了解决以上提到四种异常。
高级别范式的依赖于低级别的范式,1NF 是最低级别的范式。
属性不可分。
每个非主属性完全函数依赖于键码。
可以通过分解来满足。
分解前
| Sno | Sname | Sdept | Mname | Cname | Grade |
|---|---|---|---|---|---|
| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 |
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 |
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 |
| 3 | 学生-3 | 学院-2 | 院长-2 | 课程-2 | 95 |
以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖:
Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。
Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。
分解后
关系-1
| Sno | Sname | Sdept | Mname |
|---|---|---|---|
| 1 | 学生-1 | 学院-1 | 院长-1 |
| 2 | 学生-2 | 学院-2 | 院长-2 |
| 3 | 学生-3 | 学院-2 | 院长-2 |
有以下函数依赖:
关系-2
| Sno | Cname | Grade |
|---|---|---|
| 1 | 课程-1 | 90 |
| 2 | 课程-2 | 80 |
| 2 | 课程-1 | 100 |
| 3 | 课程-2 | 95 |
有以下函数依赖:
非主属性不传递函数依赖于键码。
上面的 关系-1 中存在以下传递函数依赖:
可以进行以下分解:
关系-11
| Sno | Sname | Sdept |
|---|---|---|
| 1 | 学生-1 | 学院-1 |
| 2 | 学生-2 | 学院-2 |
| 3 | 学生-3 | 学院-2 |
关系-12
| Sdept | Mname |
|---|---|
| 学院-1 | 院长-1 |
| 学院-2 | 院长-2 |
T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。
(针对未提交数据)如果一个事务中对数据进行了更新,但事务还没有提交,另一个事务可以 “看到” 该事务没有提交的更新结果,这样造成的问题就是,如果第一个事务回滚,那么,第二个事务在此之前所 “看到” 的数据就是一笔脏数据。 (脏读又称无效数据读出。一个事务读取另外一个事务还没有提交的数据叫脏读。 )
例子:
Mary 的原工资为 1000, 财务人员将 Mary 的工资改为了 8000 (但未提交事务)
Mary 读取自己的工资,发现自己的工资变为了 8000,欢天喜地!
而财务发现操作有误,回滚了事务,Mary 的工资又变为了1000
像这样,Mary记取的工资数8000是一个脏数据。
解决办法:
把数据库的事务隔离级别调整到 READ_COMMITTED
图解:
T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(同时操作,事务1分别读取事务2操作时和提交后的数据,读取的记录内容不一致。不可重复读是指在同一个事务内,两个相同的查询返回了不同的结果。 )
例子:
(1)在事务1中,Mary 读取了自己的工资为1000,操作并没有完成
con1 = getConnection();
select salary from employee empId ="Mary";
(2)在事务2中,这时财务人员修改了 Mary 的工资为 2000,并提交了事务.
con2 = getConnection();
update employee set salary = 2000;
con2.commit();
(3)在事务1中,Mary 再次读取自己的工资时,工资变为了2000
//con1
select salary from employee empId ="Mary";
在一个事务中前后两次读取的结果并不致,导致了不可重复读。
解决办法:
如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。把数据库的事务隔离级别调整到REPEATABLE_READ
图解:
T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
事务 T1 读取一条指定的 Where 子句所返回的结果集,然后 T2 事务新插入一行记录,这行记录恰好可以满足T1 所使用的查询条件。然后 T1 再次对表进行检索,但又看到了 T2 插入的数据。 (和可重复读类似,但是事务 T2 的数据操作仅仅是插入和删除,不是修改数据,读取的记录数量前后不一致)
幻读的重点在于新增或者删除 (数据条数变化)
同样的条件,第1次和第2次读出来的记录数不一样
例子:
目前工资为1000的员工有10人。 (1)事务1,读取所有工资为 1000 的员工(共读取 10 条记录 )
con1 = getConnection();
Select * from employee where salary =1000;
(2)这时另一个事务向 employee 表插入了一条员工记录,工资也为 1000
con2 = getConnection();
Insert into employee(empId,salary) values("Lili",1000);
con2.commit();
事务1再次读取所有工资为 1000的 员工(共读取到了 11 条记录,这就像产生了幻读)
//con1
select * from employee where salary =1000;
解决办法:
如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题。把数据库的事务隔离级别调整到 SERIALIZABLE_READ
图解:
T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
所有事务一个接着一个的执行,这样可以避免幻读 (phantom read),对于基于锁来实现并发控制的数据库来说,串行化要求在执行范围查询的时候,需要获取范围锁,如果不是基于锁实现并发控制的数据库,则检查到有违反串行操作的事务时,需回滚该事务。
所有被 Select 获取的数据都不能被修改,这样就可以避免一个事务前后读取数据不一致的情况。但是却没有办法控制幻读,因为这个时候其他事务不能更改所选的数据,但是可以增加数据,即前一个事务有读锁但是没有范围锁,为什么叫做可重复读等级呢?那是因为该等级解决了下面的不可重复读问题。
引申:现在主流数据库都使用 MVCC 并发控制,使用之后RR(可重复读)隔离级别下是不会出现幻读的现象。
被读取的数据可以被其他事务修改,这样可能导致不可重复读。也就是说,事务读取的时候获取读锁,但是在读完之后立即释放(不需要等事务结束),而写锁则是事务提交之后才释放,释放读锁之后,就可能被其他事务修改数据。该等级也是 SQL Server 默认的隔离等级。
最低的隔离等级,允许其他事务看到没有提交的数据,会导致脏读。
总结
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
|---|---|---|---|
| 未提交读 | √ | √ | √ |
| 提交读 | × | √ | √ |
| 可重复读 | × | × | √ |
| 可串行化 | × | × | × |
对于初学者来说我们通常不关注存储引擎,但是 MySQL 提供了多个存储引擎,包括处理事务安全表的引擎和处理非事务安全表的引擎。在 MySQL 中,不需要在整个服务器中使用同一种存储引擎,针对具体的要求,可以对每一个表使用不同的存储引擎。
MySQL 中的数据用各种不同的技术存储在文件(或者内存)中。这些技术中的每一种技术都使用不同的存储机制、索引技巧、锁定水平并且最终提供广泛的不同的功能和能力。通过选择不同的技术,你能够获得额外的速度或者功能,从而改善你的应用的整体功能。存储引擎说白了就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法。
例如,如果你在研究大量的临时数据,你也许需要使用内存存储引擎。内存存储引擎能够在内存中存储所有的表格数据。又或者,你也许需要一个支持事务处理的数据库(以确保事务处理不成功时数据的回退能力)。
在MySQL中有很多存储引擎,每种存储引擎大相径庭,那么又改如何选择呢?
MySQL 5.5 以前的默认存储引擎是 MyISAM, MySQL 5.5 之后的默认存储引擎是 InnoDB
不同存储引起都有各自的特点,为适应不同的需求,需要选择不同的存储引擎,所以首先考虑这些存储引擎各自的功能和兼容。
MySQL 5.5 版本之前的默认存储引擎,在 5.0 以前最大表存储空间最大 4G,5.0 以后最大 256TB。
Myisam 存储引擎由 .myd(数据)和 .myi(索引文件)组成,.frm文件存储表结构(所以存储引擎都有)
特性
应用场景
MySQL 5.5 及之后版本的默认存储引擎
特性
应用场景
文件系统存储特点
.csv文件存储表内容.csm文件存储表的元数据,如表状态和数据量.frm存储表的结构CSV存储引擎特点
引用场景
特性
a.arz,a.frm)应用场景
特性
也称为 HEAP 存储引擎,所以数据保存在内存中(数据库重启后会导致数据丢失)
支持 HASH 索引(等值查找应选择 HASH)和 BTree 索引(范围查找应选择)
所有字段都为固定长度,varchar(10) == char(10)
不支持 BLOG 和 TEXT 等大字段
Memory 存储使用表级锁(性能可能不如 innodb)
最大大小由 max_heap_table_size 参数决定
Memory存储引擎默认表大小只有 16M,可以通过调整 max_heap_table_size 参数
应用场景
注意: Memory 数据易丢失,所以要求数据可再生
特性
使用 Federated
默认是禁止的。如果需要启用,需要在启动时增加Federated参数
两者比较
总结 强烈建议:对Innodb引擎使用独立表空间(mysql5.6版本以后默认是独立表空间)
系统表转移为独立表的步骤(非常繁琐)
参考条件:
重要一点: 不要混合使用存储引擎 强烈推荐: Innodb
区别:
应用场景:
仅作拓展延伸,详情请转向:为什么不建议innodb使用亿级大表 | 峰云就她了
| 类型 | 存储 | 存储 | 最小值 | 最大值 |
|---|---|---|---|---|
| byte | bit | signed | signed | |
| TINYINT | 1 | 8 | -27 = -128 | 27-1 = 127 |
| SMALLINT | 2 | 16 | ||
| MEDIUMINT | 3 | 24 | ||
| INT | 4 | 32 | -231 = -2147483648 | 231-1 = 2147483647 |
| BIGINT | 8 | 64 |
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。
INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。
MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。
能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
它与时区无关。
默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。
和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。
它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。
MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。
默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。
索引能够轻易将查询性能提升几个数量级。
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
定义一条数据记录为一个二元组 [key, data],B-Tree 是满足下列条件的数据结构:
查找算法:首先在根节点进行二分查找,如果找到则返回对应节点的 data,否则在相应区间的指针指向的节点递归进行查找。
由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、旋转等操作以保持 B-Tree 性质。
与 B-Tree 相比,B+Tree 有以下不同点:
一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。
红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B Tree 作为索引结构,主要有以下两个原因:
(一)更少的检索次数
平衡树检索数据的时间复杂度等于树高 h,而树高大致为 O(h)=O(logdN),其中 d 为每个节点的出度。
红黑树的出度为 2,而 B Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B Tree 大非常多,因此检索的次数也就更多。
B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,检索效率会更高。
(二)利用计算机预读特性
为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。
操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入。
更多内容请参考:MySQL 索引背后的数据结构及算法原理
| 特性 | 说明 | InnoDB | MyISAM | MEMORY |
|---|---|---|---|---|
| B树索引 (B-tree indexes) | 自增ID物理连续性更高, 二叉树,红黑树高度不可控 |
√ | √ | √ |
| R树索引 (R-tree indexes) | 空间索引 | √ | ||
| 哈希索引 (Hash indexes) | 无法做范围查询 | √ | √ | |
| 全文索引 (Full-text indexes) | √ | √ |
B+Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组。
可以指定多个列作为索引列,多个索引列共同组成键。
B+Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。
如果不是按照索引列的顺序进行查找,则无法使用索引。
InnoDB 的 B+Tree 索引分为主索引和辅助索引。
主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找。
InnoDB 引擎有一个特殊的功能叫 “自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
哈希索引能以 O(1) 时间进行查找,但是失去了有序性,它具有以下限制:
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。
全文索引一般使用倒排索引实现,它记录着关键词到其所在文档的映射。
InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
MyISAM 存储引擎支持空间数据索引,可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
必须使用 GIS 相关的函数来维护数据。
美团面经:哪些情况下不会使用索引?
如果MySQL估计使用全表扫秒比使用索引快,则不适用索引。
例如,如果列key均匀分布在1和100之间,下面的查询使用索引就不是很好:select * from table_name where key>1 and key<90;
如果条件中有or,即使其中有条件带索引也不会使用
例如:select * from table_name where key1='a' or key2='b';如果在key1上有索引而在key2上没有索引,则该查询也不会走索引
复合索引,如果索引列不是复合索引的第一部分,则不使用索引(即不符合最左前缀)
例如,复合索引为(key1,key2),则查询select * from table_name where key2='b';将不会使用索引
如果like是以 % 开始的,则该列上的索引不会被使用。
例如select * from table_name where key1 like '%a';该查询即使key1上存在索引,也不会被使用如果列类型是字符串,那一定要在条件中使用引号引起来,否则不会使用索引
如果列为字符串,则where条件中必须将字符常量值加引号,否则即使该列上存在索引,也不会被使用。
例如,select * from table_name where key1=1;如果key1列保存的是字符串,即使key1上有索引,也不会被使用。
如果使用MEMORY/HEAP表,并且where条件中不使用“=”进行索引列,那么不会用到索引,head表只有在“=”的条件下才会使用索引
更多资料:MySQL索引背后的数据结构及算法原理
B+ 树只有叶节点存放数据,其余节点用来索引,而 B- 树是每个索引节点都会有 Data 域。所以从 InooDB 的角度来看,B+ 树是用来充当索引的,一般来说索引非常大,尤其是关系性数据库这种数据量大的索引能达到亿级别,所以为了减少内存的占用,索引也会被存储在磁盘上。
那么 MySQL如何衡量查询效率呢?答:磁盘 IO 次数
B 树相对于红黑树的区别
两个或更多个列上的索引被称作联合索引,联合索引又叫复合索引。对于复合索引:Mysql 从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。
例如索引是key index (a,b,c),可以支持[a]、[a,b]、[a,b,c] 3种组合进行查找,但不支 [b,c] 进行查找。当最左侧字段是常量引用时,索引就十分有效。
在执行 CREATE TABLE 语句时可以创建索引,也可以单独用 CREATE INDEX 或 ALTER TABLE 来为表增加索引。
ALTER TABLE
ALTER TABLE 用来创建普通索引、UNIQUE 索引或 PRIMARY KEY 索引。
例如:
ALTER TABLE table_name ADD INDEX index_name (column_list)
ALTER TABLE table_name ADD UNIQUE (column_list)
ALTER TABLE table_name ADD PRIMARY KEY (column_list)
其中 table_name 是要增加索引的表名,column_list 指出对哪些列进行索引,多列时各列之间用逗号分隔。索引名 index_name 可选,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE 允许在单个语句中更改多个表,因此可以在同时创建多个索引。
CREATE INDEX
CREATE INDEX 可对表增加普通索引或 UNIQUE 索引。
例如:
CREATE INDEX index_name ON table_name (column_list)
CREATE UNIQUE INDEX index_name ON table_name (column_list)
table_name、index_name 和 column_list 具有与 ALTER TABLE 语句中相同的含义,索引名不可选。另外,不能用 CREATE INDEX 语句创建 PRIMARY KEY 索引。
在创建索引时,可以规定索引能否包含重复值。如果不包含,则索引应该创建为 PRIMARY KEY 或 UNIQUE 索引。对于单列惟一性索引,这保证单列不包含重复的值。对于多列惟一性索引,保证多个值的组合不重复。 PRIMARY KEY 索引和 UNIQUE 索引非常类似。
事实上,PRIMARY KEY 索引仅是一个具有名称 PRIMARY 的 UNIQUE 索引。这表示一个表只能包含一个 PRIMARY KEY,因为一个表中不可能具有两个同名的索引。 下面的SQL语句对 students 表在 sid 上添加 PRIMARY KEY 索引。 ALTER TABLE students ADD PRIMARY KEY (sid)
可利用 ALTER TABLE 或 DROP INDEX 语句来删除索引。类似于 CREATE INDEX 语句,DROP INDEX 可以在 ALTER TABLE 内部作为一条语句处理,语法如下。
DROP INDEX index_name ON talbe_name
ALTER TABLE table_name DROP INDEX index_name
ALTER TABLE table_name DROP PRIMARY KEY
其中,前两条语句是等价的,删除掉 table_name 中的索引 index_name。
第3条语句只在删除 PRIMARY KEY 索引时使用,因为一个表只可能有一个 PRIMARY KEY 索引,因此不需要指定索引名。如果没有创建 PRIMARY KEY 索引,但表具有一个或多个 UNIQUE 索引,则 MySQL 将删除第一个 UNIQUE 索引。
如果从表中删除了某列,则索引会受到影响。对于多列组合的索引,如果删除其中的某列,则该列也会从索引中删除。如果删除组成索引的所有列,则整个索引将被删除。
更多请转向:MySQL-联合索引 - 简书
| 定义 | 作用 | 个数 | |
|---|---|---|---|
| 主键 | 唯一标识一条记录,不能有重复的,不允许为空 | 用来保证数据完整性 | 主键只能有一个 |
| 外键 | 表的外键是另一表的主键,外键可以有重复的,可以是空值 | 用来和其他表建立联系用的 | 一个表可以有多个外键 |
| 索引 | 该字段没有重复值,但可以有一个空值 | 是提高查询排序的速度 | 一个表可以有多个惟一索引 |
https://www.cnblogs.com/s-b-b/p/8334593.html
聚集索引一定是唯一索引。但唯一索引不一定是聚集索引。
聚集索引,在索引页里直接存放数据,而非聚集索引在索引页里存放的是索引,这些索引指向专门的数据页的数据。
注:更多 Redis 相关内容将在 Redis 中进行展开,请转向。
简单来说,数据的切分就是通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主机)中,以达到分散单台设备负载的效果,即分库分表。
数据的切分根据其切分规则的类型,可以分为如下两种切分模式。
垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。
在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库 payDB、用户数据库 userBD 等。
拆分后业务清晰,拆分规则明确
系统之间进行整合或扩展很容易
按照成本、应用的等级、应用的类型等将表放到不同的机器上,便于管理
便于实现动静分离、冷热分离的数据库表的设计模式
数据维护简单
水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。
当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
哈希取模:hash(key) % NUM_DB
范围:可以是 ID 范围也可以是时间范围
映射表:使用单独的一个数据库来存储映射关系
使用分布式事务来解决,比如 XA 接口。
可以将原来的 JOIN 查询分解成多个单表查询,然后在用户程序中进行 JOIN。
主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。
主服务器用来处理写操作以及实时性要求比较高的读操作,而从服务器用来处理读操作。
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
MySQL 读写分离能提高性能的原因在于:
Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
比较重要的字段有:
mysql> explain select * from user_info where id = 2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: user_info
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
更多内容请参考:MySQL 性能优化神器 Explain 使用分析
(一)只返回必要的列
最好不要使用 SELECT * 语句。
(二)只返回必要的行
使用 WHERE 语句进行查询过滤,有时候也需要使用 LIMIT 语句来限制返回的数据。
(三)缓存重复查询的数据
使用缓存可以避免在数据库中进行查询,特别要查询的数据经常被重复查询,缓存可以带来的查询性能提升将会是非常明显的。
最有效的方式是使用索引来覆盖查询。
一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
rows_affected = 0
do {
rows_affected = do_query(
"DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
} while rows_affected > 0
将一个大连接查询(JOIN)分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有:
SELECT * FROM tab
JOIN tag_post ON tag_post.tag_id=tag.id
JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
MySQL/InnoDB 的加锁,一直是一个面试中常问的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?在工作过程中,也会经常用到,乐观锁,排它锁等。
注:MySQL 是一个支持插件式存储引擎的数据库系统。下面的所有介绍,都是基于 InnoDB 存储引擎,其他引擎的表现,会有较大的区别。
版本查看
select version();
存储引擎查看
MySQL 给开发者提供了查询存储引擎的功能,我这里使用的是 MySQL5.6.4,可以使用:
SHOW ENGINES
用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
举例
1、数据库表设计
三个字段,分别是 id,value,version
select id,value,version from TABLE where id=#{id}
2、每次更新表中的value字段时,为了防止发生冲突,需要这样操作
update TABLE
set value=2,version=version+1
where id=#{id} and version=#{version};
与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟 Java 中的 synchronized 很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。
以排它锁为例:
要使用悲观锁,我们必须关闭 mysql 数据库的自动提交属性,因为 MySQL 默认使用 autocommit 模式,也就是说,当你执行一个更新操作后,MySQL 会立刻将结果进行提交。
我们可以使用命令设置 MySQL 为非 autocommit 模式:
set autocommit=0;
# 设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:
# 1. 开始事务 (三者选一就可以)
begin; / begin work; / start transaction;
# 2. 查询表信息
select status from TABLE where id=1 for update;
# 3. 插入一条数据
insert into TABLE (id,value) values (2,2);
# 4. 修改数据为
update TABLE set value=2 where id=1;
# 5. 提交事务
commit;/commit work;
共享锁又称读锁(read lock),是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
如果事务 T 对数据 A 加上共享锁后,则其他事务只能对 A 再加共享锁,不能加排他锁。获得共享锁的事务只能读数据,不能修改数据
打开第一个查询窗口
#三者选一就可以
begin; / begin work; / start transaction;
SELECT * from TABLE where id = 1 lock in share mode;
然后在另一个查询窗口中,对 id 为 1 的数据进行更新
update TABLE set name="www.souyunku.com" where id =1;
此时,操作界面进入了卡顿状态,过了超时间,提示错误信息
如果在超时前,执行 commit,此更新语句就会成功。
[SQL]update test_one set name="www.souyunku.com" where id =1;
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
加上共享锁后,也提示错误信息
update test_one set name="www.souyunku.com" where id =1 lock in share mode;
[SQL]update test_one set name="www.souyunku.com" where id =1 lock in share mode;
[Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'lock in share mode' at line 1
在查询语句后面增加 lock in share mode,MySQL 会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。
加上共享锁后,对于 update,insert,delete 语句会自动加排它锁。
排他锁 exclusive lock(也叫 writer lock)又称写锁。
排它锁是悲观锁的一种实现,在上面悲观锁也介绍过。
若事务 1 对数据对象 A 加上 X 锁,事务 1 可以读 A 也可以修改 A,其他事务不能再对 A 加任何锁,直到事物 1 释放 A 上的锁。这保证了其他事务在事物 1 释放 A 上的锁之前不能再读取和修改 A。排它锁会阻塞所有的排它锁和共享锁
读取为什么要加读锁呢:防止数据在被读取的时候被别的线程加上写锁
使用方式:在需要执行的语句后面加上 for update 就可以了
行锁又分共享锁和排他锁,由字面意思理解,就是给某一行加上锁,也就是一条记录加上锁。
注意:行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。
共享锁:
名词解释:共享锁又叫做读锁,所有的事务只能对其进行读操作不能写操作,加上共享锁后在事务结束之前其他事务只能再加共享锁,除此之外其他任何类型的锁都不能再加了。
#结果集的数据都会加共享锁
SELECT * from TABLE where id = "1" lock in share mode;
排他锁:
名词解释:若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务结束之前,其他事务不能对其进行加任何锁,其他进程可以读取,不能进行写操作,需等待其释放。
select status from TABLE where id=1 for update;
可以参考之前演示的共享锁,排它锁语句
由于对于表中 id 字段为主键,就也相当于索引。执行加锁时,会将 id 这个索引为 1 的记录加上锁,那么这个锁就是行锁。
如何加表锁
innodb 的行锁是在有索引的情况下,没有索引的表是锁定全表的.
Innodb中的行锁与表锁
前面提到过,在 Innodb 引擎中既支持行锁也支持表锁,那么什么时候会锁住整张表,什么时候或只锁住一行呢? 只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!
在实际应用中,要特别注意 InnoDB 行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。
行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁。行级锁的缺点是:由于需要请求大量的锁资源,所以速度慢,内存消耗大。
死锁(Deadlock) 所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。
解除正在死锁的状态有两种方法:
第一种:
show OPEN TABLES where In_use > 0;
show processlist
kill id
第二种:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
杀死进程
kill 进程ID
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。 产生死锁的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
虽然不能完全避免死锁,但可以使死锁的数量减至最少。将死锁减至最少可以增加事务的吞吐量并减少系统开销,因为只有很少的事务回滚,而回滚会取消事务执行的所有工作。由于死锁时回滚而由应用程序重新提交。
下列方法有助于最大限度地降低死锁:
按同一顺序访问对象
避免事务中的用户交互
保持事务简短并在一个批处理中
使用低隔离级别
使用绑定连接
说明:间隙锁相关锁知识待补充
参考资料:
抢订单环节一般会带来2个问题:
1、高并发
比较火热的秒杀在线人数都是10w起的,如此之高的在线人数对于网站架构从前到后都是一种考验。
2、超卖
任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难题。
将存库MySQL前移到Redis中,所有的写操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。然后通过队列等异步手段,将变化的数据异步写入到DB中。
优点:解决性能问题
缺点:没有解决超卖问题,同时由于异步写入DB,存在某一时刻DB和Redis中数据不一致的风险。
引入队列,然后将所有写DB操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
优点:解决超卖问题,略微提升性能。
缺点:性能受限于队列处理机处理性能和DB的写入性能中最短的那个,另外多商品同时抢购的时候需要准备多条队列。
**将提交操作变成两段式,先申请后确认。然后利用Redis的原子自增操作(相比较MySQL的自增来说没有空洞),同时利用Redis的事务特性来发号,保证拿到小于等于库存阀值的号的人都可以成功提交订单。**然后数据异步更新到DB中。
优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。
缺点:由于异步写入DB,可能存在数据不一致。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。
参考资料:
数据库主库和从库不一致,常见有这么几种优化方案:
(1)业务可以接受,系统不优化
(2)强制读主,高可用主库,用缓存提高读性能
(3)在cache里记录哪些记录发生过写请求,来路由读主还是读从
参考资料: