范型擦数
范型擦数: 就是泛型信息在源代码里面有,而在生成的字节码里面就没有了,之所以这么干主要是为了向下兼容老的java版本。那问题就很明了了,字节码中都不存在泛型的信息了.
范型擦除的表现就是:
public static void main(String [] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 底下这里会输出true
System.out.println(strList.getClass() == intList.getClass());
}
如何复原?
范型既然被擦除了, 那么我们再从class对象在加载成对象后, 依然能够获得具体的类型呢?
泛型能复原是因为具体的泛型信息 通过某种方式存储在了字节码中.
间隙锁的工作原理
如果查询条件是唯一值
如果查询的结果是某一个值, 锁的是索引树上的上一个元素的next指针, 当前元素的行记录, 当前记录到下一个元素的next指针.
如果查询条件是范围值
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;
对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
范围就相当于是对范围的上一个next指针, 整个范围内的所有next指针, 整个范围内的行记录.
如果查询范围延伸到了所有记录, 那么也会锁住剩下的所有的还没有新建的记录空间.
线程池数目的选择
线程和哪些因素有关
- CPU
线程共享进程的上下文环境,为更细粒度的CPU时间段。所以线程数的确定和CPU有关。
- IO
IO分为磁盘IO和网络IO。影响磁盘的关键因数是磁盘服务时间,即磁盘完成一个I/O请求所花费的时间,它由寻道时间、旋转延迟和数据传输时间三部分构成。衡量其关键指标,大致是IOPS、吞吐量等。影响网络IO的关键因素是服务器响应延时 + 带宽限制 + 网络延时 + 跳转路由延时 + 本地接收延时。
-
并行 多个cpu实例或者多台机器同时执行一段处理逻辑
-
并发 CPU不断切换线程来实现多路复用,以提升效率。通过cpu调度算法,看上去同时执行,实际上从cpu操作层面不是真正的同时。通常会用TPS或者QPS来反应这个系统的处理能力
CPU密集型任务
CPU 密集型任务,比如大量复杂计算需要耗费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的线程数,实际上并不会起到很好的效果。
此时假设我们设置的线程数量是 CPU 核心数的 2 倍以上,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的,而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。
一般配置线程数=CPU总核心数+1 (+1是为了利用等待空闲)
IO密集型任务
任务是耗时 IO 型,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。
对于这种任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。
而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。
一般配置线程数=CPU总核心数 * 2 +1
计算方法
线程数 = CPU 核心数 *(1 + 平均等待时间/平均工作时间)
通过这个公式,我们可以计算出一个合理的线程数量,如果任务的平均等待时间长,线程数就随之增加,而如果平均工作时间长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。
如果想要更准确的话,可以进行压测,监控 JVM 的线程情况以及 CPU 的负载情况,根据实际情况衡量应该创建的线程数,合理并充分利用资源。
栈内存会溢出吗
栈和线程的关系
栈指的就是java 创建出来的线程, 然后这个线程拥有的空间叫栈.
在java中每new一个线程,jvm都是向操作系统请求new一个本地线程,此时操作系统会使用剩余的内存空间来为线程分配内存,而不是使用jvm的内存。这样,当操作系统的可用内存越少,则jvm可用创建的新线程也就越少
栈容量溢出 StackOverflowError
jvm会为每个线程分配一个栈空间, 这个栈空间的大小由-Xss来限制, 然后应用程序唤起一个方法调用时就会在调用栈上分配一个栈帧, 这个栈帧包含引用方法的参数,本地参数,以及方法的返回地址。
如果递归调用过多, 添加了太多的栈帧情况下, 就会报出StackOverflowError的错误.
栈内存溢出 OutOfMemoryError
其实准确来讲, 就是当java去创建线程的时候, 发现操作系统已经没有剩余的内存空间了, 此时就会报出内存溢出的错误.
注意 虽然创建线程使用的不是jvm内存空间, 但是是由jvm去向操作系统申请的, 因此jvm可以报出内存溢出的异常
唯一没有定义OutOfMemoryError的区域
程序计数器
堆上内存泄漏
8种情况
(1) 静态集合类
如HashMap、LinkedList等等。如果这些容器为静态的(也就是static修饰的),那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。
(2) 各种连接,如数据库连接、网络连接和IO连接
在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
(3) 变量不合理的作用域
也就是本来可以将某个属性定义到方法内部作为一个临时变量的, 但是你定义成实例的属性, 这样实例在存在的时候, 该属性就不会被回收, 但是它实际上已经没有用途了.
(4) 内部类持有外部类
如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
(5) 改变哈希值
当一个对象被存储进HashSet集合中作为key之后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露.
(6) 使用栈时弹出的元素
当我们用pop方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用(obsolete reference)。
因此这部分对象需要手动置为null
(7) 缓存泄漏
日常业务中可能会将一些数据放到map中作为缓存使用, 如果放的数据过多的话, 就会占据很大一部分内存, 如果内存紧张的情况下, 可以考虑用弱引用来使用这些缓存的内容, 这样在gc后内存不足的情况下, 就会回收这部分内存, 保证系统正常.
或者使用weakHashMap, 每次gc的时候, 无论内存够不够, 如果weakHashMap的key没有被其它对象引用的话, 就会直接回收. 此时可能存在key为null, 但是value不为null的情况, 导致value一直处于内存泄漏.
(8) 监听器回调导致的内存泄漏
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
解决办法:需要确保回调立即被当作垃圾回收的最佳方法是只保存他的弱引用,例如将他们保存成为WeakHashMap中的键。
堆内存泄漏的排查工具
常规排查
(1) 利用jstat查看虚拟机统计信息, 看一下各个分代的使用情况, 以及gc的次数, 判断一下是否是发生了内存泄漏.
(2) 利用jmap查看堆中对象的数量, 是否有某个类的实例存在了太多. jmap也可以将堆对象的快照信息dump下来.
(3) jstack可以用来查看相应的线程, 分析一下有没有可能是哪些线程异常的创建了大量的实例, 可以得到更多的现场信息.
(4) 根据异常的实例, 去分析代码, 看一下是不是哪部分代码写的有问题.
BTrace工具
BTrace 是一个动态安全的 Java 追踪工具,它通过向运行中的 Java 程序植入字节码文件,来对运行中的 Java 程序热更新,方便的获取程序运行时的数据信息,并且,保证自己的消耗特别小,大部分情况下不会影响 Java 程序的性能。
BTrace 的最大好处,是可以通过自己编写的脚本,获取应用的一切调用信息,而不需要不断的修改代码,然后重启应用。
堆外内存泄漏
如果堆内存每次gc后比较正常, 但是java进程运行后, 机器的内存使用一直在增加, 就考虑是堆外内存使用的时候出现了内存泄漏.
例如创建了大量空转的线程, 一些依赖jar包在实现相关功能的时候, 会去申请堆外内存啊, 一些native method方法执行的时候导致的啊.
因此堆外内存的部分, 如果项目明确知道会大量使用, 可以考虑使用内存池来对堆外内存进行管理.
堆外内存泄漏排查工具
可以使用google-perftools工具进行排查, 它的原理是在java应用程序运行时,当调用malloc时换用它的libtcmalloc.so,这样就能做一些统计了, 也就是对于堆外内存的申请, 会统计到相应的情况.
使用方法就是在linux服务器上安装, 然后运行你的java程序, 一段时间后, 可以使用命令pprof --text myApp out.prof , 查看指定app的内存申请情况, myApp就是你的应用名称, 这样就可以得到到底哪些类在申请堆外内存了.
压测工具
JMeter
Jmeter是基于Java的压测工具, 安装好后, 可以通过编写脚本进行压测
利用CountDownLatch自己实现压测工具
CountDownLatch可以实现多个线程并发执行的情况, 它需要一个数量, 当这个数量减为0的时候, 所有执行了countDownLatch.await()方法的地方, 结束等待, 同时往下执行.
我每创建一个线程, 执行一次countDownLatch.countDown()方法, 将数量减去1, 然后执行countDownLatch.await()方法, 如果数量此时不为0, 当前线程就会阻塞.
直到数量为0的时候, 所有线程同时从countDownLatch.await()方法开始向下执行, 就实现了并发的效果.
CountDownLatch的底层原理
借助AQS实现的, 在设置数量100的时候, 即new CountDownLatch(100), 就是让state = 100, state就作为一个共享锁.
await()方法就是尝试去获取共享锁, 本质就是判断state是否为0, 如果不为0, 就加入队列中一直等待.
countDown()就是将state的值减1
当计数为0了之后, 执行await()方法的线程, 在尝试获取共享锁的时候, 就可以正确的获取到了, 然后通过doAcquireSharedInterruptibly()方法唤醒所有在队列中等待的线程, 大家就都获得锁了.
然后所有的线程就都可以执行wait()方法之后的逻辑了.
CountDownLatch是通过“共享锁”实现的。在创建CountDownLatch中时,会传递一个int类型参数count,该参数是“锁计数器”的初始状态,表示该“共享锁”最多能被count给线程同时获取。当某线程调用该CountDownLatch对象的await()方法时,该线程会等待“共享锁”可用时,才能获取“共享锁”进而继续运行。而“共享锁”可用的条件,就是“锁计数器”的值为0!而“锁计数器”的初始值为count,每当一个线程调用该CountDownLatch对象的countDown()方法时,才将“锁计数器”-1;通过这种方式,必须有count个线程调用countDown()之后,“锁计数器”才为0,而前面提到的等待线程才能继续运行!
如何简单实现CountDownLatch的功能?
CountDownLatch本身就是通过共享锁实现的, 共享锁实际上也就是一个共享的int变量, 因此如果是我们自己实现的话, 也就是用一个多线程之间的共享变量, 不断的去判断共享变量的值, 在修改的时候, 不要加锁, 利用CAS操作修改即可.
CyclicBarrier的原理
基本原理和CountDownLatch类似, 都是借助AQS实现的, 只不过CyclicBarrier在每次调用wait()方法的时候, 进行减1操作, 并且支持所有线程满足条件后, 可以同时执行某些操作(通过传入一个实现了Runnable接口的实例). 这样每个线程里面就不用实现一些逻辑了.
CyclicBarrier在所有线程的数量满足一次条件后, 会自动进入下一轮, 此时如果你的线程不再执行wait()方法了, 那么那些线程就正常结束了, 不会再被CyclicBarrier阻塞了.
如果你的线程是一个死循环, 再次执行wait()方法, 那么就会参与到CyclicBarrier的下一代当中, 继续等待其他的线程. 同时这也是CyclicBarrier的好处, 因为这样就可以继续使用它进行拦截等待.
同时CyclicBarrier还支持reset()方法, 让你重置计数的功能, 直接进入下一代.
压测结果观察
可以用JMX来监控, 除了自带的监控数据以外, 还可以自定义一些JMX的bean.
利用JConsole可以查看相应的结果.
http的header里都有些啥关键内容
Accept:浏览器能够处理的内容类型
Accept-Charset:浏览器能够显示的字符集
Accept-Encoding:浏览器能够处理的压缩编码
Accept-Language:浏览器当前设置的语言
Connection:浏览器与服务器之间连接的类型, 是否需要长连接
Cookie:当前页面设置的任何Cookie
Content-Type : 请求的与实体对应的MIME信息
User-Agent:发出请求的用户信息, 例如手机系统啊, 版本啊
Authorization 用来告知服务器用户代理的认证信息, 用于验证用户身份的凭证。 于是检查request里面有没有"Authorization"的http header 如果有,则判断Authorization里面的内容是否在用户列表里面
Proxy-Authorization 用于用户代理给代理服务器发送身份验证的凭证
如何设计一个登陆
单系统登陆
如果是单系统的话, 在用户登陆后, 返回sessionId, 然后客户端将它设为cookie, 每次请求的时候根据cookie的内容校验是否是已经登陆的用户, 然后再进行权限管理, 然后恢复用户的登录状态.
session是HttpServletRequest自带的一个字段, 如果我们设置了这个字段, 服务器就会开辟一个区域来存储session的值, 然后每次就可以根据session里面的内容, 来恢复用户的登录信息.
sessionID和session对象的映射关系, 需要使用session监听器配合一个静态的hashmap即可实现。
多系统共享cookie
单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的,这个限制就是cookie的域(通常对应网站的域名),浏览器发送http请求时会自动携带与该域匹配的cookie,而不是所有cookie
为什么不将web应用群中所有子系统的域名统一在一个顶级域名下,例如“*.baidu.com”,然后将它们的cookie域设置为“baidu.com”,这种做法理论上是可以的,甚至早期很多多系统登录就采用这种同域名共享cookie的方式。
然而,可行并不代表好,共享cookie的方式存在众多局限。首先,应用群域名得统一;其次,应用群各系统使用的技术(至少是web服务器)要相同,不然cookie的key值(tomcat为JSESSIONID)不同,无法维持会话,共享cookie的方式是无法实现跨语言技术平台登录的,比如java、php、.net系统之间;第三,cookie本身不安全。
单点登陆
单点登录英⽂全称Single Sign On,简称就是SSO。它的解释是:在多个应⽤系统中,只需要登录⼀次,就可以访问其他相互信任的应⽤系统。
sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权
共享session的方式
将session写入redis中, 然后登陆不同的系统中, 都根据cookied的sessionId完成校验.
这样求存在redsi中的内容, 必须能被所有系统都正确解析获得. 例如php啊, .net啊, 之类都可能不同
令牌授权的方式
如果未登陆的用户, 在访问应用的地址时, 会自动跳转到一个单点的服务完成登陆操作, 然后获取到一个令牌, 该令牌在访问应用时会被校验(通过用户服务进行校验), 如果有效, 就不用登陆就可以访问, 如果无效就需要重新登陆.
令牌可以包含过期时间, 通过私钥揭秘后能得到签名时间等信息, 来进行是否过期的判断.
用户是否是已注册的用户
通过校验用户的账号密码和数据库中的账号密码是否一致, 如果是手机验证码登陆的话, 可以在获取验证码的时候, 就将该手机的相关信息存到redis中, 这样用户在验证码通过后, 就可以直接生成相应的session或者token了.
bigdecimal的底层原理, 为啥没有精度问题
float double为啥不能用来做精密计算
float和double类型的主要设计目标是为了科学计算和工程计算。他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。
浮点数是什么?
浮点的意思就是小数点不在它真正的位置, 而是只在第一个数字的后面.
绝大多数现代的计算机系统采纳了所谓的浮点数表达方式。这种表达方式利用科学计数法来表达实数,即用一个尾数(Mantissa ),一个基数(Base),一个指数(Exponent)以及一个表示正负的符号来表达实数。
比如7.823E5 = 782300 这里E5表示10的5次方,再比如54.3E-2 = 0.543这里E-2表示10的-2次方.
这里的尾数是7.823, 基数就是E, 代表10, 5就是指数.
浮点数表示为啥会有问题?
计算器存储的时候, 都存储的是二进制数, 但是一个小数, 对应的二进制数, 它的位数不一定是有限的.
能转化成有限二进制数的小数
如果一个小数能分解为以(1/2)^n为单位的十进制小数,可以转化为有限位数的二进制小数。
如十进制数:13/16=0.8125,它可以是拆成:13/16=1/2+1/4+1/16,或者直接可以看作是13个1/16所组成。而1/2,1/4,1/16这些数都是符合(1/2)^n形式的数。
所以13/16转化为4位二进制小数:0.1101。
不能转化成有限二进制数的小数
如十进制小数0.7,转化为二进制小数是:0.1011001100110......,循环节是0110。 计算机无法保存循环节, 因此就必然会截断, 导致精度丢失的问题
因此说明float和double在计算的时候, 都可能存在精度丢失的问题, 因此无法拿来做金额计算.
bigDecimal选用哪个构造函数
bigDecimal支持两种构造函数, 一种入参是double类型的, 如果此时double的值在计算机中已经不是一个准确的值, 那转成bigDecimal也没用.
另一种入参是字符串类型的, 这种才是应该选择的方式.
bigdecimal做了什么?
BigDecimal的原理很简单,就是将小数扩大N倍,转成整数后再进行计算,同时结合指数,得出没有精度损失的结果.
使用十进制(BigInteger)+ 小数点位置(scale)来表示小数.
也就是100.001 = 100001 * 0.1^3。这种表示方式下,避免了小数的出现,当然也就不会有精度问题了。十进制,也就是整数部分使用了BigInteger来表示,小数点位置只需要一个整数scale来表示就OK了。
也就是计算的时候都是整数之间的计算, 然后再乘上相应的小数位数;
在加减乘除的时候, 也是得到整数的结果, 再乘上应该有的小数位数.
BigInteger在底层是用byte[]数组来存储的, 因此可以存储无限长度的数字.
bigDecimal的使用
比较的时候, 使用compareTo方法.
BigDecimal都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。例如单纯的b1.add(b2), 此时b1的值是不变的, 必须是b1 = b1.add(b2);
设计一个秒杀系统需要考虑哪些点?
(1) 前端设置静态页面的部分, 在请求的时候只对一些必须的动态数据进行请求. 例如库存量等.
(2) 库存扣减和查询通过redis来进行, 提前将数据加载到redis中, 利用缓存的原子特性保证同时只有一个线程操作库存, 例如分布式锁等.
或者使用Lua脚本, 使得redis的查询和扣减命令让redsi挨着执行, 这样就不会出现超卖的情况. 实现了类似CAS操作.
(3) 限制用户的请求次数, 不能连续点击, 例如必须刷新后点击, 或者间隔5s点击, 除了前端控制以外, 后端也可以通过redis, 设置一个5s过期的key, 如果该key存在, 不接受该用户的请求
(4) 具体的订单数据可以放入消息队列异步慢慢改变数据库. 使得数据库和缓存达到最终一致性.
(5) 设置服务降级的策略, 同时也可以把秒杀的服务器和其它服务器隔离开, 这样秒杀压力过大也不会影响正常的服务. 做壁仓隔离
(6) 如果无法使用redis做库存扣减功能的话 segmentfault.com/a/119000002…
对数据库进行分库分表, 将秒杀商品放到单独的数据库表中, 然后在应用层通过加入分布式锁等机制, 来控制对db的访问量.
分库分表
分库分表就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成 ,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。
垂直分表
将表按照字段分成多表, 即将一张表 其中几个字段单独提取出来作为一个新表. 例如将订单详情从订单表中提取出来作为单独一个表.
垂直分库
将本身在一个库里的表, 拆分到两个库中, 不同数据库部署在不同的服务器上.
水平分库
将库中的表, 按照规则, 拆分成两个库, 每个库中, 有一个同样的表, 只是数据不同.
水平分表
将一张表里的数据, 拆分成两个表, 两个表结构相同, 但是内容不同.
分库分表后的路由规则
分库分表后如何查询
如果客户端能够在查询的时候是知道具体是哪张表, 就简单了.
www.jianshu.com/p/f81422b1c… 如果不行的话, 可以使用类似Mycat的东西, 将分库视为同一个数据库.
关于分库分表,Mycat已经帮我们在内部实现了路由的功能,我们只需要在Mycat中配置以下切分规则即可,对于开发者来说,我们就可以把Mycat看做是一个数据库
分布式的大量日志中, 找到频次前100条数据
(1) 哈希分治 + 每个小文件内利用hashMap统计每个出现的频次, 然后取频次前100的数据, 接着将每个文件前100的数据, 利用大小为100的小顶堆合并, 得到最终频次前100的数据
(2) 可以写2个MapReduce,第一个MapReduce统计每个姓名出现的频率,第二个MapReduce利用Shuffle Sort 将姓名和出现频率当作KEYr然后取Top N .
(3) 利用前缀树, 在需要统计的时候遍历一遍前缀树中所有的节点, 然后返回统计的数量, 再进行一个排序.
延迟队列
应用场景
下单后,30分钟内未付款就自动取消订单等; 支付后,24小时未评论自动好评;
基于java延时队列DelayQueue实现
DelayQueue是一个BlockingQueue(无界阻塞)队列,它本质就是封装了一个PriorityQueue(优先队列),我们在向DelayQueue队列中添加元素时,会给元素一个Delay(延迟时间)作为排序条件,队列中最小的元素会优先放在队首。队列中的元素只有到了Delay时间才允许从队列中取出。
DelayQueue的put方法是线程安全的,因为put方法内部使用了ReentrantLock锁进行线程同步。DelayQueue还提供了两种出队的方法poll()和take() , poll()为非阻塞获取,没有到期的元素直接返回null;take()阻塞方式获取,没有到期的元素线程将会等待。
基于定时任务实现
(1) 可以利用Scheduled注解, java服务在运行时, 不断执行定时任务, 一旦java服务不在运行状态, 定时任务就会失效
(2) 利用单独部署的任务中心, 来执行任务调度.
基于redis zset实现
Redis由于其自身的Zset数据结构,也同样可以实现延时的操作。 Zset本质就是Set结构上加了个排序的功能,除了添加数据value之外,还提供另一属性score,这一属性在添加元素时候可以指定,每次指定score后,Zset会自动重新按新的值调整顺序。
(1) 如果score代表的是想要执行时间的时间戳,在某个时间将它插入Zset集合中,它会按照时间戳大小进行排序,也就是对执行时间前后进行排序。
往delay_queue这个zset集合中, 添加一个分值为100的, 叫小明的对象 ZADD delay_queue 100 小明
(2) 不断地进行取第一个key值,如果当前时间戳大于等于该key值的socre就将它取出来进行消费删除,就可以达到延时执行的目的。 注意不需要遍历整个Zset集合,以免造成性能浪费。
ZRANGE delay_queue 0 0 withscores 查询得到delay_queue这个zset集合中, 按照分值升序的第一个元素的对象以及分值.
ZREM delay_queue 小明 根据对象名 移除对象和分值
注意这里的查询判断和移除可以用LUA脚本来保证一起执行.
需要一个线程不断的去访问消息zset集合
redis 的过期回调
Redis的key过期回调事件,也能达到延迟队列的效果,简单来说我们开启监听key是否过期的事件,一旦key过期会触发一个callback事件。
需要两个操作
(1) 创建RedisListenerConfig作为配置bean, 添加到容器中
(2) 创建事件监听类RedisKeyExpirationListener, 当redis某个key失效后, 就会触发RedisKeyExpirationListener中的回调方法.
RedisKeyExpirationListener在继承时, 需要传入RedisListenerConfig类
RabbitMQ 实现延迟队列
RabbitMQ支持为消息设置过期时间, 当消息过期后, 会被探测到, 然后可以路由转发到另一个消息队列中.
因此我们只需要两个队列, 第一个队列作为延迟队列, 专门用来让消息过期, 然后这些过期的消息被探测到后, 自动转发到另一个消息队列中被正常消费.
TTL: 指的是消息的存活时间,RabbitMQ可以通过x-message-tt参数来设置指定Queue(队列)和 Message(消息)上消息的存活时间,它的值是一个非负整数,单位为微秒。
DLX: 即死信交换机,绑定在死信交换机上的即死信队列。RabbitMQ的Queue(队列)可以配置两个参数x-dead-letter-exchange和x-dead-letter-routing-key(可选),一旦队列内出现了Dead Letter(死信),则按照这两个参数可以将消息重新路由到另一个Exchange(交换机),让消息重新被消费。
时间轮实现延迟队列
这里通过介绍kafka的时间轮的时间, 来介绍时间轮是个什么东西, 其实本质就是一个定时任务触发的功能, 目前java没有现成的时间轮的库, 如果需要使用, 需要自己实现.
kafka的时间轮
kafka本身就支持延迟生产消息, 延迟拉取消息, 延迟删除消息等功能, Kafka 并没有使用JDK自带的Timer 或DelayQueue来实现延时的功能,而是基于时间轮的概念自定义实现了一个用于延时功能的定时器(SystemTimer)。
时间轮的应用并非Kafka独有,其应用场景还有很多,在Netty、Akka、Quartz、Zookeeper等组件中都存在时间轮的踪影。
时间轮是多层的, 第一层的时间轮是真正被持有的时间轮, 其它高层的时间轮是由底层的时间轮创建被持有的.
第一层时间轮, 每一个间隔是1ms, 整个时间轮的大小为固定数目, 假设为20, 那么第一层时间轮就能囊括0 - 20s, 然后第二层时间轮每一个间隔就是第一层时间轮的整体时间, 即为20s, 同时第二层也是大小为20, 第二层就能囊括0 - 400ms, 第三层的话, 每一个间隔就是400ms.
第一层时间轮的0位置就是currentTime, 然后整体时间轮囊括的时间范围就是currentTime + 最高一层时间轮的范围.
具体的定时任务, 就会被插入到时间轮的对应的间隔中, 每个间隔会保存一个双向链表, 链表的每个节点就是一个定时任务. 当高层的时间轮, 推进到当前间隔的时候, 需要执行对应时间间隔的链表任务, 当发现定时任务的时间还没到的时候, 会对该任务降级, 放入到底层的时间轮中, 因此最终真正执行的定时任务, 一定是在第一层时间轮被检查到的.
时间轮中的延迟队列
时间轮上可能很多间隔都是没有定时任务的, 如果时间轮一直一个间隔一个间隔的运行的话, 其实有很多情况都是在空推进.为了解决这个问题, kafka额外维护了一个jdk的延迟队列delayQueue, 延迟队列中保存的是时间轮中的每个间隔, 每个间隔会有个延迟时间, 延迟队列会对间隔进行排序,
在新建定时任务的时候, 会往对应间隔中添加定时任务, 同时也会将该间隔添加到延迟队列中delayQueue.
会有一个线程, 不断的从延迟队列中, 获取头部的间隔, 然后判断其中的定时任务是否到期, 如果到期了, 要么将一些任务进行时间轮的降级, 要么就说明该任务到期了(例如第一层时间轮的间隔), 就直接执行该任务.
然后推进时间轮的指针, 即推动时间轮的时间.
时间轮的优点
(1) 相比于常用的DelayQueue的时间复杂度O(logN),TimingWheel的数据结构在插入任务时只要O(1),获取到达任务的时间复杂度也远低于O(logN)。
(2) 同时多层时间轮的设计, 对于时间很长的定时任务, 也能完成很好的映射, 只需要5层时间轮,可表示的时间跨度已经长达24年(216000小时)。
ThrealLocal底层实现
首先ThreadLocal类是一个线程数据绑定类, 它可以将数据绑定到自己的线程上, 每个线程, 通过调用threadLocal.set(String value)的方法, 可以将这个value数据绑定到当前线程上, 通过threadLocal.get()方法, 获取绑定的值, 注意一个threadLocal对象, 只能在当前线程上绑定一个value.
实际上这里的value绑定, 并不是把threadLocal实例的某个属性设置为value, 而是将threadLocal对象作为key, value作为值存到一个线程独有的map中.
(1) 每个Thread类拥有自己的ThreadLocalMap<ThreadLocal, value>, 每个线程通过map管理多个threadLocal 和对应的value值.
(2) threadLocal的set方法? 本质是获取当前线程的ThreadLocalMap, 然后将threadLocal和value作为一个entry添加进去
(3) threalLocal的get方法? 获取当前线程的ThreadLocalMap, 然后获取key为threalLocal的value
(4) threalLocal为什么同一个对象可以作为不同线程的key? threalLocal对象通常作为某个类的静态变量, 然后在不同线程里添加不同的value, 这就是因为threalLocal只是作为key, 然后每个线程自己的map添加的是不同的value, 因此多个线程可以使用同一个threalLocal对象.
(5) ThreadLocalMap为啥在存储thralLocal的时候要用WeakReference? 设计目的是为了不造成内存泄漏
ThreadLocalMap 在存储key的时候, 采用WeakReference指向对象, 这样在没有强引用指向threadLocal对象的时候, 在GC的时候, 该threalLocal对象就会被回收. 这是这里采用虚引用的初衷, 但是有个前提, 就是threalLocal没有强引用指向了(但是4中刚说了threalLocal对象通常会作为类的静态变量, 那么就一直会有强引用指向它, 这种情况下虚引用就无法保证回收了)
因此只有当每个线程里创建threadLocal对象的时候, map的虚引用设计才能避免内存泄露?
弱引用带来的内存泄漏问题, 当ThreadLocal对象如果没有强引用指向它的时候, 那么在gc的时候就会被回收, 此时就在每个线程threadLocalMap存在一个key为null的entry, 此时如果线程一直不结束, 那么这个value就一直无法被gc掉, 因此就出现了内存泄漏.
(6) 为啥线程在确定不再需要使用threalLocal对象后, 需要主动调用remove()方法.
remove()方法就是删除当前线程内ThreadLocalMap的对应entry. 目的主要有两个
第一是为了避免5中的内存泄漏的情况
第二是因为通常都会采用线程池技术, 这样一个线程不会自然结束, 那么它的threalLocalMap中的值就不会被清空, 如果线程拥有的map的threalLocal没有清除, 就可能造成误用或者逻辑混乱的问题.
kafka如何避免reblance
简单来说,会导致崩溃的几个点是:
消费者心跳超时,导致 rebalance。 消费者处理时间过长,导致 rebalance。
kakfa如何查看消息堆积的情况
kafka的集群管理用的zookepeer!!! redis用的不是
redis集群管理
redis在持久化的时候, 启动的是线程还是进程
是fork 出来一个进程, 是利用copy on write的特性, 只是将页表复制并且加上标记, 而不是复制父进程的全部内存.
aof模式, 恢复数据到内存中的时候, 采用的是伪客户端的形式
redis在启动的时候会伪造一个客户端, 然后不断执行AOF命令, 完成redis服务器的重建
redis 内存管理
删除过期键
定时: 为每个设置了过期时间的键设置了定时器 惰性: 不会主动去删除, 在使用某个key的时候, 判断是否过期, 如果过期则删除 定期: 定期去检查key是否过期, 然后删除
当内存达到 maxmemory 时触发内存移除控制策略,强制删除选择出来的键值对象
提供了以下8种方法:
1)noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返 回客户端错误信息(error)OOM command not allowed when used memory,此 时Redis只响应读操作。
2)volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直 到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
3)allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性, 直到腾出足够空间为止。
-
volatile-lfu -> 根据LFU算法删除设置了超时属性(expire)的键,直 到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
-
allkeys-lfu -> 根据LFU算法删除键,不管数据有没有设置超时属性, 直到腾出足够空间为止。
6)allkeys-random:随机删除所有键,直到腾出足够空间为止。
7)volatile-random:随机删除过期键,直到腾出足够空间为止。
8)volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略
http2.x 有啥缺点
http 2.x 解决了http协议自身的队头阻塞的问题, 但是并没有解决tcp阻塞的问题, 在tcp重传的时候, 依旧会阻塞数据. 并且因为 2.x 只使用一个TCP连接, 而1.x 可以最多建立6个TCP连接.
因此发生TCP阻塞的时候, 2.x性能可能还不如1.x
java对象头包括哪些
对象头中包括markword和classs指针
注意java对象是由三部分组成, 对象头, 实例数据, 填充对齐. 不要把填充对齐记到对象头中了
svg模板如何使用
svg使用xml格式定义图像, 符合w3c标准. 属于开源的格式
可以用org.w3c.dom 将svg模板解析成一个document, 然后就可以按照层级访问到里面的每个元素.
就是将XML看做是一颗树,DOM就是对这颗树的一个数据结构的描述
状态机选用时有哪些比较吗?
静态状态机的好处
(1) 在进入一个状态和退出一个状态的时候, 都可以执行相应的方法
(2) 有人在维护, 逐步更新的
(3) 使用简单, 编写脚本即可生成相应的代码. 使用的时候改造一下相应的状态枚举类即可
(4) 可以生成图片
(5) 其它的状态机需要按照要求自己实现代码
redis实现消息队列
lpush 和 brpop结合
生产者通过lpush往消费队列中添加消息
消费者通过brpop阻塞式的从队列中读取消息.
BRPOP key [key ...] timeout
BRPOP就类似redis读取, 为了减少空消息队列频繁的轮询, 使用阻塞式命令并设置超时时间, 阻塞读在队列没有数据时会立即进入休眠状态,一旦数据到来则立即被唤醒,消息的延迟几乎为零。在等待超过超时时间后, 才会断开这次redis连接.
优点
实现简单 Reids支持持久化消息,意味着消息不会丢失,可以重复查看(注意不是消费,只看不用,LRANGE类的指令)。 可以保证顺序,保证使用LPUSH命令,可以保证消息的顺序性 使用RPUSH,可以将消息放在队列的开头,达到优先消息的目的,可以实现简易的消息优先队列。 这种方案相对于发布订阅模式的好处是数据可靠性提高了,只有在Redis宕机且数据没有持久化的情况下会丢失数据。可以根据业务通过AOF和缩短持久化间隔来保证较高的可靠性,也可以通过多个客户端来提高消息速度。
缺点
做消费确认ACK比较麻烦,就是不能保证消费者在读取之后,未处理后的宕机问题。导致消息意外丢失。通常需要自己维护一个Pending列表,保证消息的处理确认。 不能做广播模式,例如典型的Pub/Discribe模式。 不能重复消费,一旦消费就会被删除 不支持分组消费,需要自己在业务逻辑层解决
hashMap 有几种构造函数
(1) 无参的 (2) 指定初始容量的 (3) 指定初始容量和负载因子的
遍历map的几种方式
(1) foreach 利用Map.Entry<>, 注意遍历的是map.entrySet()
for(Map.Entry<String, String> entry : map.entrySet()) {
}
(2) 只遍历key Map.keySet()
(3) 只遍历value Map.values()
(4) java 8 lamba表达式的.foreach((k, v) -> )的写法
ForkJoinPool
ForkJoinPool是自java7开始,jvm提供的一个用于并行执行的任务框架。其主旨是将大任务分成若干小任务,之后再并行对这些小任务进行计算,最终汇总这些任务的结果。得到最终的结果。其广泛用在java8的stream中。
这个描述实际上比较接近于单机版的map-reduce。都是采用了分治算法,将大的任务拆分到可执行的任务,之后并行执行,最终合并结果集。区别就在于ForkJoin机制可能只能在单个jvm上运行,而map-reduce则是在集群上执行。
此外,ForkJoinPool采取工作窃取算法,以避免工作线程由于拆分了任务之后的join等待过程。这样处于空闲的工作线程将从其他工作线程的队列中主动去窃取任务来执行。
ForkJoinPool中每个任务就是一个线程
工作窃取
每个工作线程都维护一个双端队列, 自己获取任务的时候, 通过队尾获取, 如果有线程处理完自己的任务, 会去别的线程队列头部获取一个任务来处理.
注意任务添加进队列的时候, 是添加到队列的尾部, 因此当前线程是优先处理新添加的任务的.
如何使用
实现Fork-join框架有两个类,分别是ForkJoinPool以及提交的任务抽象类ForkJoinTask。对于ForkJoinTask,虽然有很多子类,但是我们在基本的使用中都是使用了带返回值的RecursiveTask和不带返回值的RecursiveAction类。
ForkJoinPool pool = new ForkJoinPool();
pool.submit(new PrintTask(1,50));
pool.awaitTermination(2, TimeUnit.SECONDS);
pool.shutdown();
PrintTask继承自RecursiveAction需要实现compute方法, 具体的任务分割和任务计算的逻辑就在这个方法中, 如果任务规模大, 就会再次创建多个PrintTask, 然后调用invokeAll方法再次尝试执行.
也就是任务执行的时候是逐渐分裂成多个子任务的, 每次只能分割成两个, 而不是一次性分裂的. 最后的结果会join起来. 就类似join方法会等待, 例如任务1 分裂成任务2和任务3, 那么任务1会一直等待任务2, 任务3执行完毕.
@Override
protected void compute() {
if(end - start < THRESHOLD) {
for(int i=start;i<=end;i++) {
System.out.println(Thread.currentThread().getName()+",i="+i);
}
}else {
int middle = (start + end) / 2;
// 拆分成两个任务
PrintTask firstTask = new PrintTask(start,middle);
PrintTask secondTask = new PrintTask(middle+1,end);
invokeAll(firstTask,secondTask);
}
}
拆分的时候都是一个线程新建两个线程, 然后两个线程, 每个再新建两个线程执行任务.
ParallelStreams
java 8中stream的一个方法.
ParallelStreams使用JVM默认的forkJoin框架的线程池由当前线程去执行并行操作
但是需要注意的是, 同一个jvm中, 不同的ParallelStreams执行的时候, 使用的都是同一个ForkJoinPool, 因此就存在互相之间线程抢占的问题,
所有使用parallel streams的程序都有可能成为阻塞程序的源头,并且在执行过程中程序中的其他部分将无法访问这些workers,这意味着任何依赖parallel streams的程序在什么别的东西占用着common ForkJoinPool时将会变得不可预知并且暗藏危机.
垃圾收集器
垃圾收集算法只是垃圾回收的方法论, 但是垃圾收集器, 才是垃圾回收的真正执行者.
STW是什么?
不管选择哪种GC算法,stop-the-world都是不可避免的。
Stop-the-world意味着从应用中停下来并进入到GC执行过程中去。
一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。
GC调优通常就是为了改善stop-the-world的时间
GC需要一个安静的状态来完成垃圾的回收,所以需要将用户线程停止,完成垃圾回收后,再继续用户线程
CMS收集器(标记-清除算法)
CMS收集器仅适用于老年代的垃圾收集器.
需要搭配搭配Serial垃圾收集器, Serial用来收集年轻代的, 利用的都是复制算法.
以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
a)初始标记 单线程 会停顿程序, 停顿时间很短, 只标记直接和GC Roots相连的对象
b)并发标记 并发标记, 从初识标记的对象出发, 递归标记所有可达的对象, 比较耗时
c)重新标记 单线程的, 修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记时间短.
记录下这段时间内由于用户线程的操作, 导致变成可达的对象, 对他们进行标记, 如果是可达变成不可达, 不会去消除标记.
因此就保证了所有可达的一定不会被删除, 这是最重要的.
d)并发清除 并发的对于没有标记的对象进行清除
优点
(1) 并发标记和并发清除阶段, 是用户线程一起参与并发执行的, 大大降低了耗时
由于在整个过程和中最耗时的并发标记和 并发清除过程收集器程序都可以和用户线程一起工作,所以总体来说,Cms收集器的内存回收过程是与用户线程一起并发执行的
缺点
(1) 因为是并发执行, 所以对cpu资源比较敏感.
(2) 在重新标记和并发清除的过程中, 如果有对象从可达变成不可达了, 这就叫浮动垃圾, CMS不会去管这部分垃圾, 此时回收就会放弃对他们的回收, 等待下次GC的时候再回收.
(3) 标记-清除算法会产生大量内存碎片
G1收集器(标记-整理)
G1收集器不仅适用于老年代, 也适用于年轻代.
G1垃圾回收器把堆划分成一个个大小相同的Region。在HotSpot的实现中,整个堆被划分成2048左右个Region。每个Region的大小在1-32MB之间,具体多大取决于堆的大小。
1、初始标记(stop the world事件 CPU停顿只处理垃圾); 同CMS
2、并发标记(与用户线程并发执行);(不会触发stop the world事件) 同CMS
3、最终标记(stop the world事件 ,CPU停顿处理垃圾); 同CMS
4、筛选回收(stop the world事件 根据用户期望的GC停顿时间回收); (注意:CMS 在这一步不需要stop the world) 最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
也就是G1在最终回收的时候, 是以region为最小单位的. 因此不会产生内存碎片.
AQS 用的CAS + volatile来实现的锁的功能
AQS 的全称是 AbstactQueuedSynchronizer 即抽象队列同步器。
java并发包下很多API都是基于AQS来实现的加锁和释放锁等功能的,AQS是java并发包的基础类。比如:ReetrantLock ,ReentrantReadWriteLock 都是基于AQS来实现的。
ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。
ReentrantLock 实现加锁和锁释放就是通过AQS来实现的。
lock方法会发生什么
AQS 中维护了一个很重要的变量 state, 它是int型的,表示加锁的状态,初始状态值为0, 用volatile修饰;
state其实就是相当于被加锁的对象.
另外 AQS 还维护了一个很重要的变量exclusiveOwnerThread, 是一个thread的引用,它表示的是获得锁的线程,也叫独占线程。AQS中还有一个用来存储获取锁失败线程的队列,以及head 和 tail 结点,
(1) 线程A执行reentrantLock.lock()的时候, 就会采用CAS操作去修改satae从0变成1, 如果state已经不是0了, 表示锁被其它线程占用, CAS操作就会失败. 如果CAS操作成功, 就表示获得了这把锁, 同时exclusiveOwnerThread修改为当前线程.
(2) 如果线程A没有释放锁, 继续去执行另一个需要加锁的代码块, reentTrantLock通过比较exclusiveOwnerThread是否是当前线程, 来判断是否是重入.
(3) 如果此时其它线程尝试获得锁, 但是state不为0, 此时CAS操作就会失败, 会被放入到等待队列中.
unlock方法
(1) 判断调用unlock方法的线程是否是exclusiveOwnerThread对应的线程
(2) 将state的值减为0, 同时将exclusiveOwnerThread设置为null
(3) 唤醒等待队列中的头部对象, 让他去尝试获取锁.
公平锁和非公平锁的区别
公平锁: reentrantLock释放锁后, 别的线程在获取锁, 调用tryAcquire方法的时候, 会先去判断等列中是否有等待的线程, 如果有, 则先将锁分配给等待的线程. 将当前来竞争的其它锁, 按照顺序添加到队列中.
非公平锁: 调用tryAcquire方法的时候, 并不会优先安排队列中的线程, 而是让队列中被唤醒的线程和前来竞争获取锁的线程共同竞争, 这样新来的线程就有可能插队.
公平锁情况下, 能够保证每个线程都能够得到执行, 非公平锁就有可能饿死某些线程.
非公平锁的效率略高于公平锁, 因为有些线程可以不入队列就获取到锁.
NoClassDefFoundError和ClassNotFoundException有什么区别?
NoClassDefFoundError产生的原因
NoClassDefFoundError是一种致命错误。
当JVM尝试通过new关键字创建一个类实例或者方法调用来加载一个类时找不到这个类的定义就会出现这个错误。
通常是编译时正常编译,但是运行时找不到这个类。
通常发生在执行动态代码块或者初始化静态字段时报了异常,从而导致类初始化失败而引发NoClassDefFoundError。
例如我创建一个类的时候, 类有个一个属性static int data = 1 / 0; 然后new的时候去创建, 但是因为属性会抛出异常, 导致创建这个类的时候, 找不到这个类的定义. 就会报这个错误.
ClassNotFoundException
当应用尝试在类路径中用全限量名去加载某个类时,如果找你不到它的定义就会报CLassNotFoundException 。它是一个可检测异常。
通常出现在用Class.forName(), ClassLoader.loadClass()或 ClassLoader.findSystemClass()这三个方法加载类的时候。我们在使用反射的时候,要特别注意这个异常。
就是我们尝试去加载一个类的时候, 类并不在classPath下面.
策略模式
实现某一个功能有多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能。
如何实现
(1) 每个策略的实现类都需要实现一个Strategy接口, 表示当前类为策略类, 同时添加一个注解, 标记策略的关键词.
(2) 定义一个策略选择器的类, 里面维护一个routeMap负责路由, 维护策略关键词和策略实现类的关系.
(3) 使用的时候根据策略关键词, 选择不同的策略实现类返回, 返回对象为Strategy
观察者模式
观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
通知方式
被观察者会维护一套观察者实例对象的集合, 当被观察者发生变化的时候, 就会依次通知自己维护的观察者对象. 共同实现某个方法
发布订阅模式
和观察者模式不同, 发布者并不清楚订阅者有哪些, 发布者发布消息后, 由第三方负责将消息通知订阅者, 就像项目中的eventlistener一样.
CAS操作的三个问题
CAS操作的ABA问题
利用版本号
CAS操作的自旋
破坏掉for死循环,当超过一定时间或者一定次数时,return退出
CAS只能单变量
利用锁 或者将多个变量封装成一个变量
缓存相关
缓存穿透
即缓存无法命中, 每次都去查库
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
加锁的方案
加锁指的是对某个redis的key, 在java层面进行加锁, 这样对于某个实例来说, 并发的对同一个key的查询, 就会变成串行, 就不会对数据库造成太大的压力, 同时也不会影响redis的缓存值的写入. 注意加锁的代码里面需要在此查询redis, 这样可以使得当第一个查询写入缓存后, 其它等锁的线程, 获得锁后, 能够直接得到返回, 而不用去查询数据库
public object GetProductListNew()
{
const int cacheTime = 30;
const string cacheKey = "product_list"; // 对key加锁
const string lockKey = cacheKey;
var cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null)
{
return cacheValue;
}
else
{
lock (lockKey)
{
cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null)
{
return cacheValue; // 加锁的代码中, 需要再次尝试获取redis的值
}
else
{
cacheValue = GetProductListFromDB(); //这里一般是 sql查询数据。
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
}
}
return cacheValue;
}
}
缓存击穿(针对热点数据过期的问题)
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
(1) 使用分布式锁
利用一个分布式锁, 当缓存失效的时候, 并不是全部去从数据库加载数据, 而是先尝试获得分布式锁, 如果获取到了, 采取从数据库加载数据并添加到缓存中, 而其它的请求则是重试获取缓存值
(2) 缓存不失效
对于热点数据, 定时刷新缓存中的值, 就存在可能访问到旧数据的可能, 但是是可以容忍的.
热点key
比如在双11的时候有一些非常火爆的商品,用户同时抢购这些商品。这时就会有十几万甚至甚至更大的请求去访问redis上的某个特定key。
而redis集群在选择具体使用哪台redis server的时候, 是根据算法来的, 因为Redis集群内置插槽为16384个,所以Redis会将每个键的键名的有效部分使用CRC16算法计算出散列值,然后对16384的取余。余数为多少就表示该键应该被分配到哪个节点,这样的话,每个键都可以被分配到16384个插槽中,而集群中的每个节点都会被分配一定的插槽。 因此相同的key很有可能访问的就是那几台server.
大量的请求会造成流量过于集中,达到例如Redis单实例瓶颈(一般是10W QPS级别),或者物理网卡上限,从而导致这台redis的服务器Hold不住,甚至压垮整个缓存服务。
如何发现热点key
(1) 业务逻辑上的预估
(2) 抓包进行评估:Redis使用TCP协议与客户端进行通信,通信协议采用的是RESP,所以能进行拦截包进行解析
(3) Redis自带命令查询:Redis4.0.4版本提供了redis-cli –hotkeys就能找出热点Key
解决方案
(1) 对于很热点的数据, 在服务器内存中进行缓存. 避免使用redis.
同时服务器可以对于redis中的热点数据进行监听, 一旦热点数据发生变化, 同步更新内存中的值, 对于redis数据的监听, 是redis支持的. 利用MessageListener
(2) 备份热点key, 即通过热点key + 随机数的方式, 这样redsi中就存在了相同value, 不同key的多分数据, 这样热点数据在被访问的时候, 请求不会被打到同一台redis实例上. 但是请求的时候 请求的也是不同的key,
mysql like走索引吗
like %a% 不走索引
like %a 不走索引
上面两个不走索引的原因是, % 在开头, mysql无法判断索引是否符合.
like a% 走索引
如果想要%a%的效果, 同时又要走索引怎么办
使用POSITION方法, POSITION('substr' IN field), 返回的是substr在属性中的位置, 如果不存在, 就返回0;
redis value的最大值
最大值512M.
redis hash的key 最多有多少个field - value
最多有 2^32 - 1个, 即40多亿个
redis 大key
redis为啥不能存大key
redis 大key多key拆分方案
multi 就是redis开启事务的语法, 开启事务后, 就可以查询多个redis key, 然后将结果组合, 就可以解决一个key存储一个很大的value, 拆分成多个k - v之后的查询问题.
multi事务只能保证一次提交多个命令, 其中任何一个命令执行失败, 都不会影响其它的执行结果.
现在有一个文件10G,但是内存只有1G可以进行排序,排完后放到一个10G的硬盘中,怎么办?
利用外排序, 首先将10G大小的文件拆分成15个小文件(不能刚好拆分成10个, 因为程序运行也需要内存, 不能光考虑数据的大小), 这里拆分的时候, 直接按大小拆分即可, 无需根据hash函数, 因为只是根据数字大小来比较.
拆分完后, 每个文件都可以读入内存, 然后在内存中利用快排进行排序, 将排序后的结果写入一份新的文件.
将15个文件排序后的结果, 进行归并的合并. 例如按照升序排列, 15个文件的第一个数字就是每个文件的最小值, 记录15个文件的指针, 取出第一个元素, 比较15个数字的大小, 假设第3个文件元素最小, 则第3个文件的指针 + 1, 然后第三个文件取出第二个元素和其它14个文件的第一个元素比较, 得到最小值.
每次比较的结果写入一个新文件, 归并完成后, 所有元素排序完成
现在有一个文件10G,但是内存只有1G,寻找其中最大的100个数字?
同样将文件进行拆分成15份, 然后用第一个文件, 构建一个大小为100的小顶堆, 依次遍历剩下的所有文件的元素, 如果比小顶堆堆顶顶元素大, 则取代堆顶顶元素, 然后调整一次堆. 比较完所有的元素后, 堆里的100个元素就是最大的.
如果文件是可以放入内存的话, 可以采用快排的思想进行分区, 然后缩小问题规模, 寻找前100大的数字
两个栈实现最小值栈?
入栈的时候: (1) A正常入栈 (2) 比较入栈元素和B的栈顶元素的大小, 如果比B栈顶的大, 那么将B栈顶的元素复制一份, 放入B (3) 如果比B栈顶的元素小, 则将元素直接放入B
例如 2要入栈, 此时A是2, B是2 3要入栈, 此时A是3, 2 B是2, 2 1要入栈, 此时A是1, 3, 2 B是1, 2, 2
出栈的时候: (1) A出栈, B也出栈
获取最小值的时候: (1) peek B栈顶的元素即可, 注意获取最小值, 并不需要弹出元素.
@Transactional失效的场景
hdfs系统的架构和简介
hdfs 就是一个分布式文件存储系统, 一个很大的文件, 单机可能无法存储完, 就分布式的存储在不同的节点上, 每个节点的数据都会有备份;
hdfs写入和读取数据的流程
平衡二叉树的使用场景
平衡的二叉树在查找的时候, 性能稳定, 不会出现一边很重的情况, 通常会和查找树一起工作
java byte转字符串
byte
byte对应的是8位二进制数, 对应的10进制数的范围在-128 - 127, 对于16进制数来说就是两位16进制.
在文件读取或传输的时候, 读取的都是byte类型的数据, 本身byte对应的就是数字, 能展示字母啊, 中文啊, 都是编码后的结果.
byte是8位, 为啥只能表示127?
www.cnblogs.com/zl181015/p/… 因为最高位是符号位, 为0表示正数, 为1表示负数
负数的值是 取反后加一 然后加个负号得到得值.
比如:10000001.最高位是1 为负数,值是多少?取反得到 01111110 加1 得到 01111111 ,那么值为 -127
不难理解,byte的最大正数就是 01111111(最高位必须是0),也就是 127。
那么你可能会想 byte的最小负数就是 11111111 了,对不对? 这么想就
大错特错了。让我们看看11111111这个二进制数表示多少。
根据上面的提示 我们知道这是一个负数。它的值是先取反再加1 。
11111111取反得到:00000000,加1得到 00000001 。最后得到的值为-1.
这可是最大的负数啊。由此你是不是想到了最小的负数会不会是10000000呢?
让我们算一下 取反:01111111 加1得到 10000000 最后得到 -128.
127是01111111 然而 -128是10000000.
如果将byte对应的数字变成字符串呢
(1) 先转成16进制的Integer的数字类型
// 自动向上转型 自动从byte转成了int类型, xt[i] & 0xff
(2) 再转成字符串.
利用的Integer.toHexString()方法, 此方法返回的字符串表示的无符号整数参数所表示的值以十六进制(基数为16)。
为啥要把Byte和)0xFF进行与操作?
tomcat的架构
juejin.cn/post/686378… 连接器Connector: 负责处理请求和响应, 根据协议解析字节流, 转化生成统一的tomcat request和tomcat response对象, 然后再转化成servletRequest和得到servletResponse
容器Container: 就是负责装载你的web应用, 就会将你的请求和响应传给你的web应用
servlet是啥
juejin.cn/post/693507… 原始的servlet是一个部署在服务器的小程序, 能够动态的修改web的内容, 但是现在的servlet都是指的dispathServlet, 就是前端控制器, 负责进行路径映射的
SpringMVC是基于Servlet的架构,而DispatcherServlet则是SpringMVC拦截处理所有请求的Servlet,所以web.xml需要配置DispatcherServlet。
DispatcherServlet进行路由, 是通过handler的
getServletConfigClasses和getRootConfigClasses的区别
如果用继承AbstractAnnotationConfigDispatcherServletInitializer类来进行web应用的上下文的加载, 需要实现两个方法,getServletConfigClasses和getRootConfigClasses
这两个方法针对的就是Spring ioc容器加载bean的情况
getServletConfigClasses是用来加载dispatchServlet的, 一个java应用可以加载多个. 每个DispatcherServlet都有自己的WebApplicationContext
getRootConfigClasses是用来加载公共的bean的, 在Web MVC框架中,每个DispatcherServlet都有自己的WebApplicationContext,它继承了Root WebApplicationContext中已定义的所有bean。 可以在特定于servlet的作用域中重写这些继承的bean,并且可以为给定的Servlet实例定义新的作用域特定的bean。
如何在创建一个bean的时候, 动态的注入属性
利用BeanFactoryPostProcessor
创建一个类继承这个类, 该类允许Spring IOC容器在创建bean之前, 对于bean的元数据进行修改, 允许改变属值.
具体使用就是通过重写一个方法, 获取ConfigurableListableBeanFactory的容器对象的实例, 然后获取BeanDefinition进行修改.
BeanDefinition beanDefinition =
beanFactory.getBeanDefinition("myTestBean");
System.out.println("修改属性name值");
beanDefinition.getPropertyValues().add("name", "liSi");
spring拦截器和过滤器
都是在配置类里完成注册和使用的
拦截器和过滤器的区别
-
Filter是基于函数回调(doFilter()方法)的,而Interceptor则是基于Java反射的(AOP思想)。
-
Filter依赖于Servlet容器,而Interceptor不依赖于Servlet容器。
-
Filter对几乎所有的请求起作用,而Interceptor只能对action请求起作用。
-
Interceptor可以访问Action的上下文,值栈里的对象,而Filter不能。
-
在action的生命周期里,Interceptor可以被多次调用,而Filter只能在容器初始化时调用一次。
-
Filter在过滤是只能对request和response进行操作,而interceptor可以对request、response、handler、modelAndView、exception进行操作。
jvm调优
jvm调优就是指的的是利用命令或者可视化工具, 观测java进程运行时的堆情况, gc情况, 线程情况, 栈的情况等.
然后发现其中可能存在异常, 以及能够优化的地方, 再对代码进行调优, 或者加参数进行限制.
常用查看运行情况的命令
- jps: JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
- jstat: JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
- jmap: JVM Memory Map命令用于生成heap dump文件
- jhat: JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
- jstack: 用于生成java虚拟机当前时刻的线程快照。
- jinfo: JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
常用查看运行情况的工具
jconsole,是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控
调优的措施
修改代码
比如不合理的变量范围, 内存溢出的问题, 单例模式的问题
增加jvm启动时的命令参数
设定堆内存大小
-Xmx:堆内存最大限制。
设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
- -XX:NewSize:新生代大小
- -XX:NewRatio 新生代和老生代占比
- -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
设定垃圾回收器
- 年轻代用 -XX:+UseParNewGC
- 年老代用-XX:+UseConcMarkSweepGC
redis主从同步集群的同步方式
全量同步: 主master fork出一个进程, 然后生成RDB文件(因为调用的是BGSAVE命令完成RDB持久化的, 因此fork出来的就是进程), 将快照文件同步给从redis, 然后从redis舍弃自己的数据, 完成全量同步
增量同步: 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
主从同步下的高可用
简单的主从集群有个问题,就是主节点挂了之后,无法从新选举新的节点作为主节点进行写操作,导致服务不可用。
所以接下来介绍Sentinel(哨兵)功能的使用。哨兵是一个独立的进程,哨兵会实时监控master节点的状态,当master不可用时会从slave节点中选出一个作为新的master,并修改其他节点的配置指向到新的master。
redis分片集群的高可用
(1) 如果对默认的平均分配不满意,我们可以对集群进行重新分片。
(2) 分片集群每个实例都是主从的
一个接受消息的类型, 即如何优化if-else
即使用策略模式. www.cnblogs.com/hollischuan…
rabbitMq如何保证消息不丢失 对于生产者来说
事务机制
可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。
缺点
但是问题是,RabbitMQ 事务机制是同步的,你提交一个事务之后会阻塞在那儿,采用这种方式基本上吞吐量会下来,因为太耗性能。
confirm机制
事务机制和 confirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 confirm 机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。
kafka如何保证消息不丢失 对于生产者来说
也是confirm机制
秒杀系统如何保证不超卖?
如果后端直接使用数据库进行库存扣减
(1) 利用悲观锁 在读的时候就加锁
select xxxx for update
(2) update的时候加上条件 num > 0
update goods set num = num - 1 WHERE id = 1001 and num > 0
(3) 利用乐观锁 加版本号
出现并发的情况, 其它的请求通过乐观锁异常失败
(4) 利用队列 转换成单线程
将所有的库存扣减从并发变成串行
(5) 利用redis作分布式锁 转换成单线程
也是将并发转化成单线程
(6) 异步下单的方式
用户秒杀完后, 没有立即知道结果, 而是后续通过异步的方式通知是否秒杀成功.
后端利用redis进行库存扣减
(1) 利用LUA脚本 保证库存的查询以及扣减两个命令是原子性的
lua脚本本身不允许使用在分片集群中, 因为lua脚本中的多条命令可能不在同一台实例上, 但是你只要保证lua脚本没问题, 可以直接通过redis的connecttion对象去使用(即redis本身是不限制的), 而RedisTemplate是不允许的
获取库存是单独的命令, 也就是高并发系统下, 其实获取到的库存都不一定是准的, 只要保证扣减的时候, 是对真正的库存即可, 就不会出现超卖
具体的扣减命令可以使用Incrby命令, 每次lua脚本先查出来剩余的库存数量, 比较后再用Incrby命令扣减.
最后的库存数据再从redis同步到mysql中