关系型数据库(RDBMS,Relational Database Management System)就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。
SQL 是一种结构化查询语言(Structured Query Language)
MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。
添加链接描述
主键(主码) :主键用于唯一标识一个元组,不能有重复,不允许为空。一个表只能有一个主键。
外键(外码) :外键用来和其他表建立联系用,外键是另一表的主键,外键是可以有重复的,可以是空值。一个表可以有多个外键。
ER 图 全称是 Entity Relationship Diagram(实体联系图),提供了表示实体类型、属性和联系的方法。
实体 :通常是现实世界的业务对象,当然使用一些逻辑对象也可以。比如对于一个校园管理系统,会涉及学生、教师、课程、班级等等实体。在 ER 图中,实体使用矩形框表示。(一个对象实例)
属性 :即某个实体拥有的属性,属性用来描述组成实体的要素,对于产品设计来说可以理解为字段。在 ER 图中,属性使用椭圆形表示。 (对象的属性)
联系 :即实体与实体之间的关系,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。例如,一个班级会有多个学生就是一种实体间的联系。
触发器是与表有关的数据库对象,当触发器所在表上出现指定事件并满足定义条件的时候,将执行触发器中定义的语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性。触发器是一个特殊的存储过程,不同的是存储过程要用call来调用,而触发器不需要使用call,也不需要手工调用,它在插入,删除或修改特定表中的数据时触发执行,它比数据库本身标准的功能有更精细和更复杂的数据控制能力。
假设系统中有两个表:
班级表 class(班级号 classID, 班内学生数 stuCount)
学生表 student(学号 stuID, 所属班级号 classID)
要创建触发器来使班级表中的班内学生数随着学生的添加自动更新,代码如下:
DELIMITER $
create trigger tri_stuInsert after insert
on student for each row
begin
declare c int;
set c = (select stuCount from class where classID=new.classID);
update class set stuCount = c + 1 where classID = new.classID;
end$
DELIMITER ;
帮助Mysql高效获取数据的数据结构
在实际场景应用当中,MySQL表数据,一般情况下都是比较庞大、海量的。如果使用红黑树,树的高度会特别高,红黑树虽说查询效率很高。但是在海量数据的情况下,树的高度并不可控。如果我们要查询的数据,正好在树的叶子节点。那查询会非常慢。故而MySQL并没有采用红黑树来组织索引。
不管平衡二叉查找树还是红黑树,都会随着插入的元素增多,而导致树的高度变高,这就意味着磁盘 I/O 操作次数多,会影响整体数据查询的效率。
主键索引:加速查询 + 列值唯一(不可以有 NULL)+ 表中只有一个。
普通索引:仅加速查询。
唯一索引:加速查询 + 列值唯一(可以有 NULL)。
覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。
联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
全文索引:对文本的内容进行分词,进行搜索。目前只有 CHAR、VARCHAR ,TEXT 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
当字段类型为字符串时,有时候需要索引很长的字符串,这会让索引变得很大,查询时,浪费大量的磁盘IO,影响查询效率。此时可以只将字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。
在使用联合索引,Mysql会根据联合索引的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如 >、<)才会停止匹配。所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
explain 执行计划来分析索引失效
资料1
资料2
资料3
explain每一列的含义
索引下推(Index Condition Pushdown) 是 MySQL 5.6 版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。
索引下推的下推其实就是指将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理。
添加链接描述
1.select * from T where a=x and b=y and c=z
2.select * from T where a=x and b>y and c=z
3.select * from T where c=z and a=x and b=y
4.select (a,b) from T where a=x and b>y
5.select count(*) from T where a=x
6.select count(*) from T where b=y
7.select count(*) form T
8.select * from T where a=x and c=z
1.a、b、c三个字段都可以走联合索引
2.a走索引,b因为在索引中有序,依旧可以走索引,c需要回表查询。
(但是在 mysql 5.6 版本后,c 字段虽然无法走联合索引,但是因为有索引下推的特性,c 字段在 inndob 层过滤完满足查询条件的记录后,才返回给server 层进行回表,相比没有索引下推,减少了回表的次数。)
3.查询条件的顺序不影响,优化器会优化,所以a、b、c三个字段都可以走联合索引
4.a和b都会走联合索引,查询是覆盖索引,不需要回表
5.a 可以走联合索引
6.只有b,无法使用联合索引,由于表存在联合索引,所以 count(*) 选择的扫描方式是扫描联合索引来统计个数,扫描的方式是type=index
7.由于表存在联合索引,所以 count(*) 选择的扫描方式是扫描联合索引来统计个数,扫描的方式是type=index
8.只有a列使用了索引,c列没有使用索引,因为中间跳过了b列
CREATE INDEX index_name ON table_name(column_name);
CREATE INDEX index_product_no_name ON product(product_no, name);
前缀索引优化;
覆盖索引优化;
主键索引最好是自增的;
防止索引失效;
添加链接描述
count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。
所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。
再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。
添加链接描述
每个节点(每一页) 16kb大小
主键BigInt 8个byte,int 4个byte,指针设置6个byte; 非叶子节点
数据只存在叶子节点,假设1kb 叶子节点
两层总数 = 非叶子节点(根) * 叶子节点。
三层总数 = 非叶子节点(根) * 非叶子节点 * 叶子节点。
数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行 。
原子性
包含事务的操作要么全部执行成功,要么全部失败回滚
一致性
事务在执行前后状态一致。例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
隔离性
一个事务所进行的修改在最终提交前,对其他事务是不可见的
持久性
数据一旦提交,其所作的修改将永久保存到数据库中
锁和MVCC(多版本并发控制)
锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。
串行化隔离级别是通过锁来实现的,提交读 和 可重复读 隔离级别是基于 MVCC 实现的
在隔离级别为RR时,MySQL已经可以避免脏读和重复读,但还是无法避免幻读,MySQL采用next-key锁(当前读)与MVCC(快照读)(多版本并发控制)来避免幻读.
资料1
资料2 重点看后边的场景
MVCC作用
在不加锁的情况下,解决数据库读写冲突问题,并且解决脏读,幻读,不可重复读等问题,但不能解决丢失修改的问题。
底层实现
MVCC 的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_ID 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改
Read View(可见性)
做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”
活跃事务:启动了但还没提交的事务
可见性三种情况讨论:
如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。
如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。
处于中间,如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
**当前读 (一致性锁定读)**就是给行记录加 X 锁或 S 锁。
快照读(一致性非锁定读)就是单纯的 SELECT 语句
只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用一致性非锁定读
一般情况下select * from …where …是快照读,不会加锁,
而 for update,lock in share mode,update,delete都属于当前读
读未提交:没有视图的概念,直接返回记录上的最新值;
读已提交:每个SQL语句执行时重新创建一个视图;
可重复读:事务启动时创建视图,整个事务期间查询操作都是使用这个视图
串行化:直接加锁避免并行访问;
mysql的默认隔离级别是可重复读,可重复读隔离级别在开启事务后,执行第一个selete 语句的时候,会生成一个 Read View,后面整个事务期间的selete都在用这个 Read View,所以事务期间上读取的数据都是一致的,不会出现前后读取的数据不一致的问题,所以避免了不可重复读。
对于读未提交隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
对于串行化隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
对于读提交和可重复读隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。
MyISAM默认是表级锁,InnoDB默认行级锁
不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类
间隙锁
Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。
间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。
临键锁
Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
死锁是指两个或两个以上进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
查看线程情况 添加链接描述
在分析innodb中锁阻塞时,几种方法的对比情况:
(1)使用show processlist查看不靠谱;
(2)直接使用show engine innodb status查看,无法判断到问题的根因;
(3)使用mysqladmin debug查看,能看到所有产生锁的线程,但无法判断哪个才是根因;
(4)开启innodb_lock_monitor后,再使用show engine innodb status查看,能够找到锁阻塞的根因。
尽量避免大事务。锁的生命周期是从加锁到事务提交才会释放,所以事务越大,其持有的锁越多,更容易造成死锁。
合理设计索引。区分度较高的提到联合索引前面,使查询通过索引定位到更少的行,减少加锁范围。反例:“update … where name = ‘xxx’”, name 字段上没有索引,这个语句将会对全表加锁。
统一执行顺序。不仅是加锁访问不同表数据的顺序要一致,对同一个表的加锁访问也得一致。而且比较容易忽视。
对热点数据尽量放在事务后面,减少加锁时间,减少锁冲突,提高并发。
数据库定义语言DDL:create、drop等对逻辑结构有操作的
数据操纵语言(DML):CRUD
事务控制语言 (TCL):用于管理数据库中的事务
数据控制语言DCL:主要是权限控制操作,包括grant、revoke
一个查询的结果在另外一个查询中用
自增ID,Mysql的InnoDB存储引擎中,主键索引是一种聚簇索引,主键索引的B+树的叶子节点按照顺序存储了主键值及数据,如果主键索引是自增ID,只需要将顺序往后排列即可,如果是UUID,ID是随机生成的,在插入数据时会造成大量的数据移动,产生大量的内存碎片,造成插入性能下降。
select * from table limit (start-1)*pageSize,pageSize;
使用mybatis-plus
@Test
public void testPage() {
System.out.println("----- selectPage method test ------");
//分页参数
Page<User> page = Page.of(1,10);
//queryWrapper组装查询where条件
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getAge,13);
userMapper.selectPage(page,queryWrapper);
page.getRecords().forEach(System.out::println);
}
char的长度是不可变的,而varchar的长度是可变的,也就是说,定义一个char[10]和varchar[10],如果存进去的是‘csdn’,那么char所占的长度依然为10,除了字符‘csdn’外,后面跟六个空格,而varchar就立马把长度变为4了,取数据的时候,char类型的要用trim()去掉多余的空格,而varchar是不需要的。
在内连接中,使用on或者where没有区别。
在外连接里,例如使用left join时:
on是在生成临时表时使用的条件,不管on的条件是否为真,都会返回左边表中的全部记录。
where条件是在临时表生成好后,再对临时表进行过滤的条件,条件不为真的记录就全部过滤掉,包括左边的表。
MySQL 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。
MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
MySQL数据库的数据备份、主备、主主、主从都离不开bin log,需要依靠binlog来同步数据,保证数据一致性。
哪个层(除了undo),什么时候产生,有什么作用
binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。
作用:数据备份、主备、主主、主从 读写分离,保持数据一致性
redo log(重做日志)是InnoDB存储引擎独有的,它让MySQL拥有了崩溃恢复能力。
比如 MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性。
事务的持久性是通过 redo log 实现的。
我们修改某条记录,其实该记录并不是马上刷入磁盘的,而是将 Innodb 的 Buffer Pool 标记为脏页,等待后续的异步刷盘。
Buffer Pool 是提高了读写效率没错,但是问题来了,Buffer Pool 是基于内存的,而内存总是不可靠,万一断电重启,还没来得及落盘的脏页数据就会丢失。
为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改**以 redo log 的形式记录下来,**这个时候更新就算完成了。
后续,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 WAL (Write-Ahead Logging)技术。
WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。
redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。
在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。
当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。
InnoDB 存储引擎为 redo log 的刷盘策略提供了 innodb_flush_log_at_trx_commit 参数,它支持三种策略:
硬盘上存储的 redo log 日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redo日志文件大小都是一样的。
我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
在准备阶段,MySQL先将数据修改写入redo log,并将其标记为prepare状态,表示事务还未提交。然后将对应的SQL语句写入bin log。
在提交阶段,MySQL将redo log标记为commit状态,表示事务已经提交。然后根据sync_binlog参数的设置,决定是否将bin log刷入磁盘。
限定数据范围
读写分离
垂直分表
水平分表
对单表进行优化:对表中的字段、索引、查询进SQL进行优化
添加缓存
慢查询日志
分库 就是将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。
垂直分库 就是把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。
水平分库 是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。
分表 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。
垂直分表 是对数据表列的拆分,把一张列比较多的表拆分为多张表。
水平分表 是对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。
单表的数据达到千万级别以上,数据库读写速度比较缓慢。
数据库中的数据占用的空间越来越大,备份时间越来越长。
应用的并发量太大
分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。
哈希分片:求指定 key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。
范围分片:按照特性的范围区间(比如时间区间、ID 区间)来分配数据,比如 将 id 为 1~299999 的记录分到第一个库, 300000~599999 的分到第二个库。范围分片适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
地理位置分片:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。
停机迁移,写个脚本老库的数据写到新库中。比如你在凌晨 2 点,系统使用的人数非常少的时候,挂一个公告说系统要维护升级预计 1 小时。然后,你写一个脚本将老库的数据都同步到新库中。
双写方案: 我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
想要在项目中实施双写还是比较麻烦的,很容易会出现问题。我们可以借助上面提到的数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。
ShardingSphere 项目