前言:前几天有粉丝问我,网上java面试题总是参差不齐,希望我为他整理一套全面的java面试题,并且这套java面试题有个要求,必须能够让他20天就能看完这些面试题,他说现在很多面试题我看都得看3个月才能看完,然后我才能去找工作,但是自己得时间有限所以想让我帮他总结一套万能面试java求职笔记。接下来我将会至少分为12个章节进行阐述这套Java求职笔记,感兴趣的可以坚持看完!觉得不错的可以点个赞。
1为什么写这套Java求职笔记
很早的时候我就想写一些Java求职方向的一些文章,由于很长时间耽搁了加上,有粉丝后台问我,可不可以帮他总结一些java求职笔记,他看了很多Java技术有很多技术,很迷茫不知道学哪些,我笑着说,不要着急,无论你到什么时候你都会迷茫,因为技术一直在更新,谢谢你信任我,解救你的迷茫唯一办法就是多看LRyab博客,就是这样我带着粉丝的问题,开始编写了这套Java求职笔记,看完这套求职笔记,我相信很多人都会找到属于自己的心仪工作。
2单体架构、集群架构、分布式架构技术知识大纲
3单体架构、集群架构、分布式架构
3.1单体架构应用场景
单体架构是什么?一个典型的单体应用就是将所有业务场景的表示层、业务逻辑层和数据访问层放在一个工程中,最终经过编译、打包,部署在一台服务器上。
例如开发一个进销存的系统,我们可以将项目打包成war包并部署到服务器上,这样的一个war包,涵盖了很多模块,如下图所示。
3.2负载均衡+集群架构
Keepalived是一种高性能的服务器可用或者热备的解决方案,Keeplived可以防止服务器单点故障发生,通过配合Nginx可以实现web前端服务的高可用。
负载均衡:将对集群的并发请求按需分配到不同的服务节点上。
服务器集群:多台服务器构成的一个整体。
服务器节点:集群中的某台服务器。
3.3分布式架构向微服务架构演化
在集群架构的技术上进行分布式拓展,比如引入分布式事务、基于redis实现分布式锁、分布式数据库mycat、redis集群、数据库中间件、消息中间件、分布式搜索系统Elasticsearch。最终实现系统的高并发、高可用、高性能的一种技术架构称之为分布式架构。
集群不一定是分布式,分布式一定是集群,它需要多个服务器来协同工作。
3.4集群架构下session一致性解决方案
会话开始时,会分配一个唯一的会话标识(sessionId),保存到浏览器的cookie中,浏览器每次请求时都会携带这个cookie,告诉web服务器请求是来自哪个会话的,在web服务器中,各个会话都有独立的存储,保存不同的会话信息。集群后如果我们不做处理,由于session都是保存在本地服务器上的,就会出现session不一致的问题。
分布式下session一致性问题解决方案?
方案1:session sticky
单机情况下,会话保存在单机上,请求也由这台机器处理,所以不会有问题;web服务器集群后,如果保证同一个会话请求都在同一个web服务器上处理,对这个会话的个体来说,跟单机情况是一样的,要做到这样,就需要负载均衡能够根据每次请求的标识来进行请求转发到同一台服务器上,称为Session Sticky方式。
原理 根据ip做hash计算,同一个ip的请求始终会定位到同一台tomcat
缺点:如果一台tomcat宕机,就会出现用户session的缺失。
方案2:基于服务器session复制
每台web服务器上都保存一份session信息,各服务器之间保持session复制同步;一般的应用容器都支持这种方式,与Session Sticky相比,对负载均衡没有那么多的要求,这个方案是靠应用容器来完成session复制,应用本身不需要关心。
存在问题:
1.session同步增加带宽开销
2.服务器很多时,每台都保存session信息,如果并发访问多的话,每台保存session数据占用严重
方案3:session数据集中存储统一管理
原理 session不由tomcat管理,而是统一放到一个地方集中式管理,读取和写入session都依赖第三方软件
例如 redis mongodb mysql或者其他分布式存储系统等等。
存在问题:1.读写session数据引入了网络操作,存在延时和不稳定性,不过通信基本发生在内网,问题不大
2.如果集中存储session的机器或者集群有问题,就会影响我们的应用
3.5分布式架构下redis分布式锁应用场景
基于redis分布式锁实现"秒杀",高并发情况下下很容易造成库存超卖的场景方案
业务场景:
所谓秒杀,从业务角度看,是短时间内多个用户"争抢"资源,这里的资源在大部分秒杀场景里是商品,将业务抽象,技术角度看,秒杀就是多个线程对资源进行操作,所以实现秒杀,就必须控制线程对资源的争抢,既要保证高效并发,也要保证操作的正确。
单体应用秒杀高并发解决方案
实现秒杀的关键点是控制线程对资源的争抢,根据基本的线程知识,可以采取jdk自带的synchronized实现。
jdk自带的synchronized实现商品秒杀方案举例:
1、秒杀在技术层面的抽象应该就是一个方法,在这个方法里可能的操作是将商品库存-1,将商品加入用户的购物车等等,在不考虑缓存的情况下应该是要操作数据库的。那么最简单直接的实现就是在这个方法上加上synchronized关键字,通俗的讲就是锁住整个方法。
2、锁住整个方法这个策略简单方便,但是似乎有点粗暴。可以稍微优化一下,只锁住秒杀的代码块,比如写数据库的部分。
3、既然有并发问题,那我就让他“不并发”,将所有的线程用一个队列管理起来,使之变成串行操作,自然不会有并发问题。
第一和第二种方法本质上是“加锁”,但是锁粒度依然比较高。什么意思?试想一下,如果两个线程同时执行秒杀方法,这两个线程操作的是不同的商品,从业务上讲应该是可以同时进行的,但是如果采用第一二种方法,这两个线程也会去争抢同一个锁,这其实是不必要的。第三种方法也没有解决上面说的问题。那么如何将锁控制在更细的粒度上呢?可以考虑为每个商品设置一个互斥锁,以和商品ID相关的字符串为唯一标识,
这样就可以做到只有争抢同一件商品的线程互斥,不会导致所有的线程互斥。分布式锁恰好可以帮助我们解决这个问题。
因此,在单机应用中,防止超卖可以使用jdk自带的synchronized关键字来处理。但是在分布式系统应用下synchronized只作用于同一个jvm下,所以synchronized关键字就不生效了。
分布式应用下高并发秒杀解决方案
分布式锁简介:
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候, 往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
分布式下的秒杀场景:
我们来假设一个最简单的秒杀场景:数据库里有一张表,column分别是商品ID,和商品ID对应的库存量,秒杀成功就将此商品库存量-1。
现在假设有1000个线程来秒杀两件商品,500个线程秒杀第一个商品,500个线程秒杀第二个商品。我们来根据这个简单的业务场景来解释一下分布式锁。通常具有秒杀场景的业务系统都比较复杂,承载的业务量非常巨大,并发量也很高。这样的系统往往采用分布式的架构来均衡负载。那么这1000个并发就会是从不同的地方过来,商品库存就是共享的资源,也是这1000个并发争抢的资源,这个时候我们需要将并发互斥管理起来。这就是分布式锁的应用。而key-value存储系统,如redis,因为其一些特性,是实现分布式锁的重要工具。
解决方案一:基于redis中的setIfAbsent
可以使用redis中的setIfAbsent(相当于jedis中的setnx)
setIfAbsent、setnx介绍
将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是SET if Not eXists的简写。
如果键不存在则新增,存在则不改变已经有的值。
加锁和释放锁?
怎么实现加锁?“锁”其实是一个抽象的概念,将这个抽象概念变为具体的东西,就是一个存储在redis里的key-value对,key是于商品ID相关的字符串来唯一标识,value其实并不重要,因为只要这个唯一的key-value存在,就表示这个商品已经上锁。
如何释放锁?既然key-value对存在就表示上锁,那么释放锁就自然是在redis里删除key-value对。
如何处理异常情况?
比如一个线程把一个商品上了锁,但是由于各种原因,没有完成操作(在上面的业务场景里就是没有将库存-1写入数据库),自然没有释放锁,这个情况笔者加入了锁超时机制,利用redis的expire命令为key设置超时时长,过了超时时间redis就会将这个key自动删除,即强制释放锁(可以认为超时释放锁是一个异步操作,由redis完成,应用程序只需要根据系统特点设置超时时间即可)。
循环模拟500个线程:
int splitPoint = 500;
Thread[] threads = new Thread[100];
for(int i= 0;i < splitPoint;i++){
threads[i] = new Thread(new Runnable() {
public void run() {
try {
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
代码实现业务逻辑:加锁只对抢夺同一件商品进行扣减库存进行加锁操作
当线程进来抢夺商品扣减库存时:进行分布式锁实现加锁:
// 改造成redis的setnx
//productkey为商品ID
String productkey = "1001商品ID";
// 添加uuid的value,防止程序运行10秒以上之后锁被自动清除,执行finally的时候会把后面进来的请求加好的锁删除,会造成超卖现象
String clientId = String.valueOf(UUID.randomUUID());
Boolean bool = false;
try {
// 设置redis锁,10秒后自动清除
bool = redisTemplate.opsForValue().setIfAbsent(productkey, clientId, 10, TimeUnit.SECONDS);
if (!bool) {
log.warn("当前商品正在被其他人秒杀系统繁忙,请稍后再试!");
return;
}
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int remainingStock = stock - 1;
redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
log.info("扣减成功!剩余库存:" + remainingStock);
} else {
log.error("库存不足!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁操作
if (bool && clientId.equals(redisTemplate.opsForValue().get(productkey))) {
redisTemplate.delete(productkey);
}
}
}
一般业务下是没有问题的,但是在超级高并发的情况下,也有可能造成超卖的情况。虽然概率很小,但是我们也要优化到最优。
解决方案二:redisson实现分布式锁
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
redisson.getLock底层实现原理
会生成一个长达15秒的锁,在业务代码执行过程中,redisson会判断当前业务是否执行完毕,若某种原因造成业务无法15秒内执行完,redisson会自动延长15秒的时间。我们只需要最后释放锁就可以了,操作简单。
核心代码:
定义 redisson操作类
private final Redisson redisson;
try{
// 获取锁对象
RLock lock = redisson.getLock(lockKey);
// 加锁
lock.lock();
业务代码some code......
}catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
在一般情况下可以使用第一种方案,如果是并发非常高的业务场景下可以使用第二种解决方案。
3.6RabbitMQ消息队列应用场景
RabbitMQ消息队列实现分布式事务
目前使用较多的分布式事务解决方案有几种:
一、结合MQ消息中间件实现的可靠消息最终一致性
二、TCC补偿性事务解决方案
三、最大努力通知型方案
第一种方案:可靠消息最终一致性,需要业务系统结合MQ消息中间件实现,在实现过程中需要保证消息的成功发送及成功消费。即需要通过业务系统控制MQ的消息状态。
第二种方案:TCC补偿性,分为三个阶段TRYING-CONFIRMING-CANCELING。每个阶段做不同的处理。
TRYING阶段主要是对业务系统进行检测及资源预留
CONFIRMING阶段是做业务提交,通过TRYING阶段执行成功后,再执行该阶段。默认如果TRYING阶段执行成功,CONFIRMING就一定能成功。
CANCELING阶段是回对业务做回滚,在TRYING阶段中,如果存在分支事务TRYING失败,则需要调用CANCELING将已预留的资源进行释放。
第三种方案:最大努力通知型,这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知。
RabbitMQ解决分布式事务思路:
使用RabbitMQ技术需要保证以下三个要素
1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制)
2、MQ消费者消息能够正确消费消息,采用手动ACK模式(注意重试幂等性问题)
3、如何保证第一个事务先执行,采用补偿机制,在创建一个补单消费者进行监听,如果订单没有创建成功,进行补单。
业务场景
点外卖-外卖配送
以目前流行点外卖的案例,用户下单后,调用订单服务,然后订单服务调用派单系统通知送外卖人员送单,这时候订单系统与派单系统采用MQ异步通讯。
订单系统与派单系统进行MQ异步通信。
RabbitMQ解决分布式事务原理:采用最终一致性原理。
队列实现方式:
订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
派单系统:订阅下单的消息,采用拉/推的方式,获取下单信息,进行通知外卖人员送单
实现订单系统与派单系统的应用解耦
支付宝转账
从支付宝转账1万块钱到余额宝,支付宝扣除1万之后,如果系统挂掉怎么办,这时余额宝账户并没有增加1万,数据就会出现不一致状况了。
抽象为技术代码层面实质为:当一个表数据更新后,怎么保证另一个表的数据也必须要更新成功。
1 本地事务
以支付宝转账余额宝为例,假设有
支付宝账户表:A(id,userId,amount)
余额宝账户表:B(id,userId,amount)
用户的userId=1;
从支付宝转账1万块钱到余额宝的动作分为两步:
1)支付宝表扣除1万:update A set amount=amount-10000 where userId=1;
2)余额宝表增加1万:update B set amount=amount+10000 where userId=1;
如何确保支付宝余额宝收支平衡呢?
可以直接使用数据库本地事务:
Begin
transaction
update
A set
amount=amount-10000
where userId=1;
update
B set
amount=amount+10000
where userId=1;
End
transaction
commit;
非常正确,如果你使用spring的话一个注解就能搞定上述事务功能。
@Transactional(rollbackFor=Exception.class)
public
void
update() {
updateATable();
//更新A表
updateBTable();
//更新B表
}
如果系统规模较小,数据表都在一个数据库实例上,上述本地事务方式可以很好地运行,但是如果系统规模较大,
比如支付宝账户表和余额宝账户表显然不会在同一个数据库实例上,他们往往分布在不同的物理节点上,这时本地事务已经失去用武之地。既然本地事务失效,分布式事务自然就登上舞台。
使用消息队列来避免分布式事务
仔细观察生活的话,生活的很多场景已经给了我们提示。
比如在北京很有名的姚记炒肝点了炒肝并付了钱后,他们并不会直接把你点的炒肝给你,而是给你一张小票,然后让你拿着小票到出货区排队去取。为什么他们要将付钱和取货两个动作分开呢?
原因很多,其中一个很重要的原因是为了使他们接待能力增强(并发量更高)。
还是回到我们的问题,只要这张小票在,你最终是能拿到炒肝的。同理转账服务也是如此,当支付宝账户扣除1万后,我们只要生成一个凭证(消息)即可,
这个凭证(消息)上写着“让余额宝账户增加 1万”,只要这个凭证(消息)能可靠保存,我们最终是可以拿着这个凭证(消息)让余额宝账户增加1万的,
即我们能依靠这个凭证(消息)完成最终一致性。
3.1 如何可靠保存凭证(消息)
有两种方法:
3.1.1 业务与消息耦合的方式
支付宝在完成扣款的同时,同时记录消息数据,这个消息数据与业务数据保存在同一数据库实例里(消息记录表表名为message)。
Begin
transaction
update
A set
amount=amount-10000
where userId=1;
insert
into message(userId, amount,status) values(1,
10000,
1);
End
transaction
commit;
上述事务能保证只要支付宝账户里被扣了钱,消息一定能保存下来。
当上述事务提交成功后,我们通过实时消息服务将此消息通知余额宝,余额宝处理成功后发送回复成功消息,支付宝收到回复后删除该条消息数据。
3.1.2 业务与消息解耦方式
上述保存消息的方式使得消息数据和业务数据紧耦合在一起,从架构上看不够优雅,而且容易诱发其他问题。为了解耦,可以采用以下方式。
1)支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;
2)当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送该消息;
3)当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在得到取消发送指令后,该消息将不会被发送;
4)对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态并进行更新。为什么需要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。
优点:消息数据独立存储,降低业务系统与消息系统间的耦合;
缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口。
3.2 如何解决消息重复投递的问题
还有一个很严重的问题就是消息重复投递,以我们支付宝转账到余额宝为例,如果相同的消息被重复投递两次,那么我们余额宝账户将会增加2万而不是1万了。
为什么相同的消息会被重复投递?比如余额宝处理完消息msg后,发送了处理成功的消息给支付宝,正常情况下支付宝应该要删除消息msg,但如果支付宝这时候悲剧的挂了,重启后一看消息msg还在,就会继续发送消息msg。
解决方法很简单,在余额宝这边增加消息应用状态表(message_apply),通俗来说就是个账本,用于记录消息的消费情况,每次来一个消息,在真正执行之前,先去消息应用状态表中查询一遍,如果找到说明是重复消息,丢弃即可,如果没找到才执行,同时插入到消息应用状态表(同一事务)。
for
each
msg in
queue
Begin
transaction
select
count(*) as
cnt from message_apply where msg_id=msg.msg_id;
if
cnt==0
then
update
B set
amount=amount+10000
where userId=1;
insert
into message_apply(msg_id) values(msg.msg_id);
End
transaction
commit;
用户秒杀成功发送邮件通知消息--RabbitMQ实现消息异步发送
用户秒杀商品成功之后,进行异步消息发送到邮箱
Rabbitmq消息积压问题
如何解决消息队列的延时以及过期失效问题?
消息队列满了以后该怎么处理?
有几百万消息持续积压几小时,说说怎么解决?
先假设一个场景,我们现在消费端出故障了,然后大量消息在 mq 里积压,现在出事故了,慌了。
几千万条数据在 MQ 里积压了七八个小时,从下午 4 点多,积压到了晚上 11 点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复 consumer 的问题,
让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。
大量消息在 mq 里积压了几个小时了还没解决
一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:
1、先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
2、新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
3、然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
4、接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
5、等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
mq 中的消息过期失效了
假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。
这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。
假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
mq 都快写满了
如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,
消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
3.7开源分布式组件Mycat介绍
Mycat是一个开源的分布式数据库系统,其核心功能是分表分库,即将一个大表水平分割为多个小表,存储在后端MySQL或者其他数据库里。取名Mycat原因一是简单好记,另一个则是希望未来能够入驻 Apache,Apache的开源产品Tomcat也是一只猫。
4结束语
从0到1,是个困难的过程,边做边学,边栽跟头边站起来,是跟上这个时代最好的折腾办法。
5个人说明
我是LRyab博客,专注电商项目实战开发,擅长网站搭建与技术问题指导,经验是由一点一点积累的,思维也是由一天一天训练出来的。谢谢大家的阅读,原创不易,如果你认为文章对你有所帮助,就点个赞感谢大家支持,你的点赞是我持续写作的动力!