自我介绍
一.MySQL
1.联合索引的最左前缀匹配原则
MySQL中的联合索引遵循最左前缀匹配原则,它指的是在使用联合索引时,查询条件必须从索引的最左侧开始匹配。如果一个联合索引包含多个列,那么查询条件必须包含第一个列的条件,然后是第二个列,以此类推。
底层逻辑: 由于B+树中联合索引的排序方式遵循从左到右的顺序,在查找时会优先使用第一个字段作为匹配依据。若跳过最左侧字段会导致无法利用该索引。
index(a,b,c)
where a=1 , c=2;
对于上述索引和查询条件,在5.6版本之前也只能用到a=1条件进行过滤,
在MySQL5.6版本之后对联合索引进行了优化,提供了索引下推技术(MyISAM和InnoDB均支持),即查询得到a=1的数据后进行下推,继续利用c=1过滤掉不符合的数据再返回。
2.联合索引的字段顺序
例如联合索引(a,b,c),在B+树中的排序是:先按a的值排序,再按b的值排序,再按c的值排序。
若遇到 开区间范围查询(a>2或a<2)就会停止匹配,索引不生效;
若遇到 闭区间范围查询 、 BETWEEN、前缀like(a>=2、a<=2)等则不会停止匹配:因为闭区间包含一个等值判断,可以通过该值定位到某数据,然后往后扫描即可。
注:闭区间的范围查询情况下,对应的索引只在区间边缘生效,而对于之后的区间遍历进行时索引其实并未生效。
拓展:什么情况下索引会失效?
- 不符合最左前缀匹配原则
- 索引对应的查询条件中存在运算
- 索引对应的查询条件中存在函数处理
- like、or在语句中的随意使用
- order by后的不是主键或不是覆盖索引
3.索引是否越多越好?
不是越多越好。索引的建立既需要 时间成本,也需要 空间成本。
从 时间 上看:
每次对表中的数据进行增删改操作时,都需要更新对应的索引,增加了写入操作的开销。而且B+树还有页分裂等操作,会使得时间开销更大。
此外,MySQL的查询优化器会分析当前查询并寻找对应的最佳计划,就会考虑索引的查询成本。如果索引过多也会影响到优化器的时间开销与索引选择。
从 空间 上看:
每建立一个二级索引就需要新建一个B+树,每个数据页默认大小16kb,如果数据量大,且索引又很多,会消耗较多的空间。
4.对长字符字段如何添加索引?
《MySQL——给长字符串加索引》_mysql 字符串太长怎么建立索引-CSDN博客
对于长字符串,可用如下方式建立索引:
(1)前缀索引
前缀索引占用的空间更小,但是会增加扫描的次数。
使用前缀索引,定义好长度,就可以做到既节省空间,又不额外增加太多查询成本。
在建立索引时关注区分度,可区分度越高,意味着重复的键值越少(2)字符串倒叙+前缀索引
前缀索引对覆盖索引的影响:
如果我们的语句是:
select id,email from User where email = ‘1540984562@qq.com’;
这个语句只要求返回id和email字段。
如果使用的整个字符串作为索引,会触发覆盖索引,不需要回表ID索引再查询。
而如果使用的是前缀索引,就还得回表ID索引去获取email字段的完整值
所以如果使用了前缀索引就相当于放弃掉了覆盖索引对查询性能的优化了
(2)字符串倒叙+前缀索引
(3)添加hash字段+并在hash字段上加索引
(4)字段拆分(一个字段可拆分为两个以上)
总结如下:
长字段索引
##5.B+树查询的复杂度
B+ 树搜索时间复杂度到底是什么:mlogmN / logN? - 知乎 (zhihu.com)
6.为什么MySQL要用B+树作为索引的数据结构?
B+树具有三个优点:
- 高效的查找性能: B+树是一种自平衡树,每个叶子节点到根节点的路径长度相同,而B+树在插入和删除节点时会进行分裂和合并以保持树的平衡,但又会有一定的冗余节点,使得树结构变化小,更加高效
- 树的高度增长较慢,使得查询磁盘的I/O次数减少
- 范围查询能力强:B+树特别适合范围查询,叶子节点通过链表链接,从根节点定位到叶子节点查找到范围的起点之后,只需顺序扫描后续的数据,非常高效
拓展:B+树与B树之间的区别?
- B+树的非叶子节点只存储了key和指针,完整数据存储在叶子节点,这使得B+树可以在内存中存放更多的索引页,减少磁盘查询次数;而B树每个节点都存储了完整的数据
- B+树叶子组成了链表,便于区间查找,而B树只能每一层遍历查找
- B+树的查找时间更平均、稳定,每一次都需要从根节点扫描到叶子节点;而B树则在非叶子节点就可能找到对应的数据并返回
7.简要叙述一下一条SQL语言的执行过程
- 连接器校验权限
- 分析器进行SQL语句的词法分析和语法分析,构建分析树
- 优化器选择合适的索引和表连接顺序,选择合适的执行计划
- 执行器调用存储引擎查询数据后返回结果集给客户端
注:在MySQL早期版本(8.0以前)存在查询缓存,会在分析器工作之前先查询缓存。然而由于MySQL内部的查询缓存在高并发操作时的性能瓶颈,8.0版本之后被移除了,在实际开发过程中更多使用的是应用程序级缓存Redis等
8.什么是分库分表?
9.MySQL事务的隔离级别及其原理
共有四种,从低到高分别是:
-
读未提交(Read Uncommitted):
事务中的修改,即使没有提交,对其它事务也是可见的
问题:可能脏读,读到其他事务未提交的数据
-
读已提交(Read Commited):
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的
问题:不可重复读,同一事务中可能返回不同结果
-
可重复读(Repeatable Read)(MySQL默认隔离级别):
保证同一个事务中多次读取同样数据的结果是一样的
问题:幻读,同一事务中可能返回不同数量的行
-
可串行化(Serializable):
事务间严格串行运行,
问题:导致并发性能降低
注:幻读与不可重复读之间的区别:
幻读指的是事务期间数据总量发生变化,主要针对的是数据总量;而不可重复读针对的是数据内容发生了改变
拓展:事务的ACID特征分别指什么?
事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。
-
原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
-
一致性。事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态
-
隔离性。一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰
-
持续性。也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响
大厂选择第二事务隔离级别的原因是什么?
10.MySQL的MVCC是什么?
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。也就是快照读,而非当前读。当前读其实就是加锁读取,是悲观锁的实现。
MVCC的实现原理:
依赖记录中的四个隐式字段、undo日志和Read View来实现的。
11.什么是覆盖索引?
覆盖索引是指二级索引中包含了查询所需的所有字段,从而使查询可以仅通过访问二级索引而不需要访问实际的表数据
12.MySQL有哪些存储引擎?
MySQL的主要存储引擎包括:
-
InnoDB(重点)(MySQL5.5.5之后的默认引擎)
- 支持事务、行级锁和外键
- 存储限制 64TB
- 支持数据缓存, 聚簇索引、多版本并发控制(MVCC)
- 提供 高并发性能,适用于高负载的应用
- 支持全文索引
适用场景:
- 事务处理系统
- 高并发读写应用
- 数据可靠性要求高的场景
-
MyISAM(重点)(5.5.5之前的默认引擎)
- 不支持事务、行级锁和外键,最小锁粒度为表级锁
- 存储限制 256TB
- 不支持数据缓存、聚簇索引、MVCC,只支持索引缓存
- 具有较高的读性能和较快的表级锁定,占用资源较少
适用场景:
- 读密集型应用
- 数据仓库和数据分析系统
- 嵌入式系统和移动应用
-
MEMORY
- 数据存储在内存中,速度快,但数据在服务器重启后会消失
- 适用于临时数据存储或快速缓存
-
NDB
- 支持高可用性和数据分布,适合大规模分布式应用
- 提供行级锁和自动分区
-
ARCHIVE
- 用于存储大量历史数据,支持高效的插入和压缩
- 不支持索引,适合日志数据存储
13.MySQL都有哪些日志?
MySQL中有多种日志类型,例如错误日志error.log、查询日志general_log、事务日志redo log和undo log、二进制日志binlog、慢查询日志slow.log等。
其中binlog、redo log、undo log是MySQL中与事务和事务一致性密切相关的三大日志。
拓展:三大日志间的区别与其具体实现
-
Binlog:二进制逻辑日志,用于备份恢复和主从复制
作用:
- 备份与恢复,支持通过Binlog恢复到指定的时间点
- 主从复制,主库把Binlog传输给从库,从库重放BinLog更新自身数据
记录方式:
- STATEMENT:记录SQL语句(逻辑日志)
- ROW:记录行数据变化(物理日志)
- MIXED:根据情况分别选择以上两种模式
刷盘控制:
通过
sync_binlog
参数控制刷盘频率 -
**Redo log:**重做日志,保证事务的持久性
作用:
- 保证事务的持久性,即使宕机也能恢复提交的数据
- 提升写性能。将随机写转化为顺序写
工作机制
- 更新操作先写入内存,同时记录Redo Log,标记为脏页
- 后台线程在合适时机将脏页写入磁盘
- 若发生宕机,可通过Redo Log恢复未刷盘的修改
写入磁盘时机:
- 事务提交时写入磁盘(由
innodb_flush_log_at_trx_commit
参数进行控制) - 后台线程每秒刷盘一次
-
**Undo log:**支持事务回滚和MVCC,实现事务的原子性和隔离
作用:
- 事务回滚:记录修改前的数据,用于回滚操作,保证事务的原子性
- MVCC支持:通过版本链实现多版本并发控制
工作机制:
- 每次修改数据前,记录旧值到Undo Log
- 形成版本链,配合Read View实现MVCC快照读
14.悲观锁和乐观锁?
悲观锁和乐观锁是并发控制的两种不同策略:
- 悲观锁:在修改数据之前先获取锁,阻塞其他线程的访问。适用于写多读少的情况。
- 乐观锁:假定没有其他线程会修改数据,只有在实际提交更新时,才检查是否有冲突。适用于读多写少的情况。
二.Redis
0.Redis是什么?怎么用?什么时候用?
Redis是一个开源的、基于内存的键值对存储中间件。其速度极快,适合用于缓存。此外Redis支持多种数据结构,各类编程语言的客户端,支持持久数据,生态广泛。
适用场景:
- 缓存
- 实时系统(排行榜、点赞、收藏、点击统计)
- 消息队列
- 分布式锁(确保分布式系统中的资源安全访问)
- 计数器(点赞、收藏)
1.Redis和MySQL的双写一致性如何保证?
双写一致性指的是一个数据同时存在于缓存和数据库中,任何一方的更新都需要确保另一方的数据同步更新,以保证双方的一致状态。解决一致性问题的核心在于如何正确处理缓存与数据库的读写交互,防止出现数据不一致的情况。
一致性一般可以分为两种要求进行考虑:
强一致性: 所有节点在任何时刻看到的都是相同的数据。任何更新操作都对任何节点可见。往往实现强一致性需要付出较高的代价,例如增加通信开销和降低系统的可用性。
弱一致性: 系统中的数据在某些情况下可能会出现不一致的状态,但最终会收敛到一致状态。弱一致性下的系统允许一定时间内不同节点看到不同的数据状态。
缓存常见读取/修改方式:
缓存与数据库的数据同步总计可以通过六种方式进行:
- 先更新缓存,再更新数据库;
- 先更新数据库,再更新缓存;
- 先删除缓存,再更新数据库,后续查询时再将数据存入缓存中;
- 先更新数据库,再删除缓存,后续查询时再将数据存入缓存中;(最常用)
- 缓存双删策略。更新数据库前后都删一次缓存,后续查询时再将数据存入缓存中;
- 使用Binlog异步更新缓存,监听Binlog变化,异步更新缓存。
其中1,2,3策略在面对并发场景下都非常容易出现数据不一致的情况,因此不使用。
4,5,6均只能实现弱一致性
可以用分布式读写锁实现强一致性,保证读写操作的互斥。
2.Redis中的缓存击穿、缓存穿透和缓存雪崩分别指的是什么?
缓存击穿:指某个热点数据在缓存中突然失效,导致大量请求直接访问数据库,此时由于瞬间高并发,可能会导致数据库崩溃。
解决方案 :
- 使用互斥锁,确保同一时刻只有一个请求可以去查询数据库并更新缓存
- 热点数据永不过期
缓存穿透:指查询一个不存在的数据,缓存中没有相应的记录,每次都会向数据库进行查询,造成数据库负担加重。
解决方案 :
- 使用布隆过滤器,过滤掉数据不存在的请求,避免直接访问数据库
- 对查询结果进行缓存,即使是不存在的数据也加入缓存标识,减少对数据库的请求
注:布隆过滤器是什么?
布隆过滤器(BloomFilter)是一种高效的概率数据结构,常用于检测一个元素是否在一个集合中,可以有效地减少数据库的查询次数。可以通过位图(BitMap)或Redis模块RedisBloom实现。
缓存雪崩:指多个缓存数据在同一时刻过期,导致大量请求访问数据库造成数据库崩溃。
解决方案 :
- 采用随机过期时间策略,避免多个数据同时过期
- 使用双缓存策略,将数据同时缓存在两层缓存中,减少数据库的直接请求
3.Redis为什么那么快?
主要有3方面的原因, 存储方式、优秀的线程模型及IO模型、高效的数据结构
-
存储方式:
Redis是基于内存的,内存访问速度远远大于磁盘速度(ns级别与ms级别的差距)
-
优秀的线程模型及IO模型:
Redis使用 单线程模型结合I/O多路复用模型 ,避免了多线程上下文切换和竞争条件,提高了并发处理效率。在6.0版本之后引入了多线程机制,进一步提升了I/O性能
详细说明 :Redis的线程模型与IO模型
Redis的单线程模型主要指的是Redis网络I/O和键值对的读写这些操作是由一个线程完成的。
随着数据规模和请求量增长,Redis的性能瓶颈主要在于网络I/O,而原先的I/O多路复用模型正是为了解决这个瓶颈,然而其本质上仍然是同步I/O。
因此Redis从4.0版本开始就开始引入多线程指令,6.0版本之后正式引入了多线程的机制,而这里的多线程只是针对网络请求过程使用多线程,对于数据读写命令的处理依然是单线程的,也就不会有线程安全问题。
默认多线程是禁用的,需配置
io-threads-do-reads
为yes -
高效的数据结构
Redis有多种高效的数据结构,如字符串、哈希、列表和集合等,其中大多读写操作的时间复杂度均为O(1),能实现快速的数据处理
4.Redis中如何实现分布式锁?
首先,分布式锁是在分布式系统中用于控制对共享资源或临界区的访问的一种机制。它可以确保在多个节点或实例上同一时间只有一个进程能够获取锁,从而保证数据一致性和避免并发访问下的数据竞争与冲突。
分布式锁的实现方式有:
- 基于数据库实现分布式锁:可以通过数据库的乐观锁或悲观锁实现分布式锁,但是由于数据库的 IO 操作比较慢,不适合高并发场景。
- 基于 Zookeeper 实现分布式锁:Zookeeper 是一个高可用性的分布式协调服务,可以通过它来实现分布式锁。但是使用 Zookeeper 需要部署额外的服务,增加了系统复杂度。
- 基于 Redis 实现分布式锁:Redis 是一个高性能的内存数据库,支持分布式部署,可以通过 Redis 的原子操作实现分布式锁,而且具有高性能和高可用性。
综合以上方案来看,基于数据库实现的分布式锁不适用于高并发场景,而基于 Zookeeper 实现的分布式锁又需要额外部署 Zookeeper 服务,增加了运营成本,所以使用 Redis 实现分布式锁是目前主流的实现方案。
Redis中最简单的分布式锁实现方法是利用setnx
命令(set if not exists
)结合Lua脚本来实现
- 获取锁:使用
SETNX
+PX
尝试获取锁,并用设置锁的过期时间 - 释放锁:使用Lua脚本确保只有持有锁的客户端才能删除锁
注意锁必须设置过期时间。
Redisson:开源的、分布式的Redis客户端,也是一个分布式数据网格,提供了基于Redis各种分布式的数据结构和便捷的操作接口,如分布式锁、分布式队列、分布式集合等
Redisson的看门狗机制:开启一个监听进程,如果方法还没执行完,就会自动续期。默认时间是30s,每次续期10s。
拓展:除了Redisson还有哪些操作Redis的客户端?
Spring-Data-Redis,Spring Data,Jedis,Lettuce等
5.Redis都有哪些数据结构,可以用来做什么
共有5种基础数据结构,随着版本更新后续又出现了4种特殊数据结构
5种基础结构与常见使用场景:
-
String:缓存,计数器,session存储
-
List(双端链表):消息队列
-
Set(无序集合):标签、点赞、收藏等
-
Hash(散列表):缓存(比String更直观且节省空间)
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象
-
Zset(有序集合):排行榜
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序
底层通过两种数据结构实现:
-
ZipList
在同时满足以下两个条件时使用ZipList,其他时候用SkipList
- 集合内保存的元素数量小于128个
- 集合内保存的所有元素长度均小于64字节
-
SkipList(跳表)
跳表主要通过多层链表实现,底层链表保存所有元素,每一层都是下一层的子集
-
4种特殊结构与其开始加入的版本号:
- HyperLogLogs(基数统计,2.2)
- Bitmap(位存储,2.8)
- geospatial(地理位置,3.2)
- Stream(5.0):主要功能就是为了实现消息队列
三.计算机网络,操作系统
1.线程与进程的区别
进程:是资源分配的基本单位,每个进程都有自己独立的内存空间,可以看作是一个正在运行的程序示例。进程间相互独立
线程:是CPU调度的基本单位,属于进程,一个进程中可以包含多个线程。线程共享进程的内存空间和资源
2.浏览器输入URL回车后的执行流程
- 浏览器解析URL,根据请求信息生成对应的HTTP报文
- 浏览器通过DNS协议,获取域名对应的IP地址
- 浏览器根据IP地址和端口号,向目标服务器发起一个TCP请求
- 浏览器在TCP连接上向服务器发送HTTP报文,请求获取网页内容
- 服务器收到请求后进行处理,返回HTTP相应报文给浏览器
- 浏览器收到HTTP相应报文后,解析 HTML代码渲染网页,同时根据HTML中的资源URL再次发起请求获取资源内容,直到网页完全展示
- 在结束通信后,可主动关闭TCP连接或等待服务器关闭请求
3.HTTP请求包含哪些内容
主要由4部分组成:
- 请求行(Request Line):包括请求方法(GET,POST等)、请求资源路径以及HTTP协议版本
- 请求头(Request Head):包含各种键值对,传递客户端环境、请求内容和认证信息
- 空行(Blank Line):用于分隔请求头与请求体
- 请求体(Request Body):仅在POST、PUT等方法中存在,包含需要发送到服务器的数据
4.HTTP与HTTPS的区别
-
数据传输安全性:
- HTTP是超文本传输协议,信息是用明文传输,存在安全风险
- HTTPS则通过SSL/TLS安全协议对数据进行加密后再传输,保障了数据机密性和完整性
-
性能差异:
- HTTP建立过程简单,只需要TCP三次握手后即可进行报文传输
- HTTPS在建立TCP连接后,还需进行SSL/TLS的握手过程才能进行数据加密传输,握手时间稍长,但现代硬件与协议优化后差距减小
-
申请CA:
HTTPS协议还需要向CA(证书权威机构)申请数字证书,才能保证服务器的身份是可靠的
-
默认端口号:
- HTTP:80
- HTTPS:443
-
SEO(搜索引擎优化)差异:
搜索引擎更倾向与优先展示HTTPS网站
5.HTTPS是如何解决HTTP的安全问题的(包括CA简介)
首先,HTTP协议由于是明文传输,故具有三种安全风险:
- 窃听风险
- 篡改风险
- 冒充风险
而HTTPS则通过混合加密、摘要算法和数字证书分别解决了这三个安全问题。
-
混合加密:
采用对称加密和非对称加密的方式来实现:
- 在通信建立前采用非对称加密的方式交换会话密钥,后续就不再使用非对称加密
- 在通信过程中全部使用对称加密的会话密钥加密明文数据
为什么使用混合加密?
- 对称加密只使用一个密钥,运算速度快,然而无法做到安全的密钥交换
- 非对称加密使用公钥和私钥,解决了密钥交换问题但是速度较慢
-
摘要算法
-
数字证书:
解决公钥被伪造的风险。通过数字证书认证机构,将服务器公钥放在数字证书中,只要证书是可信的公钥就是可信的。
6.TCP三次握手和四次挥手
如果只有两次,那么如果发送方第一次发送的请求由于某些原因被阻塞了,接收方就无法识别当前的请求是旧的请求还是新的请求
7.TCP拥塞控制如何实现
拥塞控制包括以下三个步骤:
- 慢启动
- 拥塞避免
- 快速重传与快速恢复
8.HTTP的请求方法有哪些,分别有什么用处
HTTP/1.0定义了以下三种基础请求方法:
- GET:请求指定的资源
- POST:提交数据以处理请求
- HEAD:请求资源的响应头信息
HTTP/1.2引入了更多方法:
- OPTIONS:获取服务器支持的请求方法
- PUT:上传文件或者更新资源
- PATCH:对资源进行部分修改。与PUT类似但只修改部分数据,而非替换整体
- DELETE:删除指定的资源
- TRACE: 回显服务器收到的请求,主要用于诊断
- CONNECT:建立一个隧道用于代理服务器的通信,通常用于 HTTPS
四.Linux,Git
1.常用的Linux操作
文件与目录
ls
cd
mkdir
rm
mv
cp
touch
文件查看与编辑
cat
more
less
vim
grep
网络编辑
netstat
ifconfig
/ip
权限修改
chmod
2.Git
五.Java相关
1.String, StringBuffer和StringBuilder的区别?
此三者都是Java中用来处理字符串的类,其区别体现在可变性、线程安全性和性能上。
- String
- **不可变性:**String是不可变类,字符串一旦创建就无法更改其内容。每次对String进行的操作(拼接、截取等)实际上都会创建一个新的String对象
- **线程安全:**由于它是不可变类,因此他是线程安全的。多个线程可以共享同一个String对象而不需要同步控制
- 适用场景:适用于字符串内容不需要频繁修改的场景,例如少量字符串、少量的字符串拼接等
- StringBuffer
- **可变性:**与String不同,StringBuffer是可变的。这意味着一旦创建一个StringBuffer对象后,其内容是可以被修改的
- **线程安全:**StringBuffer是线程安全的。它的内部采用了synchronized关键字来保证多线程环境下的安全性
- **适合场景:**适合在多线程环境下字符串需要频繁修改的场景,如构建大量字符串的应用程序
- StringBuilder
- **可变性:**与StringBuffer一样,Builder也是可变的,支持字符串的增删改操作
- **非线程安全性:**StringBuilder的方法不是同步的,因此它不是线程安全的。这也使得StringBuilder的性能比StringBuffer要好,适合单线程环境
- **适合场景:**适用于单线程环境下,字符串需要频繁修改的场景。由于没有同步开销,StringBuilder的性能通常比StringBuffer要好
拓展:1.什么是不可变类?
不可变类是指在创建后,其状态无法被修改的类。一旦对象被创建,它的所有属性就不能被更改。这种类的实例在整个生命周期内保持不变
关键特征:
- 声明类为final,防止父类继承
- 类的所有字段都是privat和final
- 通过构造函数初始化所有字段
- 不提供任何可变对象的引用
优点:
线程安全,缓存友好,防止状态不一致
缺点:
性能开销较大
常见不可变类有:String,Integer,BigDecimal、LocalDate等
2.什么是线程安全?
线程安全指的就是多线程在访问同一种共享资源时,能保证一致性和正确性。即无论线程如何交替执行,程序都能产生预期的结果,且不会出现数据竞争或内存冲突。
常见的线程安全措施有:
- 同步锁:通过Synchronized关键字或ReentrantLock实现对共享资源的同步控制
- **原子操作类:**利用Java提供的AtomicInteger、AtomicReference等确保多线程环境下的原子性操作
- 线程安全容器:ConcurrentHashMap、CopyOnWriteArrayList等,避免自己手动加锁
- 局部变量:线程内独立的局部变量是天然线程安全的,因为每个线程都有自己的栈空间
- ThreadLocal:类似局部变量,属于线程本地资源,通过线程隔离来保证线程安全
3.StringBuffer的底层是如何实现的?
为了解决String的对象不可变性,StringBuffer的底层提供了高效的字符串拼接和修改操作等。
大致实现如下:
- 内部使用字符数组(char[] value)来存储字符序列
- 通过append、insert等操作,直接修改内部的字符数组,而不是像String那样直接创建新的对象
- 每次对字符串进行操作时,若发现当前数组容量不足,就会进行2倍扩容,减少扩展次数以提高性能
由于StringBuilder底层使用char数组存放字符,而数组是连续内存的结构,为了防止频繁地复制和申请内存,需要设置合适地capacity参数来初始化数组大小。默认大小为16。
追问:String底层也是用char数组存放,两者的区别在哪?
String内部的char数组被private和final修饰,是不可变的,是典型的Immutable类,因此其不可变性保证了线程安全,能实现字符串常量池等;而StringBuffer没有用private和final进行修饰。
2.ArrayList和LinkedList的区别?
他们都是List集合的实现类。区别如下:
-
底层的数据结构不同:
- ArrayList:基于动态数组实现,元素在内存中是连续存储的
- LinkedList:基于双向链表实现,元素通过节点链接,内存中不需要连续存储
-
性能差异:
ArrayList:
- 随机访问速度快,查找元素的时间复杂度为O(1)
- 插入和删除操作慢,在中间插入或删除时时间复杂度为O(n)
LinkedList:
- 随机访问速度慢,查找元素时间复杂度O(n)
- 插入和删除操作快,复杂度为O(1)
拓展:Arraylist的扩容机制?
默认初始容量为10,当发生扩容时,Arraylist会创建一个新的数组,其容量为原数组1.5倍,然后将原数组中的元素通过copyOf方法复制到新数组中
3.多线程如何实现并发?(如何实现线程同步?)
通过线程同步的方式,确保在多线程环境下,同一时刻下只有同一线程可以执行共享资源来实现多线程的并发执行。线程同步通过对并发线程中的关键代码加锁,使得同一时刻只有一个线程能够访问共享资源。
Java中常见的线程同步方式有:
-
Synchronized
Java原生提供的加锁关键字,用于在方法上或代码块上加锁,确保同一时刻只有一个线程能够执行同步的方法或代码块。在Synchronized中可以使用wait、notify、notifyAll等操作,实现条件等待通知来控制线程并发。
-
ReenTrantLock
是JUC(java.util.concurrent包)提供的基于AQS实现的可重入锁,相比Synchronized更灵活。
它使用condition对象提供了更灵活的等待、通知机制。每个ReentrantLock都可以创建一个或多个Condition对象。
-
JUC其他工具类
Java还提供了一些高级的并发工具类,如Semaphore、CountDownLatch等,它们用于实现一些复杂的同步需求
4.Synchronized和ReentrantLock是什么?有什么区别?
Synchronized是Java中内置的关键字,是基于JVM层面实现的基本同步机制,不支持超时,非公平,不可中断,不支持多条件。
ReentrantLock是JUC提供的,基于AQS实现的可重入锁,由JDK1.5引入,支持设置超时时间,可以避免死锁,相比Synchronized比较灵活,且支持公平锁,可中断,支持多条件判断。在较为复杂的情况下用ReentrantLock
ReentrantLock需要手动解锁,而Synchronized不需要。他们都是可重入锁。
5.Java中的CAS、AQS是什么?
CAS:Compare And Swap
是一种硬件级别的原子操作,它比较内存中某个值是否为预期值,如果是,则更新为新值,否则就不修改
工作原理:
- 比较:检查内存中的某个值是否与预期值相等
- 交换:如果相等则将内存中的值改为新值
- 失败重试:如果不相等则说明有其他线程已经修改了该值,CAS操作失败,一般会进行重试知道成功
优点:
- 无锁并发:CAS操作不使用锁,因此不会导致线程阻塞,提高了系统的并发性能
- 原子性:CAS操作是原子性的,保证了线程安全
缺点:
-
ABA问题:CAS无法检测数值从A变为B再变为A的变化,可能造成错误(版本号或时间戳解决)
-
自旋开销:CAS通常通过自旋实现,在高并发情景下很有可能导致CPU资源浪费
-
单变量限制:仅适用于单变量的更新,不使用与多个变量的操作
AQS:AbstractQueuedSynchronizer
AQS就是一个用于构建锁和同步器的基础框架,它通过维护一个共享状态(state)和一个先进先出的等待队列管理多个线程的同步操作,AQS提供了以下核心功能:
- 资源状态管理
- 线程排队管理
- 提供模板方法
6.Java如何实现内存回收?
有三种算法:
- 标记-清除算法
- 标记-整理算法
- 复制算法
7.HashMap实现原理?
HashMap是基于哈希表的数据结构,用于存储键值对。其核心是将键的哈希值映射到数组索引位置,通过数组+链表的方式(Java8之后是数组+链表+红黑树)来处理哈希冲突。
HashMap默认容量为16,负载因子0.75。在元素数量超过12个时就会触发扩容操作,容量扩大至当前2倍并重新分配元素位置。
解决hash冲突的方法:
- 链地址法:将哈希表中的槽变为链表,当哈希值相同时存储在同一个链表中
- 开放寻址法:出现碰撞就寻找下一个可用位置
- 再哈希法:出现冲突时再次进行hash值的计算
8.接口与抽象类之间的关系?
-
相同点
- 两者都是抽象类,都不能实例化。
- interface实现类及abstrct class的子类都必须要实现已经声明的抽象方法。
-
不同点
- interface需要实现,要用implements,而abstract class需要继承,要用extends。
- 一个类可以实现多个interface,但一个类只能继承一个abstract class。
- interface强调特定功能的实现,而abstract class强调所属关系。
- interface是完全抽象的,只能声明方法,而且只能声明pulic的方法,不能声明private及protected的方法,不能定义方法体,也不能声明实例变量,但interface可以声明常量变量;abstract class在interface及Class中起到了承上启下的作用。一方面,abstract class是抽象的,可以声明抽象方法,以规范子类必须实现的功能;另一方面,它又可以定义缺省的方法体,供子类直接使用或覆盖。另外,它还可以定义自己的实例变量,以供子类通过继承来使用。
六、Spring、MyBatis相关
1.Spring的循环依赖是什么?如何解决?
解决方式:
通过三级缓存来解决循环依赖。
2.@Autowired和@Resource有什么区别?
@Autowired 和 @Resource 的区别主要体现在以下 5 点:
-
来源不同:
@Autowired 和 @Resource 来自不同的“父类”,其中 @Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解,它来自于 JSR-250
-
依赖查找的顺序不同:
@Autowired 先根据类型(byType)查找,如果存在多个(Bean)再根据名称(byName)进行查找;@Resource先根据名字查找,如果(根据名称)查找不到,再根据类型(byType)进行查找
-
支持的参数不同:
二者支持的参数以及参数的个数完全不同,其中 @Autowired 只支持设置一个 required 的参数,而 @Resource 支持 7 个参数
-
依赖注入的用法不同:
常见依赖注入有以下 3 种实现:
- 属性注入
- 构造方法注入
- Setter 注入
@Autowired 支持属性注入、构造方法注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入
-
编译器 IDEA 的提示不同
3.
4.
5.MyBatisPlus相比MyBatis的区别是什么?
- 强大的条件构造器,满足各类使用需求。
- 内置的Mapper和通用的Service,通过少量配置即可实现单表大部分CRUD操作。
- 支持Lambda形式调用。
- 提供了基本的CRUD功能,连SQL语句都不需要编写。
- 自动解析实体关系映射转换为MyBatis内部对象注入容器
其他
1.正则表达式
2.消息队列是什么,有什么用
消息队列是一种异步通信机制,用于在分布式系统中解耦发送方与接收方之间的通信。通过引入中间缓冲区,将消息存储在broker中,然后由消费者从broker中读取和处理消息
3.Cookie,Session与Token的区别、各自的使用场景
三者都是常用的Web登录授权方案,都有各自的特定应用场景及局限性。
其中Cookie和Session都是Web开发中用于跟踪用户状态的技术,但他们的存储位置、数据容量、安全性、生命周期 和 性能 等方面存在差异
Cookie:
概念:
- Cookie是服务器生成并存储在客户端的一小段数据。是对无状态协议(HTTP的补充机制),用于维持客户端与服务器之间的状态
- Cookie会随着每次HTTP请求一起发送到服务器,以便让服务器能够识别客户端的状态
主要用途:
- 会话管理:记录登录状态
- 个性化设置:存储用户偏好
- 跟踪与分析用户行为
特点:
- 存储位置:存储在客户端
- 数据容量:数据大小一般限制为4kb
- 安全性:安全性有限,容易被篡改和伪装
- 生命周期:可设置过期时间自动删除,也可设置为会话Cookie,关闭浏览器自动删除
- 性能:每次请求都会携带并传输Cookie数据,造成不必要的开销
**适用场景:**用于存储轻量级状态数据,如用户偏好、主题设置和非敏感的标识信息
Session:
概念:
- 是一种在服务器端的状态管理机制,每个用户会话都生成一个唯一的SessionID,主要用于跟踪用户在服务器上的状态信息,通过Cookie或URL参数传递给客户端。
- Session的实际数据存储在服务器端,客户端仅保存SessionID
主要用途:
- 用户身份验证:用于存储用户登录状态
- 敏感数据存储:如购物车数据和临时会话数据
- 会话隔离:通过SessionID区分不同的会话
- 实现跨页面访问:将数据保存在Session中,提供给用户不同在不同页面遍历
- 安全校验:防止非法登录到某个页面
特点:
- 存储位置:存储在服务器端。为每个用户分配唯一的SessionID,通常借助Cookie或URL重写的方式发送给客户端
- 数据容量:理论上不受数据大小限制,受限于服务器内存的大小
- 安全性:由于数据存储在服务器端,比Cookie更安 全
- 生命周期:默认情况下,关闭浏览器后Session就过期。服务器也可以设置Session的超时时间,超过时间未活动也会失效
- 性能:每次请求都需要查询服务器上的Session数据,可能会增加服务器的负载,特别是在高并发场景下
适用场景:适用于需要高安全性、依赖服务器存储的用户身份验证场景。适合会话管理、单点登录的场景
Token:
概念:
- Token本质上是一种加密的字符串,用于无状态的身份验证机制。包含用户的身份信息和权限声明,通常用加密技术确保安全。
- Token的核心特性是无状态,即服务器不用存储Token数据,而通过验证Token的有效性确定用户身份。
主要用途:
- 分布式认证
- 移动和单页应用:更适合无状态的客户端
- 第三方授权
特点:
- 无状态: Token 不存储在服务器端,客户端每次带上 Token,服务器通过验证 Token 的完整性来确定用户身份。可扩展性好.
- 安全性高: Token 通常是加密的,难以伪造。常用的 Token(如 JWT)还支持签名和过期时间。
- 跨平台: 可以在浏览器、移动端、桌面应用等不同平台使用。
- 数据自包含: Token 中可以携带用户信息,因此不需要额外的数据库查询。
- 生命周期: 通常设置较短的有效期(如 15 分钟),并配合刷新机制(Refresh Token)延长认证状态
适用场景:适用于分布式系统、跨域或跨平台的身份认证,特别是在移动端和单页应用中。适用于有效期短、一次性使用的场景
JWT(JSON Web Token)
通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。由3部分组成,中间用点隔开:
- 标头(Header)
- 有效载荷(Payload)
- 签名(Signature)
相比传统Session的优点:
- 简洁(Compact):可以通过URL,POST参数或者在HTTP header发送,数据量小,传输速度也很快;
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库;
- Token是以JSON加密的形式保存在客户端,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务
总结:
属性 | Cookie | Session | Token |
---|---|---|---|
存储位置 | 客户端(浏览器) | 服务器端 | 客户端(浏览器、移动设备) |
状态 | 无状态 | 有状态 | 无状态 |
安全性 | 较低,易被篡改和伪造 | 较高:数据存储在服务器端 | 较高:Token通常加密且支持签名和过期机制 |
生命周期 | 可配置,过期后失效 | 会话结束或超时失效 | 短期Access Token + 长期Refresh Token |
适用场景 | 轻量级状态存储 | 用户身份认证、临时会话数据存储 | 分布式认证、移动端和单页应用 |
适用协议 | 仅支持HTTP/HTTPS | HTTP/HTTPS | HTTP/WebSocket协议 |
性能开销 | 较低 | 较高(查询服务端数据库) | 较低(无服务端存储,仅验证Token) |
扩展性 | 较低 | 较低(借助分布式Session) | 较高(适用于分布式) |
跨域支持 | √ | × | √ |