试题列表:html
1:如何避免对外开放接口被攻击,有哪些经常使用的防御手段能够用上?前端
------------------------------------------------------------------------------------------------------------------------java
阿里面试题:mysql
(1):ThreadLocal 以及使用场景nginx
(2):BIO、 NIOgit
(3):使用说明RPC , duboo使用说明方式进行通讯,讲讲Netty线程模型程序员
(4):Tcp粘包黏包 github
(5):使用说明消息队列,你用到的rabbitMQ 中消息是按顺序的吗?web
(6):数据库分库分表 面试
(7):线程池知识
(8):volatile关键字,volatile 是原子性吗?
(9):redis为何能支持高并发,redis数据持久化
(10):工做中遇到哪些技术挑战
(11):对本身将来有什么指望
------------------------------------------------------------------------------------------------------------------------
其余公司面试题:
(1):HashMap(底层数据结构、初始化大小、扩容)为何不是线程安全的,举个例子或者那个操做会致使线程不安全
(2):线上机器频繁FullGC
(3):用户访问网站愈来愈慢,怎么排查缘由
(4):springMVC 流程
(5):spring IOC AOP
(6):Spring bean 是线程安全的吗
(7):Spring事务隔离级别 事务隔离机制
(8):单例模式
(9):数据库优化、数据库索引优化
(10):数据库索引会失效吗?什么状况下会失效
(11):死锁是怎么发生的
(12):缓存穿透 、如何解决?
(13):分布式锁 怎么释放锁? 锁的失效时间怎么设定? 若是业务执行时间很快 超过锁的失效时间 提早释放锁 会怎么样,或者业务执行时间大于缓存失效时间怎么样?
(14):消息队列:如何进行消息可靠性,以及消息的幕等性(即消息不被重复消费)
(15):高并发下的接口幂等性
(16):springboot使用、springCloud和dubbo有什么区别?
(17):hibernate ,mybatis区别
(18):mybatis中的 #和$有什么区别?
(19):缓存与数据库一致性如何保证?缓存和数据库谁先更新。
(20):StringBuilder为何线程不安全
(21):分布式事务是怎么处理的?
(22):数据库查询,where条件是大的数据放在前面仍是放在后面?
(23):数据库,是小表驱动大表,仍是大表驱动小表?
(24):Spring框架是如何解决bean的循环依赖问题?
------------------------------------------------------------------------------------------------------------------------
网络上的面试题
(1)java线程中,调用start()方法就会执行run()方法,为何咱们不能直接调用run()方法?
答: new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就能够开始运行了。 start() 会执行线程的相应准备工做,而后自动执行 run() 方法的内容,这是真正的多线程工做。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,因此这并非多线程工做。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,仍是在主线程里执行。
------------------------------------------------------------------------------------------------------------------------
解答:
1:如何避免对外开放接口被攻击,有哪些经常使用的防御手段能够用上?
服务端对外开放API接口,尤为对移动应用开放接口的时候,更须要关注接口安全性的问题,要确保应用APP与API之间的安全通讯,防止数据被恶意篡改等攻击。
开放接口时最基本须要考虑到接口不该该被别人随意访问,而我也不能随意访问到其余用户的数据,从而保证用户与用户之间的数据隔离。这个时候咱们就有必要引入Token机制了。
具体的作法: 在用户成功登陆时,系统能够返回客户端一个Token,后续客户端调用服务端的接口,都须要带上Token,而服务端须要校验客户端Token的合法性。Token不一致的状况下,服务端须要拦截该请求。
服务端从某种层面来讲须要验证接受到数据是否和客户端发来的数据是否一致,要验证数据在传输过程当中有没有被注入攻击。这时候客户端和服务端就有必要作签名和验签。具体作法:
客户端对全部请求服务端接口参数作加密生成签名,并将签名做为请求参数一并传到服务端,服务端接受到请求同时要作验签的操做,对称加密对请求参数生成签名,并与客户端传过来的签名进行比对,如签名不一致,服务端须要拦截该请求
服务端仍然须要识别一些恶意请求,防止接口被一些丧心病狂的人玩坏。对接口访问频率设置必定阈值,对超过阈值的请求进行屏蔽及预警。
异常封装:服务端须要构建异常统一处理框架,将服务可能出现的异常作统一封装,返回固定的code与msg,防止程序堆栈信息暴露。
其它小手段例如:
(1)图形验证码
(2)短信发送间隔设置:设置同一号码重复发送的时间间隔,通常设置为60-120秒;
(3)IP限定:置每一个IP天天的最大发送量;
(4)发送量限定:设置每一个手机号码天天的最大发送量;
HTTPS可以有效防止中间人攻击,有效保证接口不被劫持,对数据窃取篡改作了安全防范。但HTTP升级HTTPS会带来更多的握手,而握手中的运算会带来更多的性能消耗。这也是不得不考虑的问题。
总得来讲,咱们很是有必要在设计接口的同时考虑安全性的问题,根据业务特色,采用的安全策略也不全相同。固然大多数安全策略更多的都是提升安全门槛,并不能保证100%的安全,但该作的仍是不能少。
------------------------------------------------------------------------------------------------------------------------
阿里面试:
(1):ThreadLocal 以及使用场景
连接: ThreadLocal类详解
(2):BIO、 NIO
(3):使用说明RPC , duboo使用说明方式进行通讯,讲讲Netty线程模型
(4):Tcp粘包黏包
(5):使用说明消息队列,你用到的rabbitMQ 中消息是按顺序的吗?
(6):数据库分库分表
(7):线程池知识
连接: java 线程池 - ThreadPoolExecutor
连接: java线程池实现原理
(8):volatile关键字,volatile 是原子性吗?
咱们知道对于可见性,Java提供了volatile关键字来保证可见性、有序性。但不保证原子性。
普通的共享变量不能保证可见性,由于普通共享变量被修改以后,何时被写入主存是不肯定的,当其余线程去读取时,此时内存中可能仍是原来的旧值,所以没法保证可见性。
背景:为了提升处理速度,处理器不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其余)后再进行操做,但操做完不知道什么时候会写到内存。
总结下来:
最重要的是:
举2个例子:
/线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true;
原文:这段代码是很典型的一段代码,不少人在中断线程时可能都会采用这种标记办法。可是事实上,这段代码会彻底运行正确么?即必定会将线程中断么?不必定,也许在大多数时候,这个代码可以把线程中断,可是也有可能会致使没法中断线程(虽然这个可能性很小,可是只要一旦发生这种状况就会形成死循环了)。
下面解释一下这段代码为什么有可能致使没法中断线程。在前面已经解释过,每一个线程在运行过程当中都有本身的工做内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在本身的工做内存当中。
那么当线程2更改了stop变量的值以后,可是还没来得及写入主存当中,线程2转去作其余事情了,那么线程1因为不知道线程2对stop变量的更改,所以还会一直循环下去。
可是用volatile修饰以后就变得不同了:
第一:使用volatile关键字会强制将修改的值当即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会致使线程1的工做内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:因为线程1的工做内存中缓存变量stop的缓存行无效,因此线程1再次读取变量stop的值时会去主存读取。
到这里可能看起来没什么问题,咱们来看例子2:
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
原文:你们想一下这段程序的输出结果是多少?也许有些朋友认为是10000。可是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。
可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操做,因为volatile保证了可见性,那么在每一个线程中对inc自增完以后,在其余线程中都能看到修改后的值啊,因此有10个线程分别进行了1000次操做,那么最终inc的值应该是1000*10=10000。
这里面就有一个误区了,volatile关键字能保证可见性没有错,可是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,可是volatile没办法保证对变量的操做的原子性。
在前面已经提到过,自增操做是不具有原子性的,它包括读取变量的原始值、进行加1操做、写入工做内存。那么就是说自增操做的三个子操做可能会分割开执行,就有可能致使下面这种状况出现:
假如某个时刻变量inc的值为10,
线程1对变量进行自增操做,线程1先读取了变量inc的原始值,而后线程1被阻塞了;
而后线程2对变量进行自增操做,线程2也去读取变量inc的原始值,因为线程1只是对变量inc进行读取操做,而没有对变量进行修改操做,因此不会致使线程2的工做内存中缓存变量inc的缓存行无效,因此线程2会直接去主存读取inc的值,发现inc的值时10,而后进行加1操做,并把11写入工做内存,最后写入主存。
而后线程1接着进行加1操做,因为已经读取了inc的值,注意此时在线程1的工做内存中inc的值仍然为10,因此线程1对inc进行加1操做后inc的值为11,而后将11写入工做内存,最后写入主存。
那么两个线程分别进行了一次自增操做后,inc只增长了1。
解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?而后其余线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,可是要注意,线程1对变量进行读取操做以后,被阻塞了的话,并无对inc值进行修改。而后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,可是线程1没有进行修改,因此线程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并发编程的艺术》中有这一段描述:“在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操做的时候,会从新从系统内存中把数据读处处理器缓存里。”咱们须要注意的是,这里的修改操做,是指的一个操做。
(9):redis为何能支持高并发,redis数据持久化
连接: redis 单线程的理解
(10):工做中遇到哪些技术挑战
(11):对本身将来有什么指望
------------------------------------------------------------------------------------------------------------------------
其余公司面试题:
(1):HashMap(底层数据结构、初始化大小、扩容)为何不是线程安全的,举个例子或者那个操做会致使线程不安全
(2):线上机器频繁FullGC
堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,以下图所示:
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来讲的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候常常伴随至少一次的Minor GC,但非绝对的。Major GC的速度通常会比Minor GC慢10倍以上。下边看看有那种状况触发JVM进行Full GC及应对策略。
一、System.gc()方法的调用
此方法的调用是建议JVM进行Full GC,虽然只是建议而非必定,但不少状况下它会触发 Full GC,从而增长Full GC的频率,也即增长了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机本身去管理它的内存,可经过经过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
老年代空间只有在新生代对象转入及建立为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出以下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种情况引发的Full GC,调优时应尽可能作到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要建立过大的对象及数组。
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的状况下也会执行Full GC。若是通过Full GC仍然回收不了,那么JVM会抛出以下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满形成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
对于采用CMS进行老年代GC的程序而言,尤为要注意GC日志中是否有promotion failed和concurrent mode failure两种情况,当这两种情况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下形成的;concurrent mode failure是在执行CMS GC的过程当中同时有对象要放入老年代,而此时老年代空间不足形成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多致使暂时性的空间不足触发Full GC)。
对应措施为:增大survivor space、老年代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会因为JDK的bug29致使CMS在remark完毕
后好久才触发sweeping动做。对于这种情况,可经过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
这是一个较为复杂的触发状况,Hotspot为了不因为新生代对象晋升到旧生代致使旧生代空间不足的现象,在进行Minor GC时,作了一个判断,若是以前统计所获得的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,若是小于6MB,则执行Full GC。
当新生代采用PS GC时,方式稍有不一样,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
除了以上4种情况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认状况下会一小时执行一次Full GC。可经过在启动时经过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或经过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
所谓大对象,是指须要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,可是没法找到足够大的连续空间来分配给当前对象,此种状况就会触发JVM进行Full GC。
为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务以后额外免费赠送一个碎片整理的过程,内存整理的过程没法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。
连接: JVM — 性能调优
(3):用户访问网站愈来愈慢,怎么排查缘由
可能缘由是:
一、服务器出口带宽不够用。
二、服务器负载过大忙不过来,没法承担巨大的流量。
三、数据库的瓶颈,数据库文件过大,形成读取缓慢,没有创建索引,形成每次查询都对数据库进行全局查询。
四、没有设置CDN。
五、可能遭受到了分布式拒绝攻击即DDOS攻击。
六、jvm分配内存太少了。
七、并发高了,网站太多人访问
八、代码问题,对象建立太多
解决办法:
1.查看线上服务器的负载状况,CPU负载,内存负载,网络带宽,查看是否已通过载。
2.查看网络链接状况,是否受到DDOS攻击,消耗尽带宽资源,形成没法提供服务。
3.查看MySQL数据库的日志文件,查看mysql慢查询日志,查看形成MySQL访问过慢的缘由。
4.能够查看应用程序的日志,如Apache,nginx,PHP,Tomcat日志文件,找出报错缘由,查看是不是代码问题。
(4):springMVC 流程
具体步骤:
一、 首先用户发送请求到前端控制器,前端控制器根据请求信息(如 URL)来决定选择哪个页面控制器进行处理并把请求委托给它,即之前的控制器的控制逻辑部分;图中的 一、2 步骤;
二、 页面控制器接收到请求后,进行功能处理,首先须要收集和绑定请求参数到一个对象,这个对象在 Spring Web MVC 中叫命令对象,并进行验证,而后将命令对象委托给业务对象进行处理;处理完毕后返回一个 ModelAndView(模型数据和逻辑视图名);图中的 三、四、5 步骤;
三、 前端控制器收回控制权,而后根据返回的逻辑视图名,选择相应的视图进行渲染,并把模型数据传入以便视图渲染;图中的步骤 六、7;
四、 前端控制器再次收回控制权,将响应返回给用户,图中的步骤 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域)
第十一步:前端控制器向用户响应结果
一、 DispatcherServlet 在 web.xml 中的部署描述,从而拦截请求到 Spring Web MVC
二、 HandlerMapping 的配置,从而将请求映射处处理器
三、 HandlerAdapter 的配置,从而支持多种类型的处理器
注:处理器映射求和适配器使用纾解的话包含在了注解驱动中,不须要在单独配置
四、 ViewResolver 的配置,从而将逻辑视图名解析为具体视图技术
五、 处理器(页面控制器)的配置,从而进行功能处理
View是一个接口,实现类支持不一样的View类型(jsp、freemarker、pdf...)
(5):spring IOC AOP
连接: Spring IOC
连接: Spring AOP
(6):Spring bean 是线程安全的吗
连接: http://www.javashuo.com/article/p-aebnslvp-hc.html
(7):Spring事务隔离级别 事务隔离机制
连接: 数据库的4种隔离级别
Spring事务隔离级别
事务隔离级别指的是一个事务对数据的修改与另外一个并行的事务的隔离程度,当多个事务同时访问相同数据时,若是没有采起必要的隔离机制,就可能发生如下问题:
再必须强调一遍,不是事务隔离级别设置得越高越好,事务隔离级别设置得越高,意味着势必要花手段去加锁用以保证事务的正确性,那么效率就要下降,所以实际开发中每每要在效率和并发正确性之间作一个取舍,通常状况下会设置为READ_COMMITED,此时避免了脏读,并发性也还不错,以后再经过一些别的手段去解决不可重复读和幻读的问题就行了。
Spring设置事务隔离级别
配置文件的方式
<tx:advice id="advice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="fun*" propagation="REQUIRED" isolation="DEFAULT"/> </tx:attributes> </tx:advice>
@Transactional(isolation=Isolation.DEFAULT) public void fun(){ dao.add(); dao.udpate(); }
总结
Spring建议的是使用DEFAULT,就是数据库自己的隔离级别,配置好数据库自己的隔离级别,不管在哪一个框架中读写数据都不用操心了。并且万一Spring没有把这几种隔离级别实现的很完善,出了问题就麻烦了。
(8):单例模式
连接:http://www.javashuo.com/article/p-aqenvkdb-hr.html
(9):数据库优化、数据库索引优化
索引的优势
索引的缺点
一:数据库优化
1. 如何发现有问题的SQL? 使用mysql慢查询日志对有效率问题的Sql进行监视
(1) show variables like 'slow_query_log'; --查看慢查询日志是否开启 (2) set global slow_qeury_log_file = '/home/mysql/sql_log/mysql_slow.log' --设置慢查询日志文件的位置 (3) set global log_queries_not_using_indexes = on --把没有使用索引的SQL存入慢查询日志 (4) set global long_query_time = 1 --设置时间限制,即超过这个时间的SQL就记录到日志中
这里能够使用查看变量的方式,查看上面参数的默认值 好比:show variables like 'slow%' 能够看到慢查询日志的默认存放位置
2. 慢查询日志包含的内容
3. 经常使用的慢查询日志分析工具
(1) mysqldumpslow 工具(通常在安装mysql时就已经有了) 用法: mysqldumpslow + 参数 + 慢查询日志文件路径
经常使用参数:
-t 数字: 显示前n条日志 能够使用mysqldumpslow -h 查看全部可携带的参数
(2) pt-query-digest 工具
使用这个工具分析慢查询日志时的输出 共有三部分:
第一部分:显示日志的时间范围,总的SQL数量和不一样的SQL数量
第二部分:
第三部分:显示具体的SQL语句
4.根据日志中的指标发现有问题的SQL
(1) 查询次数多且每次查询占用时间长的SQL 一般为pt-query-digest分析的前几个查询
(2) IO大的SQL 注意pt-query-digest 分析中的Rows examine (即扫描的行数)项
(3) 未命中索引的SQL 注意pt-query-digest 分析中Rows examine 和 Rows Send 的对比
5. 有问题的SQL被发现后,使用explain从句查询SQL的执行计划,explain返回的是一个表格,下面是各列的含义:
5. 优化子查询
尽可能使用连表查询代替子查询
当有重复数据时,能够使用distinct进行去重。
6. 优化limit查询
(1) 优化方案:使用有索引的列或主键进行order by 操做
(2) 优化方案:记录上次返回的主键,在下次查询时使用主键过滤(方向就是避免扫描过多的记录)
select film_id, description from film where film_id > 55 and film_id <= 60 order by film_id limit 1,5
(10):数据库索引会失效吗?什么状况下会失效
例如:一张USER表 有字段属性 name,age 其中name为索引
下面列举几个索引失效的状况
1. select * from USER where name=‘xzz’ or age=16;
例如这种状况:当语句中带有or的时候 即便有索引也会失效。
2.select * from USER where name like‘%xzz’ ;
例如这种状况:当语句索引 like 带%的时候索引失效(注意:若是上句为 like‘xzz’此时索引是生效的)
3.select * from USER where name=123;(此处只是简单作个例子,实际场景中通常name不会为数字的)
例如这种状况:若是列类型是字符串,那必定要在条件中将数据使用引号引用起来,不然不使用索引
4.若是mysql估计使用全表扫描要比使用索引快,则不使用索引(这个不知道咋举例子了 )
5.假如上述将name和age设置为联合索引,必定要注意顺序,mysql联合因此有最左原则,下面以name,age的顺序讲下
(1)select * from USER where name=‘xzz’ and age =11;
(2)select * from USER where age=11 and name=‘xzz’;
例如上诉两种状况:以name,age顺序为联合索引,(1)索引是生效的,(2)索引是失效的
6.好比age为索引:select * from USER where age-1>11;
例如这种状况:索引失效,不要在索引字段上进行表达式操做,不然索引会失效(是有相似时间转换的问题和上诉问题同样)
7.where语句中使用 Not In
看了别人写的文章,有说“应尽可能避免在where 子句中对字段进行null 值判断,不然将致使引擎放弃使用索引而进行全表扫描”,实测没有全表扫描。
(11):死锁是怎么发生的
(12):缓存穿透 、如何解决?
了解什么是 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 所有都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,好比 set -999 UNKNOWN
。而后设置一个过时时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效以前,均可以直接从缓存中取数据。
缓存击穿,就是说某个 key 很是热点,访问很是频繁,处于集中式高并发访问的状况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方式也很简单,能够将热点数据设置为永远不过时;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存以后,再释放锁,进而其它请求才能经过该 key 访问数据。
(13):分布式锁 怎么释放锁? 锁的失效时间怎么设定? 若是业务执行时间很快 超过锁的失效时间 提早释放锁 会怎么样,或者业务执行时间大于缓存失效时间怎么样?
(14):消息队列:如何进行消息可靠性,以及消息的幕等性(即消息不被重复消费)
这个是确定的,用 MQ 有个基本原则,就是数据不能多一条,也不能少一条,不能多,就是重复消费和幂等性问题。不能少,就是说这数据别搞丢了。那这个问题你必须得考虑一下。
若是说你这个是用 MQ 来传递很是核心的消息,好比说计费、扣费的一些消息,那必须确保这个 MQ 传递过程当中绝对不会把计费消息给弄丢。
数据的丢失问题,可能出如今生产者、MQ、消费者中,我们从 RabbitMQ 和 Kafka 分别来分析一下吧。
生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,由于网络问题啥的,都有可能。
此时能够选择用 RabbitMQ 提供的事务功能,就是生产者发送数据以前开启 RabbitMQ 事务 channel.txSelect
,而后发送消息,若是消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就能够回滚事务 channel.txRollback
,而后重试发送消息;若是收到了消息,那么能够提交事务 channel.txCommit
。
// 开启事务 channel.txSelect try { // 这里发送消息 } catch (Exception e) { channel.txRollback // 这里再次重发这条消息 } // 提交事务 channel.txCommit
可是问题是,RabbitMQ 事务机制(同步)一搞,基本上吞吐量会下来,由于太耗性能。
因此通常来讲,若是你要确保说写 RabbitMQ 的消息别丢,能够开启 confirm
模式,在生产者那里设置开启 confirm
模式以后,你每次写的消息都会分配一个惟一的 id,而后若是写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack
消息,告诉你说这个消息 ok 了。若是 RabbitMQ 没能处理这个消息,会回调你的一个 nack
接口,告诉你这个消息接收失败,你能够重试。并且你能够结合这个机制本身在内存里维护每一个消息 id 的状态,若是超过必定时间还没接收到这个消息的回调,那么你能够重发。
事务机制和 confirm
机制最大的不一样在于,事务机制是同步的,你提交一个事务以后会阻塞在那儿,可是 confirm
机制是异步的,你发送个消息以后就能够发送下一个消息,而后那个消息 RabbitMQ 接收了以后会异步回调你的一个接口通知你这个消息接收到了。
因此通常在生产者这块避免数据丢失,都是用 confirm
机制的。
就是 RabbitMQ 本身弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入以后会持久化到磁盘,哪怕是 RabbitMQ 本身挂了,恢复以后会自动读取以前存储的数据,通常数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,本身就挂了,可能致使少许数据丢失,可是这个几率较小。
设置持久化有两个步骤:
deliveryMode
设置为 2必需要同时设置这两个持久化才行,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 个参数:
replication.factor
参数:这个值必须大于 1,要求每一个 partition 必须有至少 2 个副本。min.insync.replicas
参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟本身保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。acks=all
:这个是要求每条数据,必须是写入全部 replica 以后,才能认为是写成功了。retries=MAX
(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。咱们生产环境就是按照上述要求配置的,这样配置以后,至少在 Kafka broker 端就能够保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。
若是按照上述的思路设置了 acks=all
,必定不会丢,要求是,你的 leader 接收到消息,全部的 follower 都同步到了消息以后,才认为本次写成功了。若是没知足这个条件,生产者会自动不断的重试,重试无限次。
其实这是很常见的一个问题,这俩问题基本能够连起来问。既然是消费消息,那确定要考虑会不会重复消费?能不能避免重复消费?或者重复消费了也别形成系统异常能够吗?这个是 MQ 领域的基本问题,其实本质上仍是问你使用消息队列如何保证幂等性,这个是你架构里要考虑的一个问题。
回答这个问题,首先你别听到重复消息这个事儿,就一无所知吧,你先大概说一说可能会有哪些重复消费的问题。
首先,好比 RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。由于这问题一般不是 MQ 本身保证的,是由咱们开发来保证的。挑一个 Kafka 来举个例子,说说怎么重复消费吧。
Kafka 实际上有个 offset 的概念,就是每一个消息写进去,都有一个 offset,表明消息的序号,而后 consumer 消费了数据以后,每隔一段时间(定时按期),会把本身消费过的消息的 offset 提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的 offset 来继续消费吧”。
可是凡事总有意外,好比咱们以前生产常常遇到的,就是你有时候重启系统,看你怎么重启了,若是碰到点着急的,直接 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 的消费是幂等性的,须要结合具体的业务来看。
(15):高并发下的接口幂等性
文章出处:高并发下接口幂等性解决方案
1、背景
咱们实际系统中有不少操做,是无论作多少次,都应该产生同样的效果或返回同样的结果。 例如
1. 前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果;
2. 咱们发起一笔付款请求,应该只扣用户帐户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱;
3. 发送消息,也应该只发一次,一样的短信发给用户,用户会哭的;
4. 建立业务订单,一次业务请求只能建立一个,建立多个就会出大问题等等不少重要的状况都须要幂等的特性来支持。
2、幂等性概念
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中.一个幂等操做的特色是其任意屡次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指能够使用相同参数重复执行,并能得到相同结果的函数。这些函数不会影响系统状态,也不用担忧重复执行会对系统形成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数. 更复杂的操做幂等保证是利用惟一交易号(流水号)实现. 个人理解:幂等就是一个操做,不论执行多少次,产生的效果和返回的结果都是同样的
3、技术方案
一、查询操做:查询一次和查询屡次,在数据不变的状况下,查询结果是同样的。select是自然的幂等操做;
二、删除操做:删除操做也是幂等的,删除一次和屡次删除都是把数据删除。(注意可能返回结果不同,删除的数据不存在,返回0,删除的数据多条,返回结果多个) ;
三、惟一索引,防止新增脏数据。好比:支付宝的资金帐户,支付宝也有用户帐户,每一个用户只能有一个资金帐户,怎么防止给用户建立资金帐户多个,那么给资金帐户表中的用户ID加惟一索引,因此一个用户新增成功一个资金帐户记录。要点:惟一索引或惟一组合索引来防止新增数据存在脏数据(当表存在惟一索引,并发时新增报错时,再查询一次就能够了,数据应该已经存在了,返回结果便可);
四、token机制,防止页面重复提交。业务要求: 页面的数据只能被点击提交一次;发生缘由: 因为重复点击或者网络重发,或者nginx重发等状况会致使数据被重复提交;解决办法: 集群环境采用token加redis(redis单线程的,处理须要排队);单JVM环境:采用token加redis或token加jvm内存。处理流程:1. 数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间;2. 提交后后台校验token,同时删除token,生成新的token返回。token特色:要申请,一次有效性,能够限流。注意:redis要用删除操做来判断token,删除成功表明token校验经过,若是用select+delete来校验token,存在并发问题,不建议使用;
五、悲观锁——获取数据的时候加锁获取。
select * from table_xxx where id='xxx' for update;
注意:id字段必定是主键或者惟一索引,否则是锁表,会死人的悲观锁使用时通常伴随事务一块儿使用,数据锁定时间可能会很长,根据实际状况选用;
六、乐观锁——乐观锁只是在更新数据那一刻锁表,其余时间不锁表,因此相对于悲观锁,效率更高。乐观锁的实现方式多种多样能够经过version或者其余状态条件:1. 经过版本号实现
update table_xxx set name=#name#,version=version+1 where version=#version#
以下图(来自网上);2. 经过条件限制
update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0
要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是作数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;
注意:乐观锁的更新操做,最好用主键或者惟一索引来更新,这样是行锁,不然更新时会锁表,上面两个sql改为下面的两个更好
update table_xxx set name=#name#,version=version+1 where id=#id# and version=#version#; update table_xxx set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0;
7.分布式锁——仍是拿插入数据的例子,若是是分布是系统,构建全局惟一索引比较困难,例如惟一性的字段无法肯定,这时候能够引入分布式锁,经过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,而后作操做,以后释放锁,这样实际上是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,能够在流程执行以前根据某个标志(用户ID+后缀等)获取分布式锁,其余流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供);
8. select + insert——并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就能够了。注意:核心高并发流程不要用这种方法;
9. 状态机幂等——在设计单据相关的业务,或者是任务相关的业务,确定会涉及到状态机(状态变动图),就是业务单据上面有个状态,状态在不一样的状况下会发生变动,通常状况下存在有限状态机,这时候,若是状态机已经处于下一个状态,这时候来了一个上一个状态的变动,理论上是不可以变动的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,必定要深入理解状态机,对业务系统设计能力提升有很大帮助
10. 对外提供接口的api如何保证幂等。如银联提供的付款接口:须要接入商户提交付款请求时附带:source来源,seq序列号
source+seq在数据库里面作惟一索引,防止屡次付款(并发时,只能处理一个请求) 。重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面作联合惟一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,必定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。
4、总结
幂等与你是否是分布式高并发还有JavaEE都没有关系。关键是你的操做是否是幂等的。一个幂等的操做典型如:把编号为5的记录的A字段设置为0这种操做无论执行多少次都是幂等的。一个非幂等的操做典型如:把编号为5的记录的A字段增长1这种操做显然就不是幂等的。要作到幂等性,从接口设计上来讲不设计任何非幂等的操做便可。譬如说需求是:当用户点击赞同时,将答案的赞同数量+1。改成:当用户点击赞同时,确保答案赞同表中存在一条记录,用户、答案。赞同数量由答案赞同表统计出来。总之幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤为是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确,因此不能出现多扣款,多打款等问题,这样会很难处理,用户体验也很差。
(16):springboot使用、springCloud和dubbo有什么区别?
(17):hibernate ,mybatis区别
(18):mybatis中的 #和$有什么区别?
(19):缓存与数据库一致性如何保证?缓存和数据库谁先更新。
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就必定会有数据一致性的问题,那么你如何解决一致性问题?
通常来讲,若是容许缓存能够稍微的跟数据库偶尔有不一致的状况,也就是说若是你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要作这个方案,即:读请求和写请求串行化,串到一个内存队列里去。
串行化能够保证必定不会出现不一致的状况,可是它也会致使系统的吞吐量大幅度下降,用比正常状况下多几倍的机器去支撑线上的一个请求。
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
为何是删除缓存,而不是更新缓存?
缘由很简单,不少时候,在复杂点的缓存场景,缓存不仅仅是数据库中直接取出来的值。
好比可能更新了某个表的一个字段,而后其对应的缓存,是须要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的。是否是说,每次修改数据库的时候,都必定要将其对应的缓存更新一份?也许有的场景是这样,可是对于比较复杂的缓存数据计算的场景,就不是这样了。若是你频繁修改一个缓存涉及的多个表,缓存也频繁更新。可是问题在于,这个缓存到底会不会被频繁访问到?
举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;可是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,若是你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就从新计算一次而已,开销大幅度下降。用到缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都从新作复杂的计算,无论它会不会用到,而是让它到须要被使用的时候再从新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都把里面的 1000 个员工的数据也同时查出来啊。80% 的状况,查这个部门,就只是要访问这个部门的信息就能够了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。
问题:先更新数据库,再删除缓存。若是删除缓存失败了,那么会致使数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路:先删除缓存,再更新数据库。若是数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。由于读的时候缓存没有,因此去读了数据库中的旧数据,而后更新到缓存中。
数据发生了变动,先删除了缓存,而后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变动的程序完成了数据库的修改。完了,数据库和缓存中的数据不同了...
为何上亿流量高并发场景下,缓存会出现这个问题?
只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实若是说你的并发量很低的话,特别是读并发很低,天天访问量就 1 万次,那么不多的状况下,会出现刚才描述的那种不一致的场景。可是问题是,若是天天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的状况。
解决方案以下:
更新数据的时候,根据数据的惟一标识,将操做路由以后,发送到一个 jvm 内部队列中。读取数据的时候,若是发现数据不在缓存中,那么将从新执行“读取数据+更新缓存”的操做,根据惟一标识路由以后,也发送到同一个 jvm 内部队列中。
一个队列对应一个工做线程,每一个工做线程串行拿到对应的操做,而后一条一条的执行。这样的话,一个数据变动的操做,先删除缓存,而后再去更新数据库,可是还没完成更新。此时若是一个读请求过来,没有读到缓存,那么能够先将缓存更新的请求发送到队列中,此时会在队列中积压,而后同步等待缓存更新完成。
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一块儿是没意义的,所以能够作过滤,若是发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操做进去了,直接等待前面的更新操做请求完成便可。
待那个队列对应的工做线程完成了上一个操做的数据库的修改以后,才会去执行下一个操做,也就是缓存更新的操做,此时会从数据库中读取最新的值,而后写入缓存中。
若是请求还在等待时间范围内,不断轮询发现能够取到值了,那么就直接返回;若是请求等待的时间超过必定时长,那么这一次直接从数据库中读取当前的旧值。
高并发的场景下,该解决方案要注意的问题:
因为读请求进行了很是轻度的异步化,因此必定要注意读超时的问题,每一个读请求必须在超时时间范围内返回。
该解决方案,最大的风险点在于说,可能数据更新很频繁,致使队列中积压了大量更新操做在里面,而后读请求会发生大量的超时,最后致使大量的请求直接走数据库。务必经过一些模拟真实的测试,看看更新数据的频率是怎样的。
另一点,由于一个队列中,可能会积压针对多个数据项的更新操做,所以须要根据本身的业务状况进行测试,可能须要部署多个服务,每一个服务分摊一些数据的更新操做。若是一个内存队列里竟然会挤压 100 个商品的库存修改操做,每一个库存修改操做要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能获得数据,这个时候就致使读请求的长时阻塞。
必定要作根据实际业务系统的运行状况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操做,可能会致使最后一个更新操做对应的读请求,会 hang 多少时间,若是读请求在 200ms 返回,若是你计算事后,哪怕是最繁忙的时候,积压 10 个更新操做,最多等待 200ms,那还能够的。
若是一个内存队列中可能积压的更新操做特别多,那么你就要加机器,让每一个机器上部署的服务实例处理更少的数据,那么每一个内存队列中积压的更新操做就会越少。
其实根据以前的项目经验,通常来讲,数据的写频率是很低的,所以实际上正常来讲,在队列中积压的更新操做应该是不多的。像这种针对读高并发、读缓存架构的项目,通常来讲写请求是很是少的,每秒的 QPS 能到几百就不错了。
咱们来实际粗略测算一下。
若是一秒有 500 的写操做,若是分红 5 个时间片,每 200ms 就 100 个写操做,放到 20 个内存队列中,每一个内存队列,可能就积压 5 个写操做。每一个写操做性能测试后,通常是在 20ms 左右就完成,那么针对每一个内存队列的数据的读请求,也就最多 hang 一下子,200ms 之内确定能返回了。
通过刚才简单的测算,咱们知道,单机支撑的写 QPS 在几百是没问题的,若是写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每一个机器 20 个队列。
这里还必须作好压力测试,确保恰巧碰上上述状况的时候,还有一个风险,就是忽然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,须要多少机器才能扛住最大的极限状况的峰值。
可是由于并非全部的数据都在同一时间更新,缓存也不会同一时间失效,因此每次可能也就是少数数据的缓存失效了,而后那些数据对应的读请求过来,并发量应该也不会特别大。
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操做,以及执行缓存更新操做的请求,都经过 Nginx 服务器路由到相同的服务实例上。
好比说,对同一个商品的读写请求,所有路由到同一台机器上。能够本身去作服务间的按照某个请求参数的 hash 路由,也能够用 Nginx 的 hash 路由功能等等。
万一某个商品的读写请求特别高,所有打到相同的机器的相同的队列里面去了,可能会形成某台机器的压力过大。就是说,由于只有在商品数据更新的时候才会清空缓存,而后才会致使读写并发,因此其实要根据业务系统去看,若是更新频率不是过高的话,这个问题的影响并非特别大,可是的确可能某些机器的负载会高一些。
Redis为持久化提供了两种方式:
本文将经过下面内容的介绍,但愿可以让你们更全面、清晰的认识这两种持久化方式,同时理解这种保存数据的思路,应用于本身的系统设计中。
为了使用持久化的功能,咱们须要先知道该如何开启持久化的功能。
# 时间策略
save 900 1
save 300 10
save 60 10000
# 文件名称
dbfilename dump.rdb
# 文件保存路径
dir /home/work/app/redis/data/
# 若是持久化出错,主进程是否中止写入
stop-writes-on-bgsave-error yes
# 是否压缩
rdbcompression yes
# 导入时是否检查
rdbchecksum yes
配置其实很是简单,这里说一下持久化的时间策略具体是什么意思。
save 900 1
表示900s内若是有1条是写入命令,就触发产生一次快照,能够理解为就进行一次备份save 300 10
表示300s内有10条写入,就产生快照下面的相似,那么为何须要配置这么多条规则呢?由于Redis每一个时段的读写请求确定不是均衡的,为了平衡性能与数据安全,咱们能够自由定制什么状况下触发备份。因此这里就是根据自身Redis写入状况来进行合理配置。
stop-writes-on-bgsave-error yes
这个配置也是很是重要的一项配置,这是当备份进程出错时,主进程就中止接受新的写入操做,是为了保护持久化的数据一致性问题。若是本身的业务有完善的监控系统,能够禁止此项配置, 不然请开启。
关于压缩的配置 rdbcompression yes
,建议没有必要开启,毕竟Redis自己就属于CPU密集型服务器,再开启压缩会带来更多的CPU消耗,相比硬盘成本,CPU更值钱。
固然若是你想要禁用RDB配置,也是很是容易的,只须要在save的最后一行写上:save ""
# 是否开启aof
appendonly yes
# 文件名称
appendfilename "appendonly.aof"
# 同步方式
appendfsync everysec
# aof重写期间是否同步
no-appendfsync-on-rewrite no
# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 加载aof时若是有错如何处理
aof-load-truncated yes
# 文件重写策略
aof-rewrite-incremental-fsync yes
仍是重点解释一些关键的配置:
appendfsync everysec
它其实有三种模式:
通常状况下都采用 everysec 配置,这样能够兼顾速度与安全,最多损失1s的数据。
aof-load-truncated yes
若是该配置启用,在加载时发现aof尾部不正确是,会向客户端写入一个log,可是会继续执行,若是设置为 no
,发现错误就会中止,必须修复后才能从新加载。
关于原理部分,咱们主要来看RDB与AOF是如何完成持久化的,他们的过程是如何。
在介绍原理以前先说下Redis内部的定时任务机制,定时任务执行的频率能够在配置文件中经过 hz 10
来设置(这个配置表示1s内执行10次,也就是每100ms触发一次定时任务)。该值最大可以设置为:500,可是不建议超过:100,由于值越大说明执行频率越频繁越高,这会带来CPU的更多消耗,从而影响主进程读写性能。
定时任务使用的是Redis本身实现的 TimeEvent,它会定时去调用一些命令完成定时任务,这些任务可能会阻塞主进程致使Redis性能降低。所以咱们在配置Redis时,必定要总体考虑一些会触发定时任务的配置,根据实际状况进行调整。
在Redis中RDB持久化的触发分为两种:本身手动触发与Redis定时触发。
针对RDB方式的持久化,手动触发能够使用:
而自动触发的场景主要是有如下几点:
save m n
配置规则自动触发;bgsave
;debug reload
时;shutdown
时,若是没有开启aof,也会触发。因为 save
基本不会被使用到,咱们重点看看 bgsave
这个命令是如何完成RDB的持久化的。
这里注意的是 fork
操做会阻塞,致使Redis读写性能降低。咱们能够控制单个Redis实例的最大内存,来尽量下降Redis在fork时的事件消耗。以及上面提到的自动触发的频率减小fork次数,或者使用手动触发,根据本身的机制来完成持久化。
AOF的整个流程大致来看能够分为两步,一步是命令的实时写入(若是是 appendfsync everysec
配置,会有1s损耗),第二步是对aof文件的重写。
对于增量追加到文件这一步主要的流程是:命令写入=》追加到aof_buf =》同步到aof磁盘。那么这里为何要先写入buf在同步到磁盘呢?若是实时写入磁盘会带来很是高的磁盘IO,影响总体性能。
aof重写是为了减小aof文件的大小,能够手动或者自动触发,关于自动触发的规则请看上面配置部分。fork的操做也是发生在重写这一步,也是这里会对主进程产生阻塞。
手动触发: bgrewriteaof
,自动触发 就是根据配置规则来触发,固然自动触发的总体时间还跟Redis的定时任务频率有关系。
下面来看看重写的一个流程图:
对于上图有四个关键点补充一下:
不能是RDB仍是AOF都是先写入一个临时文件,而后经过
rename
完成文件的替换工做。
数据的备份、持久化作完了,咱们如何从这些持久化文件中恢复数据呢?若是一台服务器上有既有RDB文件,又有AOF文件,该加载谁呢?
其实想要从这些文件中恢复数据,只须要从新启动Redis便可。咱们仍是经过图来了解这个流程:
启动时会先检查AOF文件是否存在,若是不存在就尝试加载RDB。那么为何会优先加载AOF呢?由于AOF保存的数据更完整,经过上面的分析咱们知道AOF基本上最多损失1s的数据。
经过上面的分析,咱们都知道RDB的快照、AOF的重写都须要fork,这是一个重量级操做,会对Redis形成阻塞。所以为了避免影响Redis主进程响应,咱们须要尽量下降阻塞。
在线上咱们到底该怎么作?我提供一些本身的实践经验。
本文的内容主要是运维上的一些注意点,但咱们开发者了解到这些知识,在某些时候有助于咱们发现诡异的bug。接下来会介绍Redis的主从复制与集群的知识。
(20):StringBuilder为何线程不安全
StringBuilder和StringBuffer的区别在哪?
答:StringBuilder不是线程安全的,StringBuffer是线程安全的
那StringBuilder不安全的点在哪儿?
在分析设个问题以前咱们要知道StringBuilder和StringBuffer的内部实现跟String类同样,都是经过一个char数组存储字符串的,不一样的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。
首先经过一段代码去看一下多线程操做StringBuilder对象会出现什么问题
public class StringBuilderDemo { public static void main(String[] args) throws InterruptedException { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < 10; i++){ new Thread(new Runnable() { @Override public void run() { for (int j = 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; //已经使用的字符数组的数量 int count;
再看StringBuilder的append()方法:
@Override public StringBuilder append(String str) { super.append(str); return this; }
StringBuilder的append()方法调用的父类AbstractStringBuilder的append()方法
1.public AbstractStringBuilder append(String str) { 2. if (str == null) 3. return appendNull(); 4. int len = str.length(); 5. ensureCapacityInternal(count + len); 6. str.getChars(0, len, value, count); 7. count += len; 8. return this; 9.}
咱们先无论代码的第五行和第六行干了什么,直接看第七行,count += len不是一个原子操做。假设这个时候count值为10,len值为1,两个线程同时执行到了第七行,拿到的count值都是10,执行完加法运算后将结果赋值给count,因此两个线程执行完后count值为11,而不是12。这就是为何测试代码输出的值要比10000小的缘由。
咱们看回AbstractStringBuilder的append()方法源码的第五行,ensureCapacityInternal()方法是检查StringBuilder对象的原char数组的容量能不能盛下新的字符串,若是盛不下就调用expandCapacity()方法对char数组进行扩容。
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity); }
扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再经过System.arryCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。
void expandCapacity(int minimumCapacity) { //计算新的容量 int newCapacity = value.length * 2 + 2; //中间省略了一些检查逻辑 ... value = Arrays.copyOf(value, newCapacity); }
Arrys.copyOf()方法
public static char[] copyOf(char[] original, int newLength) { char[] copy = new char[newLength]; //拷贝数组 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
AbstractStringBuilder的append()方法源码的第六行,是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面,代码以下:
str.getChars(0, len, value, count);
getChars()方法
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { //中间省略了一些检查 ... System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); }
拷贝流程见下图
线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。
至此,StringBuilder为何不安全已经分析完了。若是咱们将测试代码的StringBuilder对象换成StringBuffer对象会输出什么呢?
那么StringBuffer用什么手段保证线程安全的?这个问题你点进StringBuffer的append()方法里面就知道了。
(21):分布式事务是怎么处理的?
参考: 分布式事务的一种解决思路
分布式事务解决方案汇总:2PC、消息中间件、TCC、状态机+重试+幂等
(22):数据库查询,where条件是大的数据放在前面仍是放在后面?
(23):数据库,是小表驱动大表,仍是大表驱动小表?
(24):Spring框架是如何解决bean的循环依赖问题?
参考: Spring框架是怎么解决Bean之间的循环依赖的 (转)
面试题模块系列汇总
(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)如何进行熔断?熔断框架都有哪些?具体实现原理知道吗?
(1)如何进行降级?