项目
秒杀项目
1. 项目做了什么
- redis实现分布式Session
- redis做缓存减轻数据库压力
- 服务器内存标记减少对redis的访问
- rabbitmq进行异步处理,提高了系统的响应速度,减少了用户的无响应等待时间
- 秒杀URL隐藏、对恶意请求进行限流防刷
- 超买超卖问题
2. 用户的登录状态如何保持:分布式session
- 用户登录后,将user和token存入redis,将token存入cookie,redis和cookie设置相同的存活时间,将cookie返回给前端
- 客户端下一次请求时,从request请求头中拿出token,根据token从redis中查询user,判断用户的状态,进行同步,添加到Model中
3. 缓存是如何预热的
- 实现InitilizingBean接口,重写afterPropertiesSet()方法,从数据库中查询goodsid和库存,存入redis,使用HashMap标记goodsid为false
4. 如何进行限流:AccessLimit注解、拦截器进行计数校验
- 使用AccessLimit注解,设置两个属性,maxcount和second
- 在获取URL的模块上使用这个注解,初始化两个属性maxcount和second为5
- web拦截器拦截请求
- 反射判断当前模块是否使用了AccessLimit注解,没有就放行,有就进行属性校验
- 从request中获取token,根据token从redis中获取user
- 反射获取注解的两个属性,second和maxcount,根据userid从redis中获取当前对象的count计数,如果count为null,将count设置为1,过期时间设置为second。如果count小于maxcount,incr+1,如果count大于maxcount,提示警告
5. 数据库补充库存之后如何反馈到内存标记:单机和集群
- 单机:将hashMap设置为public,定期检测数据库库存,如果有库存,就重置内存标记
- 集群:将hashMap设置为public,定期检测数据库库存,如果有库存,rabbitmq通知服务器重置本地的内存标记
7. Redis预减库存成功但数据库库存没有减少出现什么情况:rabbitmq消息丢失
- 问题可能出现在rabbitmq,rabbitmq消息丢失可以分为三种情况:生产者丢失了消息、rabbitmq丢失了消息、消费者丢失了消息
- 生产者丢失了消息
- 开启事务模式,rabbitmq成功接收消息就提交事务,失败就回滚事务重发消息。事务模式是同步的,吞吐量低
- 开启confirm模式,rabbitmq成功接收消息返回一个ack消息,失败回调一个nack接口,重发消息。confirm模式是异步的,吞吐量大
- rabbitmq丢失了消息
- 队列持久化:durable=true
- 消息持久化:发送消息时,deliverMode(2)
- 消费者丢失了消息:消费过程中消息被删除
- 关闭自动ack机制,消费者消费完之后手动进行ack
8. 缓存数据一致性:延时双删
- 对于这个系统来说,并没有强制要求缓存和数据库的库存一致。不一致的情况又两种
- 缓存的库存比数据库的小,这种情况会导致商品卖不完,不行
- 缓存的库存比数据库的大,这种情况其实是可以的,这样不会发生超卖问题的,因为超卖问题是在数据库的SQL语句层面解决的,SQL语句限制只有当数据库的库存足够时才能减库存下订单
- 所以可以在预热数据时将数据库的库存稍微设置大一点,这样既能保证商品能够卖完又不会发生超卖问题
- 对于严格要求缓存数据一致的项目,为缓存的key设置过期时间是解决这个问题的最终方案
- 采用延时双删:先删除缓存,再更新数据库,休眠一段时间,然后删除缓存。这样就可以将回填到缓存的脏数据删除,后面的读会回填正确的数据到缓存
9. 下单后如何减库存:下单就减库存、付款减库存、临时订单
- 三种策略
- 下单就减库存。最理想但是不可能实现,用户不可能在抢到订单的同时就付款减库存
- 付款减库存。用户可能因为网络问题没来得及付款导致购买失败
- 生成临时订单,用户在订单有效期内付款,才生成真实订单和减数据库库存
- 可以采用第三种策略
- 用户下订单后,预减redis的库,然后在redis中生成一个临时订单,设置过期时间,前端开启倒计时
- 如果用户在期限内付款,触发检查,检查数据库的库存是否大于0,在redis和数据库生成真实订单,减数据库库存
- 如果临时订单过期,redis库存回填+1,通知用户购买失败
10. 为什么要进行异步处理、使用MQ和没使用MQ的流程
- 如果不使用MQ进行异步处理,用户从抢购开始到获得抢购的结果,系统需要执行的操作量多,抢购和获取结果的操作要全部完成才能返回响应给前端,用户等待的时间久
- 查询redis,判断用户是否超买
- 查询内存标记,判断库存是否不足
- redis预减库存,如果预减后库存小于0,将内存标记置为false
- 查询数据库,判断数据库库存是否不足,判断用户是否超买
- 下订单,减库存
- 将抢购结果返回给前端,如果是抢购成功,显示订单详情页面。如果抢购失败,通知用户
- 使用MQ进程异步处理后,将请求入mq后,先返回状态码让前端页面显示正在获取结果,减少了用户的无响应等待时间,然后再去获取抢购结果,将抢购和获取抢购结果分开执行
- 查询redis,判断用户是否超买
- 查询内存标记,判断库存是否不足
- redis预减库存,如果预减后库存小于0,将内存标记置为false
- 将请求入MQ,返回一个状态码给前端,前端显示用户等待,前端再请求到result模块获取结果
- 与此同时,rabbitmq会进行下订单减库存,在redis和数据库中生成订单
- 前端请求到result模块,从redis和数据库中查询是否存在当前用户的订单,有久显示订单详情页面,否则提示用户购买失败
11. rabbitmq使用了哪一种工作模式、一共有哪几种工作模式:simple、work、发布订阅、routing、topic
- 使用了routing路由模式:为queue添加了一个字符串标识,接收者监听标识了这个字符串的queue
- 一共5种
- simple:一个发送者一个接收者
- work:一个发送者多个接收者,谁先拿到就消费
- 订阅发布:发送者将消息发送给交换机,交换机将消费转发给订阅者
- routing:使用标识标注queue,接收者监听标识的queue
- topic:routing的一种,标识可以使用通配符
12. rabbitmq如何保证消息有序:为什么要有序、项目中如何实现有序、乱序的情况、有序的情况
- 为什么要有序:如果多个消息之间存在上下文的关系,消息乱序执行可能会导致数据错误
- 项目如何实现有序:一个消息队列一个消费者,消费者没有使用多线程处理
- 乱序:消费者使用多线程,多个消费者读取,执行时机不定
- 有序:读取消息后存入队列,有序执行
13. 如何保证消息不会被重复消费:保证消息的幂等性
- 保证消息的幂等性,如果消息已经被消费过,丢弃不消费
- 发送消息时,生成一个唯一的id,将消息存入redis,消费时根据id从redis中查询,如果有,消费,消费完之后删除,没有就丢弃
14. 如何解决超买超卖:唯一索引和SQL限制
- 超买:为订单表的userid和goodsid添加唯一索引,确保一个用户对一个商品不会生成两张订单
- 为了减少对数据库的访问,下订单时在redis中生成订单,下一次判断用户超卖就可以直接查询redis来判断,不用请求到数据库
- 超卖:SQL语句限制只有当库存大于0时才能减数据库库存
- 为了减少对数据库的访问,使用redis和内存标记进行数据预热,用户抢购时,先判断超买,然后查询内存标记,最后再减redis库存
15. 项目压测:JMeter
- 5W并发对秒杀接口进行压测,创建5000个HTTP请求,循环10次
- 直接访问数据库时,QPS1300+
- 使用了redis、内存标记、MQ后,QPS2100+
- 因为秒杀URL要动态生成,所以先使用工具类生成5000个用户,保存到数据库中,然后将这5000个用户登陆到系统,生成token令牌,将token保存到txt文件,JMeter导入txt文件,提取token,进行压测,查看聚合报告,查看QPS
16. Nginx负载均衡和反向代理有什么区别
- 负载均衡是将并发的请求分发给后端的服务器集群,更注重对请求的分发减轻并发压力
- 反向代理是Nginx作为代理服务器,将前端请求转发给指定的后端服务器,将后端服务器的响应转发给前端
17. Nginx负载均衡策略:轮询、权重、iphash、urlhash、fair、least_conn
- 轮询:默认,顺序访问服务器
- weight权重:在轮询的基础上,对服务器节点配置了权重,权重越大,越容易被击中
- iphash:对客户端的ip进行hash计算,再对服务器数量进行取模,每个客户端ip都有一个固定的服务器节点
- urlhash:对url进行hash计算,每个url都有对应的服务器节点
- least_conn:请求到最少连接的服务器节点
- fair:请求到响应速度最快的服务器节点
18. 一致性hash
- iphash需要对服务器数量进行取模运算,如果进行扩容和缩容影响范围大
- 一致性hash:设置一个hash环,先对服务器ip进行hash计算,放置到hash环上,客户端请求时,对客户端ip进行hash计算,对应到hash环上,顺时针查找最近的服务器节点
19. 如何设计一个秒杀系统(设计的思路)
- 秒杀系统可能出现的问题
- 高并发
- 超买超卖,超买不一定要处理但是超卖一定要处理
- 安全性问题
- 针对上述问题展开进行考虑解决
- 高并发:系统响应慢、防止数据库被打崩
- 防止数据库被打崩:使用redis作为缓存减轻数据库压力,使用内存标记减轻redis压力
- 系统响应慢:使用mq进行异步处理,将抢购和通知抢购结果分成两部分,减少用户的等待时间
- 超买超卖
- 超买:订单表唯一索引限制
- 超卖:SQL语句限制库存大于0时才能减
- 安全问题
- 接口动态生成:根据当前user使用MD5加密生成URL,存入到redis进行校验
- 接口限流:使用注解对获取URL的模块进行限流,限制5秒内只能请求5次
- 高并发:系统响应慢、防止数据库被打崩
RPC项目
1. 微服务和分布式
- 微服务:是一种设计架构方式,简单来说一个微服务就是一个功能,可以单独部署和运行。服务粒度小、拓展方便、团队开发时分工明确
- 分布式:是一种系统部署方式,将大的系统拆分成多个小的模块,分别部署到不同的服务器上。分布式可以实现高并发和高可用
- 微服务和分布式的区别:微服务是分布式部署,但是分布式不一定是微服务,比如分布式集群,就是将多个功能相同的应用部署到多台服务器上,实现高并发和高可用,整个集群还是单一功能的应用
2. 微服务框架有哪些
- SpringCloud:基于HTTP请求,自带一个Eureka注册中心
- 阿里的Dubbo:基于RPC请求,只支持Java,需要第三方注册中心
- 谷歌的GRPC:基于HTTP2.0请求,使用protobuf序列的IDL文件生成服务器和客户端通信的代码
- Dubbo支持Java,可以自定义协议和序列化方式
3. RPC是什么、架构、RPC和HTTP有什么区别
- RPC是remote procedure call,远程过程调用,客户端通过网络调用远程服务器的服务进行处理的一种请求方式,
- 架构:
- 客户端:服务发现,提供接口服务名请求到注册中心,得到服务所在的服务器的IP和端口进行远程调用
- 服务器:服务暴露,服务器将接口服务和自身IP地址和端口注册到注册中心
- 注册中心
- 服务发现:根据客户端提供的接口名查询对应服务器的IP和端口
- 服务管理:将服务器接口注册成永久节点和临时节点
- 负载均衡:对客户端的请求进行负载均衡
- 核心功能
- 序列化和反序列化:实现跨网络传输
- 编码和解码:选用TCP协议,通过收发双方约定数据包的长度解决TCP粘包问题
- 网络传输:大部分RPC框架都是使用TCP协议的,也可以使用HTTP协议,比如GRPC选用的就是HTTP2.0协议
- RPC和HTTP
- HTTP:
- 只能使用HTTP协议,如果是HTTP1.1版本,请求报文存在大量的无用信息,报文体积大,传输效率低,也可以使用HTTP2.0,封装成RPC来使用。
- 传输的是JSON信息
- 负载均衡需要借助Nginx等外部组件实现
- 一般用于对外的请求调用
- RPC:
- 可以使用HTTP或者是TCP,自定义TCP报文格式,使请求报文的体积减小,解决TCP粘包问题。也可使用HTTP2.0,有效减小请求报文的体积,提高传输效率。
- 传输的是byte字节
- 可以自定义负载均衡策略
- 公司内部的服务调用,传输效率高
- HTTP:
4. 拆分Dubbo功能:基于RPC请求、传输byte、TCP粘包、第三方注册中心功能
- 基于RPC请求方式:使用Netty实现高效的TCP传输
- 传输byte:序列化和反序列化
- TCP粘包:编码和解码,编码时将byte数组的长度传入buffer,解码时提取byte数组的长度,创建相同大小的byte数组接收
- 第三方注册中心:zookeeper实现服务管理、负载均衡
5. Netty:为什么要用Netty、Netty模型、解决TCP粘包、序列化与反序列化
1. Netty是什么
- Netty是一个网络通信框架,它对Java的NIO非阻塞IO进行了封装,简化了使用的难度
2. 为什么用Netty:并发高、传输快、封装好
- 并发高:Netty是基于Java中的NIO开发的,对应操作系统中的多路复用IO,将监听和处理分开进行,由selector多路复用器同时监听多个客户端,有事件就将请求交给线程进行处理,可以让单个线程多次处理请求。而BIO是由线程独立完成监听和处理,这样单个线程能够处理的请求就变少了
- 传输快:NIO的零拷贝特性,bytebuf
- 封装好:Netty对Nio进行了封装,简化了Nio的使用难度,比如Nio在解决TCP粘包问题时,需要调用的API包多,处理起来繁琐。而Netty提供了两个类,MessageToByte和ByteToMessage,继承这两个类来自定义编码器和解码器,编码时先将序列化之后的byte数组的长度发送到buffer,解码时先从buffer中提取byte数组的长度,创建相同长度的byte数组进行接收。
2. Netty模型:单Reactor单线程、单Reactor多线程、主从Reactor、Netty的IO模型
- 单Reactor单线程:使用一个Reactor处理器同时监听多个客户端,有事件发生就读取分发给服务器的Eventhandler处理
- 单Reactor多线程:使用一个Reactor处理器同时监听多个客户端,如果是连接请求,分发给Acceptor处理器进行连接。如果是读写请求,分发给handler,handler调用send方法将请求发送给线程池处理,调用read读取结果
- 主从Reactor多线程:有父子Reactor,父Reactor负责监听客户端,如果是连接请求,分发给Acceptor处理器进行连接。如果是读写请求,则分发给子Reactor,子Reactor将请求分发给handler,handler调用send将请求分发给线程池,调用read读取结果
- Netty的IO模型:Bossgroup负责监听客户端,Bossgroup的Nioeventloop循环调用accept读取事件,将事件通过channel注册到Workergroup,Workergroup的Nioeventloop循环调用accept读取事件,分发给channelhandler进行处理
3. TCP粘包问题:为什么会出现TCP粘包、如何解决TCP粘包
- 发送方在发送时为了提高发送效率,将发送间隔短的几个数据包打包成一个大的包发送,接收方无法拆分数据包
- 编码:继承MessageToByte
- 调用序列化方法,将数据序列化为byte数组
- 将byte数组的长度写入buffer
- 将byte数组写入buffer
- 解码:继承ByteToMessage
- 提取编码的序列化方式,读取byte数组长度
- 创建相同大小的byte数组,接收byte数组
4. 序列化和反序列化:序列化原理、主流的序列化方式
- 序列化原理:跨网络传输,序列化把java对象序列化为byte数组(MessageToByte),反序列化把byte数组恢复为java对象(ByteToMessage)
- 主流的序列化方式
- Serialized:Java自带的序列化方式,无法跨语言,效率低
- Protobuf:json和xml,跨语言,拓展性好,支持c++,java,pyhton
- fastjson:json,效率高,但是可能出现类型转换错误
- jackson:和fastjson类似
- gson:json,功能强大,但是性能比fastjson稍微差
- JSON格式:使用fastjson,可能出现类型丢失
- 序列化:object.toJsonObject()
- 反序列化
- request请求(客户端 to 服务器),遍历byte数组,request.setParam()存入参数
- respons回应(服务器 to 客户端),遍历byte数组,response.setData()存入数据
- Serialized格式
- 序列化(对象 to byte):ObjectOutputStream写入ByteArrayOutputStream
- 反序列化(byte to 对象):ByteArrayOutputStream写入ObjectOutputStream
5. Netty实现长连接的心跳检测:以写事件检测为例
- 在客户端中加入IdleStateHandler,设置写事件触发时间为5秒
- 如果客户端超过5秒未写入数据,触发心跳检测,向服务器发送心跳包
- 服务器收到心跳包后做出回应
- 如果服务器连续收到三次客户端的心跳包,说明客户端可能已经挂了,就断开连接
- 之后,如果客户端重新上线,发现连接已经断开了,客户端重新发起连接请求建立连接
6.zookeeper注册中心:注册中心是什么、有哪些注册中心、zookeeper心跳检测
1. 注册中心是什么
- 注册中心在RPC模型中相当于一个中间代理,客户端如果想要调用远程服务器的服务,必须要先连接到远程服务器,但是如果每一次都要事先知道服务器的IP和端口,很不方便。所以出现了注册中心,服务器可以将业务挂载到注册中心,客户端请求到注册中心,提供服务名,查找目标服务器进行连接。
2. 服务暴露:永久节点和临时节点
- 永久节点:服务器下线后,不删除节点
- 临时节点:服务器下线后,节点被删除,临时节点没有子节点
3. 服务发现
- 客户端提供接口名,请求到注册中心,注册中心调用getChildren.getPath()查询目标服务器的IP和端口,客户端连接到远程服务器
4. 负载均衡:轮询和随机
- 轮询:从-1开始递增,ipList.size()取余
- 随机:random.nextInt(ipList.size())随机获取节点
5. zookeeper的心跳检测
- 目的:维持客户端和服务器之间的长连接
- 原理:
- 客户端:如果60s内没有收到服务器的消息,发送心跳包,如果3次(180s)都没有收到服务器的消息,客户端主动断开这个连接
- 服务器:如果60s内没有收到客户端的消息,发送心跳包,如果3次(180s)都没有收到客户端的消息,服务器主动断开这个连接