印宏网络科技 Java开发 二面 面经
1. 深入聊聊你的项目,技术架构和核心模块的设计思路
项目整体介绍:
我做的是一个在线协作平台项目,类似于在线文档编辑和团队协作工具。项目支持多人实时协作编辑文档、即时通讯、文件共享等功能。上线后有大约2万日活用户,高峰期同时在线用户3000左右。
技术架构设计:
整体采用前后端分离的架构。前端使用Vue.js,后端使用Spring Boot构建微服务。主要拆分为用户服务、文档服务、协作服务、通讯服务四个核心服务。
用户服务负责用户注册登录、权限管理、个人信息维护。文档服务负责文档的创建、存储、版本管理。协作服务负责多人协作编辑的冲突解决和状态同步。通讯服务负责即时消息、在线状态、通知推送。
数据存储方面,MySQL存储用户信息、文档元数据、权限关系等结构化数据。MongoDB存储文档内容和历史版本,因为文档内容是非结构化的,MongoDB更灵活。Redis用于缓存热点数据、存储在线用户状态、实现分布式锁。
核心模块设计:
实时协作编辑是最核心的功能。我们使用WebSocket建立长连接,用户的每次编辑操作都实时推送给其他协作者。
为了解决并发编辑冲突,我们使用了OT算法(操作转换算法)。每个编辑操作都有一个版本号,服务器收到操作后,根据当前文档版本进行转换,然后应用到文档上,再广播给其他用户。
文档版本管理使用了快照加增量的方式。每隔一定时间或操作次数保存一个完整快照,中间的修改保存为增量操作。这样既能快速恢复到任意版本,又节省存储空间。
权限管理使用了RBAC模型。定义了所有者、编辑者、查看者三种角色,每种角色有不同的权限。使用Spring Security实现权限控制,在接口层面做权限校验。
技术难点和解决方案:
最大的挑战是高并发场景下的性能问题。多人同时编辑一个文档时,操作频繁,服务器压力很大。
我们的优化方案是:第一,使用Netty替代Tomcat处理WebSocket连接,Netty的NIO模型可以支持更多并发连接。第二,操作合并,把短时间内的多个小操作合并为一个大操作,减少网络传输和处理次数。第三,使用Redis缓存文档内容,避免频繁读取MongoDB。第四,使用消息队列异步处理非实时操作,比如保存历史版本、发送通知等。
另一个难点是数据一致性。用户的编辑操作要同步到所有协作者,同时要保存到数据库。我们使用了最终一致性方案,操作先通过WebSocket推送给用户,然后异步保存到数据库。如果保存失败,通过补偿机制重试。
项目成果:
优化后系统可以支持50人同时编辑一个文档,操作延迟在100毫秒以内。文档加载时间从2秒优化到500毫秒。系统稳定性达到99.5%以上。用户反馈协作体验流畅,满意度很高。
2. Spring Boot的自动配置原理,如何自定义starter?
自动配置原理:
Spring Boot的核心特性是自动配置,让开发者可以零配置快速搭建项目。自动配置的原理是通过条件注解和自动装配实现的。
启动类上的SpringBootApplication注解包含三个核心注解。SpringBootConfiguration标识这是一个配置类。ComponentScan扫描当前包及子包的组件。EnableAutoConfiguration启用自动配置,这是自动配置的入口。
EnableAutoConfiguration注解导入了AutoConfigurationImportSelector类。这个类会读取所有jar包中META-INF/spring.factories文件,加载EnableAutoConfiguration对应的配置类。
每个自动配置类都有条件注解。ConditionalOnClass表示类路径存在某个类时才生效。ConditionalOnMissingBean表示容器中不存在某个Bean时才生效。ConditionalOnProperty表示配置文件中存在某个属性时才生效。
比如DataSourceAutoConfiguration,只有类路径存在DataSource类时才生效。如果用户没有自定义DataSource,就使用默认配置创建。如果用户自定义了,就使用用户的配置。
自定义starter:
自定义starter可以把通用功能封装起来,在多个项目中复用。
第一步是创建一个Maven项目,命名为xxx-spring-boot-starter。按照约定,第三方starter命名为xxx-spring-boot-starter,官方starter命名为spring-boot-starter-xxx。
第二步是创建自动配置类,使用Configuration注解标识。在配置类中定义需要自动装配的Bean,使用条件注解控制生效条件。
第三步是创建配置属性类,使用ConfigurationProperties注解绑定配置文件的属性。这样用户可以通过配置文件自定义参数。
第四步是在resources目录下创建META-INF/spring.factories文件,配置自动配置类的全限定名。
第五步是打包发布,其他项目引入这个starter依赖,就可以自动装配功能。
实际案例:
我在项目中封装了一个短信发送的starter。定义了SmsAutoConfiguration自动配置类,创建SmsService Bean。定义了SmsProperties配置属性类,绑定短信服务商的配置。
用户只需要引入依赖,在配置文件中配置短信服务商的参数,就可以直接注入SmsService使用。不需要关心具体的实现细节,大大简化了使用。
3. JVM的类加载机制,如何实现热部署?
类加载过程:
类加载分为加载、验证、准备、解析、初始化五个阶段。
加载阶段通过类的全限定名获取二进制字节流,转换为方法区的数据结构,在堆中生成Class对象。
验证阶段确保字节码的安全性,包括文件格式验证、元数据验证、字节码验证、符号引用验证。
准备阶段为类变量分配内存并设置初始值。注意是初始值不是代码中的值,int是0,引用是null。但final常量会直接赋值。
解析阶段将符号引用替换为直接引用,把类名、方法名转换为内存地址。
初始化阶段执行类构造器方法,执行静态变量赋值和静态代码块。
类加载器:
Java有三层类加载器。启动类加载器加载核心类库,比如rt.jar。扩展类加载器加载扩展类库。应用类加载器加载应用类路径的类。
类加载器使用双亲委派机制。收到加载请求时,先委派给父加载器,父加载器无法加载才自己加载。这样保证了核心类库不会被替换,保证了安全性。
热部署原理:
热部署是指在不重启应用的情况下,更新类的定义。实现热部署需要自定义类加载器。
每次加载类时,创建一个新的类加载器实例。修改代码后,丢弃旧的类加载器,创建新的类加载器加载新的类。因为类的唯一性由类加载器和类名共同决定,不同类加载器加载的类被认为是不同的类。
Spring Boot DevTools就是使用这个原理实现热部署。它使用两个类加载器,一个加载不变的类(第三方jar),一个加载会变的类(项目代码)。代码修改后,只重新加载会变的类,速度很快。
实现方式:
自定义类加载器继承ClassLoader,重写findClass方法。在方法中读取class文件的字节码,调用defineClass方法定义类。
监听文件变化,当检测到class文件修改时,创建新的类加载器,加载新的类。使用反射创建新类的实例,替换旧的实例。
注意热部署有局限性。只能修改方法体,不能修改类结构,比如添加字段、修改方法签名。而且静态变量、单例对象可能无法更新。
在开发环境使用热部署可以提高效率,但生产环境不建议使用,因为可能导致内存泄漏和状态不一致。
4. 分布式系统的CAP理论,如何在实际项目中权衡?
CAP理论:
CAP理论指出,分布式系统不可能同时满足一致性、可用性、分区容错性三个特性,最多只能满足其中两个。
一致性是指所有节点在同一时间看到的数据是一致的。可用性是指系统一直可以响应请求,不会出现超时或错误。分区容错性是指系统在网络分区的情况下仍能正常工作。
在分布式系统中,网络分区是无法避免的,所以分区容错性是必须保证的。这样就只能在一致性和可用性之间权衡,
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经,带你练透java圣经
查看18道真题和解析