恒生一面 10.17 20min 已挂
自我介绍
1.hashmap是怎么实现的?
反问了是底层原理吗
- 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
- 根据键值key计算hash值得到数组索引这个哈希方法首先计算出key的hashCode值,然后通过这个hash值右移16位后的二进制进行按位异或运算得到最后的hash值。在putValue的方法中,计算数组下标的时候使用hash值与数组长度取模得到存储数据下标的位置,hashmap为了性能更好,并没有直接采用取模的方式,而是使用了数组长度-1 得到一个值,用这个值按位与运算hash值,最终得到数组的位置。
- 判断table[i]==null,条件成立,直接新建节点添加
- 如果table[i]==null ,不成立4.1 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value4.2 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对4.3 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value
- 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。
2.hashmap是线程安全的吗
currenthashmap是如何实现的
ConcurrentHashMap 1.7
- 数据结构:
- 提供了一个segment数组,在初始化ConcurrentHashMap 的时候可以指定数组的长度,默认是16,一旦初始化之后中间不可扩容
- 在每个segment中都可以挂一个HashEntry数组,数组里面可以存储具体的元素,HashEntry数组是可以扩容的
- 在HashEntry存储的数组中存储的元素,如果发生冲突,则可以挂单向链表
存储流程
- 先去计算key的hash值,然后确定segment数组下标
- 再通过hash值确定hashEntry数组中的下标存储数据
- 在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁是被会使用cas自旋锁进行尝试
- 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了
- 索引计算
- 假设大数组长度是2^m ,key 在大数组内的索引是 key 的二次 hash 值的高 m 位
- 假设小数组长度是 2^n,key 在小数组内的索引是 key 的二次 hash 值的低 n 位
- 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
- Segment[0] 原型:首次创建其它小数组时,会以此原型为依据,数组长度,扩容因子都会以原型为准
小数组容量为 容量/并发度
16 0.75
ConcurrentHashMap 1.8
- 在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表采用 CAS + Synchronized来保证并发安全进行实现
- CAS控制数组节点的添加
- synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升
- 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容
- 扩容条件:Node 数组满 3/4 时就会扩容
- 扩容单位:以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode
- 扩容时并发 get
- 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞
- 如果链表长度超过 1,则需要对节点进行复制(创建新节点),怕的是节点迁移后 next 指针改变
- 如果链表最后几个元素扩容后索引不变,则节点无需复制
- 扩容时并发 put
- 如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞
- 如果 put 的线程操作的链表还未迁移完成,即头节点不是 ForwardingNode,则可以并发执行
- 如果 put 的线程操作的链表已经迁移完成,即头结点是 ForwardingNode,则可以协助扩容
- 与 1.7 相比是懒惰初始化
- capacity 代表预估的元素个数,capacity / factory 来计算出初始数组大小,需要贴近
- loadFactor 只在计算初始数组大小时被使用,之后扩容固定为 3/4
- 超过树化阈值时的扩容问题,如果容量已经是 64,直接树化,否则在原来容量基础上做 3 轮扩容
3.一条sql语句如何优化
1.慢SQL优化思路。
- 慢查询日志记录慢SQL
- explain分析SQL的执行计划
- profile 分析执行耗时
- Optimizer Trace分析详情
- 确定问题并采用相应的措施
1.1 慢查询日志记录慢SQL
如何定位慢SQL呢、我们可以通过慢查询日志来查看慢SQL。默认的情况下呢,MySQL数据库时不开启慢查询日志(slow query log)呢。所以我们需要手动把它打开。
查看下慢查询日志配置,我们可以使用show variables like 'slow_query_log%'命令,如下:
- slow query log表示慢查询开启的状态
- slow_query_log_file表示慢查询日志存放的位置
我们还可以使用show variables like 'long_query_time'命令,查看超过多少时间,才记录到慢查询日志,如下:
- long_query_time表示查询超过多少秒才记录到慢查询日志。
我们可以通过慢查日志,定位那些执行效率较低的SQL语句,重点关注分析。
1.2 explain查看分析SQL的执行计划
当定位出查询效率低的SQL后,可以使用explain查看SQL的执行计划。
当explain与SQL一起使用时,MySQL将显示来自优化器的有关语句执行计划的信息。即MySQL解释了它将如何处理该语句,包括有关如何连接表以及以何种顺序连接表等信息。
一条简单SQL,使用了explain的效果如下:
一般来说,我们需要重点关注type、rows、filtered、extra、key。
1.2.1 type
type表示连接类型,查看索引执行情况的一个重要指标。以下性能从好到坏依次:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
- system:这种类型要求数据库表中只有一条数据,是const类型的一个特例,一般情况下是不会出现的。
- const:通过一次索引就能找到数据,一般用于主键或唯一索引作为条件,这类扫描效率极高,,速度非常快。
- eq_ref:常用于主键或唯一索引扫描,一般指使用主键的关联查询
- ref : 常用于非主键和唯一索引扫描。
- ref_or_null:这种连接类型类似于ref,区别在于MySQL会额外搜索包含NULL值的行
- index_merge:使用了索引合并优化方法,查询使用了两个以上的索引。
- unique_subquery:类似于eq_ref,条件用了in子查询
- index_subquery:区别于unique_subquery,用于非唯一索引,可以返回重复值。
- range:常用于范围查询,比如:between ... and 或 In 等操作
- index:全索引扫描
- ALL:全表扫描
1.2.2 rows
该列表示MySQL估算要找到我们所需的记录,需要读取的行数。对于InnoDB表,此数字是估计值,并非一定是个准确值。
1.2.3 filtered
该列是一个百分比的值,表里符合条件的记录数的百分比。简单点说,这个字段表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。
1.2.4 extra
该字段包含有关MySQL如何解析查询的其他信息,它一般会出现这几个值:
- Using filesort:表示按文件排序,一般是在指定的排序和索引排序不一致的情况才会出现。一般见于order by语句
- Using index :表示是否用了覆盖索引。
- Using temporary: 表示是否使用了临时表,性能特别差,需要重点优化。一般多见于group by语句,或者union语句。
- Using where : 表示使用了where条件过滤.
- Using index condition:MySQL5.6之后新增的索引下推。在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。
1.2.5 key
该列表示实际用到的索引。一般配合possible_keys列一起看。
注意:有时候,explain配合show WARNINGS; (可以查看优化后,最终执行的sql),效果更佳哦。
1.3 profile 分析执行耗时
explain只是看到SQL的预估执行计划,如果要了解SQL真正的执行线程状态及消耗的时间,需要使用profiling。开启profiling参数后,后续执行的SQL语句都会记录其资源开销,包括IO,上下文切换,CPU,内存等等,我们可以根据这些开销进一步分析当前慢SQL的瓶颈再进一步进行优化。
profiling默认是关闭,我们可以使用show variables like '%profil%'查看是否开启,如下:
可以使用set profiling=ON开启。开启后,可以运行几条SQL,然后使用show profiles查看一下。
show profiles会显示最近发给服务器的多条语句,条数由变量profiling_history_size定义,默认是15。如果我们需要看单独某条SQL的分析,可以show profile查看最近一条SQL的分析。也可以使用show profile for query id(其中id就是show profiles中的QUERY_ID)查看具体一条的SQL语句分析。
除了查看profile ,还可以查看cpu和io,如上图。
1.4 Optimizer Trace分析详情
profile只能查看到SQL的执行耗时,但是无法看到SQL真正执行的过程信息,即不知道MySQL优化器是如何选择执行计划。这时候,我们可以使用Optimizer Trace,它可以跟踪执行语句的解析优化执行的全过程。
我们可以使用set optimizer_trace="enabled=on"打开开关,接着执行要跟踪的SQL,最后执行select * from information_schema.optimizer_trace跟踪,如下:
大家可以查看分析其执行树,会包括三个阶段:
- join_preparation:准备阶段
- join_optimization:分析阶段
- join_execution:执行阶段
1.5 确定问题并采用相应的措施
最后确认问题,就采取对应的措施。
- 多数慢SQL都跟索引有关,比如不加索引,索引不生效、不合理等,这时候,我们可以优化索引。
- 我们还可以优化SQL语句,比如一些in元素过多问题(分批),深分页问题(基于上一次数据过滤等),进行时间分段查询
- SQl没办法很好优化,可以改用ES的方式,或者数仓。
- 如果单表数据量过大导致慢查询,则可以考虑分库分表
- 如果数据库在刷脏页导致慢查询,考虑是否可以优化一些参数,跟DBA讨论优化方案
- 如果存量数据量太大,考虑是否可以让部分数据归档
我之前写了一篇文章,有关于导致慢查询的12个原因,大家看一看一下哈:盘点MySQL慢查询的12个原因
作者:捡田螺的小男孩链接:https://juejin.cn/post/7151548964964139021来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1.避免使用select*
SELECT*会消耗更多的CPU。
SELECT*无用字段增加网络带宽资源消耗,增加数据传输时间,尤其是大字段(如varchar.blob、text)。
SELECT*无法使用MySQL优化器覆盖索引的优化(基于MySQL优化器的"覆盖索引"策略又是速度极快,效率极高,业界极为推荐的查询优化方式)
SELECT<字段列表>可减少表结构变更带来的影响。
2.分页优化
3.避免使用多表join
根据阿里巴巴开发者手册的规定,join表的数量不应该超过3个。
4.建议不要使用外键与级联(对分库分表不友好)
5.选择合适的字段
a.将IP地址转换为整型数据
b.对于非负型的数据(如直增ID,整型IP,年龄)来说,优先使用无符号整型来存储
c.小数值类型(比如年龄、状态表示如0/1)优先使用TINYINT类型
d.对于日期类型来说,DateTime类型耗费空间更大且没有时区信息,建议使用Timestamp.
e.金额字段用decimal,避免精度丢失
f.尽量使用自增id作为主键
6.尽量用UNION ALL代替UNION
7.批量操作
8.Show Profile分析SQL执行性能
9.优化慢SQL
10.正确使用索引
a.选择合适的字段创建索引
1.不为NULL的字段
2.被频繁查询的字段
3.被作为条件查询的字段
4.频繁需要排序的字段
5.被经常频繁用于连接的字段
b.被频繁更新的字段应该慎重建立索引
c.尽可能的考虑建立联合索引而不是单例索引
d.注意避免冗余索引
e.考虑在字符串类型的字段上使用前缀索引代替普通索引
f.避免索引失效
g.删除长期未使用的索引
4.mysql主从复制是如何实现的
5.null 设置为1 有值设置为原值?怎么写
6.如何增加一个主键?(创建一个主键?)
CREATE TABLE table_name ( column1 datatype PRIMARY KEY, column2 datatype, ... );
ALTER TABLE table_name ADD PRIMARY KEY (column1);
7.linux修改权限的命令
chmod 777
8.快排如何实现的,时间复杂度?
9.dijkstra的思路
10.死锁是如何产生的?
如何解决
1. 避免死锁:通过合理的设计和编程,避免产生死锁的情况。可以使用以下策略来避免死锁:
○ 避免使用多个锁:尽量减少对多个资源的同时访问,从而减少死锁的可能性。
○ 按顺序获取锁:确保线程按照相同的顺序获取锁,避免出现循环依赖的情况。
○ 设置超时时间:在获取锁时设置超时时间,如果在一定时间内无法获取到锁,就放弃或重试。
2. 检测和恢复死锁:通过监控和检测死锁的发生,然后采取相应的措施来恢复。常见的方法有:
○ 使用死锁检测算法:如银行家算法、资源分配图算法等,来检测死锁的发生。
○ 强制释放资源:当检测到死锁时,可以选择强制终止某个线程,释放其占用的资源,从而恢复正常执行。
3. 预防死锁:通过合理的资源分配和管理,预防死锁的发生。可以采取以下策略:
○ 资源有序分配:按照固定的顺序分配资源,避免出现循环依赖。
○ 避免资源抢占:线程在获取资源时,如果无法获取到,就释放已经持有的资源,等待重新获取所需的资源。
4. 解除死锁:当发生死锁时,可以通过解除死锁来恢复正常执行。常见的方法有:
○ 重新启动程序:将产生死锁的程序重启,重新分配资源,从而解除死锁。
○ 强制终止线程:终止其中一个或多个线程,释放其占用的资源,从而解除死锁。反问