基础问题
HashMap的hash算法和寻址算法的优化
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
原hash值与右移后的hash值进行异或运算(一样就是0,不一样就是1)
原hash值 1111 1111 1111 1111 1111 1010 0111 1100
右移的值 0000 0000 0000 0000 1111 1111 1111 1111
异或后的值1111 1111 1111 1111 0000 0101 1000 0011 转换成int值返回
重点:从异或运算的结果看:hash值高位值不变,低位值是原低位值与原高位置的异或后的结果。
寻址优化
找到key所处的数组索引并不是通过取模来计算的 而是通过(n - 1) & hash
来计算的(这是一个数学问题,类似于定理,就是当n是2的指数方那么(n - 1) & hash
和取模运算的效果是一样的,但是前者效率要高很多),这也是为什么HashMap
的长度一定是2的n次方的原因。
然后我们再来看(n - 1) & hash
这个确定key在数组的位置的算法(寻址算法)。n是HashMap的长度,我们想一想n转换为2进制的结果基本上是高位值全为0,很少有长度大于2的16次方的长度 对不对?这带来的结果就是 ,在寻址算法中高位值基本相当于不参与运算。如果不使用上面低位 = 原高位异或原低位的运算的话,那么带来的结果是有可能大量的key所定位到的数组位置是一样的,使得数据在HashMap中的分布不均匀。而经过异或运算后的低位值就相当于同时集成了原高位置和原低位值的属性 换句话说就是高位值和低位值都参与了运算
1111 1111 1111 1111 0000 0101 1000 0011(经过优化和二进制位运算的新的hash值)
0000 0000 0000 0000 0000 0000 0000 1111 (n-1的2进制值,其高位值大多为0)
HashMap的hash冲突
两个key,多个key,他们算出来的hash的值,与n-1,与运算之后,发现定位出来的数组的位置还是一样的。这就是hash碰撞也叫hash冲突。
因为有上面的hash冲突,所以数组的每一个元素是链表或者红黑树。为什么要引入红黑树?因为当链表长度太长的时候 遍历的效率会变低O(n),红黑树O(logn)要高一些
HashMap扩容的时候的rehash
HashMap底层是一个数组,当这个数组满了之后,他就会自动进行扩容,变成一个更大的数组,让你在里面可以去放更多的元素,下面演示HashMap从16扩容到32的一个例子。
n - 1 0000 0000 0000 0000 0000 0000 0000 1111
hash1 1111 1111 1111 1111 0000 1111 0000 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)
n - 1 0000 0000 0000 0000 0000 0000 0000 1111
hash2 1111 1111 1111 1111 0000 1111 0001 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)
在数组长度为16的时候,他们两个hash值的位置是一样的,用链表来处理,出现一个hash冲突的问题,如果数组的长度扩容之后 = 32,重新对每个hash值进行寻址,也就是用每个hash值跟新数组的length - 1进行与操作
n-1 0000 0000 0000 0000 0000 0000 0001 1111
hash1 1111 1111 1111 1111 0000 1111 0000 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)
n-1 0000 0000 0000 0000 0000 0000 0001 1111
hash2 1111 1111 1111 1111 0000 1111 0001 0101
&结果 0000 0000 0000 0000 0000 0000 0001 0101 = 21(index = 21的位置)
判断二进制结果中是否多出一个bit的1,如果没多,那么就是原来的index,如果多了出来,那么就是index + oldCap(21 = 5 + 16),通过这个方式,就避免了rehash的时候,用每个hash对新数组.length取模,取模性能不高,位运算的性能比较高
ConcurrentHashMap线程安全原理
JDK 1.7时 ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样
JDK 1.8以后,优化细粒度 采用了 CAS + synchronized 来保证并发安全性,数组里每个元素进行put操作,都是有一个不同的锁,刚开始进行put的时候,如果两个线程都是在数组[5]这个位置进行put,这个时候,对数组[5]这个位置进行put的时候,采取的是CAS的策略,如果你是对数组里同一个位置的元素进行操作,才会加锁串行化处理;如果是对数组不同位置的元素操作,此时大家可以并发执行的
AQS的实现原理是什么
AQS,Abstract Queue Synchronizer,抽象队列同步器,其实就是CAS加锁的时候,如果加锁失败,就将该线程放到等待队列里面去,锁的公平与非公平也就在于这个队列,公平的意思就是新线程如果队列不为空 才去尝试CAS,公平的意思就是新线程过来,二话不说 直接先去尝试CAS,如果CAS失败 再去队列里排队
image.png
Java内存模型的理解
image.png对象是在堆内存中创建的,包括实例变量
happens-before原则
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作,比如说在代码里有先对一个lock.lock(),lock.unlock(),lock.lock()
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作,volatile变量写,再是读,必须保证是先写,再读
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作,thread.start(),thread.interrupt()
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
这些规则写的非常的拗口,晦涩难懂,在面试的时候比如面试官问你,happens-before原则,你必须把8条规则都背出来,反问,没有任何一个人可以随意把这个规则背出来的
volatile底层是如何基于内存屏障保证可见性和有序性的?
底层使用的是大量的内存屏障来实现的。具体说不清楚
Spring的 IOC 机制的初步理解
image.png比如在我们的一个tomcat+servlet的这样的一个很low的系统里,有几十个地方,都是直接用MyService myService = new MyServiceImpl(),直接创建、引用和依赖了一个MyServiceImpl这样的一个类的对象。
假设现在不想使用MyServiceImpl,我们想用 NewServiceManagerImpl implements MyService,所有的实现逻辑都不同了,此时我们很麻烦,我们需要在很low的系统里,几十个地方,都去修改对应的MyServiceImpl这个类,切换为NewServiceManagerImpl这个类,改动代码成本很大,改动完以后的测试的成本很大,改动的过程中可能很复杂,出现一些bug,此时就会很痛苦,归根结底,代码里,各种类之间完全耦合在一起,出现任何一丁点的变动,都需要改动大量的代码,重新测试,可能还会有bug,
使用Spring之后,将对象的创建权交给Spring容器,实现组件解耦
说说你对Spring的AOP机制的理解可以吗?
以事务为例,就是反射动态生成代理对象,然后在调用业务代码前后加上统一的事务处理机制(执行前开启事务,执行后如果正常则提交事务,异常就回滚事务)
cglib动态代理与jdk动态代理
优先是jdk动态代理,其次是cglib动态代理,网上搜一下两种动态代理的代码示例
其实就是动态的创建一个代理类出来,创建这个代理类的实例对象,在这个里面引用你真正自己写的类,所有的方法的调用,都是先走代理类的对象,他负责做一些代码上的增强,再去调用你写的那个类。
如果你的类是实现了某个接口的,spring aop会使用jdk动态代理,生成一个跟你实现同样接口的一个代理类,构造一个实例对象出来,jdk动态代理,他其实是在你的类有接口的时候,就会来使用。
很多时候我们可能某个类是没有实现接口的,spring aop会改用cglib来生成动态代理,他是生成你的类的一个子类,他可以动态生成字节码,覆盖你的一些方法,在方法里加入增强的代码。
Spring中的Bean是线程安全的吗?
Spring容器中的bean可以分为5个范围:
(1)singleton:默认,每个容器中只有一个bean的实例
(2)prototype:为每一个bean请求提供一个实例
一般来说下面几种作用域,在开发的时候一般都不会用,99.99%的时候都是用singleton单例作用域
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收
(4)session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效
(5)global-session
答案是否定的,绝对不可能是线程安全的,spring bean默认来说,singleton,都是线程不安全的,java web系统,一般来说很少在spring bean里放一些实例变量,一般来说他们都是多个组件互相调用,最终去访问数据库的。
Spring事务传播机制
TCP/IP 的四层网络模型和 OSI 七层网络模型
1 物理层:
物理层就指的这个,就是怎么把各个电脑给联结起来,形成一个网络,物理层负责传输0和1的电路信号,是最底的一层。
2 数据链路层:
物理层给各个电脑连接起来了,还传输最底层的0和1电路信号,这还不行,得定义清楚哪些0和1分为一组,这才能进行通信。所以数据链路层就干这事儿,定义一下电路信号咋分组,分组的意义是 将数据分离,以区别哪些数据是描述性信息(头),哪些是数据实体(数据)。描述性的信息包括发送者、接收者和数据类型之类的等等。
数据从一台电脑到另外一台电脑,必须要通过网卡来完成。以太网协议规定了,接入网络里的所有设备,都得有个网,每个网卡必须得包含一个mac地址,mac地址就是这个网卡的唯一标识,所以在以太网里传输数据包的时候,必须指定接收者的mac地址才能传输数据。
但是以太网的数据包怎么从一个mac地址发送到另一个mac地址?这个不是精准推送的,以太网里面,如果一个电脑发个数据包出去,会广播给局域网内的所有电脑设备的网卡,然后每台电脑都从数据包里获取接收者的mac地址,跟自己的mac地址对比一下,如果一样,就说明这是发给自己的数据包。这种广播的方式,仅仅针对一个子网(局域网)内的电脑,会广播,否则一个电脑不能广播数据包给全世界所有的其他电脑吧。
image.png
3 网络层:
子网内的电脑,通过以太网发个数据包,对局域网内的电脑,是广播出去的。那么怎么知道哪些电脑在一个子网内呢?这就得靠网络层了。
网络层里有IP协议,IP协议定义的地址就叫做IP地址。IP地址有IPv4和IPv6两个版本,目前广泛使用的是IPv4,是32个二进制数字组成的,但是一般用4个十进制数字表示,范围从0.0.0.0到255.255.255.255之间。
如果几台电脑是一个子网的,那么前面的3个十进制数字一定是一样的。举个例子:大家平时做实验,玩儿虚拟机吧,自己win上开几个linux虚拟机,你会发现,win上的ip地址可能是192.168.0.103,然后几个虚拟机的ip地址是192.168.0.182,192.168.0.125,192.168.0.106,类似这样的。
这个win机器和几个虚拟机,前面3个十进制数字都是192.168.0,就代表大家是一个子网内的,最后那个数字是这个子网的不同主机的编号
但是实际上上面就是举个例子,其实单单从ip地址是看不出来哪些机器是一个子网的,因为从10进制是判断不出来的。需要通过ip地址的二进制来判断,结合一个概念来判断,叫做子网掩码
比如说ip地址是192.168.56.1,子网掩码是255.255.255.0。知道了子网掩码之后,如果要判断两个ip地址是不是一个子网的,就分别把两个ip地址和自己的子网掩码进行二进制的与运算,与运算之后,比较一下结果里面的代表网络的那部分,如果一样,代表是一个子网的。
192.168.56.1和192.168.32.7,判断是不是一个子网的,拿子网掩码255.255.255.0,跟两个ip地址的二进制做与运算,如果两台机器的子网掩码一致的,那么上面说的就成立了
11000000.10101000.00111000.00000001
11111111.11111111.11111111.00000000
如果两台电脑在同一个子网内,那么他们可以直接通信,如果不在呢? 那么这个时候就需要路由器和交换机来登场了。家里的路由器是包含了交换机和路由的两个功能的。
路由器负责将多个子网进行连接,每个电脑都可以搞多个网卡的,不是只有一个网卡,一般笔记本电脑都有以太网网卡和wifi网卡,发送数据包的时候要决定走哪个网卡。路由器,其实就是配置了多个网卡的一个专用设备,可以通过不同的网卡接入不同的网络。
网络交换机,也是一种设备,是工作在数据链路层的,路由器是工作在网路层的。
网络交换机是通过mac地址来寻址和传输数据包的;但是路由器是通过ip地址寻址和传输数据包的。网络交换机主要用在局域网的通信,一般你架设一个局域网,里面的电脑通信是通过数据链路层发送数据包,通过mac地址来广播的,广播的时候就是通过网络交换机这个设备来把数据广播到局域网内的其他机器上去的;路由器一般用来让你连入英特网。
4 传输层:
上面我们大概明白了通过网络层的ip地址怎么划分出来一个一个的子网,然后在子网内部怎么通过mac地址广播通信;跨子网的时候,怎么通过ip地址 -> mac地址 -> 交换机 -> 路由器 -> ip地址 -> mac地址 -> 交换机的方式来通过路由器进行通信。
但是这里还有一个问题,就是一台机器上,是很多个程序用一个网卡进行网络通信的,比如说浏览器、QQ、这些软件都用了一个网卡往外面发送数据,然后从网卡接收数据,所以还需要一个端口号的概念,然后那个机器上监听那个端口的程序,就可以提取发送到这个端口的数据,知道是自己的数据。端口号是065536的范围内,01023被系统占用了,别的应用程序就用1024以上的端口就ok了。
这里面的通信,就是通过socket来实现的,通过socket就可以基于tcp/ip协议完成刚才上面说的一系列的比如基于ip地址和mac地址转换和寻址啊,通过路由器通信啊之类的,而且会建立一个端口到另外一个端口的连接。
所以总结一下:网络层,是基于ip协议,进行主机和主机间的寻址和通信的,然后传输层,其实是建立某个主机的某个端口,到另外一个主机的某个端口的连接和通信的。
应用层
通过传输层的tcp协议可以传输数据,但是人家收到数据之后,怎么来解释?比如说收到个邮件你怎么处理?收到个网页你怎么处理?类似这个意思。这就是应用层干的事(比如http通信)。这个应用层,我们就假设综合了会话层、表示层和应用层了,3层合成1层,
然后我们看下自己的网络设置,一般包含了ip地址、子网掩码、网关地址、DNS地址。前面3个我们其实都知道啥意思了。ip地址和子网掩码用来划分子网的,判断哪些ip地址在一个子网内。同时你的ip地址和mac地址关联起来的,唯一定位了你的网卡。网关地址,你就认为是路由器上的那个网卡的ip地址吧,路由器的网卡也有mac地址,mac地址对应了一个ip地址。
DNS地址是啥呢?Domain Name System。因为我们一般定位是通过ip地址+mac地址+端口号来定位一个通信目标的,但是如果在浏览器上输入一个www.baidu.com,咋整?这个时候是先把www.baidu.com发给DNS服务器,然后DNS服务器告诉你www.baidu.com对应的ip地址的。
最后总结一下:
4 层模型的说法
数据链路层(以太网协议)->网络层(ip协议)->传输层(tcp协议)->应用层(http协议)
7层模型发的说法
物理层(网线,光缆)->数据链路层->网络层->传输层(这里分为三层,会话层,表示层,应用层)->应用层
浏览器请求www.baidu.com的全过程
现在我们先假设,我们给电脑设置了几个东西:
ip地址:192.168.31.37
子网掩码:255.255.255.0
网关地址:192.168.31.1
DNS地址:8.8.8.8
- 1 这时,我们打开一个浏览器,请求www.baidu.com地址,这个时候找DNS服务器,DNS服务器解析域名之后,返回一个ip地址,比如172.194.26.108。
- 2 接着会判断两个ip地址是不是一个子网的,用子网掩码255.255.255.0,对两个ip地址做与运算,拿到192.168.31.0和172.194.26.0,明显不是一个子网的。
-
3 那就得发送一个数据包给网关,其实你就认为是我们的路由器吧,就是192.168.31.1,而且我们是可以拿到网关ip地址的mac地址的,现在我们从应用层出发,通过浏览器访问一个网站,是走应用层的http协议的,并且要把浏览器发出的请求打包成数据包,要把哪些东西给放到数据包中去呢?如下图
image.png -
4 接着就是跑传输层来了,这个层是tcp协议,这个tcp协议会让你设置端口,发送方的端口随机选一个,接收方的端口一般是默认的80端口
这个时候,会把应用层数据包给封装到tcp数据包中去,而且会加一个tcp头,这个tcp数据包是对应一个tcp头的,这个tcp头里就放了端口号信息。如图:
image.png -
5 接着跑到网络层来了,走ip协议,这个时候会把tcp头和tcp数据包,放到ip数据包里去,然后再搞一个ip头,ip头里本机和目标机器的ip地址。
因为,通过ip协议,可以判断说,两个ip地址不是在一个子网内的,所以此时只能将数据包先通过以太网协议广播到网关上去,通过网关再给他发送出去,如图:
image.png -
6 接着是数据链路层,这块走以太网协议,这里是把ip头和ip数据包封到以太网数据包里去,然后再加一个以太网数据包的头,头里放了本机网卡mac地址,和网关的mac地址。但是以太网数据包的限制是1500个字节,但是假设这个时候ip数据包都5000个字节了,那么需要将ip数据包切割一下。分成4个包发送
image.png
这4个以太网数据包都会通过交换机发到你的网关上,然后你的路由器是可以联通别的子网的,这个是时候你的路由器就会转发到别的子网的可能也是某个路由器里去,然后以此类推吧,N多个路由器或者你叫网关也行,N多个网关转发之后,就会跑到百度的某台服务器,接收到4个以太网数据包。
百度服务器接收到4个以太网数据包以后,根据ip头的序号,把4个以太网数据包里的ip数据包给拼起来,就还原成一个完整的ip数据包了。接着就从ip数据包里面拿出来tcp数据包,再从tcp数据包里取出来http数据包,读取出来http数据包里的各种协议内容,接着就是做一些处理,然后再把响应结果封装成htp响应报文,封装在http数据包里,再一样的过程,封装tcp数据包,封装ip数据包,封装以太网数据包,接着通过网关给发回去。
image.png
TCP三次握手流程图?为啥是三次而不是二次或者四次呢?
三次挥手的过程
image.png
为啥不是2次或者4次握手呢
image.png
假设两次握手就ok了,要是客户端第一次握手过去,结果卡在某个地方了,没到服务端;完了客户端再次重试发送了第一次握手过去,服务端收到了,ok了,大家来回来去,三次握手建立了连接。
结果,尴尬的是,后来那个卡在哪儿的老的第一次握手发到了服务器,服务器直接就返回一个第二次握手,
假设tcp建立连接只需要2次握手的话,这个时候服务器开辟了资源准备客户端发送数据啥的,结果呢?客户端根本就不会理睬这个发回去的二次握手,因为之前都通信过了。那么服务器干等着???
但是如果是三次握手,那个二次握手发回去,客户端发现根本不对,就会发送个复位的报文过去,让服务器撤销开辟的资源,别等着了。
Https通信原理
(1)浏览器把自己支持的加密规则发送给网站
(2)网站从这套加密规则里选出来一套加密算法和hash算法,然后把自己的身份信息用证书的方式发回给浏览器,证书里有网站地址、加密公钥、证书颁发机构
(3)浏览器验证证书的合法性,然后浏览器地址栏上会出现一把小锁;浏览器接着生成一串随机数密码,然后用证书里的公钥进行加密,这块走的非对称加密;用约定好的hash算法生成握手消息的hash值,然后用密码对消息进行加密,然后把所有东西都发给网站,这块走的是对称加密
(4)网站,从消息里面可以取出来公钥加密后的随机密码,用本地的私钥对消息解密取出来密码,然后用密码解密浏览器发来的握手消息,计算消息的hash值,并验证与浏览器发送过来的hash值是否一致,最后用密码加密一段握手消息,发给浏览器
(5)浏览器解密握手消息,然后计算消息的hash值,如果跟网站发来的hash一样,握手就结束,之后所有的数据都会由之前浏览器生成的随机密码,然后用对称加密来进行进行加密。
image.png
MyISAM和InnoDB存储引擎的区别
myisam,不支持事务,不支持外键约束,索引文件和数据文件分开,这样在内存里可以缓存更多的索引,对查询的性能会更好,适用于那种少量的插入,大量查询的场景。比如报表系统。
innodb,主要特点就是支持事务,走聚簇索引,强制要求有主键,支持外键约束,高并发、大数据量、高可用等相关成熟的数据库架构,分库分表、读写分离、主备切换,全部都可以基于innodb存储引擎来玩儿,如果真聊到这儿,其实大家就可以带一带,说你们用innodb存储引擎怎么玩儿分库分表支撑大数据量、高并发的,怎么用读写分离支撑高可用和高并发读的。
MySQL的索引实现原理?各种索引平时都怎么用?
mysql的索引是怎么实现的?是一颗b+树,说b+树之前, 是先来聊聊b-树是啥
定义就不说了,现在又不是大学考试。反正大概就长上面那个样子,查找的时候,就是从根节点开始二分查找。
b+树是b-树的变种,mysql里面一般就是b+树来实现索引,所以b+树很重要。
b+树跟b-树不太一样的地方在于:
每个节点的指针上限为2d而不是2d+1。
内节点不存储data,只存储key;叶子节点不存储指针。
B+树
myism存储引擎的索引实现
在myisam存储引擎的索引中,每个叶子节点的data存放的是数据行的物理地址,比如0x07之类的东西,然后我们可以画一个数据表出来,一行一行的,每行对应一个物理地址。
image.png
比如一个查询:select * from table where id = 15 -> 0x07物理地址(先找到地址再根据地址去数据查询文件) -> 15,张三,22
myisam最大的特点是数据文件和索引文件是分开的,大家看到了么,先是索引文件里搜索,然后到数据文件里定位一个行的。
innodb存储引擎的索引
跟myisam最大的区别在于说,innodb的数据文件本身就是个索引文件,就是主键key,然后叶子节点的data就是那个数据的所在行。
image.png
innodb存储引擎,要求必须有主键,会根据主键建立一个默认索引,叫做聚簇索引。innodb的数据文件本身同时也是个索引文件,索引存储结构大致如下:
15,data:0x07,完整的一行数据,(15,张三,22)
22,data:完整的一行数据,(22,李四,30)
就是因为这个原因,innodb表是要求必须有主键的。innodb存储引擎下,如果对某个非主键的字段创建个索引,那么(对于这个字段形成的B+树来说)最后那个叶子节点的值就是主键的值,因为可以用主键的值到聚簇索引里根据主键值再次查找到数据,即所谓的回表,例如:
select * from table where name = ‘张三’
先到name的索引里去找,找到张三对应的叶子节点,叶子节点的data就是那一行的主键,id=15,然后再根据id=15,到数据文件里面的聚簇索引(根据主键组织的索引)根据id=15去定位出来id=15这一行的完整的数据
所以这里就明白了一个道理,为啥innodb下不要用UUID生成的超长字符串作为主键?因为这么玩儿会导致所有的索引的data都是那个主键值,最终导致索引会变得过大,浪费很多磁盘空间。
还有一个道理,一般innodb表里,建议统一用auto_increment自增值作为主键值,因为这样可以保持聚簇索引直接加记录就可以,如果用那种不是单调递增的主键值,可能会导致b+树分裂后重新组织,会浪费时间。
索引的使用规则
创建联合索引 :create index (shop_id,product_id,gmt_create)
(1)全列匹配
这个就是说,你的一个sql里,正好where条件里就用了这3个字段,那么就一定可以用到这个联合索引的:
select * from product where shop_id=1 and product_id=1 and gmt_create=’2018-01-01 10:00:00’
(2)最左前缀匹配
这个就是说,如果你的sql里,正好就用到了联合索引最左边的一个或者几个列表,那么也可以用上这个索引,在索引里查找的时候就用最左边的几个列就行了:
select * from product where shop_id=1 and product_id=1,这个是没问题的,可以用上这个索引的
(3)最左前缀匹配了,但是中间某个值没匹配
这个是说,如果你的sql里,就用了联合索引的第一个列和第三个列,那么会按照第一个列值在索引里找,找完以后对结果集扫描一遍根据第三个列来过滤,第三个列是不走索引去搜索的,就是有一个额外的过滤的工作,但是还能用到索引,所以也还好,例如:
select * from product where shop_id=1 and gmt_create=’2018-01-01 10:00:00’
就是先根据shop_id=1在索引里找,找到比如100行记录,然后对这100行记录再次扫描一遍,过滤出来gmt_create=’2018-01-01 10:00:00’的行
这个我们在线上系统经常遇到这种情况,就是根据联合索引的前一两个列按索引查,然后后面跟一堆复杂的条件,还有函数啥的,但是只要对索引查找结果过滤就好了,根据线上实践,单表几百万数据量的时候,性能也还不错的,简单SQL也就几ms,复杂SQL也就几百ms。可以接受的。
(4)没有最左前缀匹配
那就不行了,那就在搞笑了,一定不会用索引,所以这个错误千万别犯
select * from product where product_id=1,这个肯定不行
(5)前缀匹配
这个就是说,如果你不是等值的,比如=,>=,<=的操作,而是like操作,那么必须要是like ‘XX%’这种才可以用上索引,比如说
select * from product where shop_id=1 and product_id=1 and gmt_create like ‘2018%’
(6)范围列匹配
如果你是范围查询,比如>=,<=,between操作,你只能是符合最左前缀的规则才可以范围,范围之后的列就不用索引了
select * from product where shop_id>=1 and product_id=1
这里就在联合索引中根据shop_id来查询了
(7)包含函数
如果你对某个列用了函数,比如substring之类的东西,那么那一列不用索引
select * from product where shop_id=1 and 函数(product_id) = 2
上面就根据shop_id在联合索引中查询
创建索引时的注意点
1 常用查询字段适合做索引
2 选择性高的字段适合做索引 select count(DISTINCT col) / count(*) FROM t_worker,这个值越大越好
事务隔离级别
(1)读未提交
(2)读已提交
(3)可重复读
(4)串行化
MySQL是如何实现Read Repeatable的吧?因为一般我们都不修改这个隔离级别,但是你得清楚是怎么回事儿,MySQL是通过MVCC机制来实现的,就是多版本并发控制,multi-version concurrency control。
当我们使用innodb存储引擎,会在每行数据的最后加两个隐藏列,一个保存行的创建事务id,一个保存行的删除事务id, 事务id是mysql自己维护的自增的,全局唯一。如以下数据
image.png
查:事务id=121的事务,在执行select * from table where id=1
的时候,隐含的条件是WHERE current_trantion_id >= create_trantion_id and (delete_trantion_id >= create_trantion_id OR delete_trantion_id IS NULL)
也就是说创建事务版本id<=当前事务版本号(事务id)<删除事务版本id(或者删除事务版本id为null),也就是说查是查的快照,从而来实现可重复读。
改: 修改数据的时候不是直接覆盖,而是创建一条新的数据,同时将之前数据的delete_trantion_id 设置为当前事务id,如上面的小李四。
删:将delete_trantion_id 设置为当前事务id。
mysql锁
mysql 锁一般分为,表锁,行锁,页锁
myIsam执行查询的时候,会默认加一个共享锁,别人只能来看 不能写。myIsam执行写的时候 会加一个表独占锁,别人不能读也不能写。
页锁一般没人用。
行锁,innodb 执行增删改的时候会自动给那一行加行级排它锁,读的时候什么锁都不加,因为innodb 读的时候是读快照。
悲观锁与乐观锁
悲观锁的应用场景,就比如查出一条数据,要在内存中经过一番操作再去进行更新数据库,这个过程中别人是不能更新的。高并发场景下不能使用悲观锁,因为当一个事务锁住了数据,那么其他事务都会发生阻塞,会导致大量的事务发生积压拖垮整个系统。悲观锁一般用的很少。就是比较版本号是否已经改变,如果已经改变那么就重新查询再来执行操作。
悲观锁语法:select * from user_info where id=1 for update;
这里一定要加筛选条件,否则就升级为表锁了。另外还要置于事务环境下
乐观锁:就是不需要提前加锁,但是需要加一个版本号version,先查出数据来,然后更新的时候带上版本号,UPDATE col = 'xx',version = #{version} +1 user_info WHERE id =1 AND version = #{version}
死锁: 事务A持有A锁的情况下,再去要B锁资源,事务B持有B锁的情况下再去要A锁资源。死锁情况下去找一下DBA看日志
SQL调优
互联网公司一般都不需要SQL调优,基本都是单表查询,join逻辑放到代码里面去实现。这样是为了后面数据库好扩展。
实在调优就explain一下,看看查询有没有走索引。