数据保存在内存
优点: 存取速度快
缺点: 数据不能永久保存
数据保存在文件
优点: 数据永久保存
缺点:1)速度比内存操作慢,频繁的IO操作。2)查询数据不方便
数据保存在数据库
1)数据永久保存
2)使用SQL语句,查询方便效率高。
3)管理数据方便
SQL 是用于访问和处理数据库的标准的计算机语言。
MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下产品。MySQL 最流行的关系型数据库管理系统,在 WEB 应用方面MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。MySQL是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。MySQL所使用的 SQL 语言是用于访问数据库的最常用标准化语言。MySQL 软件采用了双授权政策,它分为社区版和商业版,其体积小、速度快、总体拥有成本低,并且开源。
第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。
第二范式:要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。
第三范式:任何非主属性不依赖于其它非主属性。
在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。
①STATEMENT,基于语句的日志记录,把所有写操作的sql语句写入 binlog (默认)
例如update xxx set update_time = now() where pk_id = 1,这时,主从的 update_time 不一致
优点:
成熟的技术。
更少的数据写入日志文件。当更新或删除影响许多行时,这将导致 日志文件所需的存储空间大大减少。这也意味着从备份中获取和还原可以更快地完成。
日志文件包含所有进行了任何更改的语句,因此它们可用于审核数据库。
缺点:
有很多函数不能复制,例如now()、random()、uuid()等
②ROW,基于行的日志记录,把每一行的改变写入binlog,假设一条sql语句影响100万行,从节点需要执行100万次,效率低。
优点:可以复制所有更改,这是最安全的复制形式
缺点:如果该SQL语句更改了许多行,则基于行的复制可能会向二进制日志中写入更多的数据。即使对于回滚的语句也是如此。这也意味着制作和还原备份可能需要更多时间。此外,二进制日志被锁定更长的时间以写入数据,这可能会导致并发问题。
③MIXED,混合模式,如果 sql 里有函数,自动切换到 ROW 模式,如果 sql 里没有会造成主从复制不一致的函数,那么就使用STATEMENT模式。(存在问题:解决不了系统变量问题,例如@@host name,主从的主机名不一致)
MySQL中定义数据字段的类型对你数据库的优化是非常重要的。
MySQL支持多种类型,大致可以分为三类:数值、日期/时间和字符串(字符)类型。
MySQL支持所有标准SQL数值数据类型。
这些类型包括严格数值数据类型(INTEGER、SMALLINT、DECIMAL和NUMERIC),以及近似数值数据类型(FLOAT、REAL和DOUBLE PRECISION)。
关键字INT是INTEGER的同义词,关键字DEC是DECIMAL的同义词。
BIT数据类型保存位字段值,并且支持MyISAM、MEMORY、InnoDB和BDB表。
作为SQL标准的扩展,MySQL也支持整数类型TINYINT、MEDIUMINT和BIGINT。下面的表显示了需要的每个整数类型的存储和范围。
类型 | 大小 | 范围(有符号) | 范围(无符号) | 用途 |
---|---|---|---|---|
TINYINT | 1 byte | (-128,127) | (0,255) | 小整数值 |
SMALLINT | 2 bytes | (-32 768,32 767) | (0,65 535) | 大整数值 |
MEDIUMINT | 3 bytes | (-8 388 608,8 388 607) | (0,16 777 215) | 大整数值 |
INT或INTEGER | 4 bytes | (-2 147 483 648,2 147 483 647) | (0,4 294 967 295) | 大整数值 |
BIGINT | 8 bytes | (-9,223,372,036,854,775,808,9 223 372 036 854 775 807) | (0,18 446 744 073 709 551 615) | 极大整数值 |
FLOAT | 4 bytes | (-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) | 0,(1.175 494 351 E-38,3.402 823 466 E+38) | 单精度 浮点数值 |
DOUBLE | 8 bytes | (-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) | 0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) | 双精度 浮点数值 |
DECIMAL | 对DECIMAL(M,D) ,如果M>D,为M+2否则为D+2 | 依赖于M和D的值 | 依赖于M和D的值 | 小数值 |
表示时间值的日期和时间类型为DATETIME、DATE、TIMESTAMP、TIME和YEAR。
每个时间类型有一个有效值范围和一个"零"值,当指定不合法的MySQL不能表示的值时使用"零"值。
TIMESTAMP类型有专有的自动更新特性,将在后面描述。
类型 | 大小 (bytes) |
范围 | 格式 | 用途 |
---|---|---|---|---|
DATE | 3 | 1000-01-01/9999-12-31 | YYYY-MM-DD | 日期值 |
TIME | 3 | ‘-838:59:59’/‘838:59:59’ | HH:MM:SS | 时间值或持续时间 |
YEAR | 1 | 1901/2155 | YYYY | 年份值 |
DATETIME | 8 | 1000-01-01 00:00:00/9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值 |
TIMESTAMP | 4 | 1970-01-01 00:00:00/2038 结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038年1月19日 凌晨 03:14:07 |
YYYYMMDD HHMMSS | 混合日期和时间值,时间戳 |
字符串类型指CHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT、ENUM和SET。该节描述了这些类型如何工作以及如何在查询中使用这些类型。
类型 | 大小 | 用途 |
---|---|---|
CHAR | 0-255 bytes | 定长字符串 |
VARCHAR | 0-65535 bytes | 变长字符串 |
TINYBLOB | 0-255 bytes | 不超过 255 个字符的二进制字符串 |
TINYTEXT | 0-255 bytes | 短文本字符串 |
BLOB | 0-65 535 bytes | 二进制形式的长文本数据 |
TEXT | 0-65 535 bytes | 长文本数据 |
MEDIUMBLOB | 0-16 777 215 bytes | 二进制形式的中等长度文本数据 |
MEDIUMTEXT | 0-16 777 215 bytes | 中等长度文本数据 |
LONGBLOB | 0-4 294 967 295 bytes | 二进制形式的极大文本数据 |
LONGTEXT | 0-4 294 967 295 bytes | 极大文本数据 |
注意:char(n) 和 varchar(n) 中括号中 n 代表字符的个数,并不代表字节个数,比如 CHAR(30) 就可以存储 30 个字符。
CHAR 和 VARCHAR 类型类似,但它们保存和检索的方式不同。它们的最大长度和是否尾部空格被保留等方面也不同。在存储或检索过程中不进行大小写转换。
BINARY 和 VARBINARY 类似于 CHAR 和 VARCHAR,不同的是它们包含二进制字符串而不要非二进制字符串。也就是说,它们包含字节字符串而不是字符字符串。这说明它们没有字符集,并且排序和比较基于列值字节的数值值。
BLOB 是一个二进制大对象,可以容纳可变数量的数据。有 4 种 BLOB 类型:TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB。它们区别在于可容纳存储范围不同。
有 4 种 TEXT 类型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。对应的这 4 种 BLOB 类型,可存储的最大长度不同,可根据实际情况选择。
存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。
常用的存储引擎有以下:
MyISAM与InnoDB区别
MyISAM | Innodb | |
---|---|---|
存储结构 | 每张表被存放在三个文件:frm-表格定义、MYD(MYData)-数据文件、MYI(MYIndex)-索引文件 | 所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB |
存储空间 | MyISAM可被压缩,存储空间较小 | InnoDB的表需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引 |
可移植性、备份及恢复 | 由于MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作 | 免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了 |
文件格式 | 数据和索引是分别存储的,数据.MYD ,索引.MYI |
数据和索引是集中存储的,.ibd |
记录存储顺序 | 按记录插入顺序保存 | 按主键大小有序插入 |
外键 | 不支持 | 支持 |
事务 | 不支持 | 支持 |
锁支持(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的) | 表级锁定 | 行级锁定、表级锁定,锁定力度小并发能力高 |
SELECT | MyISAM更优 | |
INSERT、UPDATE、DELETE | InnoDB更优 | |
select count(*) | myisam更快,因为myisam内部维护了一个计数器,可以直接调取。 | |
索引的实现方式 | B+树索引,myisam 是堆表 | B+树索引,Innodb 是索引组织表 |
哈希索引 | 不支持 | 支持 |
全文索引 | 支持 | 不支持 |
插入缓冲(insert buffer)
二次写(double write)
自适应哈希索引(ahi)
预读(read ahead)
如果没有特别的需求,使用默认的Innodb
即可。
MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。
索引是存储引擎用于快速检索记录行的一种数据结构,用于提升数据库的查找速度。
为什么需要使用索引:
①索引能极大的减少存储引擎需要扫描的数据量
②索引可以把随机I/O变为顺序I/O
③索引可以帮助我们进行分组、排序等操作时,避免使用临时表
索引对于良好的性能非常关键,尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要,在数据量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但当数据量逐渐增大时,性能则会急剧下降。
索引优化应该是查询性能优化最有效的手段了(可以起到立竿见影的效果),索引能够轻易将查询性能提高几个数量级,’最优’的索引有时比一个‘好的’索引性能要好两个数量级,创建一个真正’最优’的索引经常需要重写查询。
(1)主键索引:数据列不允许重复,不允许为NULL,一个表只能有一个主键。
(2)普通索引:基本的索引类型,没有唯一性的限制,允许为NULL值。
(3)唯一索引:它与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
(4)全文索引: 是目前搜索引擎使用的一种关键技术。
当人们谈论索引的时候,如果没有特别指明类型,多半说的是B-Tree索引,它使用B-Tree数据结构( InnoDB使用的是B+Tree )来存储数据。大多数MySQL存储引擎都支持这种索引。
不同的存储引擎以不同的方式使用B-Tree索引,性能也各有不同,各有优劣,例如MyISAM使用前缀压缩技术使用索引更小,但InnoDB则按照原数据格式进行存储。再如MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行。
数据库索引为什么使用B+树?
为什么不用二叉树做B-Tree索引底层数据结构?
①当数据量大时,树的高度会比较高(树的高度决定着它的IO操作次数,IO操作耗时大),查询会比较慢。
②每个磁盘块(节点/页)保存的数据太小(IO本来是耗时操作,每次IO只能读取到一个关键字,显然不合适)
没有很好的利用操作磁盘IO的数据交换特性,也没有利用好磁盘IO的预读能力(空间局部性原理),从而带来频繁的IO操作。
B树的搜索:从根节点开始,对节点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子节点;重复,直到所对应的儿子指针为空,或已经是叶子节点。 关键字集合分布在整颗树中 ,即叶子节点和非叶子节点都存放数据,搜索可能在非叶子节点结束。其搜索性能等价于在关键字全集内做一次二分查找。
假设检索26,先把磁盘块1加载到内存中,然后26与28和46比较,26比28小,然后基于P1子节点引用,P1是指向磁盘块2的一个指针地址,基于P1引用可以通过顺序IO快速加载磁盘块2,然后26与19和23比,26大于23,通过P3子节点引用,加载磁盘块7。然后命中,基于节点数据区加载数据。
B树的特点:
①不再是二叉搜索,而是m叉搜索;
②叶子节点,非叶子节点,都存储数据;
③中序遍历,可以获得所有节点;
名词解释:
局部性原理:软件设计要尽量遵循 “数据读取集中”与“使用到一个数据,大概率会使用其附近的数据”,这样磁盘预读能充分提高磁盘IO;
磁盘预读能力:磁盘读写并不是按需读取,而是按页预读,一次会读一页的数据,每次加载更多的数据,如果未来要读取的数据就在这一页中,可以避免未来的磁盘IO,提高效率;
数据交换特性:操作系统去硬盘读取一次,做一次I/O交换,一次交换数据是4k(Linux默认页大小),交换单位以页为单位,1页就是4k(索引按数据页为单位读写的,在InnoDB中,每个数据页的大小默认是16KB)
它是B-Tree数的变体,也是一种多路搜索树B+Tree和B-Tree基本相同,区别在于B-Tree树非叶子节点和叶子节点都可以存放数据,而B+Tree树关键字存储在叶子节点上,非叶子节点不存真正的数据。(B+树中根到每一个节点的路径长度一样,因此查询速度更稳定;而B树不是这样)
叶子之间,增加了链表,获取所有节点,不再需要中序遍历,直接遍历叶子节点就行;
比如查找28,其实图顶端的28是索引,并不是真实数据,他会继续往下找。
B+Tree与B-Tree比较
①B+Tree范围查找,定位min与max之后,中间叶子节点,就是结果集,不用中序回溯;
②B+Tree磁盘读写能力更强(叶子节点不保存真实数据,因此一个磁盘块能保存的关键字更多,因此每次加载的关键字越多)
③B+Tree扫表和扫库能力更强(B-Tree树需要扫描整颗树,B+Tree树只需要扫描叶子节点)
索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
索引的原理很简单,就是把无序的数据变成有序的查询
把创建了索引的列的内容进行排序
对排序结果生成倒排表
在倒排表内容上拼上数据地址链
在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据
索引算法有 BTree算法和Hash算法
BTree算法
BTree是最常用的mysql数据库索引算法,也是mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量, 例如:
select * from user where name like 'jack%';
select * from user where name like '%jack';
Hash算法
Hash Hash索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像BTree索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于BTree索引。
只有当索引帮助存储引擎快速查找到记录带来的好处大于其带来的额外工作时,索引才是有效的。对于非常小的表,大部分情况下简单的全表扫描更高效,对于中到大型的表,索引就非常有效。
正确地创建和使用索引是实现高性能查询的基础。
独立的列
如果查询中的列不是独立的,则MySQL就不会使用索引,’独立的列’是指索引列不能是表达式的一部分,也不是函数的参数。
select actor_id from skill.actor where actor_id + 1 = 5,这个查询无法使用actor_id列的索引;
select actor_id from skill.actor where to_days(current_date) - to_days(date_col) <= 10,这个也不会使用索引。
前缀索引和索引选择性
有时候需要索引很长的字符列,这会让索引变得大且慢,通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率,但这样也会降低索引的选择性,索引的选择性是指,不重复的索引值和数据表的记录总数(#T)的比值,范围从1/#T到1之间,索引的选择性越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行,唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能,对于BLOB、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整程度。
诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。
如何选择合适的前缀?
①我们可以通过left函数
②计算完整列的选择性,并使前缀的选择性接近于完整列的选择性;
select count(distinct city)/count(*) from skill.city_demo,选择性0.0312
select count(distinct left(city,6))/count(*) from skill.city_demo,选择性0.0309
select count(distinct left(city,7))/count(*) from skill.city_demo,选择性0.0310
前缀索引是一种能使索引更小、更快的有效方法,但另一方面也有其缺点:MySQL无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。
有时候后缀索引也有用途,MySQL原声不支持反向索引,但是可以把字符串反转后存储,并基于此建立前缀索引。
多列索引
一个常见的错误就是,为每个列创建一个独立的索引或者按照错误的顺序创建多列索引。
当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。
当服务器需要对多个索引做联合操作时(通常有多个OR条件),通常需要耗费大量CPU和内存资源在算法的缓存、排序合并操作上,特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候,导致该执行计划还不如直接走全表扫描,这样做不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性。
单列索引:节点中关键字【name】
联合索引:节点中关键字【name,phoneNum】
联合索引列选择原则:
①经常用的列优先【最左匹配原则】
②选择性(离散性)高的优先【离散度高原则】
③宽度小的列优先【最少空间原则】
优先级1>2>3
select * from t_user where name = ?
select * from t_user where name = ? and phoneNum = ?
create index index_name on t_user(name)
create index index_name_phoneNum on t_user(name,phoneNum)
这种做法是错误的,根据最左匹配原则,两条查询都可以走index_name_phoneNum索引,index_name索引就是冗余索引。
选择合适的索引列顺序
当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的,这时索引的作用只是用于优化WHERE条件的查找,在这种情况下,这种设计的索引确实能够最快地过滤出需要的行,对于在WHERE字句中只使用了索引部分前缀列的查询来说选择性也更高,然而,性能不只是依赖于所有索引列的选择性,也和查询条件的具体值有关,也就是和值的分布有关,可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。
聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式,具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了B+Tree索引和数据行。
当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中,术语“聚簇”表示数据行和相邻的键值紧凑地存储在一起(这并非总成立),因为无法同时把数据行存在两个不同的地方,所以一个表只能有一个聚簇索引(不过,覆盖索引可以模拟多个聚簇索引的情况)。
因为是存储引擎负责实现索引,因此不是所有的存储引擎都支持聚簇索引,这里只关注InnoDB。
例如:sex字段,只有男和女,离散性很差,因此选择性很差
通常,通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。
关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
首先要知道Hash索引和B+树索引的底层实现原理:
hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据。B+树底层实现是多路平衡查找树。对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据。
那么可以看出他们有以下的不同:
因为在hash索引中经过hash函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询。而B+树的的所有节点皆遵循(左节点小于父节点,右节点大于父节点,多叉树也类似),天然支持范围。
因此,在大多数情况下,直接选择B+树索引可以获得稳定且较好的查询速度。而不需要使用hash索引。
澄清一个概念:innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值
何时使用聚簇索引与非聚簇索引
不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。
举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select age from employee where age < 20
的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。
MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。
具体原因为:
MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。
当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。
①原子性(Atomic):事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保一组操作要么全部成功要么全部失败。
②一致性(Consistency):执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结果是相同的。
③隔离性(Isolation):并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发事务之间数据库是独⽴的。
④持久性(Durability):⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发⽣故障也不应该对其有任何影响。
什么是事务的隔离性?
隔离性是指,多个用户的并发事务访问同一个数据库时,一个用户的事务不应该被其他用户的事务干扰,多个并发事务之间要相互隔离。
咱们举例子来说明:
建表语句:
CREATE TABLE `T` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB;
数据列表:
id | name |
---|---|
1 | xiaohong |
2 | zhangsan |
3 | lisi |
案例一:
事务A,先执行,处于未提交的状态:
insert into T values(4, wangwu);
事务B,后执行,也未提交:
select * from T;
如果事务B能够读取到(4, wangwu)这条记录,事务A就对事务B产生了影响,这个影响叫做“读脏”,读到了未提交事务操作的记录。
案例二:
事务A,先执行:
select * from T where id=1;
结果集为:1, xiaohong
事务B,后执行,并且提交:
update T set name=hzy where id=1;
commit;
事务A,再次执行相同的查询:
select * from T where id=1;
结果集为:1, hzy
这次是已提交事务B对事务A产生的影响,这个影响叫做“不可重复读”,一个事务内相同的查询,得到了不同的结果。
案例三:
事务A,先执行:
select * from T where id>3;
结果集为: NULL
事务B,后执行,并且提交:
insert into T values(4, wangwu);
commit;
事务A,首次查询了id>3的结果为NULL,于是想插入一条为4的记录:
insert into T values(4, hzy);
结果集为: Error : duplicate key!
这次是已提交事务B对事务A产生的影响,这个影响叫做“幻读”。
可以看到,并发的事务可能导致其他事务:
读脏
不可重复读
幻读
InnoDB实现了四种不同事务的隔离级别:
不同事务的隔离级别,实际上是一致性与并发性的一个权衡与折衷。
InnoDB的四种事务的隔离级别,分别是怎么实现的?
InnoDB使用不同的锁策略(Locking Strategy)来实现不同的隔离级别。
一、读未提交(Read Uncommitted)
这种事务隔离级别下,select语句不加锁。
此时,可能读取到不一致的数据,即“读脏”。这是并发最高,一致性最差的隔离级别。
二、串行化(Serializable)
这种事务的隔离级别下,所有select语句都会被隐式的转化为select … in share mode.
这可能导致,如果有未提交的事务正在修改某些行,所有读取这些行的select都会被阻塞住。
这是一致性最好的,但并发性最差的隔离级别。 在互联网大数据量,高并发量的场景下,几乎不会使用上述两种隔离级别。
三、可重复读(Repeated Read, RR) 这是InnoDB默认的隔离级别,在RR下:
①普通的select使用快照读(snapshot read),这是一种不加锁的一致性读(Consistent Nonlocking Read),底层使用MVCC来实现;
②加锁的select(select … in share mode / select … for update), update, delete等语句,它们的锁,依赖于它们是否在唯一索引(unique index)上使用了唯一的查询条件(unique search condition),或者范围查询条件(range-type search condition):
四,读提交(Read Committed, RC) 这是互联网最常用的隔离级别,在RC下:
①普通读是快照读;
②加锁的select, update, delete等语句,除了在外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会封锁区间,其他时刻都只使用记录锁;
此时,其他事务的插入依然可以执行,就可能导致,读取到幻影记录。
我们聊下MySQL是如何实现Read Repeatable的吧,因为一般我们都不修改这个隔离级别,但是你得清楚是怎么回事儿,MySQL是通过MVCC机制来实现的,就是多版本并发控制,multi-version concurrency control。
innodb存储引擎,会在每行数据的最后加两个隐藏列,一个保存行的创建时间,一个保存行的删除时间,但是这儿存放的不是时间,而是事务id,事务id是mysql自己维护的自增的,全局唯一。
事务id,在mysql内部是全局唯一递增的,事务id=1,事务id=2,事务id=3
id | name | 创建事务id | 删除事务id |
---|---|---|---|
1 | 张三 | 120 | 空 |
事务ID=121的事务,查询ID=1的这一行数据,一定会找到创建事务ID<=当前事务ID的那一行,select * from table where id = 1,就可以查到上面那一行。
事务ID=122的事务,将ID=1的这一行删除了,此时就会将ID=1的行的删除事务ID设置成122
id | name | 创建事务id | 删除事务id |
---|---|---|---|
1 | 张三 | 120 | 122 |
事务ID=121的事务,再次查询ID=1的那一行,能查到,创建事务ID<=当前事务ID,当前事务ID < 删除事务ID
id | name | 创建事务id | 删除事务id |
---|---|---|---|
1 | 张三 | 120 | 122 |
2 | 李四 | 119 | 空 |
事务id=121的事务,查询id=2的那一行,查到name=李四
id | name | 创建事务id | 删除事务id |
---|---|---|---|
1 | 张三 | 120 | 122 |
2 | 李四 | 119 | 空 |
2 | 小李四 | 122 | 空 |
事务id=122的事务,将id=2的那一行的name修改成name=小李四
Innodb存储引擎,对于同一个ID,不同的事务创建或修改,每个事务都有自己的快照(会插入一条记录)
事务id=121的事务,查询id=2的那一行,答案是:李四,创建事务id <= 当前事务id,当前事务id < 删除事务id.
在一个事务内查询的时候,mysql只会查询创建事务id <= 当前事务id的行,这样可以确保这个行是在当前事务中创建,或者是之前创建的;同时一个行的删除事务id要么没有定义(就是没删除),要么是比当前事务id大(在事务开启之后才被删除);满足这两个条件的数据都会被查出来。
那么如果某个事务执行期间,别的事务更新了一条数据呢?这个很关键的一个实现,其实就是在innodb中,是插入了一行记录,然后将新插入的记录的创建时间设置为新的事务的id,同时将这条记录之前的那个版本的删除时间设置为新的事务的id。
现在get到这个点了吧?这样的话,你的这个事务其实对某行记录的查询,始终都是查找的之前的那个快照,因为之前的那个快照的创建时间小于等于自己事务id,然后删除时间的事务id比自己事务id大,所以这个事务运行期间,会一直读取到这条数据的同一个版本。
Spring支持两种事务,编程式事务,和声明式事务。
编程式事务,Spring提供了一套API,自己通过API开启事务,回滚事务,提交事务等。(没有特殊需求,一般不用这种方式)
声明式事务分为两种,一种是通过在xml里配置个AOP来声明个切面加事务(没有特殊需求,一般不用这种方式),一种是通过注解配置事务(推荐)
通过在xml里配置个AOP来声明个切面加事务(声明式事务方式一)
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">property>
bean>
<tx:advice id="interceptor" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="account*"/>
tx:attributes>
tx:advice>
<aop:config>
aop:pointcut>
aop:advisor>
aop:config>
通过注解配置事务(声明式事务方式二)
这个@Transactional注解呢,根据阿里编码规范,一般建议加在方法级别,就是要事务的方法就加事务,不要事务的方法就别加事务
@Transactional(
// 事务隔离级别
isolation= Isolation.DEFAULT,
// 事务传播行为
propagation = Propagation.REQUIRES_NEW,
// 哪些异常不会滚
noRollbackFor={
NullPointerException.class,ClassCastException.class},
// 哪些异常回滚
rollbackFor = {
Exception.class},
// 指定事务是否只读,表示这个事务只读取数据,不更新数据
// 可帮助数据库引擎优化事务
readOnly = true,
// 指定强制回滚之前事务可占用的时间
timeout = 5000
)
另外这个注解一般要加rollbackFor,就是指定哪些异常类型才要回滚事务
还有比较重要的,就是有个isolation属性,你可以自己手动调整事务的隔离级别,但是这个一般不调整,记住,别乱调整事务隔离级别,一般可重复读+mysql mvcc机制跑的很好,你别瞎折腾。
另外一个重要的事务属性,就是propagation,事务的传播行为,我们就重点先来聊一下事务的传播行为,这个在项目里可能确实是要用到的。其实说白了,这个事务的传播机制,就是说,一个加了@Transactional的事务方法,和嵌套了另外一个@Transactional的事务方法的时候,包括再次嵌入@Transactional事务方法的时候,这个事务怎么玩儿?
public class ServiceA {
@Autowired
private ServiceB b;
@Transactional
public void methodA() {
// 一坨数据库操作
for(int i = 0; i < 51; i++) {
try {
b.methodB();
} catch(Exception e) {
// 打印异常日志
}
}
// 一坨数据库操作
}
}
public class ServiceB {
@Transactional(propagation = PROPAGATION_REQUIRES_NEW)
public void methodB() throws Exception {
// 一坨数据库操作
}
}
一共有7种事务传播行为:
(1)PROPAGATION_REQUIRED:这个是最常见的,就是说,如果ServiceA.method调用了ServiceB.method,如果ServiceA.method开启了事务,然后ServiceB.method也声明了事务,那么ServiceB.method不会开启独立事务,而是将自己的操作放在ServiceA.method的事务中来执行,ServiceA和ServiceB任何一个报错都会导致整个事务回滚。这就是默认的行为,其实一般我们都是需要这样子的。
(2)PROPAGATION_SUPPORTS:如果ServiceA.method开了事务,那么ServiceB就将自己加入ServiceA中来运行,如果ServiceA.method没有开事务,那么ServiceB自己也不开事务
(3)PROPAGATION_MANDATORY:必须被一个开启了事务的方法来调用自己,否则报错
(4)PROPAGATION_REQUIRES_NEW:ServiceB.method强制性自己开启一个新的事务,然后ServiceA.method的事务会卡住,等ServiceB事务完了自己再继续。这就是影响的回滚了,如果ServiceA报错了,ServiceB是不会受到影响的,ServiceB报错了,ServiceA也可以选择性的回滚或者是提交。
(5)PROPAGATION_NOT_SUPPORTED:就是ServiceB.method不支持事务,ServiceA的事务执行到ServiceB那儿,就挂起来了,ServiceB用非事务方式运行结束,ServiceA事务再继续运行。这个好处就是ServiceB代码报错不会让ServiceA回滚。
(6)PROPAGATION_NEVER:不能被一个事务来调用,ServiceA.method开事务了,但是调用了ServiceB会报错
(7)PROPAGATION_NESTED:开启嵌套事务,ServiceB开启一个子事务,如果回滚的话,那么ServiceB就回滚到开启子事务的这个save point。
大家回头想想那个面试题,其实就是ServiceA里循环51调用ServiceB,第51次调用ServiceB失败了。第一个选项,就是两个事务都设置为PROPAGATION_REQUIRED就好了,ServiceB的所有操作都加入了ServiceA启动的一个大事务里去,任何一次失败都会导致整个事务的回滚;第二个选项,就是将ServiceB设置为PROPAGATION_REQUIRES_NEW,这样ServiceB的每次调用都在一个独立的事务里执行,这样的话,即使第51次报错,但是仅仅只是回滚第51次的操作,前面50次都在独立的事务里成功了,是不会回滚的。
其实一般也就PROPAGATION_REQUIRES_NEW比较常用,要的效果就是嵌套的那个事务是独立的事务,自己提交或者回滚,不影响外面的大事务,外面的大事务可以获取抛出的异常,自己决定是继续提交大事务还是回滚大事务。
如何使用普通锁保证一致性?
①操作数据前,加锁,实施互斥,不允许其他的并发任务操作;
②操作完成后,释放锁,让其他任务执行;如此这般,来保证一致性。
普通锁存在什么问题?
简单的锁住太过粗暴,连“读任务”也无法并行,任务执行过程本质上是串行的。
简单的锁住太过粗暴,连“读任务”也无法并行,任务执行过程本质上是串行的。于是出现了共享锁与排他锁:
共享锁(Share Locks,记为S锁),读取数据时加S锁
排他锁(eXclusive Locks,记为X锁),修改数据时加X锁
共享锁与排他锁:
共享锁之间不互斥,读读可以并行
排他锁与任何锁互斥,写读,写写不可以并行
可以看到,一旦写数据的任务没有完成,数据是不能被其他任务读取的,这对并发度有较大的影响。
有没有可能,进一步提高并发呢?
即使写任务没有完成,其他读任务也可能并发,MySQL通过多版本控制解决此问题。(快照读)
InnoDB支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,实际应用中,InnoDB使用的是意向锁。
意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。
意向锁的特点:
①首先,意向锁,是一个表级别的锁(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锁
④由于意向锁仅仅表明意向,它其实是比较弱的锁,意向锁之间并不相互互斥,而是可以并行,其兼容互斥表如下:
IS | IX | |
---|---|---|
IS | 兼容 | 兼容 |
IX | 兼容 | 兼容 |
⑤既然意向锁之间都相互兼容,那其意义在哪里呢?它会与共享锁/排它锁互斥,其兼容互斥表如下:
S | X | |
---|---|---|
IS | 兼容 | 互斥 |
IX | 互斥 | 互斥 |
补充:排它锁是很强的锁,不与其他类型的锁兼容。这也很好理解,修改和删除某一行的时候,必须获得强锁,禁止这一行上的其他并发,以保障数据的一致性。
意向锁解决什么问题?
事务 A 获取了某一行的排它锁,并未提交: select * from table where id = 6 from update
事务 B 想要获取 table 表的表锁: LOCK TABLES table READ;
因为共享锁与排它锁互斥,所以事务 B 在视图对 table 表加共享锁的时候,必须保证:
①当前没有其他事务持有 table 表的排它锁。
②当前没有其他事务持有 table 表中任意一行的排它锁 。
为了检测是否满足第二个条件,事务 B 必须在确保 table表不存在任何排它锁的前提下,去检测表中的每一行是否存在排它锁。很明显这是一个效率很差的做法,但是有了意向锁之后,事务A持有了table表的意向排它锁,就可得知事务A必然持有该表中某些数据行的排它锁,而无需去检测表中每一行是否存在排它锁
意向锁之间为什么互相兼容?
事务 A 先获取了某一行的排他锁,并未提交: select * from users where id = 6 for update
①事务 A 获取了 users 表上的意向排他锁。
②事务 A 获取了 id 为 6 的数据行上的排他锁。
之后事务 B 想要获取 users 表的共享锁: LOCK TABLES users READ;
事务 B 检测到事务 A 持有 users 表的意向排他锁。 事务 B 对 users 表的加锁请求被阻塞(排斥)。
最后事务 C 也想获取 users 表中某一行的排他锁: select * from users where id = 5 for update;
①事务 C 申请 users 表的意向排他锁。
②事务 C 检测到事务 A 持有 users 表的意向排他锁。
③因为意向锁之间并不互斥,所以事务 C 获取到了 users 表的意向排他锁。
④因为id 为 5 的数据行上不存在任何排他锁,最终事务 C 成功获取到了该数据行上的排他锁。
如果意向锁之间互斥,行级锁的意义将会失去
记录锁,它封锁索引记录,例如:
select * from t where id=1 for update; 它会在id=1的索引记录上加锁,以阻止其他事务插入,更新,删除id=1的这一行。
需要说明的是: select * from t where id=1; 则是快照读(SnapShot Read),它并不加锁,具体在《17.什么是快照读?》中做了详细阐述。
间隙锁,它封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。
存储引擎:InnoDB
隔离级别:可重复读隔离级别
建表语句:
mysql> CREATE TABLE `T` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB;
数据列表:
id | name |
---|---|
1 | xiaohong |
3 | zhangsan |
5 | lisi |
9 | wangwu |
这个SQL语句 select * from T where id between 8 and 15 for update; 会封锁区间,以阻止其他事务id=10的记录插入。
为什么要阻止id=10的记录插入? 如果能够插入成功,头一个事务执行相同的SQL语句,会发现结果集多出了一条记录,即幻影数据。
间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。
如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
更具体的,临键锁会封锁索引记录本身,以及索引记录之前的区间。
如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。
存储引擎:InnoDB
隔离级别:可重复读隔离级别
建表语句:
mysql> CREATE TABLE `T` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB;
数据列表:
id | name |
---|---|
1 | xiaohong |
3 | zhangsan |
5 | lisi |
9 | wangwu |
PK上潜在的临键锁为:
(-infinity, 1]
(1, 3]
(3, 5]
(5, 9]
(9, +infinity]
临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
对已有数据行的修改与删除,必须加强互斥锁X锁,那对于数据的插入,是否还需要加这么强的锁,来实施互斥呢?插入意向锁,孕育而生。
插入意向锁,是间隙锁(Gap Locks)的一种(所以,也是实施在索引上的),它是专门针对insert操作的。
多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。
存储引擎:InnoDB
隔离级别:可重复读隔离级别
建表语句:
mysql> CREATE TABLE `T` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB;
数据列表:
id | Name |
---|---|
10 | xiaohong |
20 | zhangsan |
30 | lisi |
事务A先执行,在10与20两条记录中插入了一行,还未提交:
insert into t values(11, xxx);
事务B后执行,也在10与20两条记录中插入了一行:
insert into t values(12, ooo);
会使用什么锁?事务B会不会被阻塞呢? 回答:虽然事务隔离级别是RR,虽然是同一个索引,虽然是同一个区间,但插入的记录并不冲突,故这里: 使用的是插入意向锁,并不会阻塞事务B
案例说明:
存储引擎:InnoDB
隔离级别:可重复读隔离级别
建表语句:
mysql> CREATE TABLE `T` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB;
数据列表:
id | name |
---|---|
1 | xiaohong |
2 | zhangsan |
3 | lisi |
事务A先执行,还未提交: insert into t(name) values(xxx);
事务B后执行: insert into t(name) values(ooo);
事务B会不会被阻塞?
案例分析:
InnoDB在RR隔离级别下,能解决幻读问题,上面这个案例中:
①事务A先执行insert,会得到一条(4, xxx)的记录,由于是自增列,故不用显示指定id为4,InnoDB会自动增长,注意此时事务并未提交;
②事务B后执行insert,假设不会被阻塞,那会得到一条(5, ooo)的记录;
此时,并未有什么不妥,但如果,
③事务A继续insert:
insert into t(name) values(xxoo);
会得到一条(6, xxoo)的记录。
④事务A再select:
select * from t where id>3;
得到的结果是:
4, xxx
6, xxoo
补充:不可能查询到5的记录,再RR的隔离级别下,不可能读取到还未提交事务生成的数据。
这对于事务A来说,就很奇怪了,对于AUTO_INCREMENT的列,连续插入了两条记录,一条是4,接下来一条变成了6,就像莫名其妙的幻影。
自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
与此同时,InnoDB提供了innodb_autoinc_lock_mode配置,可以调节与改变该锁的模式与行为。
数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
为了提高复杂SQL语句的复用性和表操作的安全性,MySQL数据库管理系统提供了视图特性。所谓视图,本质上是一种虚拟表,在物理上是不存在的,其内容与真实的表相似,包含一系列带有名称的列和行数据。但是,视图并不在数据库中以储存的数据值形式存在。行和列数据来自定义视图的查询所引用基本表,并且在具体引用视图时动态生成。
视图使开发者只关心感兴趣的某些特定数据和所负责的特定任务,只能看到视图中所定义的数据,而不是视图所引用表中的数据,从而提高了数据库中数据的安全性。
视图的特点如下:
视图的列可以来自不同的表,是表的抽象和在逻辑意义上建立的新关系。
视图是由基本表(实表)产生的表(虚表)。
视图的建立和删除不影响基本表。
对视图内容的更新(添加,删除和修改)直接影响基本表。
当视图来自多个基本表时,不允许添加和删除数据。
视图的操作包括创建视图,查看视图,删除视图和修改视图。
视图根本用途:简化sql查询,提高开发效率。如果说还有另外一个用途那就是兼容老的表结构。
下面是视图的常见使用场景:
重用SQL语句;
简化复杂的SQL操作。在编写查询后,可以方便的重用它而不必知道它的基本查询细节;
使用表的组成部分而不是整个表;
保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限;
更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。
性能。数据库必须把视图的查询转化成对基本表的查询,如果这个视图是由一个复杂的多表查询所定义,那么,即使是视图的一个简单查询,数据库也把它变成一个复杂的结合体,需要花费一定的时间。
修改限制。当用户试图修改视图的某些行时,数据库必须把它转化为对基本表的某些行的修改。事实上,当从视图中插入或者删除时,情况也是这样。对于简单视图来说,这是很方便的,但是,对于比较复杂的视图,可能是不可修改的
这些视图有如下特征:1.有UNIQUE等集合操作符的视图。2.有GROUP BY子句的视图。3.有诸如AVG\SUM\MAX等聚合函数的视图。 4.使用DISTINCT关键字的视图。5.连接表的视图(其中有些例外)
游标是系统为用户开设的一个数据缓冲区,存放SQL语句的执行结果,每个游标区都有一个名字。用户可以通过游标逐一获取记录并赋给主变量,交由主语言进一步处理。
存储过程是一个预编译的SQL语句,优点是允许模块化的设计,就是说只需要创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次SQL,使用存储过程比单纯SQL语句执行要快。
优点
1)存储过程是预编译过的,执行效率高。
2)存储过程的代码直接存放于数据库中,通过存储过程名直接调用,减少网络通讯。
3)安全性高,执行存储过程需要有一定权限的用户。
4)存储过程可以重复使用,减少数据库开发人员的工作量。
缺点
1)调试麻烦,但是用 PL/SQL Developer 调试很方便!弥补这个缺点。
2)移植问题,数据库端代码当然是与数据库相关的。但是如果是做工程型项目,基本不存在移植问题。
3)重新编译问题,因为后端代码是运行前编译的,如果带有引用关系的对象发生改变时,受影响的存储过程、包将需要重新编译(不过也可以设置成运行时刻自动编译)。
4)如果在一个程序系统中大量的使用存储过程,到程序交付使用的时候随着用户需求的增加会导致数据结构的变化,接着就是系统的相关问题了,最后如果用户想维护该系统可以说是很难很难、而且代价是空前的,维护起来更麻烦。
触发器是用户定义在关系表上的一类由事件驱动的特殊的存储过程。触发器是指一段代码,当触发某个事件时,自动执行这些代码。
使用场景
在MySQL数据库中有如下六种触发器:
数据定义语言DDL(Data Ddefinition Language)CREATE,DROP,ALTER
主要为以上操作 即对逻辑结构等有操作的,其中包括表结构,视图和索引。
数据查询语言DQL(Data Query Language)SELECT
这个较为好理解 即查询操作,以select关键字。各种简单查询,连接查询等 都属于DQL。
数据操纵语言DML(Data Manipulation Language)INSERT,UPDATE,DELETE
主要为以上操作 即对数据进行操作的,对应上面所说的查询操作 DQL与DML共同构建了多数初级程序员常用的增删改查操作。而查询是较为特殊的一种 被划分到DQL中。
数据控制功能DCL(Data Control Language)GRANT,REVOKE,COMMIT,ROLLBACK
主要为以上操作 即对数据库安全性完整性等有操作的,可以简单的理解为权限控制等。
SQL 约束有哪几种?
SELECT * FROM A,B(,C)或者SELECT * FROM A CROSS JOIN B (CROSS JOIN C)#没有任何关联条件,结果是笛卡尔积,结果集会很大,没有意义,很少使用内连接(INNER JOIN)SELECT * FROM A,B WHERE A.id=B.id或者SELECT * FROM A INNER JOIN B ON A.id=B.id多表中同时符合某种条件的数据记录的集合,INNER JOIN可以缩写为JOIN
内连接分为三类
外连接(LEFT JOIN/RIGHT JOIN)
联合查询(UNION与UNION ALL)
SELECT * FROM A UNION SELECT * FROM B UNION ...
全连接(FULL JOIN)
SELECT * FROM A LEFT JOIN B ON A.id=B.id UNIONSELECT * FROM A RIGHT JOIN B ON A.id=B.id
表连接面试题
有2张表,1张R、1张S,R表有ABC三列,S表有CD两列,表中各有三条记录。
R表
S表
select r.*
,s.*
from r,s
A | B | C | C | D |
---|---|---|---|---|
a1 | b1 | c1 | c1 | d1 |
a2 | b2 | c2 | c1 | d1 |
a3 | b3 | c3 | c1 | d1 |
a1 | b1 | c1 | c2 | d2 |
a2 | b2 | c2 | c2 | d2 |
a3 | b3 | c3 | c2 | d2 |
a1 | b1 | c1 | c4 | d3 |
a2 | b2 | c2 | c4 | d3 |
a3 | b3 | c3 | c4 | d3 |
内连接结果:
select r.*
,s.*
from r inner join s on r.c=s.c
A | B | C | C | D |
---|---|---|---|---|
a1 | b1 | c1 | c1 | d1 |
a2 | b2 | c2 | c2 | d2 |
左连接结果:
select r.*
,s.*
from r left join s on r.c=s.c
A | B | C | C | D |
---|---|---|---|---|
a1 | b1 | c1 | c1 | d1 |
a2 | b2 | c2 | c2 | d2 |
a3 | b3 | c3 |
右连接结果:
select r.*
,s.*
from r right join s on r.c=s.c
| A | B | C | C | D |
| a1 | b1 | c1 | c1 | d1 |
| a2 | b2 | c2 | c2 | d2 |
| | | | c4 | d3 |
全表连接的结果(MySql不支持,Oracle支持):
select r.*
,s.*
from r full join s on r.c=s.c
A | B | C | C | D |
---|---|---|---|---|
a1 | b1 | c1 | c1 | d1 |
a2 | b2 | c2 | c2 | d2 |
a3 | b3 | c3 | ||
c4 | d3 |
条件:一条SQL语句的查询结果做为另一条查询语句的条件或查询结果
嵌套:多条SQL语句嵌套使用,内部的SQL查询语句称为子查询。
select * from employee where salary=(select max(salary) from employee);
select * from employee where salary=(select max(salary) from employee);
select * from dept d, (select * from employee where join_date > '2011-1-1') e where e.dept_id = d.id;
select d.*, e.* from dept d inner join employee e on d.id = e.dept_id where e.join_date > '2011-1-1'
mysql中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。一直大家都认为exists比in语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。
char的特点
char表示定长字符串,长度是固定的;
如果插入数据的长度小于char的固定长度时,则用空格填充;
因为长度固定,所以存取速度要比varchar快很多,甚至能快50%,但正因为其长度固定,所以会占据多余的空间,是空间换时间的做法;
对于char来说,最多能存放的字符个数为255,和编码无关
varchar的特点
varchar表示可变长字符串,长度是可变的;
插入的数据是多长,就按照多长来存储;
varchar在存取方面与char相反,它存取慢,因为长度不固定,但正因如此,不占据多余的空间,是时间换空间的做法;
对于varchar来说,最多能存放的字符个数为65532
总之,结合性能角度(char更快)和节省磁盘空间角度(varchar更小),具体情况还需具体来设计数据库才是妥当的做法。
最多存放50个字符,varchar(50)和(200)存储hello所占空间一样,但后者在排序时会消耗更多内存,因为order by col采用fixed_length计算col长度(memory引擎也一样)。在早期 MySQL 版本中, 50 代表字节数,现在代表字符数。
是指显示字符的长度。20表示最大显示宽度为20,但仍占4字节存储,存储范围不变;
不影响内部存储,只是影响带 zerofill 定义的 int 时,前面补多少个 0,易于报表展示
对大多数应用没有意义,只是规定一些工具用来显示字符的个数;int(1)和int(20)存储和计算均一样;
int(10)的10表示显示的数据的长度,不是存储数据的大小;chart(10)和varchar(10)的10表示存储数据的大小,即表示存储多少个字符。
int(10) 10位的数据长度 9999999999,占32个字节,int型4位
char(10) 10位固定字符串,不足补空格 最多10个字符
varchar(10) 10位可变字符串,不足补空格 最多10个字符
char(10)表示存储定长的10个字符,不足10个就用空格补齐,占用更多的存储空间
varchar(10)表示存储10个变长的字符,存储多少个就是多少个,空格也按一个字符存储,这一点是和char(10)的空格不同的,char(10)的空格表示占位不算一个字符
三者都表示删除,但是三者有一些差别:
Delete | Truncate | Drop | |
---|---|---|---|
类型 | 属于DML | 属于DDL | 属于DDL |
回滚 | 可回滚 | 不可回滚 | 不可回滚 |
删除内容 | 表结构还在,删除表的全部或者一部分数据行 | 表结构还在,删除表中的所有数据 | 从数据库中删除表,所有的数据行,索引和权限也会被删除 |
删除速度 | 删除速度慢,需要逐行删除 | 删除速度快 | 删除速度最快 |
因此,在不再需要一张表的时候,用drop;在想删除部分数据行时候,用delete;在保留表而删除所有数据的时候用truncate。
对于低性能的SQL语句的定位,最重要也是最有效的方法就是使用执行计划,MySQL提供了explain命令来查看语句的执行计划。 我们知道,不管是哪种数据库,或者是哪种数据库引擎,在对一条SQL语句进行执行的过程中都会做很多相关的优化,对于查询语句,最重要的优化方式就是使用索引。 而执行计划,就是显示数据库引擎对于SQL语句的执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等。
执行计划包含的信息 id 有一组数字组成。表示一个查询中各个子查询的执行顺序;
select_type 每个子查询的查询类型,一些常见的查询类型。
id | select_type | description |
---|---|---|
1 | SIMPLE | 不包含任何子查询或union等查询 |
2 | PRIMARY | 包含子查询最外层查询就显示为 PRIMARY |
3 | SUBQUERY | 在select或 where字句中包含的查询 |
4 | DERIVED | from字句中包含的查询 |
5 | UNION | 出现在union后的查询语句中 |
6 | UNION RESULT | 从UNION中获取结果集,例如上文的第三个例子 |
table 查询的数据表,当从衍生表中查数据时会显示 x 表示对应的执行计划id partitions 表分区、表创建的时候可以指定通过那个列进行表分区。 举个例子:
create table tmp (
id int unsigned not null AUTO_INCREMENT,
name varchar(255),
PRIMARY KEY (id)
) engine = innodb
partition by key (id) partitions 5;
type(非常重要,可以看到有没有走索引) 访问类型
possible_keys 可能使用的索引,注意不一定会使用。查询涉及到的字段上若存在索引,则该索引将被列出来。当该列为 NULL时就要考虑当前的SQL是否需要优化了。
key 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL。
TIPS:查询中若使用了覆盖索引(覆盖索引:索引的数据覆盖了需要查询的所有数据),则该索引仅出现在key列表中
key_length 索引长度
ref 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
rows 返回估算的结果集数目,并不是一个准确的值。
extra 的信息非常丰富,常见的有:
【推荐】SQL性能优化的目标:至少要达到 range 级别,要求是ref级别,如果可以是consts最好。
说明:
1) consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
2) ref 指的是使用普通的索引(normal index)。
3) range 对索引进行范围检索。
反例:explain表的结果,type=index,索引物理文件全扫描,速度非常慢,这个index级别比较range还低,与全表扫描是小巫见大巫。
应用服务器与数据库服务器建立一个连接
数据库进程拿到请求sql
解析并生成执行计划,执行
读取数据到内存并进行逻辑处理
通过步骤一的连接,发送结果到客户端
关掉连接,释放资源
超大的分页一般从两个方向上来解决.
select * from table where age > 20 limit 1000000,10
这种查询其实也是有可以优化的余地的. 这条语句需要load1000000数据然后基本上全部丢弃,只取10条当然比较慢. 当时我们可以修改为select * from table where id in (select id from table where age > 20 limit 1000000,10)
.这样虽然也load了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快. 同时如果ID连续的好,我们还可以select * from table where id > 1000000 limit 10
,效率也是不错的,优化的可能性有许多种,但是核心思想都一样,就是减少load的数据.解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至redis等k-V数据库中,直接返回即可.
在阿里巴巴《Java开发手册》中,对超大分页的解决办法是类似于上面提到的第一种.
【推荐】利用延迟关联或者子查询优化超多分页场景。
说明:MySQL并不是跳过offset行,而是取offset+N行,然后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。
正例:先快速定位需要获取的id段,然后再关联:
SELECT a.* FROM 表1 a, (select id from 表1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
LIMIT 子句可以被用于强制 SELECT 语句返回指定的记录数。LIMIT 接受一个或两个数字参数。参数必须是一个整数常量。如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数目。初始记录行的偏移量是 0(而不是 1)
mysql> SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15
为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1:
mysql> SELECT * FROM table LIMIT 95,-1; // 检索记录行 96-last.
如果只给定一个参数,它表示返回最大的记录行数目:
mysql> SELECT * FROM table LIMIT 5; //检索前 5 个记录行
换句话说,LIMIT n 等价于 LIMIT 0,n。
用于记录执行时间超过某个临界值的SQL日志,用于快速定位慢查询,为我们的优化做参考。
开启慢查询日志
配置项:slow_query_log
可以使用show variables like ‘slov_query_log’
查看是否开启,如果状态值为OFF
,可以使用set GLOBAL slow_query_log = on
来开启,它会在datadir
下产生一个xxx-slow.log
的文件。
设置临界时间
配置项:long_query_time
查看:show VARIABLES like 'long_query_time'
,单位秒
设置:set long_query_time=0.5
实操时应该从长时间设置到短的时间,即将最慢的SQL优化掉
查看日志,一旦SQL超过了我们设置的临界时间就会被记录到xxx-slow.log
中
在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。
慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?
所以优化也是针对这三个方向来的,
主键是数据库确保数据行在整张表唯一性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID列作为主键。设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全。
推荐使用自增ID,不要使用UUID。
因为在InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了主键索引以及全部的数据(按照顺序),如果主键索引是自增ID,那么只需要不断向后排列即可,如果是UUID,由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进而造成插入性能的下降。
总之,在数据量大一些的情况下,用自增主键性能会好一些。
关于主键是聚簇索引,如果没有主键,InnoDB会选择一个唯一键来作为聚簇索引,如果没有唯一键,会生成一个隐式的主键。
null值会占用更多的字节,且会在程序中造成很多与预期不符的情况。
密码散列,盐,用户身份证号等固定长度的字符串应该使用char而不是varchar来存储,这样可以节省空间且提高检索效率。
解题方法
对于此类考题,先说明如何定位低效SQL语句,然后根据SQL语句可能低效的原因做排查,先从索引着手,如果索引没有问题,考虑以上几个方面,数据访问的问题,长难查询句的问题还是一些特定类型优化的问题,逐一回答。
SQL语句优化的一些方法?
select id from t where num is null
select id from t where num=
select id from t where num=10 or num=20
select id from t where num=10 union all select id from t where num=20
select id from t where num in(1,2,3)
select id from t where num between 1 and 3
select id from t where num=@num
select id from t with(index(索引名)) where num=@num
select id from t where num/2=100
select id from t where num=100*2
select id from t where substring(name,1,3)=’abc’
select id from t where name like ‘abc%’
优化原则:减少系统瓶颈,减少资源占用,增加系统的反应速度。
一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。
需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。
将字段很多的表分解成多个表
对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。
因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。
增加中间表
对于需要经常联合查询的表,可以建立中间表以提高查询效率。
通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。
增加冗余字段
设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。
表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。
注意:
冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。
当 cpu 飙升到 500%时,先用操作系统命令 top 命令观察是不是 mysqld 占用导致的,如果不是,找出占用高的进程,并进行相关处理。
如果是 mysqld 造成的, show processlist,看看里面跑的 session 情况,是不是有消耗资源的 sql 在运行。找出消耗高的 sql,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成。
一般来说,肯定要 kill 掉这些线程(同时观察 cpu 使用率是否下降),等进行相应的调整(比如说加索引、改 sql、改内存参数)之后,再重新跑这些 SQL。
也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等
当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
还有就是通过分库分表的方式进行优化,主要有垂直分表和水平分表
垂直分区:
根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。
垂直拆分的优点: 可以使得行数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
把主键和一些列放在一个表,然后把主键和另外的列放在另一个表中
水平分区:
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。
水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
水品拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。
水平拆分能够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂。
《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。
表很大,分割后可以降低在查询时需要读的数据和索引的页数,同时也降低了索引的层数,提高查询次数
下面补充一下数据库分片的两种常见方案:
分库分表后面临的问题
事务支持 分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价; 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
跨库join
只要是进行切分,跨节点Join的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。解决这一问题的普遍做法是分两次查询实现。在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。 分库分表方案产品
跨节点的count,order by,group by以及聚合函数问题 这些是一类问题,因为它们都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作。解决方案:与解决跨节点join问题的类似,分别在各个节点上得到结果后在应用程序端进行合并。和join不同的是每个结点的查询可以并行执行,因此很多时候它的速度要比单一大表快很多。但如果结果集很大,对应用程序内存的消耗是一个问题。
数据迁移,容量规划,扩容等问题 来自淘宝综合业务平台团队,它利用对2的倍数取余具有向前兼容的特性(如对4取余得1的数对2取余也是1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。总得来说,这些方案都不是十分的理想,多多少少都存在一些缺点,这也从一个侧面反映出了Sharding扩容的难度。
ID问题
一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由. 一些常见的主键生成策略
UUID 使用UUID作主键是最简单的方案,但是缺点也是非常明显的。由于UUID非常的长,除占用大量存储空间外,最主要的问题是在索引上,在建立索引和基于索引进行查询时都存在性能问题。 Twitter的分布式自增ID算法Snowflake 在分布式系统中,需要生成全局UID的场合还是比较多的,twitter的snowflake解决了这种需求,实现也还是很简单的,除去配置信息,核心代码就是毫秒级时间41位 机器ID 10位 毫秒内序列12位。
跨分片的排序分页
般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示:
主库将变更写入 binlog 日志(通过IO线程写入),然后从库连接到主库之后,从库有一个 IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个 relay 中继日志中。接着从库中有一个 SQL 线程会从中继日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL,这样就可以保证自己跟主库的数据是一样的。
这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。
而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。 所以 MySQL 实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。
这个所谓半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。
所谓并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。
补充:
binlog日志:记录关于数据变动的sql语句,例如增删改、建表、建库、删表、删库等语句
Redis 第一次复制,有清空数据动作
而 MySQL 只会从接入点开始复制,以前的数据是不是一致的他不管
①STATEMENT,基于语句的日志记录,把所有写操作的sql语句写入 binlog (默认)
例如update xxx set update_time = now() where pk_id = 1,这时,主从的 update_time 不一致
优点:
成熟的技术。
更少的数据写入日志文件。当更新或删除影响许多行时,这将导致 日志文件所需的存储空间大大减少。这也意味着从备份中获取和还原可以更快地完成。
日志文件包含所有进行了任何更改的语句,因此它们可用于审核数据库。
缺点:
有很多函数不能复制,例如now()、random()、uuid()等
②ROW,基于行的日志记录,把每一行的改变写入binlog,假设一条sql语句影响100万行,从节点需要执行100万次,效率低。
优点:可以复制所有更改,这是最安全的复制形式
缺点:如果该SQL语句更改了许多行,则基于行的复制可能会向二进制日志中写入更多的数据。即使对于回滚的语句也是如此。这也意味着制作和还原备份可能需要更多时间。此外,二进制日志被锁定更长的时间以写入数据,这可能会导致并发问题。
③MIXED,混合模式,如果 sql 里有函数,自动切换到 ROW 模式,如果 sql 里没有会造成主从复制不一致的函数,那么就使用STATEMENT模式。(存在问题:解决不了系统变量问题,例如@@host name,主从的主机名不一致)
①异步复制
网络或机器故障时,会造成数据不一致
数据不一致缓解方案:半同步,插入主库时,不会及时返回给我们的web端,他会进行等待,等待从库的I/OThread从主节点Binary log读取二进制文件并拷贝到从节点的relaybinlog之后,在进行返回。(不是等待所有,一个从节点复制过去就行了)
数据强一致性了但是性能低:可以设置超时时间(多个Slave,或者Slave非常卡,会导致响应非常慢?不会,有保护机制,超过时间就直接返回,一般情况下设置1秒)
注意:不是等待所有从节点同步从主节点Binary log读取二进制文件并拷贝到从节点的relaybinlog之后才返回,而是只要有一个节点拷贝成功就返回
根据业务场景选择同步和半同步
注意:半同步只会缓解数据不一致问题,并不能完全解决
②半同步复制(MySQL 8.0还支持通过插件实现的半同步复制接口)
默认情况下,MySQL复制是异步的。Master将事件写入其二进制日志,Slave将在事件就绪时请求它们。Master不知道Slave是否或何时检索和处理了事务,并且不能保证任何事件都会到达Slave。使用异步复制,如果Master崩溃,则它提交的事务可能不会传输到任何Slave。在这种情况下,从Master到Slave的故障转移可能会导致故障转移到缺少相对于Master的事务的服务器。
在完全同步复制的情况下,当Master提交事务时,所有Slave也都已提交事务,然后Master才返回执行该事务的会话。完全同步复制意味着可以随时从Master故障转移到任何Slave。完全同步复制的缺点是完成事务可能会有很多延迟。
半同步复制介于异步复制和完全同步复制之间。Master等待直到至少一个Slave接收并记录了事件(所需数量的Slave是可配置的),然后提交事务。Master不等待所有Slave都确认收到,它仅需要Slave的确认,而不是事件已在Slave端完全执行并提交。因此,半同步复制可确保如果Master崩溃,则它已提交的所有事务都已传输到至少一个Slave。
与异步复制相比,半同步复制提供了改进的数据完整性,因为众所周知,当提交成功返回时,数据至少存在两个位置。在半同步Master收到所需数量的Slave的确认之前,该事务处于暂挂状态且未提交。
与完全同步复制相比,半同步复制更快,因为半同步复制可以配置为平衡对数据完整性(确认已收到事务的Slave数)与提交速度的需求,提交速度较慢,因为需要等待Slave。
与异步复制相比,半同步复制对性能的影响是增加数据完整性的权衡。减慢量至少是将提交发送到Slave并等待Slave确认接收的TCP / IP往返时间。这意味着半同步复制最适合通过快速网络通信的关闭服务器,而最不适合通过慢速网络通信的远程服务器。半同步复制还通过限制二进制日志事件从Master发送到Slave的速度,对繁忙的会话设置了速率限制。当一个用户太忙时,这会减慢速度,这在某些部署情况下很有用。
Master及其Slave之间的半同步复制操作如下:
Slave表示连接到Master时是否具有半同步功能。
如果在Master端启用了半同步复制,并且至少有一个半同步Slave,则在Master块上执行事务提交的线程将等待直到至少一个半同步Slave确认已接收到该事务的所有事件,或者直到发生超时。
仅在事件已被写入其中继日志并刷新到磁盘之后,Slave才确认接收到事务事件。
如果在没有任何Slave确认事务的情况下发生超时,则Master将恢复为异步复制。赶上至少一个半同步Slave时,Master将返回到半同步复制。
必须在Master端和Slave端都启用半同步复制。如果在Master上禁用了半同步复制,或者在Master上启用了半同步复制但没有任何Slave,则Master使用异步复制。
③延迟复制
MySQL 8.0还支持延迟复制,以使副本故意在源之后至少指定的时间量
方式一)sharding jdbc
在同一个事务里,只要碰到写,后面的读都走主库,解决“写完读”不一致的问题
方式二)配置多数据源,实现读写分离
①配置主从数据库
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: true
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
②创建数据源枚举类型
public enum DataSourceType {
/**
* 主库
*/
MASTER,
/**
* 从库
*/
SLAVE
}
③数据源切换处理
public class DynamicDataSourceContextHolder {
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源变量
* @param dataSourceType
*/
public static void setDataSourceType(String dataSourceType){
log.info("切换到{}数据源", dataSourceType);
CONTEXT_HOLDER.set(dataSourceType);
}
/**
* 获取数据源变量
* @return
*/
public static String getDataSourceType(){
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType(){
CONTEXT_HOLDER.remove();
}
}
④继承AbstractRoutingDataSource
动态切换数据源主要依靠AbstractRoutingDataSource。创建一个AbstractRoutingDataSource的子类,重写determineCurrentLookupKey方法,用于决定使用哪一个数据源。这里主要用到AbstractRoutingDataSource的两个属性defaultTargetDataSource和targetDataSources。defaultTargetDataSource默认目标数据源,targetDataSources(map类型)存放用来切换的数据源。
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
// afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
super.afterPropertiesSet();
}
/**
* 根据Key获取数据源的信息
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
⑤注入数据源
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource);
return new DynamicDataSource(masterDataSource, targetDataSources);
}
}
⑥自定义多数据源切换注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 切换数据源名称
*/
DataSourceType value() default DataSourceType.MASTER;
}
⑦AOP拦截实现
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
private Logger log = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(com.wlfu.common.annotation.DataSource)")
public void dsPointCut() {
}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
if (dataSource != null) {
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
}
try {
return point.proceed();
} finally {
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
}
⑧使用切换数据源注解
@DataSource(value = DataSourceType.SLAVE)
@PostMapping("/list")
@ResponseBody
public TableDataInfo list() {
}