5、Redis数据类型你用过哪些?底层的数据结构?实际场景使用哪些数据结构?
8、二叉树、BST二叉查找(搜索)树、平衡二叉查找树AVL?
14、设计模式学过吗?结合项目,你的项目用到了哪些设计模式?
18、解释double类型计算为什么得不到精确结果?修改代码,使得得到精确结果(BigDecimal数据类型、字符串存储)
1、自我介绍
2、项目经历
-
技术难题:回答了线程池+消息队列相关的
-
JDK自带的创建线程池的方式
1.newFixedThreadPool
这个线程池的特别是线程数是固定的
2.newSingleThreadExecutor
这个线程池看名字就知道是单例线程池,线程池中只有一个工作线程在处理任务
3.newCachedThreadPool
当第一次提交任务到线程池时,会直接构建一个工作线程,
这个工作线程带执行完任务后,60秒没有任务可以执行后,会结束,
如果在等待60秒期间有任务进来,他会再次拿到这个任务去执行,
如果后续提升任务时,没有线程是空闲的,那么就构建工作线程去执行。
4. newScheduleThreadPool
这个线程池就是可以以一定周期去执行一个任务,或者是延迟多久执行一个任务一次。
本质上还是正常线程池,只不过在原来的线程池基础上实现了定时任务的功能原理是基于DelayQueue实现的延迟执行。
5.newWorkStealingPool
newWorkStealingPool是基于ForkJoinPool构建出来的。每个线程都有阻塞队列。
ForkJoin第一个特点是可以将一个大任务拆分成多个小任务,放到当前线程的阻塞队列中。其他的空闲线程就可以去处理有任务的线程中阻塞队列中的任务。
-
线程池用的Spring的还是Java自带的?区别?
Spring官方文档中写的很清楚,SpringFrameWork 的 ThreadPoolTaskExecutor 是辅助 JDK 的 ThreadPoolExecutor 的工具类,它将属性通过 JavaBeans 的命名规则提供出来,方便进行配置。
Spring中的ThreadPoolTaskExecutor是借助于JDK并发包中的java.util.concurrent.ThreadPoolExecutor来实现的。
ThreadPoolExecutor构造函数:
public ThreadPoolExecutor(int corePoolSize, //线程池维护线程的最小数量 int maximumPoolSize, //线程池维护线程的最大数量 long keepAliveTime, //空闲线程的存活时间 TimeUnit unit, //时间单位,现有微秒,毫秒,秒 枚举值 BlockingQueue<Runnable> workQueue, //持有等待执行的任务队列 ThreadFactory threadFactory, //线程工厂 RejectedExecutionHandler handler//拒绝策略四种 ) { ...}
-
底层的核心线程数、队列线程数、最大线程数怎么设置的?
如何确定核心线程数?
在设置核心线程数之前,需要先考虑线程池执行任务的类型
IO密集型任务
一般来说:文件读写、DB读写、网络请求等
推荐:假设N为计算机的CPU核数,核心线程数大小设置为2N+1
优点:可以让在等待IO的时候,有其他线程去处理别的任务,充分利用CPU的时间。
CPU密集型任务
一般来说:计算型代码、Bitmap转换、Gson转换等
推荐:假设N为计算机的CPU核数,核心线程数大小设置为N+1
缺点:使得CPU的使用率很高,若线程数过多,造成CPU过度切换
在实际的项目中,都是通过测试的方法调试出符合当前任务情况的核心参数,充分利用硬件资源,同时也要考虑到一个服务器上有多个项目。可以实现一个controller接口,通过线程池的get和set方法修改核心参数,实现动态的监控。
参考回答:
① 高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
IO密集型的任务 --> (CPU核数 * 2 + 1)
计算密集型任务 --> ( CPU核数+1 )
③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,可以参考②
-
线程池怎么工作的?
ThreadPoolExecutor执行器的处理流程:
(1)当线程池大小小于corePoolSize就新建线程,并处理请求;
(2)当线程池大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去从workQueue中取任务并处理;
(3)当workQueue放不下新入的任务时,新建线程加入线程池,并处理请求,如果池子大小撑到了maximumPoolSize就用RejectedExecutionHandler来做拒绝处理.
(4)另外,当线程池的线程数大于corePoolSize的时候,多余的线程会等待keepAliveTime长的时间,如果无请求处理任务就自行销毁。
-
拒绝策略?
RejectedExecutionHandler handler: 用来拒绝一个任务的执行,有两种情况会发生这种情况。 一是在execute方法中若addIfUnderMaximumPoolSize(command)为false,即线程池已经饱和; 二是在execute方法中, 发现runState!=RUNNING || poolSize == 0,即已经shutdown,就调用ensureQueuedTaskHandled(Runnable command),在该方法中有可能调用reject。
Reject策略预定义有四种:
1.AbortPolicy:是默认的策略,当前拒绝策略会在无法处理任务时,直接抛出一个异常。
使用场景:你想要提示一些信息的时候
2.CallerRunsPolicy:当前拒绝策略会在线程池无法处理任务时,将任务交给调用者处理。
使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用
3.DiscardPolicy:当前拒绝策略会在线程池无法处理任务时,直接将任务丢弃掉。
使用场景:如果你提交的任务无关紧要,你就可以使用它 。
4.DiscardOldestPolicy:当前拒绝策略会在线程池无法处理任务时,将队列中头部最早的任务丢弃掉,将当前任务再次尝试交给线程池处理(如果再次失败,则重复此过程)。
使用场景:发布消息和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了
3、动态规划可以解决哪些问题?实际应用场景?
回答了买卖股票的最佳时机
动态规划思想就是:通过将问题分解为更小的子问题并存储子问题的解,动态规划可以避免重复计算,提高算法的效率。 实际应用场景:包括爬楼梯、背包问题、最短路径问题、排班问题等
动态规划算法最重要的就是去定义这个状态转移方程:
比如爬楼梯问题的递推公式:dp (n) = dp (n-1) + dp (n-2)
动态规划算法与分治算法相似,都是通过组合子问题的解来求解原问题的解。但是两者之间也有很大区别:分治法将问题划分为互不相交的子问题,递归的求解子问题,再将他们的解组合起来求解原问题的解;与之相反,动态规划应用于子问题相互重叠的情况,在这种情况下,分治法还是会做很多重复的不必要的工作,他会反复求解那些公共的子问题,而动态规划算法则对相同的每个子问题只会求解一次,将其结果保存起来,避免一些不必要的计算工作。
4、排序算法?时间复杂度?实际应用中使用过排序算法吗?
实际应用场景?
应用场景分析:
(1)若数据规模较小(如n <= 50),可以使用简单的直接插入排序或者直接选择排序(不稳定)。
(2)若文件的初始状态基本有序,排序关键字移动次数较少,则应选用直接插入或冒泡排序为宜;
(3)若文件初始状态随机分布,则应选用快速排序为宜,平均时间复杂度O(nlogn);
(4)若数据规模较大,则应采用时间复杂度为O(nlogn)的排序方法:快速排序、堆排序或归并排序;
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;虽然可能退化为O(N^2),但这种概率很小。 ps:堆排序每次取一个最大值和堆底部的数据交换,重新筛选堆,把堆顶的X调整到位,有很大可能是依旧调整到堆的底部(堆的底部X显然是比较小的数,才会在底部),然后再次和堆顶最大值交换,再调整下来,可以说堆排序做了许多无用功。堆排序过程里的交换跟快排过程里的交换虽然都是常量时间,但是常量时间差很多。
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。堆排序和快速排序都是不稳定的。
若要求排序稳定,则可选用归并排序(外部排序)。从单个记录起进行两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。推荐:先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
插入排序:如果大部分数据距离它正确的位置很近或者近乎有序?例如银行的业务完成的时间
简单选择排序:当数据量较小的时候适用
堆排序:适合于数据量非常大的场合(百万数据)。 堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。 堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。
归并排序:内存空间不足的时候,或者能够使用并行计算的时候使用归并排序。
计数排序需要占用大量空间,它仅适用于数据比较集中的情况。
5、Redis数据类型你用过哪些?底层的数据结构?实际场景使用哪些数据结构?
首先,在redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。
exists key
:判断键是否存在
del key
:删除键值对
move key db
:将键值对移动到指定数据库
expire key second
:设置键值对的过期时间
type key
:查看value的数据类型
TTL
:返回key的过期时间1. string 字符串
可以是字符串、整数或浮点数。可以实现对字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减的操作。 使用场景:
计数器、缓存对象、共享session信息、分布式锁等
2.List(列表)
实际上是一个链表,链表的上的每个结点都是字符串。可以实现对链表的两端进行push和pop操作,读取单个或多个元素;根据值查找或者删除元素。
使用场景:消息队列(Lpush Rpop),栈(Lpush Lpop),Redis 的 lpush + brpop 命令组合即可实现阻塞队列。
3.Set(集合)
Redis的Set是string类型的无序集合,集合中不能出现重复的数据。Redis 中 set集合是通过哈希表实现的。redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
使用场景:点赞、共同关注。 如:一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴趣,这些兴趣就是标签,有了这些数据就可以得到喜欢同一标签的人,以及用户的共同爱好的标签。
4.Hash(哈希)
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
使用场景:缓存:哈希结构相对于字符串序列化缓存信息更加直观,并且在更新操作上更加便捷。
5.zset(sorted set 有序集合)
有序集合,它保留了集合不能有重复成员的特性,但不同得是,有序集合中的元素是可以排序的,但是它和列表(list)的使用索引下标作为排序依据不同的是:它给每个元素设置一个double类型的分数(score),作为排序的依据。
使用场景:排序场景,排行榜。
三种特殊类型
Geospatial(地理位置)
存储地理位置信息的场景,比如滴滴叫车;
Hyperloglog(基数统计)
海量数据基数统计的场景,比如百万级网页 UV 计数等;
BitMaps(位图)
使用位存储,信息状态只有 0 和 1
Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。
应用场景:状态只有两个的场景,比如签到统计、判断用户登陆状态。
Stream(5.0 版新增)
消息队列,相比于基于 List 类型实现的消息队列,它会自动生成全局唯一消息ID,支持以消费组形式消费数据。
6、HashMap怎么解决哈希冲突的?
链表->红黑树
为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时,会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时,又会将红黑树转换回单向链表提高性能。
-
HashMap的扩容机制
数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算(据说提升了5~8倍)。
数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。
链表长度大于8时才会考虑升级成红黑树,是有一个条件是 HashMap 的 Node 数组长度大于等于64(不满足则会先进行一次扩容替代升级)。
扩容 resize( ) 时,红黑树拆分成的 树的结点数小于等于临界值6个,则退化成链表
-
HashMap为什么用红黑树而不用B树或者AVL树?
B/B+树多用于外存上时,B/B+也被称为一个磁盘友好的数据结构。比如数据库索引就是用B+树实现的
HashMap本来是数组+链表的形式,链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面,这个时候遍历效率就退化成了链表。
AVL树更加严格平衡,因此可以提供更快的査找效果。因此,对于查找密集型任务使用AVL树没毛病。 但是对于插入密集型任务,红黑树要好一些。
7、红黑树概念?
红黑树是一种自平衡的二叉查找树,是一种高效的查找树。当时被称为对称二叉 B 树。
红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。因此,红黑树在应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。
普通的二叉查找树在极端情况下可退化成链表,此时的增删查效率都会比较低下。
为了避免这种情况,就出现了一些自平衡的查找树,比如 AVL,红黑树等。这些自平衡的查找树通过定义一些性质,将任意节点的左右子树高度差控制在规定范围内,以达到平衡状态。
红黑树通过它的性质定义实现自平衡:
节点是红色或黑色。
根节点是黑色。
叶子节点都是黑色(叶子是NIL节点)。
每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
从任一节点到其每个叶子的所有路径,都包含相同数目的黑色节点(简称黑高)。
操作:
旋转操作:
旋转操作分为左旋和右旋,左旋是将某个节点旋转为其右孩子的左孩子,而右旋是节点旋转为其左孩子的右孩子。
插入:
红黑树的插入过程和二叉查找树插入过程基本类似,不同的地方在于,红黑树插入新节点后,需要进行调整(变色和旋转),以满足红黑树的性质。
性质1规定红黑树节点的颜色要么是红色要么是黑色,插入的新节点应该是红色。如果插入的节点是黑色,那么这个节点所在路径比其他路径多出一个黑色节点,这个调整起来会比较麻烦。
删除:
删除操作首先要确定待删除节点有几个孩子,如果有两个孩子,不能直接删除该节点。而是要先找到该节点的前驱(该节点左子树中最大的节点)或者后继(该节点右子树中最小的节点),然后将前驱或者后继的值复制到要删除的节点中,最后再将前驱或后继删除。
红黑树有点属于一种空间换时间类型的优化,在avl的节点上,增加了 颜色属性的 数据,相当于 增加了空间的消耗。 通过颜色属性的增加, 换取,后面平衡操作的次数 减少。
rbt 的 左子树和右子树的黑节点的层数是相等的
红黑树的平衡条件,不是以整体的高度来约束的,而是以黑色节点的高度来约束的。
所以称红黑树这种平衡为黑色完美平衡。
8、二叉树、BST二叉查找(搜索)树、平衡二叉查找树AVL?
二叉查找(搜索)树(BST)具备以下特性:
左子树上所有结点的值均小于或等于它的根结点的值。(左节点≤根节点)
右子树上所有结点的值均大于或等于它的根结点的值。(根节点≤右节点)
左、右子树也分别为二叉查找树。
二叉搜索树的查找流程:
如何查找值为7的节点? 1.查看根节点8,因为7<8,所以再查看它的左子节点6 2.查看左子节点6,因为7>6,所以再查看它的右子节点7 3.查看右子节点7,因为7=7,所以就找到啦,
缺点:在不断插入的时候,很容易“退化”成链表,(如果bst 树的节点正好从大到小的插入,此时树的结构也类似于链表结构,这时候的查询或写入耗时与链表相同O(n))。
为了避免这种特殊的情况发生,引入了平衡二叉树(AVL)和红黑树(red-black tree)。
平衡二叉树也叫AVL
(发明者名字简写),也属于二叉搜索树的一种,与其不同的是AVL通过机制保证其自身的平衡。
特性1: 对于任何一颗子树的root根结点而言,它的左子树任何节点的key一定比root小,而右子树任何节点的key 一定比root大;
特性2:对于AVL树而言,其中任何子树仍然是AVL树;
特性3:AVL带有平衡条件:每个节点的左右子节点的高度之差的绝对值最多为1;(也被称为高度平衡树)
4、增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。以满足上述规则。
右旋转(左旋转)
场景: 插入的元素在不平衡元素的左侧的左侧(右侧的右侧)
缺点:AVL树可以保证二叉树的平衡,它最坏情况的时间复杂度O(logn)。
由于AVL树必须保证左右子树平衡,Max(最大树高-最小树高) <= 1,
所以在插入的时候很容易出现不平衡的情况,导致AVL需要花大量时间进行旋转以求达到平衡,故AVL树一般使用场景在于查询场景, 而不是 增加删除 频繁的场景。
红黑树(rbt)做了什么优化呢?
红黑树通过牺牲严格的平衡,换取插入/删除时少量的旋转操作,
整体性能优于AVL
红黑树插入时的不平衡,不超过两次旋转就可以解决;删除时的不平衡,不超过三次旋转就能解决
红黑树的红黑规则,保证最坏的情况下,也能在O ( log n)时间内完成查找操作。
9、MySQL里面用到什么树?
B/B+树
MySQL的索引为什么用B+树?
参考答案
B+树由B树和索引顺序访问方法演化而来,它是为磁盘或其他直接存取辅助设备设计的一种平衡查找树,在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点,各叶子节点通过指针进行链接。
B+树索引在数据库中的一个特点就是高扇出性,例如在InnoDB存储引擎中,每个页的大小为16KB。在数据库中,B+树的高度一般都在2~4层,这意味着查找某一键值最多只需要2到4次IO操作,这还不错。因为现在一般的磁盘每秒至少可以做100次IO操作,2~4次的IO操作意味着查询时间只需0.02~0.04秒。
B+树叶子节点两两相连,能够大大增加区间访问性,可使用在范围查询等(磁盘预读原理)
10、B树/B+树定义
B树:
这里的 B 表示 balance( 平衡的意思),B树是一颗多叉自平衡查找树。它类似普通的平衡二叉树,不同的一点是B-树允许每个节点有更多的子节点。
B-树有如下特点:
定义任意非叶子结点最多只有m个儿子;且m>2;
每个节点都存有索引和数据,也就是对应的key和value。
每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
搜索有可能在非叶子结点结束(最好情况O(1)就能找到数据);
所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。
在关键字全集内做一次查找,性能逼近二分查找;
B+树
B+树是B树的变体,也是一种多路搜索树, 它与 B- 树的不同之处在于:
所有数据只存储在叶子节点,内部节点(非叶子节点并不存储真正的 data)
内部结点和叶子结点中的key也都按照从小到大的顺序排列
每个叶子结点都存有相邻叶子结点的指针
通常在B+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
插入操作:
当节点元素数量大于m-1的时候,按中间元素分裂成左右两部分,中间元素分裂到父节点当做索引存储,但是,本身中间元素还是分裂右边这一部分的。
删除操作:
因为叶子节点有指针的存在,向兄弟节点借元素时,不需要通过父节点了,而是可以直接通过兄弟节点移动即可(前提是兄弟节点的元素大于m/2),然后更新父节点的索引;如果兄弟节点的元素不大于m/2(兄弟节点也没有多余的元素),则将当前节点和兄弟节点合并,并且删除父节点中的key。
因为内节点并不存储 data,所以一般B+树的叶节点和内节点大小不同,而B-树的每个节点大小一般是相同的,为一页。
B-树和B+树的区别
1、B+树内节点不存储数据,所有 data 存储在叶子节点,使得查询时间复杂度固定为O(logn)。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。
2、B+树叶子节点两两相连,能够大大增加区间访问性,可使用在范围查询等(磁盘预读原理),而B-树每个节点 key 和 data 在一起,则无法区间查找。
3、B+树更适合外部存储。由于内节点无 data 域,每个节点能索引的范围更大更精确。
11、MySQL里面的索引有哪些?
索引的理解
索引是一个单独存储在磁盘上的数据库结构,包含着对数据表里所有记录的引用指针。
所有MySQL列类型都可以被索引,对相关列使用索引是提高查询操作速度的最佳途径。
MySQL中索引的存储类型有两种,即BTREE和HASH,具体和表的存储引擎相关。MyISAM和InnoDB存储引擎只支持BTREE索引;
索引的优点主要有4条:
通过创建唯一索引,可以保证数据库表中每一行数据的唯一性。
可以大大加快数据的查询速度,这也是创建索引的主要原因。
在实现数据的参考完整性方面,可以加速表和表之间的连接。
在使用分组和排序子句进行数据查询时,也可以显著减少查询中分组和排序的时间。
增加索引也有许多不利的方面,主要表现在:
创建索引和维护索引要耗费时间,并且随着数据量的增加所耗费的时间也会增加。
索引需要占磁盘空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果有大量的索引,索引文件可能比数据文件更快达到最大文件尺寸。
当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
索引有哪几种?
普通索引和唯一索引
普通索引是MySQL中的基本索引类型,允许在定义索引的列中插入重复值和空值。
唯一索引要求索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
主键索引是一种特殊的唯一索引,不允许有空值。
单列索引和组合索引(联合索引)
单列索引即一个索引只包含单个列,一个表可以有多个单列索引。
组合索引是指在表的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用。使用组合索引时遵循最左前缀集合(联合索引本质还是一棵B+树)。
全文索引
全文索引类型为FULLTEXT,在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引可以在CHAR、VARCHAR或者TEXT类型的列上创建。
空间索引*(不问不说)
空间索引是对空间数据类型的字段建立的索引,MySQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING和POLYGON。MySQL使用SPATIAL关键字进行扩展,使得能够用创建正规索引类似的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MyISAM的表中创建。
12、MySQL一条查询语句的执行过程?
大致分为几个步骤
获取连接:使用MySQL连接器连接到数据库。
查询缓存:key为SQL语句,value为查询结果,如果查到就直接返回。但是不建议使用此缓存,在MySQL 8.0版本中已经将查询缓存删除掉了
分析器:将SQL语句进行分词和语法分析,判断语法是否正确,如果语法不正确,会在这个阶段发现并报错。
优化器:对SQL语句进行优化,选择最优的执行计划。执行计划是指在表中查询数据的方法,包括使用哪些索引、连接表的顺序等。优化器会根据表中的索引、表大小、统计信息等因素,选择最优的执行计划。
执行器:根据优化器选择的执行计划执行SQL语句。执行器负责打开表,根据表的引擎定义,使用引擎提供的接口获取数据,进行筛选和计算,最终返回查询结果。
13、SQL优化?
MySQL数据库优化原则是减少系统的瓶颈,减少资源的占用,增加系统的反应速度。
例如,
通过优化文件系统,提高磁盘I\O的读写速度;
通过优化操作系统调度策略,提高MySQL在高负荷情况下的负载能力;
优化表结构、索引、查询语句等使查询响应更快。
针对查询,我们可以通过使用索引、使用连接代替子查询的方式来提高查询速度。
针对慢查询,我们可以通过分析慢查询日志,来发现引起慢查询的原因,从而有针对性的进行优化。
针对插入,我们可以通过禁用索引、禁用检查等方式来提高插入速度,在插入之后再启用索引和检查。
针对数据库结构,我们可以通过将字段很多的表拆分成多张表、增加中间表、增加冗余字段等方式进行优化。
具体措施:
1、查询语句中不要使用
SELECT *
。2、尽量减少子查询,使用关联查询JOIN(内、左外、右外连接)替代;
3、减少使用
IN
或NOT IN
,使用EXISTS
、NOT EXISTS
或关联查询替代;4、
OR
的查询尽量使用UNION
和UNION ALL
代替(在确认没有重复数据或不用去重数据时,UNION ALL会更好);5、尽量避免在
WHERE
子句中使用!=
、<>
操作符,否则引擎将放弃使用索引而进行全表扫描,可以使用=
操作符或NOT
操作符来替代;6、尽量避免在
WHERE
子句中对字段进行NULL值判断,否则将导致引擎放弃使用索引而进行全表扫描。
explain理解?原理?
MySQL中提供了EXPLAIN语句(和DESCRIBE语句,用来分析查询语句,EXPLAIN语句的基本语法如下:
EXPLAIN [EXTENDED] SELECT select_options使用EXTENED关键字,EXPLAIN语句将产生附加信息。
执行该语句,可以分析EXPLAIN后面SELECT语句的执行情况,并且能够分析出所查询表的一些特征。对查询结果进行参数解释:(重点要关注5列:)
id:SELECT识别符。这是SELECT的查询序列号。
select_type:表示SELECT语句的类型。
table:表示查询的表。
type:表示表的连接类型。
possible_keys:给出了MySQL在搜索数据记录时可选用的各个索引。
key:是MySQL实际选用的索引。
key_len:给出索引按字节计算的长度,key_len数值越小,表示越快。
ref:给出了关联关系中另一个数据表里的数据列名。
rows:是MySQL在执行这个查询时预计会从这个数据表里读出的数据行的个数。
Extra:提供了与关联操作有关的信息。
DESCRIBE语句的使用方法与EXPLAIN语句是一样的,分析结果也是一样的,并且可以缩写成DESC。
DESCRIBE语句的语法形式如下:
DESCRIBE SELECT select_options
14、设计模式学过吗?结合项目,你的项目用到了哪些设计模式?
回答了单例模式和工厂模式
15、有哪些单例模式?
饿汉式:
public class Singleton {
private static Singleton instance = new Singleton();
// 私有构造方法,保证外界无法直接实例化。
private Singleton() {}
// 通过公有的静态方法获取对象实例
public static Singleton getInstace() {
return instance;
}
}
懒汉式:
public class Singleton {
private static Singleton instance = null;
// 私有构造方法,保证外界无法直接实例化。
private Singleton() {}
// 通过公有的静态方法获取对象实例
public static Singleton getInstace() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
线程安全的(在懒汉式基础上实现线程同步)
public class Singleton {
private static Singleton instance = null;
// 私有构造方法,保证外界无法直接实例化。
private Singleton() {}
// 通过公有的静态方法获取对象实例
synchronized public static Singleton getInstace() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
16、场景题:Redis缓存三个问题
新闻阅读应用,该应用的新闻数据存储在MySQL数据库中,并且每一篇新闻有一个唯一ID作为缓存的Key,在应用中使用Redis来作为缓存数据库,以提高应用的读取速度,考虑下面问题:
恶意攻击者通过访问不存在的新闻ID来进行攻击,导致缓存穿透的问题,进而对MySQL数据库造成大量无效的查询请求。
同时,由于某些新闻的阅读量过大,一旦Redis缓存中的某个Key过期,将会导致大量请求直接打到MySQL数据库上,引发缓存雪崩问题。
补充:缓存击穿:(量太大,缓存过期)
这种情况下如何处理?
-
缓存穿透
缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据,则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。
解决方案的话,我们通常都会用布隆过滤器来解决它
-
布隆过滤器
布隆过滤器主要是用于检索一个元素是否在一个集合中。我当时使用的是redisson实现的布隆过滤器。
它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标,然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。
当然这个也是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划算了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
-
缓存雪崩
缓存雪崩意思是设置缓存时采用了相同的过期时间,导致大量缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key同时失效,击穿是某一个key缓存失效。
解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
-
缓存击穿
缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案的话,我了解的有两种:
第一个是可以使用互斥锁:当缓存失效时,不立即去加载数据库,先使用如 Redis 的 setnx 去设置一个互斥锁,只有一个请求能够成功设置互斥锁,其他请求会在这一步被阻塞;
当成功设置互斥锁的请求返回时,再进行加载数据库的操作,并将加载的数据回设到缓存中,否则重试get缓存的方法。
第二种方案是可以设置当前key逻辑过期:
①:在设置缓存key的时候,额外存储一个过期时间字段到缓存中,但是不给当前key设置过期时间;
②:当查询请求到达时,首先从缓存中取出数据,并且额外判断一下存储key的过期时间字段,若过期则认为缓存失效;
③:当缓存失效时,开启另外一个线程进行数据的异步加载和缓存更新,当前请求直接返回缓存中的旧数据,但这部分数据可能不是最新的;
这种方案一定程度上保证了高可用性,避免了大量请求直接打到数据库。
当然两种方案各有利弊:
方案一使用了互斥锁,保证了数据的强一致性,但是性能可能会受到锁的竞争影响,而且需要考虑死锁的问题;
方案二优先考虑的是高可用和性能,但不能保证强一致性,有可能会出现缓存和数据库数据不一致的情况。
实际使用中根据我们的需求来选择要保证一致性还是可用性。
17、Redis数据持久化
在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF
RDB 和 AOF 区别:
RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。
RDB 和 AOF ,哪种恢复的比较快呢?
RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,所以通常在项目中会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我当时设置的就是每秒批量写入一次命令。
18、解释double类型计算为什么得不到精确结果?修改代码,使得得到精确结果(BigDecimal数据类型、字符串存储)
原因:
因为计算机只能识别0或1,在计算机中,存储浮点数由两部分组成:指数和尾数。
在Java中,浮点数不精确,是因为计算机内部无法用二进制的小数来精确的表达。
十进制小数到二进制小数一般是整数部分除 2 取余,逆序排列
,小数部分使用乘 2 取整数位,顺序排列
。可能会无限循环,有舍弃,所以就精度不准了。
解决方案:
使用Java提供的BigDecimal数据类型。
BigDecimal类位于java.math包下,用于对超过16位有效位的数进行精确的运算。
一般来说,double类型的变量可以处理16位有效数,但实际应用中,如果超过16位,就需要BigDecimal类来操作。
new BigDecimal(double val)
该构造方法是不可预测的,以0.1为例,你传了一个double类型的0.1,由于0.1无法用有限长度的二进制数表示,最后返回值不一定为0.1。还是无法解决精度问题。
new BigDecimal(String val):用字符串传值
该构造方法是完全可预测的,也就是说你传入一个字符串"0.1",他就会给你返回一个值完全为0.1的BigDecimal,官方也建议使用这个构造函数。
BigDecimal.valueOf(double val) 第二种构造方式已经足够优秀,假如还是想传入一个double值?
可以使用Double.toString(double val)先将double值转为String,
再调用第二种构造方式,你可以直接使用静态方法:valueOf(double val)。
总结:将double转为BigDecimal的时候,需要先把double转换为字符串,然后再作为BigDecimal(String val)构造函数的参数,这样才能避免出现精度问题。
如果精度要求很高的话,使用字符串类型存储。