Mysql
能说下myisam 和 innodb的区别吗?
myisam引擎是5.1版本之前的默认引擎,⽀持全⽂检索、压缩、空间函数等,但是不⽀持事务和⾏级锁,所以⼀般⽤于有⼤量查询少量插⼊的场景来使⽤,⽽且myisam不⽀持外键,并且索引和数据是分开存储的。
innodb是基于聚簇索引建⽴的,和myisam相反它⽀持事务、外键,并且通过MVCC来⽀持⾼并发,索引和数据存储在⼀起
说下mysql的索引有哪些吧,聚簇和⾮聚簇索引⼜是什么?
索引按照数据结构来说主要包含B+树和Hash索引。假设我们有张表,结构如下:
create table user(
id int(11) not null,
age int(11) not null,
primary key(id), key(age)
);
B+树是左⼩右⼤的顺序存储结构,节点只包含id索引列,⽽叶⼦节点包含索引列和数据,这种数据和索引在⼀起存储的索引⽅式叫做聚簇索引,⼀张表只能有⼀个聚簇索引。假设没有定义主键, InnoDB会选择⼀个唯⼀的⾮空索引代替,如果没有的话则会隐式定义⼀个主键作为聚簇索引。
编辑
这是主键聚簇索引存储的结构,那么⾮聚簇索引的结构是什么样⼦呢?⾮聚簇索引(⼆级索引)保存的是主键id值,这⼀点和myisam保存的是数据地址是不同的。
编辑
最终,我们⼀张图看看InnoDB和Myisam聚簇和⾮聚簇索引的区别
编辑
那你知道什么是覆盖索引和回表吗?
覆盖索引指的是在⼀次查询中,如果⼀个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,⽽不再需要回表查询。
⽽要确定⼀个查询是否是覆盖索引,我们只需要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,这就是脏读。
编辑
read commit 读已提交,两次读取结果不⼀致,叫做不可重复读。不可重复读解决了脏读的问题,他只会读取已经提交的事务。
⽤户开启事务读取id=1⽤户,查询到age=10,再次读取发现结果=20,在同⼀个事务⾥同⼀个查询读取到不同的结果叫做不可重复读。
编辑
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叫做多版本并发控制,实际上就是保存了数据在某个时间节点的快照。
id | name | create_version | delete_version |
1 | 张三 | 1 | |
2 | 李四 | 2 |
这时候假设⼩明去执⾏查询,此时current_version=3
1 select * from user where id<=3;
执⾏成功后的结果是这样的
1 update user set name='张三三' where id=1;
id | name | create_version | delete_version |
1 | 张三 | 1 | |
2 | 李四 | 2 | |
1 | 张三三 | 4 |
如果这时候还有⼩⿊在删除id=2的数据, current_version=5,执⾏后结果是这样的。
id | name | create_version | delete_version |
1 | 张三 | 1 | |
2 | 李四 | 2 | 5 |
1 | 张三三 | 4 |
由于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原理,我们来说什么是幻读就简单多了。举⼀个常⻅的场景,⽤户注册时,我们先查询⽤户名是否存在,不存在就插⼊,假定⽤户名是唯⼀索引。
-
⼩明开启事务current_version=6查询名字为'王五'的记录,发现不存在。
-
⼩红开启事务current_version=7插⼊⼀条数据,结果是这样:
id | Name | create_version | delete_version |
1 | 张三 | 1 | |
2 | 李四 | 2 | |
3 | 王五 | 7 |
- ⼩明执⾏插⼊名字'王五'的记录,发现唯⼀索引冲突,⽆法插⼊,这就是幻读。
那你知道什么是间隙锁吗?
间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读的问题。我们还是以user举例,假设现在user表有⼏条记录
id | Age |
1 | 10 |
2 | 20 |
3 | 30 |
当我们执⾏:
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)的区间(都是开区间)。
需要注意的是唯⼀索引是不会有间隙索引的
你们数据量级多⼤?分库分表怎么做的?
⾸先分库分表分为垂直和⽔平两个⽅式,⼀般来说我们拆分的顺序是先垂直后⽔平。
垂直分库
基于现在微服务拆分来说,都是已经做到了垂直分库了
编辑
垂直分表
如果表字段⽐较多,将不常⽤的、数据较⼤的等等做拆分
编辑
⽔平分表
⾸先根据业务场景来决定使⽤什么字段作为分表字段(sharding_key),⽐如我们现在⽇订单1000万,我们⼤部分的场景来源于C端,我们可以⽤user_id作为sharding_key,数据查询⽀持到最近3个⽉的订单,超过3个⽉的做归档处理,那么3个⽉的数据量就是9亿,可以分1024张表,那么每张表的数据⼤概就在100万左右。
⽐如⽤户id为100,那我们都经过hash(100),然后对1024取模,就可以落到对应的表上了。
那分表后的ID怎么保证唯⼀性的呢?
因为我们主键默认都是⾃增的,那么分表之后的主键在不同表就肯定会有冲突了。有⼏个办法考虑:
设定步⻓,⽐如1-1024张表我们设定1024的基础步⻓,这样主键落到不同的表就不会冲突了。
分布式ID,⾃⼰实现⼀套分布式ID⽣成算法或者使⽤开源的⽐如雪花算法这种
分表后不使⽤主键作为查询依据,⽽是每张表单独新增⼀个字段作为唯⼀主键使⽤,⽐如订单表订单号是唯⼀的,不管最终落在哪张表都基于订单号作为查询依据,更新也⼀样。
分表后⾮sharding_key的查询怎么处理呢?
可以做⼀个mapping表,⽐如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不能扫全表吧?所以我们可以做⼀个映射关系表,保存商家和⽤户的关系,查询的时候先通过商家查
询到⽤户列表,再通过user_id去查询。
打宽表,⼀般⽽⾔,商户端对数据实时性要求并不是很⾼,⽐如查询订单列表,可以把订单表同步到离线(实时)数仓,再基于数仓去做成⼀张宽表,再基于其他如es提供查询服务。
数据量不是很⼤的话,⽐如后台的⼀些查询之类的,也可以通过多线程扫表,然后再聚合结果的⽅式来做。或者异步的形式也是可以的。
List<Callable<List>>taskList=Lists.newArrayList();
for(intshardingIndex=0;shardingIndex<1024;shardingIndex++) {
taskList.add(()->
(userMapper.getProcessingAccountList(shardingIndex)));
}
Listlist=null;
try{
list=taskExecutor.executeTask(taskList);
}catch(Exceptione) {
//do something
}
publicclassTaskExecutor{
publicListexecuteTask(Collection<?extendsCallable>
tasks)throwsException{
Listresult=Lists.newArrayList();
List<Future>futures=ExecutorUtil.invokeAll(tasks);
for(Futurefuture:futures) {
result.add(future.get());
}
returnresult;
}
}
说说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
编辑
由于mysql默认的复制⽅式是异步的,主库把⽇志发送给从库后不关⼼从库是否已经处理,这样会产⽣⼀个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,⽇志就丢失了。由此产⽣两个概念。
全同步复制
主库写⼊binlog后强制同步⽇志到从库,所有的从库都执⾏完成后才返回给客户端,但是很显然这个⽅式的话性能会受到严重影响。
半同步复制
和全同步不同的是,半同步复制的逻辑是这样,从库写⼊⽇志成功后返回ACK确认给主库,主库收到⾄少⼀个从库的确认就认为写操作完成。
那主从的延迟怎么解决呢?
针对特定的业务场景,读写请求都强制⾛主库
读请求⾛从库,如果没有数据,去主库做⼆次查询
-