7 - 场景篇
7.1 设计模式
单例模式:一个类只有一个实例,并提供一个全局访问点。
- 懒汉式实现:在第一次被引用时才创建实例。
- 饿汉式实现:在类加载时就创建实例。
- 静态内部类:利用类加载机制来保证初始化实例时只有一个线程。
工厂模式:用于创建对象,而不是直接使用
new
关键字。
- 简单工厂模式:通过一个工厂类来创建对象。
- 工厂方法模式:定义一个创建对象的接口,但让子类决定要实例化的类。
- 抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
策略模式:
- 定义一系列的算法,并将每一个算法封装起来,并使它们可以相互替换。
责任链模式:
- 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。
7.1.1 单例模式
7.1.1.1 懒汉式实现
数据库连接池:使用单例模式,以确保整个应用只有一个连接池实例。懒汉式意味着,在第一次需要数据库连接时才创建连接池。
public class DatabaseConnectionPool { private static DatabaseConnectionPool instance; private java.sql.Connection connection; private DatabaseConnectionPool() { ... } // 创建连接池 public static synchronized DatabaseConnectionPool getInstance() { ... } // 连接数据库,如果第一次则创建连接池 }
7.1.1.2 饿汉式实现
日志记录器:通常是一个单例,因为它需要被应用程序的多个部分使用,并且通常在应用程序启动时就初始化。
public class Logger { private static final Logger INSTANCE = new Logger(); private Logger() { } // 私有构造函数,防止外部实例化 public static Logger getInstance() { return INSTANCE; } public void log(String message) { ... } // 记录日志 }
7.1.1.3 静态内部类
配置管理器:可以设计为单例,以确保整个应用程序中只有一个配置实例,静态内部类可以确保线程安全地创建这个实例。
public class ConfigurationManager { private static class SingletonHolder { // 静态内部类,持有外部类的实例 private static final ConfigurationManager INSTANCE = new ConfigurationManager(); } private ConfigurationManager() { } // 私有构造函数,防止外部实例化 public static ConfigurationManager getInstance() { return SingletonHolder.INSTANCE; } public String getConfig(String key) { ... } // 返回配置信息 }
7.1.2 工厂方法模式
7.1.2.1 简单工厂模式
在游戏开发中,一个简单的工厂模式可以用来创建不同类型的游戏角色,根据玩家的选择来创建不同的角色实例。
public interface GameCharacter { void speak(); } public class Warrior implements GameCharacter { public void speak() {...} } public class Wizard implements GameCharacter { public void speak() {...} }
public class GameCharacterFactory { public GameCharacter createCharacter(String type) { return type.equalsIgnoreCase("warrior") ? return new Warrior() : return new Wizard(); } }
7.1.2.2 工厂方法模式
在图形用户界面(GUI)开发中,可以使用工厂方法模式来创建不同类型的按钮,每个按钮可能有不同的外观和行为。
interface Button { void paint(); } // 抽象产品:Button class WindowsButton implements Button { // 具体产品:WindowsButton public void paint() { ... } }
interface ButtonFactory { Button createButton(); } // 抽象工厂:ButtonFactory class WindowsButtonFactory implements ButtonFactory { // 具体工厂:WindowsButtonFactory public Button createButton() { return new WindowsButton(); } }
7.1.2.3 抽象工厂模式
在办公软件中,可能需要创建不同类型的文档,抽象工厂模式可以用来创建这些文档的不同版本(如Word、Excel、PPT)。
interface Document { void display(); } // 抽象产品:Document class WordDocument implements Document { // 具体产品:WordDocument public void display() { ... } }
interface OfficeSuite { Document createDocument(String type); } // 抽象工厂:OfficeSuite class MicrosoftOffice implements OfficeSuite { // 具体工厂:MicrosoftOffice public Document createDocument(String type) { return type.equalsIgnoreCase("word") ? new WordDocument() : new ExcelDocument(); } }
7.1.3 策略模式
在支付系统中,定义多种支付策略(如信用卡、微信支付等),每种支付方式都是一个策略,可以根据用户选择动态更换支付策略。
public interface PaymentStrategy { void pay(int amount); } public class CreditCardPayment implements PaymentStrategy { public void pay(int amount) {...} } public class PayPalPayment implements PaymentStrategy { public void pay(int amount) {...} }
public class PaymentContext { private PaymentStrategy strategy; public PaymentContext(PaymentStrategy strategy) { this.strategy = strategy; } public void executePay(int amount) { strategy.pay(amount); } }
7.1.4 责任链模式
在企业审批流程中,一个请求需要多个部门审批,责任链模式确保请求按照既定的流程顺序传递给各个部门,直到被批准或拒绝。
public abstract class Approver { protected Approver successor; public void setSuccessor(Approver successor) { this.successor = successor; } public abstract void processRequest(Request request); }
public class Manager extends Approver { public void processRequest(Request request) { // 根据 request.amount,进行第一次审批 successor.processRequest(request); } } public class Director extends Approver { public void processRequest(Request request) { // 根据 request.amount,进行第二次审批 successor.processRequest(request); } } public class CEO extends Approver { public void processRequest(Request request) { // 根据 request.amount,进行第三次审批 System.out.println("CEO approved the request."); } }
7.2 技术场景
7.2.1 单点登录原理?实现的流程?
单点登录(SSO):
- 用户登录:服务端接收到登录请求后,验证用户的用户名和密码。
- 生成Token:验证成功后,服务端生成一个JWT,并将其返回给客户端。
- Token存储:客户端将Token存储在Cookie或LocalStorage中。
- Token验证:用户访问其他系统时,自动携带Token,无需再次登录。
- Session管理:服务端通过Token解析用户信息,无需在服务器上维护Session状态。
实现流程:
JWT 工具类:实现一个工具类
JwtUtil
,用于生成和解析 JWT。Spring Security配置:使用
@EnableWebSecurity
注解来创建一个安全配置类。在这个类中配置HTTP安全性,包括禁用CSRF保护、定义哪些路径是公开的以及哪些需要认证,并设置过滤器。
JWT 认证过滤器:创建一个过滤器,检查进入的请求头中是否包含有效的 JWT,并据此允许或拒绝用户访问。
登录和Token生成:实现一个登录接口,在用户成功登录后,使用工具类生成一个 JWT,并将其返回给客户端。
7.2.2 权限认证是如何实现的?
1. RBAC(Role-Based Access Control)权限模型
RBAC(基于角色的访问控制)是一种常见的权限管理模型,它通过角色来分配权限,而不是直接将权限分配给用户。
用户表(User):存储用户信息,每个用户都有一个唯一的标识符(如用户ID)。
角色表(Role):存储角色信息,每个角色都有一个唯一的标识符(如角色ID)。
权限表(Permission):存储权限信息,每个权限都有一个唯一的标识符(如权限ID)。
用户角色中间表(User_Role):关联用户和角色,表示用户可以拥有多个角色,角色也可以被多个用户拥有。
角色权限中间表(Role_Permission):关联角色和权限,表示角色可以拥有多个权限,权限也可以被多个角色拥有。
关系描述:用户通过角色间接拥有权限,即用户 - 角色 - 权限有多对多的关系。
2. Spring Security 权限框架
- 身份验证:验证用户身份的过程,即确定用户是否为他们所声称的用户。这通常通过用户名和密码等凭据来完成。
- 授权:确定已认证的用户是否有权访问特定的资源或执行特定的操作。这通常基于用户的角色或权限来决定。
- 攻击防护:提供对常见攻击(如会话固定、点击劫持、跨站请求伪造等)的检测和预防。
- Spring MVC 集成:能够与 Spring MVC 无缝集成,为基于 Spring 的 Web 应用程序提供安全支持。
- 单点登录(SSO):支持单点登录功能,允许用户通过一个账户访问多个应用程序。
7.2.3 上传数据的安全性怎么控制?
使用非对称加密(或对称加密),给前端 公钥 加密数据后传到后台,后台负责 私钥 解密后处理数据。
- 文件很大建议采用对称加密,速度较快,但不能保存敏感信息。
- 文件较小建议采用非对称加密,安全性高,但速度较慢。
7.2.4 你的项目遇到了哪些棘手的问题?
7.2.5 你是怎么做压测(性能测试)的?
- 确定测试目标:比如响应时间小于2秒,系统支持1000个并发用户。
- 选择测试工具:比如使用 JMeter。
- 编写测试脚本:模拟用户登录、数据查询等操作。
- 准备测试环境:确保测试环境与生产环境相似。
- 执行测试:逐步增加用户数,直到达到1000个并发用户。
- 监控系统:使用 JVisualVM 监控内存,使用 JMeter 监控响应时间。
- 分析结果:检查响应时间是否达标,系统是否稳定。
- 调优:如果响应时间太长,优化代码或增加服务器资源。
- 重复测试:调优后再次测试,直到满足测试目标。
- 记录结果:将测试结果和优化措施记录下来。
7.2.6 你项目中日志怎么采集的?
我们搭建了 ELK 日志采集系统。介绍 ELK 的三个组件:
Elasticsearch
是全文搜索分析引擎,可以对数据存储、搜索、分析Logstash
是一个数据收集引擎,可以动态收集数据,可以对数据进行过滤、分析,将数据存储到指定的位置Kibana
是一个数据分析和可视化平台,配合 Elasticsearch 对数据进行搜索,分析,图表化展示
7.2.7 查看日志的命令?
tail -f xx.log # 实时监控日志的变化 tail -n 100 xx.log # 按照行号查询 cat -n xx.log | grep "debug" # 按照关键字找日志的信息
7.2.8 怎么快速定位系统的瓶颈?
- 压测(性能测试),项目上线之前测评系统的压力。
- 监控工具、链路追踪工具,项目上线之后监控。
- 线上诊断工具 Arthas(阿尔萨斯),项目上线之后监控、排查。
7.2.9 生产问题怎么排查?
已经上线的 bug 排查的思路:
- 先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题。
- 通常公司的生产环境是不允许远程 debug 的,一般都是远程 debug 公司的测试环境,方便调试代码。
7.2.10 多点登录的实现?如何确保 session 共享?
- 多点登录的实现:
- 多点登录控制通常是指限制用户在多个设备或多个浏览器上同时登录同一个账户。
- 可以通过使用Spring Security的
SessionRegistry
接口来管理用户的session信息。- 另外,
ConcurrentSessionFilter
可以用来检查每个请求的session是否过期,并进行相应的处理。- 确保session共享:
- 在分布式系统中,常将session数据存储在Redis,这样所有的服务实例都可以访问同一个session数据源。
- 使用Spring Session,可以在多个应用程序之间共享HTTP session数据。支持使用Redis来存储,从而实现session共享。
- 对于跨域的session共享问题,除了使用外部存储外,还需要确保各个应用使用相同的cookie配置。
- JWT令牌跨服务传递:
- JWT确实可以跨服务传递,并且允许客户端在多个服务之间进行身份验证,而无需依赖服务器来维护会话状态。
- JWT是一个紧凑且自包含的方式,用于在各方之间安全地传输信息,因为它是经过数字签名的。
- JWT适用于授权和信息交换,特别是在单点登录场景中,用户在一处登录后,在所有相互信任的应用系统中无需重复登录。
7.2.11 Git 常见命令? 分支冲突怎么解决?
gitee、github、gogs 等,常用操作 clone、checkout、merge、commit、push、pull。
- 正常逻辑:分支开发,主干发布。异常情况:分支开发,主干开发。
- checkout 切到主干,merge 合并分支,commit 提交更改,branch -d 删除分支。
7.2.12 什么是 JWT 令牌?为什么要用 JWT?
1. JWT(JSON Web Token):
Header(头部)
令牌类型:通常设置为
JWT
。签名算法:指定了生成签名使用的算法,如
HS256
或RS256
。Payload(负载)
Registered Claims(注册声明):一组预定义的声明,如用户ID、用户名称、发行时间、过期时间等。
Public Claims(公共声明):由社区注册的声明,用于特定目的,如
auth_time
(认证时间)。Private Claims(私有声明):自定义的声明,由应用自定义,如
userId
、role
等。Signature(签名)
- 由编码后的 Header、Payload 和密钥通过指定算法生成,用于验证令牌的完整性和真实性。
2. 为什么要用 JWT?
- 无状态:JWT 是一种无状态的认证机制,这意味着服务器不需要存储用户的状态信息。
- 自包含:JWT 自身包含了所有必要的信息,服务端可以直接从Token中读取,而不需要查询数据库。
- 跨域支持:JWT 可以更容易地支持跨域认证,因为它不依赖于Cookie或Session,而是通过HTTP头部传递。
- 安全性:JWT 可以通过使用强算法(如HS256)进行签名,确保Token的完整性和真实性。
- 单点登录(SSO):JWT 可以被存储在本地(LocalStorage),并且不依赖于Cookie,它适合单点登录的场景。
7.2.13 海量数据处理分析问题?
思路:分治法 / 哈希取余 + HashMap 统计 + 堆 / 快速 / 归并排序
- 分治法 / 哈希取余:针对数据太大,内存受限,只能把大文件分散到小文件中。
- HashMap 统计:转化为了小文件之后,便可以采用 HashMap 来进行频率统计。
- 堆 / 快速 / 归并排序:统计完了频率之后,便进行排序,得到次数最多的 Top K。
举例:非常大的文件,只有1G内存,如何统计大文件中元素的出现次数?
- 使用位图(bitmap)可以帮助处理大文件中的元素出现次数,特别是当元素范围有限时。
- 32位无符号整数的范围是0到2^32-1,每个位需要1比特,所以总空间为4,294,967,296比特。
- 转换:4,294,967,296 / 8 = 536,870,912 字节。转换:536,870,912 / (1024*1024) ≈ 512 MB。
- 题目1:1GB 文件,每行一个词,词大小不超过 16B,内存限制 1MB,返回 Top 100 词。
- 使用分治法,通过哈希取余将文件分散到 5000 个小文件中,每个约 200 KB。
- 使用 HashMap 统计每个小文件中词频,然后使用最小堆找出 Top 100 词。
- 题目2:在 2.5 亿个整数中找出不重复的整数。注意:内存不足以容纳这 2.5 亿个整数。
- 使用位图法,使用 2bit 表示整数状态。
- 00 表示没出现过,01 表示出现过一次,10 表示出现了多次。
- 题目3:找出 5 亿个数的中位数。
- 使用双堆法,维护最大堆、最小堆。
- 中位数为最大堆的堆顶,或者两个堆顶的平均数。
- 题目4:有 20 个数组,每个数组有 500 个元素,并且有序排列。如何在这 20*500 个数中找出前 500 的数?
- 使用最大堆,堆大小为数组个数,存每个数组的最大值。
- 删除堆顶元素,保存到结果数组,插入下一个元素。
7.2.14 Java 异常类型、层次结构、处理机制?
1. Java 异常类型、层次结构
- Error:系统级别问题、JVM退出等,代码无法控制
- Exception:Java.lang包下,称为异常类,它表示程序本身可以处理的问题
- Runtime Exception(运行时异常):编译阶段不会报错,如:空指针异常、数组索引越界异常、类转换异常
- 除Runtime Exception(编译时异常):编译期必须处理的,如:如类未找到异常、非法参数异常、方法不存在异常
<img src="https://i-blog.csdnimg.cn/blog_migrate/9aa4b9cd6cc5e5ff055082517faf1ac1.png" alt="img" style="zoom:50%;" />
2. Java 异常处理机制
- try:这是一个“尝试”块,你可以把你认为可能会出错的代码放在这里面。
- catch:如果try块中的代码真的遇到了问题,那么程序就会“捕捉”到这个异常,并跳到catch块中去处理。
- finally:这是一个“最后”块,无论你是否捕捉到异常,finally块中的代码都会执行。
- throw:这个关键字用来手动抛出一个异常。
7.2.15 QPS 提升 10 倍怎么设计 / 如何提高系统 QPS?
硬件层面:增加服务器资源(服务器数量、负载均衡),升级硬件配置。
软件层面:优化数据库设计(索引、分库分表、缓存),优化代码逻辑。
架构层面:使用 CDN(分发静态资源,减少网络延迟),采用微服务架构(提高可扩展性和可维护性):
服务拆分与独立部署:业务功能拆分和独立部署,实现服务的独立优化和弹性扩展。
异步通信与事件驱动:采用异步通信和事件驱动架构,提高并发处理能力。
缓存与数据复制:使用缓存和数据复制提高读取性能。
负载均衡与服务发现:使用负载均衡器和服务发现工具,确保服务的高可用性和动态管理。
性能监控与优化:建立性能监控体系,根据数据持续优化。
7.2.16 cookie和session有什么区别?
- cookie:客户端存储,容量小。存储少量的数据,如用户偏好。每次请求时,浏览器会自动将相关的 cookie 发送到服务器。
- session:服务器存储,容量大。存储复杂的数据,如用户信息。使用唯一的 session ID(存储在cookie中)标识用户的会话。
7.2.17 给你10个 Tomcat,如何判断自己用哪一个?
- 应用需求:如果应用需要特定的JDK版本或特定配置,选择满足这些要求的Tomcat实例。
- 性能要求:如果应用需要处理大量并发请求,可能需要选择配置了更多内存、CPU的Tomcat实例。
- 开发阶段:可能需要使用一个专门的Tomcat实例来进行开发和测试,以避免影响生产环境。
- 环境隔离:可能需要为不同的环境(开发、测试、生产)配置不同的Tomcat实例。
- 负载均衡:如果有多个Tomcat实例,可以使用负载均衡器来分散请求,提高系统的可用性和扩展性。
7.2.18 定时任务执行失败,如何处理?
- 异常处理机制:
- 在定时任务代码中添加try-catch块来捕获异常。
- 在catch块中处理异常,如记录日志、发送警报、重试等。
- 重试机制:
- 使用
@Retryable
注解来标记可以重试的定时任务方法。- 通过
maxAttempts
属性设置最大重试次数,通过backoff
属性设置重试间隔。- 监控和报警:
- 监控定时任务的执行状态,在任务执行失败时发送报警通知,可以使用邮件、短信等方式。
- 日志记录和分析:
- 使用日志框架(如Logback或Log4j)记录任务执行失败的日志。
- 使用日志分析工具(如ELK Stack、Splunk)进行日志分析,以定位和解决问题。
7.2.19 如果有一个接口存的是大Key,QPS比较低,另外有10个接口,QPS非常高,会有什么影响?
- 高QPS接口会与大Key操作争夺CPU、内存和网络资源。
- 大 Key 会成为性能的主要限制因素,影响整体系统的稳定性和响应时间。
7.2.20 ToB 和 ToC 产品的区别?
- ToB(To Business):面向企业的产品和服务。
- ToB产品通常需要满足企业级的需求,比如稳定性、安全性、可扩展性等。
- ToB产品往往需要更复杂的功能和定制化服务,因为它们需要适应不同企业的特定需求。
- ToB产品的销售周期通常较长,决策过程涉及多个利益相关者,且价格通常较高。
- ToC(To Consumer):面向消费者的产品和服务。
- ToC产品更注重用户体验和易用性,因为它们直接面向最终用户。
- ToC产品通常需要快速迭代和更新,以适应市场变化和用户需求。
- ToC产品的决策过程较快,且价格通常较低,更依赖于品牌和营销。
7.2.21 12 个小球里找次品,次品不知道轻还是重,天平最少秤几次?
首先,将 12 个球编号并分为3组:A(1 2 3 4)、B(5 6 7 8)、C(9 10 11 12)
- 第一次称量 A 和 B,即 1 2 3 4 与 5 6 7 8:
- 如果平衡,则次品在 C(9 10 11 12),第二次称量 1 2 3 与 9 10 11:
- 如果平衡,则次品为 12,第三次称量 1 12:
- 如果左重,则 12 是轻次品;如果右重,则 12 是重次品。
- 如果左重,则 轻次品 在 9 10 11,第三次称重 9 与 10:
- 如果平衡,则是 11;如果左重,则是 10;如果右重,则是 9。
- 如果右重,则 重次品 在 9 10 11,第三次称重 9 与 10:
- 如果平衡,则是 11;如果左重,则是 9;如果右重,则是 10。
- 如果左重,则次品在 A(1 2 3 4)或 B(5 6 7 8),第二次称量 1 9 10 11 与 5 2 3 4:
- 如果平衡,则 轻次品 在 6 7 8,第三次称量 6 与 7:
- 如果平衡,则是 8;如果左重,则是 7;如果右重,则是 6。
- 如果左重,则次品在 1 5,第三次称量 1 与 12:
- 如果平衡,则 5 是轻次品;如果左重,则 1 是重次品。
- 如果右重,则 重次品 在 2 3 4,第三次称量 2 与 3:
- 如果平衡,则是 4;如果左重,则是 2;如果右重,则是 3。
- 如果左重,则次品在 A(1 2 3 4)或 B(5 6 7 8),第二次称量 1 9 10 11 与 5 2 3 4,同理。