1:如何避免对外开放接口被攻击,有哪些常用的防护手段可以用上?
------------------------------------------------------------------------------------------------------------------------
阿里面试题:
(1):ThreadLocal以及使用场景
(2):BIO、NIO
(3):使用说明RPC,duboo使用说明方式进行通信,讲讲Netty线程模型
(4):Tcp粘包黏包
(5):使用说明消息队列,你用到的rabbitMQ中消息是按顺序的吗?
(6):数据库分库分表
(7):线程池知识
(8):volatile关键字,volatile是原子性吗?
(9):redis为什么能支持高并发,redis数据持久化
(10):工作中遇到哪些技术挑战
(11):对自己未来有什么期望
其他公司面试题:
(1):HashMap(底层数据结构、初始化大小、扩容)为什么不是线程安全的,举个例子或者那个操作会导致线程不安全
(2):线上机器频繁FullGC
(3):用户访问网站越来越慢,怎么排查原因
(4):springMVC流程
(5):springIOCAOP
(6):Springbean是线程安全的吗
(7):Spring事务隔离级别事务隔离机制
(8):单例模式
(9):数据库优化、数据库索引优化
(10):数据库索引会失效吗?什么情况下会失效
(11):死锁是怎么发生的
(12):缓存穿透、如何解决?
(14):消息队列:如何进行消息可靠性,以及消息的幕等性(即消息不被重复消费)
(15):高并发下的接口幂等性
(16):springboot使用、springCloud和dubbo有什么区别?
(17):hibernate,mybatis区别
(18):mybatis中的#和$有什么区别?
(19):缓存与数据库一致性如何保证?缓存和数据库谁先更新。
(20):StringBuilder为什么线程不安全
(21):分布式事务是怎么处理的?
(22):数据库查询,where条件是大的数据放在前面还是放在后面?
(23):数据库,是小表驱动大表,还是大表驱动小表?
(24):Spring框架是如何解决bean的循环依赖问题?
(25):new一个对象的过程中发生了什么?
(26):如何用Redis统计独立用户访问量?
网络上的面试题
(1)java线程中,调用start()方法就会执行run()方法,为什么我们不能直接调用run()方法?
总结:调用start方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
解答:
服务端从某种层面来说需要验证接受到数据是否和客户端发来的数据是否一致,要验证数据在传输过程中有没有被注入攻击。这时候客户端和服务端就有必要做签名和验签。具体做法:客户端对所有请求服务端接口参数做加密生成签名,并将签名作为请求参数一并传到服务端,服务端接受到请求同时要做验签的操作,对称加密对请求参数生成签名,并与客户端传过来的签名进行比对,如签名不一致,服务端需要拦截该请求
服务端仍然需要识别一些恶意请求,防止接口被一些丧心病狂的人玩坏。对接口访问频率设置一定阈值,对超过阈值的请求进行屏蔽及预警。
异常封装:服务端需要构建异常统一处理框架,将服务可能出现的异常做统一封装,返回固定的code与msg,防止程序堆栈信息暴露。
其它小手段例如:
(1)图形验证码
(3)IP限定:置每个IP每天的最大发送量;
(4)发送量限定:设置每个手机号码每天的最大发送量;
HTTPS能够有效防止中间人攻击,有效保证接口不被劫持,对数据窃取篡改做了安全防范。但HTTP升级HTTPS会带来更多的握手,而握手中的运算会带来更多的性能消耗。这也是不得不考虑的问题。
总得来说,我们非常有必要在设计接口的同时考虑安全性的问题,根据业务特点,采用的安全策略也不全相同。当然大多数安全策略更多的都是提高安全门槛,并不能保证100%的安全,但该做的还是不能少。
阿里面试:
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
特别要注意的是,如果TCP的接受滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种情况,即服务端分多次才能将D1和D2包完全接受,期间发生多次拆包。
产生原因主要有这3种:
1、滑动窗口
TCP流量控制主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。
现在来看一下滑动窗口是如何造成粘包、拆包的?
粘包:假设发送方的每256bytes表示一个完整的报文,接收方由于数据处理不及时,这256个字节的数据都会被缓存到SO_RCVBUF(接收缓存区)中。如果接收方的SO_RCVBUF中缓存了多个报文,那么对于接收方而言,这就是粘包。
拆包:考虑另外一种情况,假设接收方的窗口只剩了128,意味着发送方最多还可以发送128字节,而由于发送方的数据大小是256字节,因此只能发送前128字节,等到接收方ack后,才能发送剩余字节。这就造成了拆包。
2、MSS和MTU分片
MSS:是MaximumSegementSize缩写,表示TCP报文中data部分的最大长度,是TCP协议在OSI五层网络模型中传输层对一次可以发送的最大数据的限制。
MTU:最大传输单元是MaxitumTransmissionUnit的简写,是OSI五层网络模型中链路层(datalinklayer)对一次可以发送的最大数据的限制。
当需要传输的数据大于MSS或者MTU时,数据会被拆分成多个包进行传输。由于MSS是根据MTU计算出来的,因此当发送的数据满足MSS时,必然满足MTU。
为了更好的理解,我们先介绍一下在5层网络模型中应用通过TCP发送数据的流程:
对于应用层来说,只关心发送的数据DATA,将数据写入socket在内核中的发送缓冲区SO_SNDBUF即返回,操作系统会将SO_SNDBUF中的数据取出来进行发送。传输层会在DATA前面加上TCPHeader,构成一个完整的TCP报文。
当数据到达网络层(networklayer)时,网络层会在TCP报文的基础上再添加一个IPHeader,也就是将自己的网络地址加入到报文中。到数据链路层时,还会加上DatalinkHeader和CRC。
当到达物理层时,会将SMAC(SourceMachine,数据发送方的MAC地址),DMAC(DestinationMachine,数据接受方的MAC地址)和Type域加入。
可以发现数据在发送前,每一层都会在上一层的基础上增加一些内容,下图演示了MSS、MTU在这个过程中的作用。
MTU是以太网传输数据方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes。刨去以太网帧的帧头(DMAC目的MAC地址48bit=6Bytes+SMAC源MAC地址48bit=6Bytes+Type域2bytes)14Bytes和帧尾CRC校验部分4Bytes(这个部分有时候大家也把它叫做FCS),那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。
由于MTU限制了一次最多可以发送1500个字节,而TCP协议在发送DATA时,还会加上额外的TCPHeader和IpHeader,因此刨去这两个部分,就是TCP协议一次可以发送的实际应用数据的最大大小,也就是MSS。
TCPHeader的长度是20字节,IPv4中IPHeader长度是20字节,IPV6中IPHeader长度是40字节,因此:在IPV4中,以太网MSS可以达到1460byte;在IPV6中,以太网MSS可以达到1440byte。
需要注意的是MSS表示的一次可以发送的DATA的最大长度,而不是DATA的真实长度。发送方发送数据时,当SO_SNDBUF中的数据量大于MSS时,操作系统会将数据进行拆分,使得每一部分都小于MSS,这就是拆包,然后每一部分都加上TCPHeader,构成多个完整的TCP报文进行发送,当然经过网络层和数据链路层的时候,还会分别加上相应的内容。
需要注意:默认情况下,与外部通信的网卡的MTU大小是1500个字节。而本地回环地址的MTU大小为65535,这是因为本地测试时数据不需要走网卡,所以不受到1500的限制。
3、Nagle算法
TCP/IP协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCPHeader+IPHeader),同时,对方接收到数据,也需要发送ACK表示确认。
即使从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的有用信息和40字节的首部数据。这种情况转变成了4000%的消耗,这样的情况对于重负载的网络来是无法接受的。
为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。
Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
Nagle算法的规则:
我们知道对于可见性,Java提供了volatile关键字来保证可见性、有序性。但不保证原子性。普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
背景:为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。
总结下来:
最重要的是:
举2个例子:
大家是不是有这样的疑问:“线程1在读取inc为10后被阻塞了,没有进行修改所以不会去通知其他线程,此时线程2拿到的还是10,这点可以理解。但是后来线程2修改了inc变成11后写回主内存,这下是修改了,线程1再次运行时,难道不会去主存中获取最新的值吗?按照volatile的定义,如果volatile修饰的变量发生了变化,其他线程应该去主存中拿变化后的值才对啊?”是不是还有:例子1中线程1先将stop=flase读取到了工作内存中,然后去执行循环操作,线程2将stop=true写入到主存后,为什么线程1的工作内存中stop=false会变成无效的?
其实严格的说,对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。在《Java并发编程的艺术》中有这一段描述:“在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。”我们需要注意的是,这里的修改操作,是指的一个操作。
堆内存划分为Eden、Survivor和Tenured/Old空间,如下图所示:
从年轻代空间(包括Eden和Survivor区域)回收内存被称为MinorGC,对老年代GC称为MajorGC,而FullGC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现FullGC的时候经常伴随至少一次的MinorGC,但非绝对的。MajorGC的速度一般会比MinorGC慢10倍以上。下边看看有那种情况触发JVM进行FullGC及应对策略。
1、System.gc()方法的调用
此方法的调用是建议JVM进行FullGC,虽然只是建议而非一定,但很多情况下它会触发FullGC,从而增加FullGC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+DisableExplicitGC来禁止RMI调用System.gc。
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,PermanetGeneration中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,PermanetGeneration可能会被占满,在未配置为采用CMSGC的情况下也会执行FullGC。如果经过FullGC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError:PermGenspace为避免PermGen占满造成FullGC现象,可采用的方法为增大PermGen空间或转为使用CMSGC。
对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotionfailed和concurrentmodefailure两种状况,当这两种状况出现时可能会触发FullGC。
promotionfailed是在进行MinorGC时,survivorspace放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrentmodefailure是在执行CMSGC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMSGC时当前的浮动垃圾过多导致暂时性的空间不足触发FullGC)。
对应措施为:增大survivorspace、老年代空间或调低触发并发GC的比率,但在JDK5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕
后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行MinorGC时,做了一个判断,如果之前统计所得到的MinorGC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发FullGC。
例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次MinorGC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行FullGC。
当新生代采用PSGC时,方式稍有不同,PSGC是在MinorGC后也会检查,例如上面的例子中第一次MinorGC后,PSGC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行FullGC。
可能原因是:
1、服务器出口带宽不够用。
2、服务器负载过大忙不过来,无法承担巨大的流量。
3、数据库的瓶颈,数据库文件过大,造成读取缓慢,没有建立索引,造成每次查询都对数据库进行全局查询。
4、没有设置CDN。
5、可能遭受到了分布式拒绝攻击即DDOS攻击。
6、jvm分配内存太少了。
7、并发高了,网站太多人访问
8、代码问题,对象创建太多
解决办法:
具体步骤:
1、首先用户发送请求到前端控制器,前端控制器根据请求信息(如URL)来决定选择哪一个页面控制器进行处理并把请求委托给它,即以前的控制器的控制逻辑部分;图中的1、2步骤;
2、页面控制器接收到请求后,进行功能处理,首先需要收集和绑定请求参数到一个对象,这个对象在SpringWebMVC中叫命令对象,并进行验证,然后将命令对象委托给业务对象进行处理;处理完毕后返回一个ModelAndView(模型数据和逻辑视图名);图中的3、4、5步骤;
3、前端控制器收回控制权,然后根据返回的逻辑视图名,选择相应的视图进行渲染,并把模型数据传入以便视图渲染;图中的步骤6、7;
4、前端控制器再次收回控制权,将响应返回给用户,图中的步骤8;至此整个结束。
第一步:发起请求到前端控制器(DispatcherServlet)
第二步:前端控制器请求HandlerMapping查找Handler(可以根据xml配置、注解进行查找)
第三步:处理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略
第四步:前端控制器调用处理器适配器去执行Handler
第五步:处理器适配器HandlerAdapter将会根据适配的结果去执行Handler
第六步:Handler执行完成给适配器返回ModelAndView
第七步:处理器适配器向前端控制器返回ModelAndView(ModelAndView是springmvc框架的一个底层对象,包括Model和view)
第八步:前端控制器请求视图解析器去进行视图解析(根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可
第九步:视图解析器向前端控制器返回View
第十步:前端控制器进行视图渲染(视图渲染将模型数据(在ModelAndView对象中)填充到request域)
第十一步:前端控制器向用户响应结果
1、DispatcherServlet在web.xml中的部署描述,从而拦截请求到SpringWebMVC
2、HandlerMapping的配置,从而将请求映射到处理器
3、HandlerAdapter的配置,从而支持多种类型的处理器
注:处理器映射求和适配器使用纾解的话包含在了注解驱动中,不需要在单独配置
4、ViewResolver的配置,从而将逻辑视图名解析为具体视图技术
5、处理器(页面控制器)的配置,从而进行功能处理
View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...)
Spring事务隔离级别
事务隔离级别指的是一个事务对数据的修改与另一个并行的事务的隔离程度,当多个事务同时访问相同数据时,如果没有采取必要的隔离机制,就可能发生以下问题:
再必须强调一遍,不是事务隔离级别设置得越高越好,事务隔离级别设置得越高,意味着势必要花手段去加锁用以保证事务的正确性,那么效率就要降低,因此实际开发中往往要在效率和并发正确性之间做一个取舍,一般情况下会设置为READ_COMMITED,此时避免了脏读,并发性也还不错,之后再通过一些别的手段去解决不可重复读和幻读的问题就好了。
Spring设置事务隔离级别
配置文件的方式
索引的优点
索引的缺点
一:数据库优化
1.如何发现有问题的SQL?使用mysql慢查询日志对有效率问题的Sql进行监视
2.慢查询日志包含的内容
3.常用的慢查询日志分析工具
(1)mysqldumpslow工具(一般在安装mysql时就已经有了)用法:mysqldumpslow+参数+慢查询日志文件路径
常用参数:
-t数字:显示前n条日志可以使用mysqldumpslow-h查看所有可携带的参数
(2)pt-query-digest工具
使用这个工具分析慢查询日志时的输出共有三部分:
第二部分:
第三部分:显示具体的SQL语句
4.根据日志中的指标发现有问题的SQL
(2)IO大的SQL注意pt-query-digest分析中的Rowsexamine(即扫描的行数)项
(3)未命中索引的SQL注意pt-query-digest分析中Rowsexamine和RowsSend的对比
5.有问题的SQL被发现后,使用explain从句查询SQL的执行计划,explain返回的是一个表格,下面是各列的含义:
5.优化子查询
尽量使用连表查询代替子查询
当有重复数据时,可以使用distinct进行去重。
6.优化limit查询
(1)优化方案:使用有索引的列或主键进行orderby操作
(2)优化方案:记录上次返回的主键,在下次查询时使用主键过滤(方向就是避免扫描过多的记录)
selectfilm_id,descriptionfromfilmwherefilm_id>55andfilm_id<=60orderbyfilm_idlimit1,5
例如:一张USER表有字段属性name,age其中name为索引
下面列举几个索引失效的情况
1.select*fromUSERwherename=‘xzz’orage=16;
例如这种情况:当语句中带有or的时候即使有索引也会失效。
2.select*fromUSERwherenamelike‘%xzz’;
例如这种情况:当语句索引like带%的时候索引失效(注意:如果上句为like‘xzz’此时索引是生效的)
3.select*fromUSERwherename=123;(此处只是简单做个例子,实际场景中一般name不会为数字的)
例如这种情况:如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
4.如果mysql估计使用全表扫描要比使用索引快,则不使用索引(这个不知道咋举例子了)
5.假如上述将name和age设置为联合索引,一定要注意顺序,mysql联合所以有最左原则,下面以name,age的顺序讲下
(1)select*fromUSERwherename=‘xzz’andage=11;
(2)select*fromUSERwhereage=11andname=‘xzz’;
例如上诉两种情况:以name,age顺序为联合索引,(1)索引是生效的,(2)索引是失效的
6.比如age为索引:select*fromUSERwhereage-1>11;
7.where语句中使用NotIn
了解什么是redis的雪崩、穿透和击穿?redis崩溃之后会怎么样?系统该如何应对这种情况?如何处理redis的穿透?
对于系统A,假设每天高峰期每秒5000个请求,本来缓存在高峰期可以扛住每秒4000个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时1秒5000个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA很着急,重启数据库,但是数据库立马又被新的流量给打死了。
这就是缓存雪崩。
大约在3年前,国内比较知名的一个互联网公司,曾因为缓存事故,导致雪崩,后台系统全部崩溃,事故从当天下午持续到晚上凌晨3~4点,公司损失了几千万。
缓存雪崩的事前事中事后的解决方案如下。
用户发送一个请求,系统A收到请求后,先查本地ehcache缓存,如果没查到再查redis。如果ehcache和redis都没有,再查数据库,将数据库中的结果,写入ehcache和redis中。
限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。
好处:
对于系统A,假设一秒5000个请求,结果其中4000个请求是黑客发出的恶意攻击。
黑客发出的那4000个攻击,缓存中查不到,每次你去数据库里查,也查不到。
举个栗子。数据库id是从1开始的,结果黑客发过来的请求id全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
缓存击穿,就是说某个key非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个key在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方式也很简单,可以将热点数据设置为永远不过期;或者基于redisorzookeeper实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该key访问数据。
这个是肯定的,用MQ有个基本原则,就是数据不能多一条,也不能少一条,不能多,就是重复消费和幂等性问题。不能少,就是说这数据别搞丢了。那这个问题你必须得考虑一下。
如果说你这个是用MQ来传递非常核心的消息,比如说计费、扣费的一些消息,那必须确保这个MQ传递过程中绝对不会把计费消息给弄丢。
数据的丢失问题,可能出现在生产者、MQ、消费者中,咱们从RabbitMQ和Kafka分别来分析一下吧。
生产者将数据发送到RabbitMQ的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。
此时可以选择用RabbitMQ提供的事务功能,就是生产者发送数据之前开启RabbitMQ事务channel.txSelect,然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。
//开启事务channel.txSelecttry{//这里发送消息}catch(Exceptione){channel.txRollback//这里再次重发这条消息}//提交事务channel.txCommit但是问题是,RabbitMQ事务机制(同步)一搞,基本上吞吐量会下来,因为太耗性能。
事务机制和confirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息RabbitMQ接收了之后会异步回调你的一个接口通知你这个消息接收到了。
所以一般在生产者这块避免数据丢失,都是用confirm机制的。
就是RabbitMQ自己弄丢了数据,这个你必须开启RabbitMQ的持久化,就是消息写入之后会持久化到磁盘,哪怕是RabbitMQ自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,RabbitMQ还没持久化,自己就挂了,可能导致少量数据丢失,但是这个概率较小。
设置持久化有两个步骤:
必须要同时设置这两个持久化才行,RabbitMQ哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个queue里的数据。
注意,哪怕是你给RabbitMQ开启了持久化机制,也有一种可能,就是这个消息写到了RabbitMQ中,但是还没来得及持久化到磁盘上,结果不巧,此时RabbitMQ挂了,就会导致内存里的一点点数据丢失。
所以,持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,RabbitMQ挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。
RabbitMQ如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ认为你都消费了,这数据就丢了。
这个时候得用RabbitMQ提供的ack机制,简单来说,就是你必须关闭RabbitMQ的自动ack,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里ack一把。这样的话,如果你还没处理完,不就没有ack了?那RabbitMQ就认为你还没处理完,这个时候RabbitMQ会把这个消费分配给别的consumer去处理,消息是不会丢的。
唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边自动提交了offset,让Kafka以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。
这不是跟RabbitMQ差不多吗,大家都知道Kafka会自动提交offset,那么只要关闭自动提交offset,在处理完之后自己手动提交offset,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。
生产环境碰到的一个问题,就是说我们的Kafka消费者消费到了数据之后是写到一个内存的queue里先缓冲一下,结果有的时候,你刚把消息写入内存queue,然后消费者会自动提交offset。然后此时我们重启了系统,就会导致内存queue里还没来得及处理的数据就丢失了。
这块比较常见的一个场景,就是Kafka某个broker宕机,然后重新选举partition的leader。大家想想,要是此时其他的follower刚好还有些数据没有同步,结果此时leader挂了,然后选举某个follower成leader之后,不就少了一些数据?这就丢了一些数据啊。
生产环境也遇到过,我们也是,之前Kafka的leader机器宕机了,将follower切换为leader之后,就会发现说这个数据就丢了。
所以此时一般是要求起码设置如下4个参数:
我们生产环境就是按照上述要求配置的,这样配置之后,至少在Kafkabroker端就可以保证在leader所在broker发生故障,进行leader切换时,数据不会丢失。
如果按照上述的思路设置了acks=all,一定不会丢,要求是,你的leader接收到消息,所有的follower都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。
其实这是很常见的一个问题,这俩问题基本可以连起来问。既然是消费消息,那肯定要考虑会不会重复消费?能不能避免重复消费?或者重复消费了也别造成系统异常可以吗?这个是MQ领域的基本问题,其实本质上还是问你使用消息队列如何保证幂等性,这个是你架构里要考虑的一个问题。
回答这个问题,首先你别听到重复消息这个事儿,就一无所知吧,你先大概说一说可能会有哪些重复消费的问题。
首先,比如RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。因为这问题通常不是MQ自己保证的,是由我们开发来保证的。挑一个Kafka来举个例子,说说怎么重复消费吧。
但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接kill进程了,再重启。这会导致consumer有些消息处理了,但是没来得及提交offset,尴尬了。重启之后,少数消息会再次消费一次。
举个栗子。
有这么个场景。数据1/2/3依次进入kafka,kafka会给这三条数据每条分配一个offset,代表这条数据的序号,我们就假设分配的offset依次是152/153/154。消费者从kafka去消费的时候,也是按照这个顺序去消费。假如当消费者消费了offset=153的这条数据,刚准备去提交offset到zookeeper,此时消费者进程被重启了。那么此时消费过的数据1/2的offset并没有提交,kafka也就不知道你已经消费了offset=153这条数据。那么重启之后,消费者会找kafka说,嘿,哥儿们,你给我接着把上次我消费到的那个地方后面的数据继续给我传递过来。由于之前的offset没有提交成功,那么数据1/2会再次传过来,如果此时消费者没有去重的话,那么就会导致重复消费。
如果消费者干的事儿是拿一条数据就往数据库里写一条,会导致说,你可能就把数据1/2在数据库里插入了2次,那么数据就错啦。
其实重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性。
举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。
一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。
幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。
所以第二个问题来了,怎么保证消息队列消费的幂等性?
其实还是得结合业务来思考,我这里给几个思路:
当然,如何保证MQ的消费是幂等性的,需要结合具体的业务来看。
一、背景我们实际系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果。例如
1.前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果;
2.我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱;
3.发送消息,也应该只发一次,同样的短信发给用户,用户会哭的;
updatetable_xxxsetname=#name#,version=version+1whereversion=#version#如下图(来自网上);2.通过条件限制
updatetable_xxxsetavai_amount=avai_amount-#subAmount#whereavai_amount-#subAmount#>=0要求:quality-#subQuality#>=,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;
注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表,上面两个sql改成下面的两个更好
8.select+insert——并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。注意:核心高并发流程不要用这种方法;
四、总结幂等与你是不是分布式高并发还有JavaEE都没有关系。关键是你的操作是不是幂等的。一个幂等的操作典型如:把编号为5的记录的A字段设置为0这种操作不管执行多少次都是幂等的。一个非幂等的操作典型如:把编号为5的记录的A字段增加1这种操作显然就不是幂等的。要做到幂等性,从接口设计上来说不设计任何非幂等的操作即可。譬如说需求是:当用户点击赞同时,将答案的赞同数量+1。改为:当用户点击赞同时,确保答案赞同表中存在一条记录,用户、答案。赞同数量由答案赞同表统计出来。总之幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤其是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确,所以不能出现多扣款,多打款等问题,这样会很难处理,用户体验也不好。
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求“缓存+数据库”必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。
串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
最经典的缓存+数据库读写的模式,就是CacheAsidePattern。
为什么是删除缓存,而不是更新缓存?
原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。
比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?
举个栗子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存更新20次、100次;但是这个缓存在1分钟内只被读取了1次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在1分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个lazy计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的list,没有必要说每次查询部门,都把里面的1000个员工的数据也同时查出来啊。80%的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询1000个员工。
问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了...
为什么上亿流量高并发场景下,缓存会出现这个问题?
只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就1万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。
解决方案如下:
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新执行“读取数据+更新缓存”的操作,根据唯一标识路由之后,也发送到同一个jvm内部队列中。
一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。
待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。
高并发的场景下,该解决方案要注意的问题:
该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。
另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压100个商品的库存修改操作,每个库存修改操作要耗费10ms去完成,那么最后一个商品的读请求,可能等待10*100=1000ms=1s后,才能得到数据,这个时候就导致读请求的长时阻塞。
如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。
其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的QPS能到几百就不错了。
我们来实际粗略测算一下。
经过刚才简单的测算,我们知道,单机支撑的写QPS在几百是没问题的,如果写QPS扩大了10倍,那么就扩容机器,扩容10倍的机器,每个机器20个队列。
这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时hang在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过Nginx服务器路由到相同的服务实例上。
比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的hash路由,也可以用Nginx的hash路由功能等等。
万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。
Redis为持久化提供了两种方式:
本文将通过下面内容的介绍,希望能够让大家更全面、清晰的认识这两种持久化方式,同时理解这种保存数据的思路,应用于自己的系统设计中。
为了使用持久化的功能,我们需要先知道该如何开启持久化的功能。
下面的类似,那么为什么需要配置这么多条规则呢?因为Redis每个时段的读写请求肯定不是均衡的,为了平衡性能与数据安全,我们可以自由定制什么情况下触发备份。所以这里就是根据自身Redis写入情况来进行合理配置。
stop-writes-on-bgsave-erroryes这个配置也是非常重要的一项配置,这是当备份进程出错时,主进程就停止接受新的写入操作,是为了保护持久化的数据一致性问题。如果自己的业务有完善的监控系统,可以禁止此项配置,否则请开启。
关于压缩的配置rdbcompressionyes,建议没有必要开启,毕竟Redis本身就属于CPU密集型服务器,再开启压缩会带来更多的CPU消耗,相比硬盘成本,CPU更值钱。
当然如果你想要禁用RDB配置,也是非常容易的,只需要在save的最后一行写上:save""
#是否开启aofappendonlyyes#文件名称appendfilename"appendonly.aof"#同步方式appendfsynceverysec#aof重写期间是否同步no-appendfsync-on-rewriteno#重写触发配置auto-aof-rewrite-percentage100auto-aof-rewrite-min-size64mb#加载aof时如果有错如何处理aof-load-truncatedyes#文件重写策略aof-rewrite-incremental-fsyncyes还是重点解释一些关键的配置:
appendfsynceverysec它其实有三种模式:
一般情况下都采用everysec配置,这样可以兼顾速度与安全,最多损失1s的数据。
aof-load-truncatedyes如果该配置启用,在加载时发现aof尾部不正确是,会向客户端写入一个log,但是会继续执行,如果设置为no,发现错误就会停止,必须修复后才能重新加载。
关于原理部分,我们主要来看RDB与AOF是如何完成持久化的,他们的过程是如何。
在介绍原理之前先说下Redis内部的定时任务机制,定时任务执行的频率可以在配置文件中通过hz10来设置(这个配置表示1s内执行10次,也就是每100ms触发一次定时任务)。该值最大能够设置为:500,但是不建议超过:100,因为值越大说明执行频率越频繁越高,这会带来CPU的更多消耗,从而影响主进程读写性能。
定时任务使用的是Redis自己实现的TimeEvent,它会定时去调用一些命令完成定时任务,这些任务可能会阻塞主进程导致Redis性能下降。因此我们在配置Redis时,一定要整体考虑一些会触发定时任务的配置,根据实际情况进行调整。
在Redis中RDB持久化的触发分为两种:自己手动触发与Redis定时触发。
针对RDB方式的持久化,手动触发可以使用:
而自动触发的场景主要是有以下几点:
这里注意的是fork操作会阻塞,导致Redis读写性能下降。我们可以控制单个Redis实例的最大内存,来尽可能降低Redis在fork时的事件消耗。以及上面提到的自动触发的频率减少fork次数,或者使用手动触发,根据自己的机制来完成持久化。
AOF的整个流程大体来看可以分为两步,一步是命令的实时写入(如果是appendfsynceverysec配置,会有1s损耗),第二步是对aof文件的重写。
对于增量追加到文件这一步主要的流程是:命令写入=》追加到aof_buf=》同步到aof磁盘。那么这里为什么要先写入buf在同步到磁盘呢?如果实时写入磁盘会带来非常高的磁盘IO,影响整体性能。
aof重写是为了减少aof文件的大小,可以手动或者自动触发,关于自动触发的规则请看上面配置部分。fork的操作也是发生在重写这一步,也是这里会对主进程产生阻塞。
对于上图有四个关键点补充一下:
数据的备份、持久化做完了,我们如何从这些持久化文件中恢复数据呢?如果一台服务器上有既有RDB文件,又有AOF文件,该加载谁呢?
启动时会先检查AOF文件是否存在,如果不存在就尝试加载RDB。那么为什么会优先加载AOF呢?因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。
通过上面的分析,我们都知道RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。
在线上我们到底该怎么做?我提供一些自己的实践经验。
本文的内容主要是运维上的一些注意点,但我们开发者了解到这些知识,在某些时候有助于我们发现诡异的bug。接下来会介绍Redis的主从复制与集群的知识。
StringBuilder和StringBuffer的区别在哪?
答:StringBuilder不是线程安全的,StringBuffer是线程安全的
那StringBuilder不安全的点在哪儿?
在分析设个问题之前我们要知道StringBuilder和StringBuffer的内部实现跟String类一样,都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。
首先通过一段代码去看一下多线程操作StringBuilder对象会出现什么问题
publicclassStringBuilderDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{StringBuilderstringBuilder=newStringBuilder();for(inti=0;i<10;i++){newThread(newRunnable(){@Overridepublicvoidrun(){for(intj=0;j<1000;j++){stringBuilder.append("a");}}}).start();}Thread.sleep(100);System.out.println(stringBuilder.length());}}我们能看到这段代码创建了10个线程,每个线程循环1000次往StringBuilder对象里面append字符。正常情况下代码应该输出10000,但是实际运行会输出什么呢?
我们看到输出了“9326”,小于预期的10000,并且还抛出了一个ArrayIndexOutOfBoundsException异常(异常不是必现)。
我们先看一下StringBuilder的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)
//存储字符串的具体内容char[]value;//已经使用的字符数组的数量intcount;再看StringBuilder的append()方法:
@OverridepublicStringBuilderappend(Stringstr){super.append(str);returnthis;}StringBuilder的append()方法调用的父类AbstractStringBuilder的append()方法
1.publicAbstractStringBuilderappend(Stringstr){2.if(str==null)3.returnappendNull();4.intlen=str.length();5.ensureCapacityInternal(count+len);6.str.getChars(0,len,value,count);7.count+=len;8.returnthis;9.}我们先不管代码的第五行和第六行干了什么,直接看第七行,count+=len不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到了第七行,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的值要比10000小的原因。
我们看回AbstractStringBuilder的append()方法源码的第五行,ensureCapacityInternal()方法是检查StringBuilder对象的原char数组的容量能不能盛下新的字符串,如果盛不下就调用expandCapacity()方法对char数组进行扩容。
privatevoidensureCapacityInternal(intminimumCapacity){//overflow-consciouscodeif(minimumCapacity-value.length>0)expandCapacity(minimumCapacity);}扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再通过System.arryCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。
voidexpandCapacity(intminimumCapacity){//计算新的容量intnewCapacity=value.length*2+2;//中间省略了一些检查逻辑...value=Arrays.copyOf(value,newCapacity);}Arrys.copyOf()方法
publicstaticchar[]copyOf(char[]original,intnewLength){char[]copy=newchar[newLength];//拷贝数组System.arraycopy(original,0,copy,0,Math.min(original.length,newLength));returncopy;}AbstractStringBuilder的append()方法源码的第六行,是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面,代码如下:
str.getChars(0,len,value,count);getChars()方法
publicvoidgetChars(intsrcBegin,intsrcEnd,chardst[],intdstBegin){//中间省略了一些检查...System.arraycopy(value,srcBegin,dst,dstBegin,srcEnd-srcBegin);}拷贝流程见下图
线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。
至此,StringBuilder为什么不安全已经分析完了。如果我们将测试代码的StringBuilder对象换成StringBuffer对象会输出什么呢?
那么StringBuffer用什么手段保证线程安全的?这个问题你点进StringBuffer的append()方法里面就知道了。
Java在new一个对象的时候,会先查看对象所属的类有没有被加载到内存,如果没有的话,就会先通过类的全限定名来加载。加载并初始化类完成后,再进行对象的创建工作。我们先假设是第一次使用该类,这样的话new一个对象就可以分为两个过程:加载并初始化类和创建对象。
java是使用双亲委派模型来进行类的加载的,所以在描述类加载过程前,我们先看一下它的工作过程:双亲委托模型的工作过程是:如果一个类加载器(ClassLoader)收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例
格式验证:验证是否符合class文件规范
语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;
操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)
被final修饰的static变量(常量),会直接赋值;
将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
解析需要静态绑定的内容。//所有不会被重写的方法和域都会被静态绑定
以上2、3、4三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。
5.1为静态变量赋值5.2执行static代码块注意:static代码块只有jvm能够调用
如果是多线程需要同时初始化一个类,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。
因为子类存在对父类的依赖,所以类的加载顺序是先加载父类后加载子类,初始化也一样。不过,父类初始化时,子类静态变量的值也有有的,是默认值。
最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句和静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。
分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量
将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值
初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法
需要注意的是,每个子类对象持有父类对象的引用,可在内部通过super关键字来调用父类对象,但在外部不可访问
通过实例引用调用实例方法的时候,先从方法区中对象的实际类型信息找,找不到的话再去父类类型信息中找。
如果继承的层次比较深,要调用的方法位于比较上层的父类,则调用的效率是比较低的,因为每次调用都要经过很多次查找。这时候大多系统会采用一种称为虚方法表的方法来优化调用的效率。
所谓虚方法表,就是在类加载的时候,为每个类创建一个表,这个表包括该类的对象所有动态绑定的方法及其地址,包括父类的方法,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。当通过对象动态绑定方法的时候,只需要查找这个表就可以了,而不需要挨个查找每个父类。
方式一:使用Hash
哈希是Redis的一种基础数据结构,Redis底层维护的是一个开散列,会把不同的key映射到哈希表上,如果是遇到关键字冲突,那么就会拉出一个链表出来。
当一个用户访问的时候,如果用户登陆过,那么我们就使用用户的id,如果用户没有登陆过,那么我们也能够前端页面随机生成一个key用来标识用户,当用户访问的时候,我们可以使用HSET命令,key可以选择URI与对应的日期进行拼凑,field可以使用用户的id或者随机标识,value可以简单设置为1。
当我们要统计某一个网站某一天的访问量的时候,就可以直接使用HLEN来得到最终的结果了。
优点:简单,容易实现,查询也是非常方便,数据准确性非常高。
缺点:占用内存过大,。随着key的增多,性能也会下降。小网站还行,如果是数亿PV的网站肯定受不了
方式二:使用Bitset
我们知道,对于一个32位的int,如果我们只用来记录id,那么只能够记录一个用户,但如果我们转成2进制,每位用来表示一个用户,那么我们就能够一口气表示32个用户,空间节省了32倍!对于有大量数据的场景,如果我们使用bitset,那么,可以节省非常多的内存。对于没有登陆的用户,我们也可以使用哈希算法,把对应的用户标识哈希成一个数字id。bitset非常的节省内存,假设有1亿个用户,也只需要100000000/8/1024/1024约等于12兆内存。
Redis已经为我们提供了SETBIT的方法,使用起来非常的方便,我们可以看看下面的例子,我们在item页面可以不停地使用SETBIT命令,设置用户已经访问了该页面,也可以使用GETBIT的方法查询某个用户是否访问。最后我们通过BITCOUNT可以统计该网页每天的访问数量。
优点:占用内存更小,查询方便,可以指定查询某个用户,数据可能略有瑕疵,对于非登陆的用户,可能不同的key映射到同一个id,否则需要维护一个非登陆用户的映射,有额外的开销。
缺点:如果用户非常的稀疏,那么占用的内存可能比方法一更大。
方式三:使用概率算法HyperLogLog
当用户访问网站的时候,我们可以使用PFADD命令,设置对应的命令,最后我们只要通过PFCOUNT就能顺利计算出最终的结果,因为这个只是一个概率算法,所以可能存在0.81%的误差。
优点:占用内存极小,对于一个key,只需要12kb。对于拼多多这种超多用户的特别适用。
缺点:查询指定用户的时候,可能会出错,毕竟存的不是具体的数据。总数也存在一定的误差。
面试题模块系列汇总
(1)为什么要进行系统拆分?如何进行系统拆分?拆分后不用dubbo可以吗?dubbo和thrift有什么区别呢?
(1)说一下的dubbo的工作原理?注册中心挂了可以继续通信吗?
(2)dubbo支持哪些序列化协议?说一下hessian的数据结构?PB知道吗?为什么PB的效率是最高的?
(3)dubbo负载均衡策略和高可用策略都有哪些?动态代理策略呢?
(4)dubbo的spi思想是什么?
(5)如何基于dubbo进行服务治理、服务降级、失败重试以及超时重试?
(6)分布式服务接口的幂等性如何设计(比如不能重复扣款)?
(7)分布式服务接口请求的顺序性如何保证?
(8)如何自己设计一个类似dubbo的rpc框架?
(1)使用redis如何设计分布式锁?使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
(1)分布式事务了解吗?你们如何解决分布式事务问题的?TCC如果出现网络连不通怎么办?XA的一致性如何保证?
(1)集群部署时的分布式session如何实现?
(1)为什么使用消息队列啊?消息队列有什么优点和缺点啊?kafka、activemq、rabbitmq、rocketmq都有什么优点和缺点啊?
(2)如何保证消息队列的高可用啊?
(3)如何保证消息不被重复消费啊(如何进行消息队列的幂等性问题)?
(4)如何保证消息的可靠性传输(如何处理消息丢失的问题)?
(5)如何保证消息的顺序性?
(6)如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
(7)如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路
(1)es的分布式架构原理能说一下么(es是如何实现分布式的啊)?
(2)es写入数据的工作原理是什么啊?es查询数据的工作原理是什么啊?底层的lucene介绍一下呗?倒排索引了解吗?
(3)es在数据量很大的情况下(数十亿级别)如何提高查询效率啊?
(4)es生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?
(1)在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?
(2)redis和memcached有什么区别?redis的线程模型是什么?为什么单线程的redis比多线程的memcached效率要高得多?
(3)redis都有哪些数据类型?分别在哪些场景下使用比较合适?
(5)redis的过期策略都有哪些?手写一下LRU代码实现?
(6)如何保证Redis高并发、高可用、持久化?redis的主从复制原理能介绍一下么?redis的哨兵原理能介绍一下么?
(7)redis的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?
(8)redis集群模式的工作原理能说一下么?在集群模式下,redis的key是如何寻址的?分布式寻址都有哪些算法?了解一致性hash算法吗?如何动态增加和删除一个节点?
(9)了解什么是redis的雪崩和穿透?redis崩溃之后会怎么样?系统该如何应对这种情况?如何处理redis的穿透?
(10)如何保证缓存与数据库的双写一致性?
(11)redis的并发竞争问题是什么?如何解决这个问题?了解Redis事务的CAS方案吗?
(12)生产环境中的redis是怎么部署的?
(1)为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的?
(2)现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?
(3)如何设计可以动态扩容缩容的分库分表方案?
(4)分库分表之后,id主键如何处理?
(1)如何实现mysql的读写分离?MySQL主从复制原理的是啥?如何解决mysql主从同步的延时问题?
(1)如何限流?在工作中是怎么做的?说一下具体的实现?
(1)如何进行熔断?熔断框架都有哪些?具体实现原理知道吗?