侧边栏壁纸
博主头像
Xee博主等级

为了早日退休而学

  • 累计撰写 44 篇文章
  • 累计创建 8 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

Mysql的那亿点东西

Xee
Xee
2021-12-19 / 0 评论 / 0 点赞 / 483 阅读 / 5,971 字

想进大厂,mysql不会那可不行,来瞅瞅看你哪些还不会吧

mysql的存储引擎有哪些?

它的存储引擎其实比较的多,这是就主要说下 myisaminnodb 吧,其他的都是垃圾,你也基本见不到

  • myisam引擎是5.1版本之前的默认引擎,支持全文检索、压缩、空间函数等,但是不支持事务和行级锁,所以一般用于有大量查询少量插入的场景来使用,而且myisam不支持外键,并且索引和数据是分开存储的。data存的是数据地址,索引放在XX.MYI文件中,数据放在XX.MYD文件中,所以也叫非聚集索引
  • innodb是基于聚簇索引建立的,和myisam相反它支持事务、外键,并且通过MVCC来支持高并发,索引和数据存储在一起。data存的是数据本身。索引也是数据。数据和索引存在一个XX.IDB文件中,所以也叫聚集索引。

索引是什么东西?

索引是帮助Mysql高效获取数据的排好序的数据结构

索引的数据结构

索引的数据结构可以是以下几种

  • 二叉树 :不是平衡树,树的高度随着数据的增加而增加,查找数据效率特别低
  • 红黑树 :是平衡树,数据量大了之后树的高度太高了,效率低
  • Hash表 :只支持等值查找且很快,但数据量大了会存在hash碰撞问题,无法进行排序和范围查询
  • B-Tree :是一种多路平衡树,能够在数据量大的时候还能保证树的高度很低,查询效率也非常高,是最常用的索引结构

B树 和 B+树的区别

B树

每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为null
image.png

B+树

只有叶子节点存储data,叶子节点包含了这棵树的所有键值,叶子节点不存储指针
image.png

区别

  • B树每个节点都存储数据,所有节点组成这棵树。B+树只有叶子节点存储数据(B+数中有两个头指针:一个指向根节点,另一个指向关键字最小的叶节点),叶子节点包含了这棵树的所有数据,所有的叶子结点使用链表相连,便于区间查找和遍历,所有非叶节点起到索引作用。
  • B树中叶节点包含的关键字和其他节点包含的关键字是不重复的,B+树的索引项只包含对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
  • B树中每个节点(非根节点)关键字个数的范围为m/2(向上取整)-1,m-1,并且具有n个关键字的节点包含(n+1)棵子树。B+树中每个节点(非根节点)关键字个数的范围为m/2(向上取整),m,具有n个关键字的节点包含(n)棵子树。
  • B+树中查找,无论查找是否成功,每次都是一条从根节点到叶节点的路径。

B树的优点

B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。

B+树的优点

  • 所有的叶子结点使用链表相连,便于区间查找和遍历。B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
  • b+树的中间节点不保存数据,能容纳更多节点元素。

共同的优点

考虑磁盘IO的影响,它相对于内存来说是很慢的。数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。所以我们要减少IO次数,对于树来说,IO次数就是树的高度,而“矮胖”就是b树的特征之一,m的大小取决于磁盘页的大小。

根据官方的定义,一个节点大概能放16kb的数据,好的索引结构需要要求树的高度小,并且还能够尽量的放下更多的数据,所以选择B+树最好,这里强烈推荐一下这个视频(愣着干嘛,戳我过去呀),把索引的结构的sql的优化讲的特别详细

知道覆盖索引和回表吗?

假设我们有张表,结构如下:

create table user(
 id int(11) not null,
  age int(11) not null,
  primary key(id),
  key(age)
);

覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,而不再需要回表查询。
而要确定一个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是“Using index”即可。

以上面的user表来举例,我们再增加一个name字段,然后做一些查询试试。

explain select * from user where age=1; //查询的name无法从索引数据获取
explain select id,age from user where age=1; //可以直接从索引获取

锁的类型有哪些?

mysql锁分为共享锁排他锁,也叫做读锁写锁

读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。
写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁行锁两种。

表锁会锁定整张表并且阻塞其他用户对该表的所有读写操作,比如alter修改表结构的时候会锁表。
行锁又可以分为乐观锁悲观锁,悲观锁可以通过for update实现,乐观锁则通过版本号实现。

事物的基本特性和隔离级别是什么?

事务基本特性ACID分别是:

  • 原子性指的是一个事务中的操作要么全部成功,要么全部失败。
  • 一致性指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设中间sql执行过程中系统崩溃A也不会损失100块,因为事务没有提交,修改也就不会保存到数据库。
  • 隔离性指的是一个事务的修改在最终提交前,对其他事务是不可见的。
  • 持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中。

而隔离性有4个隔离级别,分别是:
read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。
用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。
image.png

read commit 读已提交,两次读取结果不一致,叫做不可重复读。
不可重复读解决了脏读的问题,他只会读取已经提交的事务。
用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。
image.png

repeatable read 可重复复读,这是mysql的默认级别,就是每次读取结果都一样,但是有可能产生幻读。

serializable 串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。

ACID靠什么保证的呢?

  • A 原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
  • C 一致性一般由代码层面来保证
  • I 隔离性由MVCC来保证
  • D 持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复

什么是幻读,什么又是MVCC呢?

要说幻读,首先要了解MVCC,MVCC叫做多版本并发控制,实际上就是保存了数据在某个时间节点的快照。
我们每行数实际上隐藏了两列,创建时间版本号,过期(删除)时间版本号,每开始一个新的事务,版本号都会自动递增。
还是拿上面的user表举例子,假设我们插入两条数据,他们实际上应该长这样。
image.png

这时候假设小明去执行查询,此时current_version=3

select * from user where id<=3;

同时,小红在这时候开启事务去修改id=1的记录,current_version=4

update user set name='张三三' where id=1;

执行成功后的结果是这样的
image.png

如果这时候还有小黑在删除id=2的数据,current_version=5,执行后结果是这样的。
image.png

由于MVCC的原理是查找创建版本小于或等于当前事务版本,删除版本为空或者大于当前事务版本,小明的真实的查询应该是这样

select * from user where id<=3 and create_version<=3 and (delete_version>3 or delete_version is null);

所以小明最后查询到的id=1的名字还是'张三',并且id=2的记录也能查询到。这样做是为了保证事务读取的数据是在事务开始前就已经存在的,要么是事务自己插入或者修改的

明白MVCC原理,我们来说什么是幻读就简单多了。举一个常见的场景,用户注册时,我们先查询用户名是否存在,不存在就插入,假定用户名是唯一索引。

  • 1、小明开启事务current_version=6查询名字为'王五'的记录,发现不存在。
  • 2、小红开启事务current_version=7插入一条数据,结果是这样:

image.png

  • 3、小明执行插入名字'王五'的记录,发现唯一索引冲突,无法插入,这就是幻读。

什么又是间隙锁呢?

间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读的问题。我们还是以user举例,假设现在user表有几条记录
image.png

当我们执行:

begin;
select * from user where age=20 for update;

begin;
insert into user(age) values(10); #成功
insert into user(age) values(11); #失败
insert into user(age) values(20); #失败
insert into user(age) values(21); #失败
insert into user(age) values(30); #失败

只有10可以插入成功,那么因为表的间隙mysql自动帮我们生成了区间(左开右闭)

(negative infinity,10],(10,20],(20,30],(30,positive infinity)

由于20存在记录,所以(10,20],(20,30]区间都被锁定了无法插入、删除。
如果查询21呢?就会根据21定位到(20,30)的区间(都是开区间)。
需要注意的是唯一索引是不会有间隙索引的。

数据量大了之后,分库分表怎么做?

首先分库分表分为垂直和水平两个方式,一般来说我们拆分的顺序是先垂直后水平。

垂直分库

基于现在微服务拆分来说,都是已经做到了垂直分库了
image.png

垂直分表

如果表字段比较多,将不常用的、数据较大的等等做拆分
image.png

水平分表

首先根据业务场景来决定使用什么字段作为分表字段(sharding_key),比如我们现在日订单1000万,我们大部分的场景来源于C端,我们可以用user_id作为sharding_key,数据查询支持到最近3个月的订单,超过3个月的做归档处理,那么3个月的数据量就是9亿,可以分1024张表,那么每张表的数据大概就在100万左右。
比如用户id为100,那我们都经过hash(100),然后对1024取模,就可以落到对应的表上了。

分表后的ID怎么保证一致性呢?

因为我们主键默认都是自增的,那么分表之后的主键在不同表就肯定会有冲突了。有几个办法考虑:

  • 1、设定步长,比如1-1024张表我们设定1024的基础步长,这样主键落到不同的表就不会冲突了。
  • 2、分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种
  • 3、分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也一样。

分表后非sharding_key的查询怎么处理?

  • 1、可以做一个mapping表,比如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不能扫全表吧?所以我们可以做一个映射关系表,保存商家和用户的关系,查询的时候先通过商家查询到用户列表,再通过user_id去查询。
  • 2、打宽表,一般而言,商户端对数据实时性要求并不是很高,比如查询订单列表,可以把订单表同步到离线(实时)数仓,或者基于其他如es提供查询服务。
  • 3、数据量不是很大的话,比如后台的一些查询之类的,也可以通过多线程扫表,然后再聚合结果的方式来做。或者异步的形式也是可以的。
List<Callable<List<User>>> taskList = Lists.newArrayList();
for (int shardingIndex = 0; shardingIndex < 1024; shardingIndex++) {
    taskList.add(() -> (userMapper.getProcessingAccountList(shardingIndex)));
}
List<ThirdAccountInfo> list = null;
try {
    list = taskExecutor.executeTask(taskList);
} catch (Exception e) {
    //do something
}

public class TaskExecutor {
    public <T> List<T> executeTask(Collection<? extends Callable<T>> tasks) throws Exception {
        List<T> result = Lists.newArrayList();
        List<Future<T>> futures = ExecutorUtil.invokeAll(tasks);
        for (Future<T> future : futures) {
            result.add(future.get());
        }
        return result;
    }
}

Mysql的主从同步要怎么做呢?

首先先了解mysql主从同步的原理

  • 1、master提交完事务后,写入binlog
  • 2、slave连接到master,获取binlog
  • 3、master创建dump线程,推送binglog到slave
  • 4、slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中
  • 5、slave再开启一个sql线程读取relay log事件并在slave执行,完成同步
  • 6、slave记录自己的binglog

image.png

由于mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。

全同步复制

主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。

半同步复制

和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。

那主从延迟怎么解决呢?

这个问题貌似真的是个无解的问题,只能是说自己来判断了,需要走主库的强制走主库查询。细说的话篇幅会很长,后面会专门出一篇来讲解

0

评论区