Java后端开发——美团(牛客)

Java后端开发——美团(牛客)

Java的基本数据类型,各自的字节数

​ 老生常谈,不多说了.

类型 字节数
byte 1字节
short 2字节
int 4字节
long 8字节
float 4字节
double 8字节
boolean 1bit
char 2字节

基本数据类型和包装类的区别,Int和Integer有什么区别

  • 包装类可以new出来对象,并且拥有字段和方法。对象的调用都是通过引用对象的地址

  • 包装类型是引用的传递,基本类型是值的传递

  • 基本数据类型直接声明。包装类需要new对象

  • 基本数据类型直接将值保存在栈中,而new出来的对象则是在堆中,然后通过对象的引用来调用它们

  • 基本数据类型的初始值都有各自对应,包装类的初始值均为null

    集合中只能存放对象的引用,这个是包装类的一个用途。其次就是从数据库中拿数据出来时,有时会遇到为null的数据,如果用基本数据类型接收,那就会报错。(不过一般数据库里好像是能不使用null就不要使用,因为这个会造成性能损耗,最好是用0或者某种方式来代替)

mysql索引是如何实现的

这个东西我有一套完整的视频讲,大一时自己录的,贴到下面,另外最近还读了一篇很不错的博客,也同样分享出来

MySQL索引底层实现原理 - 做个有梦想的咸鱼 - 博客园 (cnblogs.com)

【朱江的数据结构课堂】大一数据结构翻转课堂-B树-1_哔哩哔哩_bilibili

一次IO的时间在什么量级

I/O的话,我们根据上面MySQL索引的视频又或者是文章都可以知道磁盘的读取原理。就是寻道时间旋转延迟的和,寻道时间顾名思义就是把读写磁头移动到所需磁道上的时间,而旋转延迟就是当磁道对齐后,将所需扇区移动到磁头下的时间。一般来说在ms量级

聚簇索引,非聚簇索引,索引覆盖和回表查询

顾名思义,这个聚簇索引和非聚簇索引,也要先理解了上面的MySQL索引之后再来看。而索引覆盖和回表查询,也需要理解了聚簇和非聚簇之后才能看

在MySQL中,索引属于存储引擎级别的概念,所以不同存储引擎对索引的实现方式也不一样,下面举MyISAM和InnoDB的例子

MyISAM索引:

MyISAM使用B+树作为实现索引的数据结构,叶子节点的data域直接存放的就是对应key域中主键数据所存放的物理地址,如下图所示:

Vq1O0.md.png

现在的索引就是一个主索引,找到了索引也就找到了数据存放的位置。一般来说主键会默认创建主索引,且一张表只允许有一个主索引

在MyISAM中,主和辅助索引在形式上看起来是一样的。

在MyISAM中,二者区别在于主索引要求key是唯一的,而辅助索引的key可以重复

VqgOU.md.png

上图为以Col2字段为主键创建的辅助索引.

你应该还可以注意到,辅助索引只是逻辑上的有序,实际在物理存储中并不连续

按照我个人的理解,在MyISAM中不存在所谓的“聚簇索引”的说法,我们应该统一说MyISAM中的主索引和辅助索引都是“非聚簇索引”。这是因为聚簇索引的定义是将数据存储和索引放到了一起,找到索引也就找到了数据,如果还是不好理解,看下面InnoDB的聚簇索引可能就理解了

这里还要多提一嘴,MyISAM通过key_buffer将索引缓存到了内存中,当需要访问数据的时候,首先看是否能命中,如果命中那就直接通过data域的地址找数据就行了。如果不命中,那速度就比较慢了

InnoDB索引:

InnoDB同样使用B+树作为实现索引的数据结构。只是具体实现方式略不同于MyISAM,它的两种索引还是有很大差别的。

  • InnoDB的数据文件本身就是索引文件。而MyISAM实际上索引文件和数据文件是分开的,索引只保存数据的地址,这点通过看图就能得出。在InnoDB中,表数据文件本身就是按照B+树的规定所构建出的一个索引结构。而叶节点中则保存了完整的数据记录,key则是对应记录的主键值。因此其实InnoDB表数据文件本身就是聚集索引。

    VXmNo.md.png

    上图就是InnoDB引擎中的聚集索引(同时也是表数据文件本身)的结构图。叶子节点包含了完整的数据记录,这就是聚集索引。同时我们也不难想到,因为InnoDB的表数据文件本身就是一个聚集索引,那聚集索引就要有一个主键,所以InnoDB就要求表必须拥有主键(而MyISAM则不需要),所以如果我们没有显式指定一个可以唯一标识数据的主键,InnoDB为了不产生错误,首先会默认第一个NOT NULL且UNIQUE的字段作为聚簇索引,如果实在找不到这样的索引,那么InnoDB会隐式定义一个主键row_id作为聚簇索引,这个字段的长度为6字节,类型为long

  • 在InnoDB中,基于聚集索引所创建的索引叫做辅助索引,那么根据定义,辅助索引也就是非聚簇的。辅助索引的数据访问总是需要二次查找,具体原因看下面的详细图示即可:

    VYSFm.png

​ 正如你所见,InnoDB的辅助索引中key域存放的就是指定在哪个字段创建辅助索引的全部值,而data域存放的则是对应记录的主键值。也就是说InnoDB的辅助索引都会引用主键作为data域。流程为首先在辅助索引中找到要查找记录所对应的主键值,之后再去主索引中根据主键值查找到数据(所以叫二次查找)

VA0Mi.md.png

​ 还不清楚的话,可以看上面这幅图。绿色箭头代表一次查询就可以了,也就是说我们使用了聚簇索引。而红色箭头需要二次查找,也就是说我们使用了非聚簇索引

​ 通过上述的内容,我们就可以解答一些关系数据库设计的问题了。比如为什么InnoDB不建议使用过长的字段作为主键,因为所有的辅助索引都引用主索引,过长的主索引势必会导致辅助索引变得很大。再比如,使用非单调的字段作为主键并不是推荐的做法,因为InnoDB的数据文件本身也是一颗B+树,非单调的主键在插入数据后,因为要维护整个表符合B+树的数据结构要求,所以会把整棵树进行分裂调整,造成效率低下

​ InnoDB只会聚集在同一页面中的记录,而包含相邻键值的页面可能相聚十分之远。

  • 何时使用聚簇索引,何时使用非聚簇索引

    VnWMI.png

  • 聚簇索引的优点和缺点

    • 优点

      数据访问更快

      ​ 由于行数据和叶子节点存储在一起,同一页中会有多条数据,访问同一数据的不同行记录时,已经把也加载到了Buffer中,再次访问的时候,会在内存中完成访问,不必访问磁盘

      ​ 这样由于主键和行数据时一起被载入磁盘的,找到叶子节点就可以立即将行数据返回了,如果按照主键ID来组织数据,获得数据更快

      聚簇索引对主键的排序查找和范围查找速度非常快

      ​ 聚簇索引适合用在排序的场合,非聚簇索引不适合

      ​ 取出一定范围数据的时候,使用用聚簇索引

    • 缺点

      不易使用较大的主键(详细原因上面讲到了)

      维护索引的代价是很昂贵的。例如当我们插入了大量新行又或者是主键因某种原因更新,这两种情况都可能会引起表的分页。我们可以在负载较低的时间段使用OPTIMIZE TABLE来进行碎片的清理。

      插入速度严重依赖插入顺序(这点上面也讲了。其实就是如果按照主键顺序,那么就不需要页分裂来维护,其他情况则会影响性能)

  • 为什么辅助索引使用主键作为data域内容,而不使用地址值作为data域

    先说结论:使用主键作为data域内容,减少了因行移动或者数据分页所带来的维护辅助索引的性能开销

    ​ 有了聚簇索引中维护整颗B+树有关开销的内容,这句话就很好理解了。如果我们同样使用地址值作为data域,那么当行的位置发生变化,相应的磁盘中的数据就会开始移动,就会造成辅助索引中的data域内容也要跟着改变,带来了性能开销。如果使用主键值作为data域,那么就不受影响了。

回表查询和覆盖索引

  • 回表查询

    回表查询的概念其实很简单。意味先通过辅助索引定位到聚簇索引,然后再通过聚簇索引定位到行记录数据。需要二次查找,形象来说就是返回表数据(聚簇索引表)去查询。

  • 索引覆盖

    索引覆盖和回表查询是有关系的。如果我们在一棵索引树上就能获取我们所需的全部数据,无需二次查询,那么就称这次查询是一次索引覆盖

    关于如何实现索引覆盖:

    insert into user(name,age) values('张三',30);
    insert into user(name,age) values('李四',20);
    insert into user(name,age) values('王五',40);
    insert into user(name,age) values('刘八',10);
    
    mysql> select * from user;
    +----+--------+------+
    | id | name  | age |
    +----+--------+------+
    | 1 | 张三  |  30 |
    | 2 | 李四  |  20 |
    | 3 | 王五  |  40 |
    | 4 | 刘八  |  10 |
    +----+--------+------+
    

    假如现在有这么一张表,下面是他们的聚簇索引和非聚簇索引(不再重复为什么长这样了,上面说的很详细了,另外图源知乎@张德检,感谢)

    聚簇索引

    非聚簇索引

    SELECT
    id,age
    FROM
    user
    WHERE
    age=10;
    

    如果我们执行上面这个查询操作。那么这就是一个覆盖索引的查询,原因是什么呢?一般来说如果我们想要知道本次查询能否实现覆盖索引,我们可以使用explain

    explain
    SELECT
    id,age
    FROM
    user
    WHERE
    age=10;
    

    VAJor.png

    可以看到Extra的内容是Using index,意思就是本次符合索引覆盖。那么原因是什么呢?

    WHERE子句后面的依赖是age,并不是聚簇索引的id,所以我们就到age的非聚簇索引中寻找,而非聚簇索引中data域为id,key为age,正好对应我们需要查找的两个字段,那么就不需要回表了,直接一次查到返回两个域即可

    SELECT
    id,age,name
    FROM
    user
    WHERE
    age=10;
    

    那如果是上面这样的查询,肯定就必须要回表了。

给身份证创建索引,如何做?

​ 首先我们要明确一点,也就是说当主键字段的离散度达到最佳值时,一般来说最大的时候,可以在空间和查询效率之间取得一个平衡

SELECT count(distinct lefr(filed,length)) from tableName;

​ 其中filed为想要当作主键的字段。通过这句SQL语句我们可以获得长度为length时所对应的区分度,越大越好。

​ 那言归正传,如果现在我们要给身份证创建索引,应该怎么做(默认为InnoDB引擎)。我们都知道,InnoDB使用聚簇索引或非聚簇索引时,key的长度不宜过大,否则会造成一定的开销。那身份证有18位,长度太长了,按道理来说是不适合作为索引的,但现在要求我们必须用身份证创建索引。

​ 我们可以这么做,首先分析到身份证的前6位代表地址,而中间8位是年月日。当长度过长导致不适合作为索引,我们很容易就想到前缀索引了,是的,我们可以这么做:

前缀索引:

​ 我们可以将身份证的前六位截取出来,作为前缀索引。但需要注意的一点是,如果我们的项目是一个城市居民管理等的项目,那可想而知几乎所有的数据前六位都是一样的,那么就需要截取更多的位数进行前缀索引以便区分,那这时候就发生了字段过长不适合创建索引的问题。需要注意这一点

倒序存储:

​ 因为身份证后六位的区分程度较高,所以我们可以将身份证倒叙存储,然后索引取前六位。需要注意这种情况只适用于等值存储

哈希:

​ 可以新增一个字段存储身份证号码的哈希值,加上索引,存入身份证时候,对身份证进行crc3()计算,得到的值存入id_card_crc,索引长度为4,因为hash可能会发生碰撞,所以查询时候加上身份证作为筛选条件:

​ 因为身份证不是顺序递增的,所以就导致哈希索引做区间查询的速度极慢。也就是说,哈希只适合等值查询的场景。

介绍mysql的事务(这玩意都问烂了……)

定义:

​ 最小的不可再分的工作单元;通常一个事务对应一个完整的业务(账户转账等等)

​ 一般事务中的多个操作,只会同时成功或者同时失败

四大性质:

  • 原子性(同时成功,同时失败)
  • 持久性(事务对数据库的影响是不可改变的)
  • 隔离性(多个事务之间互不影响,互不干扰)
  • 一致性(事务不破坏数据库的完整性)

事务如何保证原子性和持久性:

  • 原子性:MySQL有一个日志叫undo log,这个日志存储的是数据库更新之前的数据,相当于给事务不成功执行做的备份。如果事务不成功执行,那就把里面的数据回退即可

    undo log分为两种,一种是INSERT undo log另外一种是UPDATE undo log,前者在INSERT操作时产生,只用来进行事务回滚,在事务提交后就可以被立刻丢弃了。而后者是UPDATE和DELETE时产生的,除了可以用来进行回滚,还可以拿来快照读,这个快照读在下面一个题会说具体内容

  • 持久性:MySQL还有一个日志叫redo log,这个日志存储的是发生了修改但未成功commit的数据。

    为什么会产生这种情况呢?因为MySQL为了保证存储的效率,每次都写文件都是先对缓存池进行一次操作。之后再定期将缓冲池的内容刷新到磁盘中(刷脏),所以如果这时候主机突然断电,那就丢失了数据。

    但现在有了redo log,我们只需要再次启动之后通过日志恢复即可。但如果想在断电后通过这个日志恢复,那它肯定也是要存到磁盘里的(在事务提交时)。但MySQL既然选择这么做,那说明写redo log肯定比缓冲池快:

    首先写redo log是追加文件写,属于顺序IO,而缓冲池是随机IO;其次刷脏是以页为单位,修改一点就要整页写入

题外话:原语和事务有什么区别?

我总是在匡老师的操作系统课上说错一个东西。他每次说原语,我就老说事务事务。存在个盲区,一直以为事务和原语是一个东西,因为都是不可被中断,要么就执行成功,要么就执行失败。后来查了很多资料,发现其实两个东西不一样。

先上结论:事务是一种特殊的原语

原语是指由若干条指令所组成的程序段,用来实现某个特定的功能,在执行过程中不可被中断,可以把原语理解为一条指令。也就是说,如果有一条指令在执行过程中不可被中断,我们也可以说这是一条原语,要在管态下才能执行。

那事务又是什么呢?事务一般指要做的或所做的事情,一般来说就是DML语句,或者说DML语句才会涉及事务。由上文我们知道,事务有四大性质:原子性、持久性、隔离性、一致性

原语和事务的区别就在于,原语只强调原子性,而事务除了原子性,还强调剩余三个特性。

原谅我吧,之前我确实不懂这个……

介绍隔离级别

  • 读未提交(read uncommitted)

    事务A和事务B,事务A未提交的数据,事务B可以读取到。这样的隔离级别是最低的,只是理论上存在这种情况,数据库不会用这种隔离级别,因为关于并发带来的问题他一个也解决不了。

  • 读已提交(read committed)

    事务A只能读取到事务B已经提交的数据,可以有效避免“脏数据”,ORACLE的默认隔离级别就是这一level,会导致出现不可重复读问题

  • 可重复读

    事务A提交之后的数据,事务B读取不到。会导致出现幻读问题

  • 串行化

    事务A和事务B,当事务A在操作数据库时,事务B只能排队等待。这种做法的吞吐量太低,用户体验极差。一般不使用,但可以避免幻读

如何解决不可重复读问题

首先先说不可重复读问题是什么。不可重复读出现在读未提交和读已提交这两种隔离级别。当事务A执行时,在提交前,事务B对同一条记录进行了修改操作(读已提交只是锁了要等待事务A提交后才能读,但是没说现在不能写),那么事务A再读记录,就发现前后两次不一样了。这就是不可重复读

InnoDB使用MVCC来解决不可重复读问题,即多版本并发控制。如果同一行数据发生了读写请求,一般会上锁进行阻塞,mvcc避免锁带来的性能损耗,使用快照读来解决不可重复读问题。

  • 当前读:每次都读取一条记录最新的版本,并且会在读取时对记录进行加锁,属于悲观锁
  • 快照读:快照读基于MVCC,顾名思义就是当前读取到的数据可能并不是最新的数据,而是之前的历史版本。MVCC是一个概念,即维持一个数据的多个版本,使读写操作不会发生冲突,快照读就是MVCC的一个实例化

MVCC为每个事务分配单增的时间戳。为每个数据修改保存了一个版本,版本与时间戳相关联。而读操作只读取某事务开始前的快照版本。MVCC没法解决写-写带来的更新丢失问题,所以要和其他方法进行配合:

  • MVCC+悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突
  • MVCC+乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突

MVCC使用版本链undo logRead View来实现:

版本链:

InnoDB中的每条记录都会含有四个隐藏字段,如下:

  • db_trx_id:6字节,最近修改或创建这条记录的事务ID
  • db_roll_pointer:7字节,指向这条记录上一个版本的指针
  • db_row_id:6字节,隐含的自增ID,这个在聚簇索引说了,不再多说了。
  • flag:这个和MP中的逻辑删除是一个意思,当删除一条记录时,不会将他从数据库中删除,而是将flag置为1,加入WHERE字句后也就有了真实删除的效果,其次也保证了数据统计的正确性

undo log:

undo log在上面也讲了,这里再补充一点:每条undo log都有一个roll_pointer属性(INSERT操作没有,因为不存在更先前的版本),我们可以把所有的undo日志通过这个指针连起来,每次都指向上一个版本,直到最早的版本,形成一条链表。头节点就是最新版本,而尾节点就是最早的版本。

Read View:

  • trx_ids:当前系统活跃事务(开始执行但未提交的事务)版本号集合
  • low_limit_id:创建当前视图时,当前系统中最大事务版本号+1
  • up_limit_id:创建当前视图时,系统正处于活跃事务的最小版本号
  • creator_trx_id:创建当前read view的事务版本号

Read View可见性的判断条件:

  • db_trx_id<up_limit_id || db_trx_id == creator_trx_id(可见)

    或运算,先说第一个逻辑,当最近修改或创建这条记录的事务ID比当前系统中活跃事务最小版本号还要小,那么就说明这条记录肯定是之前就已经提交完毕的——所以可见

    第二个逻辑,如果事务ID和创建当前视图的事务版本号相同,那就说明当前记录就是本事务所创建或修改的——所以可见

  • db_trx_id>=low_limit_id(不可见)

    当最近修改或创建记录的事务ID比最大事务版本号加一还要大或者等于时,那就说明是要在当前事务之后才产生——所以不显示

  • db_trx_id是否在trx_ids中

    如果不存在,那么说明事务之前就提交了,可以显示

    如果存在,那么就说明事务还在活跃,仍未提交,所以不能看到

MVCC用来实现RC(Read Committed)和RR(Repeatable Read)这两个隔离级别。在RC中,每个快照读都会生成并获取最新的Read View,而RR隔离级别中,只有同一个事务中的第一个快照读才会创建Read View,之后快照读获取都是同一个视图

读提交和可重复读隔离级别实现上有什么区别

在读已提交隔离级别中,事务中的每执行一条语句就会生成最新的Read View,而在可重复读级别中,事务启动时生成一个Read View,到结束之前一直都使用这同一个Read View。

select、poll、epoll的区别

select()、poll()、epoll()是Linux网络编程中的三个函数,具体的作用和标准定义就不写在下面了,可以搜到。

这三个函数都实现I/O多路复用来监视fd,fd即文件描述符,这个也是Linux的基本概念,不在这里讲,默认了解

当入参中某个fd就绪(读写就绪),就能够通知程序进行相应的读写操作。但需要注意的是,这三个函数本质上都是同步I/O,因为他们都需在读写事件就绪之后,由自己来负责读写,所以就会造成阻塞,异步I/O则并不会。

  • select、poll需要主动地不断轮询所有传入的fd集合,直到fd就绪,那这期间进程可能就要被睡眠和唤醒多次。而epoll其实也调用了epoll_wait不断轮询就绪链表,期间也会经历多次睡眠和唤醒。但它的机制是当设备就绪时,会调用回调函数来通知,并把就绪的fd放到就绪链表里,并唤醒进程。epoll只需要判断就绪链表是否为空,而select和poll则需要遍历整个fd集合
  • select和poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll则只拷贝一次,并且把current往等待队列上也只挂一次。

HTTP报文结构,状态码有哪些?

这个不写答案了,太简单了。后面也都是一些个人情况题了,所以到此为止了。

参考资料

MySQL索引底层实现原理 - 做个有梦想的咸鱼 - 博客园 (cnblogs.com)

MySQL面试:谈谈你对聚簇索引的理解_OceanStar的学习笔记的博客-CSDN博客_mysql聚簇索引

MySQL 的覆盖索引与回表 - 知乎 (zhihu.com)

mysql explain详解 - 腾讯云开发者社区-腾讯云 (tencent.com)

[MySQL]Mysql自己怎么选取索引,怎么给字符串字段加上索引?(涉及到身份证怎么加索引)_pmdream的博客-CSDN博客

如何给特殊字符串加索引:如身份证、邮箱等_超人不会飞2018的博客-CSDN博客

一文搞懂select、poll和epoll区别 - 知乎 (zhihu.com)

posted @ 2022-09-22 22:28  Appletree24  阅读(377)  评论(0编辑  收藏  举报