文章目录
- 一、专业技能
- 1.1 具备扎实的Java基础,熟练掌握面向对象编码规范、集合、反射以及Java8特性等。
- 1.1.1 Java基础
- 1.1.2 集合
- 1.1.3 Java8新特性
- 1.2 熟悉常用的数据结构(链表、栈、队列、二叉树等),熟练使用排序、动态规划、DPS等算法。
- 1.2.1 数据结构
- 1.2.2算法
- 1.3 理解设计模式如工厂、代理、策略、模板、建造者等,并善用设计原则构建可复用的代码。
- 1.3.1 代理模式
- 1.3.2 工厂模式
- 1.3.3 策略模式
- 1.3.4 模板方法模式
- 1.3.5 项目中用到过什么设计模式?
- 1.4 熟悉JVM的内存结构,垃圾回收机制,类加载机制及JMM。
- 1.4.1 jvm内存分区,每个区的作用
- 1.4.2 垃圾回收的方法
- 1.4.3 类加载过程,类加载器
- 1.5 熟悉Java并发编程,了解锁机制、线程池、CAS以及JUC中常用类如ConcurrentHashMap、CountDownLatch、Semaphore、CyclicBarrier等。
- 1.5.1 线程池有哪些
- 1.5.2 Synchronized和Reentranlock
- 1.5.3 锁机制,锁升级
- 1.5.4 CAS操作
- 1.5.5 AQS抽象队列同步器
- 1.5.6 ConcurrentHashMap
- 1.5.7 CountDownLatch、Semaphore、CyclicBarrier
- 1.6 熟悉MySQL事务、存储过程、锁、索引、执行计划等,掌握复杂SQL编写、数据库优化方案以及SQL优化,在工作中有相关SQL优化经验。
- 1.6.1 事务
- 1.6.2 索引
- 1.6.3 锁 悲观锁和乐观锁
- 1.6.4 优化
- 1.6.5 执行计划
- 1.6.6 MVCC
- 1.7 熟练掌握MyBatis、Spring、SpringMVC进行整合开发项目经验,对IOC、AOP原理有一定的理解。
- 1.7.1 IOC和AOP
- 1.7.2 循环依赖问题
- 1.8 熟练使用SpringBoot框架用于快速构建项目,SpringCloud微服务治理架构以及Nacos、Kafka、RabbitMQ、Gateway、Feign、Sentinel等技术。
- 1.8.1 微服务的概念
- 1.8.2 各个组件的作用,Nacos、Kafka、RabbitMQ、Gateway、Feign、Sentinel
- 1.9 熟悉Redis数据结构、持久化、内存回收策略、常见缓存高并发场景以及生产环境常见问题解决方案(缓存雪崩、穿透、缓存db数据一致性等处理)。
- 1.9.1 Redis数据结构有哪几种
- 1.9.2 Redis的持久化方式
- 1.9.3 内存回收策略
- 1.9.4 常见使用Redis的场景
- 1.9.5 Redis和MySQL的数据一致性怎么保证
- 1.9.6 缓存击穿、缓存穿透、缓存雪崩的原因和解决方案
- 1.9.7 布隆过滤器
- 1.10 理解消息中间件落地方案和使用场景,熟悉消息队列有序性、可靠性、幂等性、事务消息、消息积压的解决方案。
- 1.10.1 mq的作用。削峰填谷、解耦
- 1.10.2 如何保证消息的有序性
- 1.10.3 如何保证消息不会丢失
- 1.10.4 消息有可能会被重复处理吗?幂等性如何保证?
- 1.10.5 事务消息如何设计
- 1.10.6 mq的应用场景
- 1.11 理解CAP和BASE理论,理解分布式场景下常见问题和解决,如分布式锁、分布式事务、分布式session、分布式调度任务等。
- 1.11.1 分布式锁
- 1.11.2 分布式事务
- 1.11.3 分布式一致性 Session的实现方案
- 1.11.4 分布式任务调度:xxl-job
- 1.12 掌握Linux常用命令,了解Nginx服务的反向代理、负载均衡、动静分离。
- 1.12.1 linux常用命令
- 1.12.2 nginx反向代理、负载均衡、动静分离
- 二、工作经历
- 2.1 利用delayqueue+scheduled+mq实现了延迟消息工具完成了常见的延迟消息场景,并且避免了引入mq延时队列插件导致有多个mq时插件不通用的问题。
- 2.2 热门数据利用caffeine和redis实现了两级缓存,解决了用redis时压力过大查询缓慢的问题,保证了接口的高性能和高可用。
- 2.3 基于本地消息记录表实现了事务消息的投递过程,引入衰减重试+错误预警保证了消息投递的可靠性。
- 2.4 基于事务消息+消费幂等+失败补偿方案,利用mq最终一致性解决了分布式事务的问题。
- 2.5 使用CompletableFuture优化查询模块,对业务功能模块中的多个异步调用进行重新编排,优化结果显示响应时间从2s降到0.2s。
- 2.6 对原有的复杂代码进行重构,采取工厂+策略模式实现了项目的各个业务产品对应的信息创建、编辑的解耦处理,解决了原先老旧代码不易维护的问题。
- 2.7 基于自定义注解+jackson序列化器优雅实现不同业务数据的脱敏处理。
- 2.8 第三方sdk接入,如短信接口对接、微信小程序js-sdk对接、内部项目接口对接、银行业务接口对接等。
- 2.9 基于Spring Cloud、Spring Boot、Spring Security、jwt、Redis技术栈,实现了一个高效、安全的微服务统一认证授权解决方案,基于拦截器实现了登录用户的权限管理。
- 2.10 基于EasyExcel实现了常见业务数据的Excel报表导入导出功能,基于poi-tl实现了动态word文档的导出,并通过自定义线程池+completable进行了多线程改造,解决了大批量导入客户公司人员信息时的效率过低的问题。
- 2.11 采用定时任务定时推送数据、发送邮件到用户,并进行错误校验,对失败的任务进行衰减式重试。
- 2.12 对慢接口进行检查分析,对跨库的多个复杂SQL查询进行调优,解决了前端报表响应过慢的问题。
- 2.13 基于Canal+Kafka实现了从MySQL到ElasticSearch的数据增量同步。
一、专业技能
1.1 具备扎实的Java基础,熟练掌握面向对象编码规范、集合、反射以及Java8特性等。
1.1.1 Java基础
- 重载与重写区别
- 接口和抽象类
- 自动装拆箱
- 流
1.1.2 集合
- 集合体系
- ArrayList,HashMap
1.1.3 Java8新特性
- stream流
1.2 熟悉常用的数据结构(链表、栈、队列、二叉树等),熟练使用排序、动态规划、DPS等算法。
1.2.1 数据结构
- 二叉树
- 栈
- 队列
- 链表
1.2.2算法
- 排序
1.3 理解设计模式如工厂、代理、策略、模板、建造者等,并善用设计原则构建可复用的代码。
1.3.1 代理模式
参考:一文搞懂代理模式
代理模式就是代理对象具备真实对象的功能,并代替真实对象完成相应操作,并能够在操作执行的前后,对操作进行增强处理。(为真实对象提供代理,然后供其他对象通过代理访问真实对象)
静态代理:中介租房案例
动态代理:
- jdk动态代理
- cglib动态代理
1.3.2 工厂模式
参考Java设计模式之创建型:工厂模式详解(简单工厂+工厂方法+抽象工厂)
简单工厂模式
缺点:不符合“开闭原则”,每次添加新产品就需要修改工厂类。在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展维护,并且工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
工厂方法模式
缺点:每增加一个产品都需要增加一个具体产品类和实现工厂类,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
抽象工厂模式
在工厂方法模式中,我们使用一个工厂创建一个产品,一个具体工厂对应一个具体产品,但有时候我们需要一个工厂能够提供多个产品对象,而不是单一的对象,这个时候我们就需要使用抽象工厂模式。
1.3.3 策略模式
参考JAVA设计模式之策略模式详解
1.3.4 模板方法模式
参考模板方法模式
举例
1.3.5 项目中用到过什么设计模式?
1.4 熟悉JVM的内存结构,垃圾回收机制,类加载机制及JMM。
参考JVM(内存区域划分、类加载机制、垃圾回收机制)
1.4.1 jvm内存分区,每个区的作用
参考1:JVM的内存区域划分
参考2:jvm-内存管理
1.4.2 垃圾回收的方法
参考JVM(内存区域划分、类加载机制、垃圾回收机制)
1.4.3 类加载过程,类加载器
参考JVM(内存区域划分、类加载机制、垃圾回收机制)
1.5 熟悉Java并发编程,了解锁机制、线程池、CAS以及JUC中常用类如ConcurrentHashMap、CountDownLatch、Semaphore、CyclicBarrier等。
1.5.1 线程池有哪些
参考1:java线程池(简单易懂)
参考2:java线程池
1.5.2 Synchronized和Reentranlock
参考1:synchronized和ReentrantLock的区别小结
1.5.3 锁机制,锁升级
参考1:锁机制(JUC)
参考2:JUC并发编程01——谈谈锁机制:轻量级锁、重量级锁、偏向锁、锁消除与锁优化
1.5.4 CAS操作
参考1:【JUC】CAS(轻量级加锁)
1.5.5 AQS抽象队列同步器
参考1:谈谈Java多线程离不开的AQS
1.5.6 ConcurrentHashMap
参考1:详解ConcurrentHashMap
1.5.7 CountDownLatch、Semaphore、CyclicBarrier
1.6 熟悉MySQL事务、存储过程、锁、索引、执行计划等,掌握复杂SQL编写、数据库优化方案以及SQL优化,在工作中有相关SQL优化经验。
1.6.1 事务
- 四大特性
- 事务的隔离级别
参考:MySQL事务(transaction) (有这篇就足够了…)
1.6.2 索引
- 索引有哪些,分类
- 聚簇索引和非聚簇索引
- innodb和myisam引擎
- 索引失效的情况
参考1:一文搞懂MySQL索引所有知识点
参考2:MySQL高级篇——索引失效的11种情况
1.6.3 锁 悲观锁和乐观锁
1.6.4 优化
- MySQL语句可以怎么优化
- 案例
参考1:sql语句优化的15个小技巧(面试必刷!)
1.6.5 执行计划
- explain关键词 type
1.6.6 MVCC
参考1:【MySQL笔记】正确的理解MySQL的MVCC及实现原理
1.7 熟练掌握MyBatis、Spring、SpringMVC进行整合开发项目经验,对IOC、AOP原理有一定的理解。
1.7.1 IOC和AOP
1.7.2 循环依赖问题
1.8 熟练使用SpringBoot框架用于快速构建项目,SpringCloud微服务治理架构以及Nacos、Kafka、RabbitMQ、Gateway、Feign、Sentinel等技术。
1.8.1 微服务的概念
1.8.2 各个组件的作用,Nacos、Kafka、RabbitMQ、Gateway、Feign、Sentinel
参考1:微服务 常用组件(常用大全)
参考2:【微服务】------架构设计及常用组件
1.9 熟悉Redis数据结构、持久化、内存回收策略、常见缓存高并发场景以及生产环境常见问题解决方案(缓存雪崩、穿透、缓存db数据一致性等处理)。
1.9.1 Redis数据结构有哪几种
1.9.2 Redis的持久化方式
1.9.3 内存回收策略
1.9.4 常见使用Redis的场景
1.9.5 Redis和MySQL的数据一致性怎么保证
1.9.6 缓存击穿、缓存穿透、缓存雪崩的原因和解决方案
缓存穿透、缓存雪崩和缓存击穿 及解决方案
1.9.7 布隆过滤器
Redis-布隆过滤器(Bloom Filter)详解
1.10 理解消息中间件落地方案和使用场景,熟悉消息队列有序性、可靠性、幂等性、事务消息、消息积压的解决方案。
1.10.1 mq的作用。削峰填谷、解耦
1.10.2 如何保证消息的有序性
1.10.3 如何保证消息不会丢失
1.10.4 消息有可能会被重复处理吗?幂等性如何保证?
1.10.5 事务消息如何设计
1.10.6 mq的应用场景
1.11 理解CAP和BASE理论,理解分布式场景下常见问题和解决,如分布式锁、分布式事务、分布式session、分布式调度任务等。
1.11.1 分布式锁
- Redisson基于Redis实现的分布式锁的原理
- 分布式锁使用场景
- 分布式锁的缺陷
1.11.2 分布式事务
- CAP和BASE理论
- 分布式事务解决方案: 2PC、TCC、MQ最终一致性
- Seata分布式事务框架实现原理
1.11.3 分布式一致性 Session的实现方案
1.11.4 分布式任务调度:xxl-job
1.12 掌握Linux常用命令,了解Nginx服务的反向代理、负载均衡、动静分离。
1.12.1 linux常用命令
1.12.2 nginx反向代理、负载均衡、动静分离
二、工作经历
2.1 利用delayqueue+scheduled+mq实现了延迟消息工具完成了常见的延迟消息场景,并且避免了引入mq延时队列插件导致有多个mq时插件不通用的问题。
2.1.1 技术选择
为什么不使用MQ自带的延迟消息工具?你的做法有什么优点?
因为RabbitMQ实现延迟消息需要引入延迟消息插件,并且这种方式不够通用。我的实现延迟消息的方案不依赖于mq,而依赖于MySQL和DelayQueue,不管项目中用的是RabbitMQ,还是Kafka,RocketMQ,都可以用这个方案来实现发送延迟消息的目的。
2.1.2 具体实现细节
你是如何具体实现延迟消息工具的?可以描述一下基本的工作流程吗?
主要是MySQL+job轮询+DelayQueue,首先我们在数据库里新建一张本地消息表,这张表里可以记录json格式的消息内容,期望的发送时间,消息的发送状态(未发送,发送成功,发送失败)等字段,每次需要新建一个延迟消息时,就在这张表里插入一条记录。然后job可以采用1分钟执行一次,每次拉取这张表中未来2分钟内需要投递的消息,将其丢到java自带的 DelayQueue 这个延迟队列中去处理,每个延迟任务延迟的时间就是当前时间到这条消息期望被投递时间的差值,任务就是延迟对应时间后,就往mq中投递这条消息。
2.1.3 你是如何处理消息发送失败的情况的?
有失败重试机制,具体就是在MySQL的本地消息表里还存在字段,比如发送状态(发送消息的过程中,可能出现异常可能),失败后是否需要重试,下次重试时间,失败次数。当某个延迟任务携带的某条消息在发送到mq的过程中产生异常时,我们会捕获这个异常,并且在本地消息表中更新这条消息的状态,包括它下次应该什么时候重试,失败次数等。通过这样,我们在job轮询的时候,从本地消息表查询出来的不只有未来2分钟需要首次投递的消息,还有未来2分钟内需要重新发送的消息,再把这些消息放在延迟队列里。
2.1.4 事务消息是怎么处理的?
如果投递延迟消息过程中存在事务,事务成功的话就不用说了,因为一定会投递,而且失败了也会重试,最差的情况就是人工处理。如果事务提交失败了,由于我们是将消息先保存到事务消息表中再从这张表里取出消息进行投递的,事务提交失败,这条消息根本就不会被保存到本地消息表里,因此不会提交,不用做特殊处理。
2.1.5 延迟消息的使用场景有哪些
2.2 热门数据利用caffeine和redis实现了两级缓存,解决了用redis时压力过大查询缓慢的问题,保证了接口的高性能和高可用。
2.1.1 缓存设计问题:
你是如何决定使用两级缓存架构的?
在项目初期,我们使用了Redis作为唯一的缓存层来提高热点数据的访问速度。随着用户量的增长,我们发现在高流量时段,Redis的访问压力非常大,网络延迟比较高。然后我们进行了一系列的性能测试和分析,发现在多数情况下,用户请求的数据即时命中了缓存,但是由于网络io交互,还能够有很大的提升空间。因此,我们决定引入两级缓存架构:第一级使用Caffeine作为本地缓存,直接将不经常修改但查询很频繁的热点数据放在内存里进行缓存,能够提供更快的访问速度和减少网络延迟;第二级使用Redis作为分布式缓存,处理跨多个应用实例的数据共享问题。这种架构可以减少对后端数据库的直接访问,提高系统的吞吐量和响应速度。
在什么场景下,两级缓存比单一缓存更有效?
略
2.1.2 技术选择问题:
为什么选择 Caffeine 作为一级缓存,它相比其他本地缓存(如 Guava、EhCache)有哪些优势?
在选择本地缓存技术的时候我去调研了一下,首先Guava在性能和功能上相对于Caffeine有明显的劣势,并且它已经被spring官方放弃了,然后对于EhCache和Caffeine我也去看了一下,虽然说EhCache的功能很强大,但是它使用时配置太复杂了,用起来很麻烦,使得在项目开发中难以灵活运用, 况且其大部分功能在实际开发场景中也没什么用, 如缓存落盘。总的来说,Caffeine性能很强,功能虽然不如EhCache丰富,但是在项目开发中足够了。
Redis 作为二级缓存,它的哪些特性让你觉得适合这个场景?
我们的项目原本就集成了Redis,因此就没考虑别的缓存技术作为二级缓存,并且Redis足够优秀。
2.1.3 性能优化问题:
你是如何评估 Redis 压力过大的?有使用哪些监控工具吗?
Grafana + Prometheus:这是一个强大的监控解决方案组合。你可以使用 Prometheus 来收集 Redis 的性能指标,然后通过 Grafana 进行可视化展示。这可以帮助你实时监控 Redis 的 CPU 利用率、内存利用率、网络延迟等关键指标。
在引入两级缓存后,性能提升了多少?你是如何衡量的?
响应时间(RT):测量引入缓存前后的系统响应时间。自己编一下。
2.1.4 高可用性问题:
你是如何确保缓存系统的高可用性的?
Redis集群
如果缓存服务宕机,你的系统有哪些降级策略?
降级:返回默认数据;限流:防止请求过多,造成数据库压力过大
2.1.5 缓存一致性问题:
在使用两级缓存时,你是如何处理缓存一致性问题的?
延迟双删保证数据库和缓存之间的一致性:删除redis->更新数据库->延时n毫秒(大于一次写操作的时间,一般2-3秒)->删除redis。这样防止线程1在更新数据库之前,线程2取到了旧数据放到缓存里。
集群情况下保证各个节点之间的caffeine缓存同步问题:采用消息中间件来进行同步,能够保证最终一致性。
是否遇到过缓存穿透、缓存击穿或缓存雪崩问题?如果有,你是如何解决的?
缓存穿透: 我们遇到过缓存穿透的问题,请求中的key对应数据本来就不存在,比如用一个不存在的用户id来获取用户信息。为了解决这个问题,常见的方案就是使用布隆过滤器,在我们项目中也是这样做的,将可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据就会被这个bitmap拦截。
缓存击穿: 对于缓存击穿问题,我们使用了互斥锁,在某个缓存失效的时候,我们并不是立马就去从数据库中加载数据,而是先用setnx命令去set一个互斥的key,当setnx操作返回成功时,再去从数据库中加载数据,再回设缓存。
缓存雪崩: 就是大面积的key同时失效,为了防止缓存雪崩,我们为不同数据设置了不同的过期时间,我们项目中使用了一个简单方案就是将缓存失效时间分散开,,比如在原有的失效时间基础上增加一个随机值,比如1-2分钟范围的随机值,就很难引发缓存key集体失效的问题。
2.1.6 数据同步问题:
当数据更新时,你是如何确保多个节点中的Caffeine缓存中的数据同步的?
在某些场景下,可能需要确保多个节点的caffeine缓存数据一致性。这时,可以采用消息队列,当一个节点更新了缓存数据后,发布一个消息到RabbitMQ中,其他服务实例订阅这个消息,并据此更新自己节点的本地缓存。
事务管理:可能有事务问题,可以讲讲分布式事务,事务消息,消费幂等,失败重试,一定会成功消费,实在不行人工介入上来说。
是否有使用发布/订阅模式或其他机制来更新缓存?
略
2.1.7 缓存配置问题:
你是如何配置 Caffeine 和 Redis 缓存的?比如缓存的大小、过期策略等。
在分布式环境中,Redis 缓存是如何进行集群部署的?
2.1.8 代码实现问题:
你能简单描述一下你在代码中是如何实现两级缓存的吗?
自定义注解DoubleCache,在定义一个切面来处理使用缓存的逻辑
切面会对这个注解标注的方法进行增强,具体增强逻辑是注入spring cache来处理本地缓存,注入RestTemplate来处理Redis缓存,对数据的查询和更新都会先后处理这两级缓存,同时,更新caffeine缓存的时候会发送消息到mq里,保证多个实例中的本地缓存服务的数据同步
你使用了 Spring Cache 还是直接使用 Caffeine 和 Redis 客户端?
2.1.9 业务理解问题:
这个缓存方案主要解决了哪个业务场景的问题?
哪些数据放在Redis,哪些数据放在Caffeine,还是说本地缓存和Caffeine缓存中的数据是一样的?
在实施两级缓存时,我们根据数据的访问频率和更新频率来决定哪些数据存放在Redis,哪些数据存放在Caffeine。
Caffeine(本地缓存):
我们选择将访问频率极高且更新频率较低的热点数据,如商品详情、用户个人信息等,存放在Caffeine中。这些数据通常是读多写少,并且对读取速度有较高要求。
Caffeine作为一级缓存,可以减少网络延迟,提供更快的访问速度,因为它直接存储在应用服务器的内存中。
Redis(远程缓存):
对于非热点数据,或者访问频率不是极高的数据,我们选择存放在Redis中。这些数据可能包括一些不经常访问的信息,或者更新频率较高的数据。
Redis作为二级缓存,可以跨多个应用实例共享,并且可以处理更大的数据集。同时,它也作为Caffeine缓存的数据备份,当Caffeine中的缓存过期或被清除时,可以从Redis中重新加载数据。
2.1.10 扩展性问题:
如果业务增长,缓存需求增加,你打算如何扩展当前的缓存架构?
考虑到未来可能的需求变化,缓存策略会如何调整?
2.3 基于本地消息记录表实现了事务消息的投递过程,引入衰减重试+错误预警保证了消息投递的可靠性。
2.3.1 设计和实现
为什么选择本地消息表来实现事务消息?还有别的方式吗
某些MQ支持事务消息,如RocketMQ的事务模式,这种方式仍然存在一定的局限性。在事务模式下,每次发送都需要等待MQ确认,这会影响性能。所以选择利用本地消息表来实现,这种方式比较通用,并且实现起来也不复杂。
事务消息,你的方案具体怎么实现的?
在数据库中建立一张本地消息表msg,这个表临时存储一些即将要被发送的消息。在业务代码中的具体实现方式是,先开启事务,再执行一些本地业务后,如果需要投递一条消息,就往数据库里写入一条数据,以json格式来记录这条消息的具体内容是什么。等待事务执行完毕后,再来处理这条消息,也就是如果事务已经提交成功,那么就取出这条数据投递到mq里,如果事务回滚了,就不需要额外的处理,因为事务回滚,那么本地消息表中刚刚插入的消息记录也会被抹去,那么当然也不会投递这条消息了。这样就实现了事务消息。
在你的方案中,如何保证可靠性呢?怎么保证你把消息记录到本地消息表后,这条消息一定就会被投递到mq,而中途不会产生例如网络不好导致的异常呢?或者讲讲你的衰减重试+错误预警是怎么保证可靠性的?
在实际开发中,存在可能消息从本地消息表中取出来后,由于网络不通畅,投递到mq失败的情况。我们是允许他失败的,但是我的处理方式是让他最终会成功就好了,因此引入了衰减重试的机制。具体是这样实现的,在本地消息表中,我们会存在一些字段,比如消息状态(成功or失败or未投递),重试次数,下次重试时间,当我们发送消息到mq时,会捕获这过程中出现的异常,如果消息发送失败了,捕获到异常时,就对数据库中的这条消息对应的记录进行更新,表示它发送失败了,规定他下一次重试是在几秒钟后重新发送到mq,这里衰减式重试是他这次发送失败后可能隔2秒钟重试,如果还失败可能隔55秒钟,再失败就10秒钟。如果它失败的次数超过了我们的规定值,我们就不进行重发了,直接采用人工的方式来兜底。这样我们就能保证,在本地消息表中记录的消息最终一定会被成功投递到mq。(怎么预警?一旦触发错误预警,我们会通过邮件或短信等方式通知运维人员,并记录详细的错误日志。)
2.4 基于事务消息+消费幂等+失败补偿方案,利用mq最终一致性解决了分布式事务的问题。
2.4.1 设计和实现
解决分布式事务问题还有别的方案吗?你为什么选择用mq最终一致性来解决?
分布式事务的解决方案有2PC、3PC、Seata框架、TCC、事务消息,其中2PC、3PC存在性能问题,seata的话还需要部署新模块,落地起来比较复杂,TCC的话对业务代码的入侵很强。而使用事务消息,是利用了mq的最终一致性来解决分布式事务问题的,在我们项目中,绝大部分业务不怎么要求强一致性,最终一致性就能满足。
详细介绍一下你的事务消息+消费幂等+失败补偿方案?
这里解决分布式事务是从生产者和消费者两头入手,在生产者端,需要实现事务消息和保证消息的可靠性,这样生产者发送消息时,如果事务提交成功,我们就能确保消息一定会被投递到mq,事务回滚了,mq里也一定不会出现这个消息,这是生产者端。在消费者端,我们要确保mq中的消息一定会被消费者消费,并且同时要保证幂等消费和消费一定会成功,这样就保证了数据的最终一致性。
你能详细解释一下你是如何实现事务消息的?具体的设计思路是什么?
同2.3
什么是消费幂等性?为什么在分布式事务处理中需要实现消费幂等性?
消费幂等性就是,即使出现了重复的消息,被同一个消费者消费,也只会成功消费一次。如果不保证消费幂等性,那如果出现了网络问题,消息被投递了多次,进而导致消费了多次,这就数据不一致了。因此一定要考虑消费的幂等处理。
如何确保消息一定会被消费者消费的?(关键在关闭自动ack+消费幂等+失败重试)
首先要关闭mq的自动ack,否则可能出现消费异常但消息丢失的情况。在消费者的具体逻辑是:消费者拉取消息时,先不要通知mq删除这条消息,然后再执行业务逻辑(执行业务逻辑的时候需要做幂等处理),最后业务处理完了,再通知mq删除这条消息。
而消费者端在消费消息的时候可能消费失败了,就需要进行失败重试。
消费者的失败重试方案就是利用延迟消息,将消费失败的消息重新插入到本地消息表中,延迟n秒,再重新投递到mq,这样消费者就能间隔n秒后重新消费了。
幂等消费是怎么实现的?
首先,在生产者端,我们需要确定:什么样的消息是重复的消息,我们可以规定一个发送到mq中的消息体的格式,字段有生产者全类名,uuid用来做消息的唯一标识,实际传递的消息内容。如果mq中出现了某条消息体的生产者类名和消息id都一样,我们就判断这是同样的重复的消息。
其次,在消费者端,我们需要建立一个添加了唯一约束的幂等辅助表,消费者拉取到消息时,将消息的唯一标识和全类名作为这个幂等辅助表的唯一key插入进去。并且插入幂等辅助表的操作要与消费的业务代码在同一个事务的代码块里,这样当多个线程同时消费某条重复消息的时候,只会有一个消费者成功消费这条消息,其他线程都会因为违反唯一约束而回滚业务代码。
2.5 使用CompletableFuture优化查询模块,对业务功能模块中的多个异步调用进行重新编排,优化结果显示响应时间从2s降到0.2s。
2.5.1 具体场景:
你能描述一下你是如何识别出原始查询模块存在的性能瓶颈的吗?
当时是我做的一个个人信息实名认证的一个接口,在业务上他涉及到对多个表的数据的查询和修改,并且还涉及到了跨数据库的调用,比如说客户修改实名认证信息的时候,可能会去查保存在我们库里的身份证信息,营业执照信息,公司信息等等这些。刚开始是对这些查询直接按顺序同步调用的,后来有客户反馈说这个页面提交响应很慢,每次用户都要好几秒钟才能看到他在前端提交页面的结果。然后我去skywalking上看了下,发现这个调用链路上的这个接口延迟很长,我就分析了一下,应该是多个有顺序的查询之间阻塞了,因此就引入了多线程
下面略过
监控和日志分析:在识别查询模块的性能瓶颈时,我们主要依赖于应用的日志记录和性能监控工具,如Prometheus搭配Grafana或者使用Spring Boot Actuator。我们发现特定的查询请求在执行时存在明显的延迟。通过分析调用链路,我们注意到每次请求都会发起多个阻塞的数据库查询和其他外部API调用,而这些操作是顺序执行的。
性能测试:我们对查询模块进行了压力测试和负载测试,模拟高并发场景下的性能表现。通过这些测试,我们发现在高负载情况下,数据库查询成为瓶颈,尤其是当多个查询操作顺序执行时,总体响应时间显著增加。
代码审查和分析:我们对查询模块的代码进行了审查,发现存在一些不必要的数据加载和复杂的关联查询,这些操作导致了数据库查询效率低下。
这个查询模块是在什么样的业务场景下运行的?它涉及哪些数据源或服务?
2.5.2 技术细节:
你能否解释一下你是如何使用CompletableFuture来优化这个模块的?
在这个项目中,我们发现原始查询模块的性能瓶颈在于它需要执行多个同步的数据库查询和外部API调用。这些操作都是顺序执行的,因此整体响应时间较长。为了解决这个问题,我决定采用 CompletableFuture 来实现这些操作的异步化。
1、首先,我将每一个独立的查询操作封装成了一个 CompletableFuture 实例。例如,对于数据库查询,我会创建一个 CompletableFuture 对象,异步执行查询任务。类似地,对于外部API调用,我也创建了相应的 CompletableFuture 并异步执行。
2、然后,我使用 thenCombine() 或 thenCompose() 方法来组合这些 CompletableFuture,以便在一个或多个操作完成后,继续执行下一个操作。例如,当数据库查询的结果准备好之后,再继续处理这些结果并与其它数据合并。
3、最后,我使用 CompletableFuture.allOf() 方法来等待所有的异步操作完成,并通过 join() 方法获取最终的结果。这种方法使得原本需要串行执行的操作变成了并行执行,从而显著减少了总响应时间。
在优化过程中,你遇到了哪些挑战?你是如何解决这些挑战的?
异常处理:异步编程的一个难点是如何优雅地处理异常。我们通过 exceptionally() 方法来捕获并处理 CompletableFuture 中抛出的异常,并在必要时向用户返回友好的错误消息。
CompletableFuture future1 = CompletableFuture.supplyAsync(() -> “任务1”).exceptionally(ex -> “默认值1”);
CompletableFuture future2 = CompletableFuture.supplyAsync(() -> “任务2”).exceptionally(ex -> “默认值2”);
CompletableFuture combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + " 和 " + result2);
- 使用 allOf() 合并多个 CompletableFuture
当你使用 CompletableFuture.allOf() 来等待多个 CompletableFuture 完成时,如果其中一个 CompletableFuture 抛出异常,allOf() 会立即完成,并且这个异常会被记录下来。你可以通过 join() 方法来检查是否有异常发生。- 组合多个 CompletableFuture 的结果
如果你需要处理多个 CompletableFuture 的结果,并且在其中一个失败时需要给出特定的响应,可以使用 thenCombine() 或 thenCompose() 方法,并在 exceptionally() 中处理异常。- 使用 whenComplete() 或 handle()
你也可以使用 whenComplete() 或 handle() 方法来处理异常,并决定最终的返回值。
CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2, future3);
try {
allFutures.join(); // 等待所有Future完成
} catch (CompletionException e) {
// 如果任何一个Future抛出异常,则在这里被捕获
System.err.println("One of the futures failed with an exception: " + e.getCause());
// 处理异常情况
}
------
CompletableFuture<Data1> future1 = CompletableFuture.supplyAsync(() -> service1.getData());
CompletableFuture<Data2> future2 = CompletableFuture.supplyAsync(() -> service2.getData());
CompletableFuture<Data3> combinedFuture = future1.thenCompose(data1 -> {
return future2.thenApply(data2 -> {
// 结合两个结果
Data3 combinedData = combineData(data1, data2);
return combinedData;
}).exceptionally(throwable -> {
// 如果future2抛出异常,则返回null或其他默认值
System.err.println("Failed to get data2: " + throwable.getMessage());
return null;
});
}).exceptionally(throwable -> {
// 如果future1抛出异常,或者前面的处理中有任何异常,则返回null或其他默认值
System.err.println("Failed to get data1 or combine data: " + throwable.getMessage());
return null;
});
combinedFuture.thenAccept(data3 -> {
if (data3 != null) {
// 正常处理数据
processCombinedData(data3);
} else {
// 异常处理逻辑
handleFailure();
}
});
------
CompletableFuture<Data3> finalFuture = future1.thenCompose(data1 -> future2.thenApply(data2 -> combineData(data1, data2)))
.handle((data3, throwable) -> {
if (throwable == null) {
return data3; // 没有异常,直接返回结果
} else {
System.err.println("Failed to combine data: " + throwable.getMessage());
return null; // 或者返回一个默认值或错误对象
}
});
finalFuture.thenAccept(data3 -> {
if (data3 != null) {
processCombinedData(data3);
} else {
handleFailure();
}
});
2.5.3 性能改进:
你能详细说明一下从2秒减少到0.2秒的具体实现方法是什么?
为了将响应时间从2秒减少到0.2秒,我们采取了以下几个关键步骤:
识别性能瓶颈:
使用SkyWalking等APM工具进行分布式追踪,分析请求路径上的各个阶段耗时情况。我们发现在原查询模块中,有多个数据库查询和外部API调用是按顺序执行的,这导致了整体响应时间较长。
重构为异步调用:
我们利用 CompletableFuture 来重构这些同步调用。对于每个独立的服务调用,我们创建了一个 CompletableFuture,并在后台线程池中异步执行这些任务。例如,对于数据库查询,我们创建了一个 CompletableFuture 并在异步线程中执行查询操作。
并行处理多个请求:
使用 CompletableFuture.allOf() 方法来启动多个 CompletableFuture 并行执行。这允许我们同时开始多个数据库查询和外部API调用,而不是依次等待它们完成。
整合结果:
当所有异步操作都完成时,我们使用 thenApply() 方法来处理每个 CompletableFuture 的结果,并将这些结果合并成最终的响应。如果需要进一步处理,我们会使用 thenCompose() 来组合结果。
异常处理:
异步调用中可能出现异常,我们使用 exceptionally() 方法来捕获并处理这些异常,确保不会因为个别操作失败而影响整个请求的完成。
测试与验证:
在实现这些变化后,我们进行了详细的单元测试和集成测试,确保所有功能正常工作,并且没有引入新的bug。我们还进行了压力测试,验证在高并发情况下系统的表现。
这个性能改进是否带来了其他方面的影响,比如内存消耗或CPU使用率?
2.5.4 并发编程经验:
在使用CompletableFuture时,你有没有遇到过线程安全或者死锁等问题?如果有,你是怎么处理的?
无
除了CompletableFuture,还有其他的并发工具或框架用在项目中实现异步需求吗?
除了 CompletableFuture,可以用线程池:
ExecutorService:
我们使用了 ExecutorService 来管理线程池,这样可以控制线程的数量,并复用已创建的线程,避免频繁创建和销毁线程带来的开销。我们根据系统的需求配置了不同类型的线程池,如固定大小的线程池 FixedThreadPool 和缓存线程池 CachedThreadPool。
2.5.5 设计模式与架构:
为了实现异步调用,你是否应用了某些设计模式?
你是否有对现有的系统架构进行任何改变以适应新的异步调用需求?
2.5.6 测试与验证:
你是如何确保异步调用的正确性的?你采用了什么样的测试方法?
优化后的系统在负载测试中的表现如何?
2.6 对原有的复杂代码进行重构,采取工厂+策略模式实现了项目的各个业务产品对应的信息创建、编辑的解耦处理,解决了原先老旧代码不易维护的问题。
2.6.1 重构的具体细节
具体场景:
你能具体描述一下原有代码存在的问题吗?为什么说它是复杂的、不易维护的?
这里的场景是:我们项目中针对客户有不同的方案应对不同的需求,比如这段时期我们主推的金融产品方案有abc三种,在某个提交产品信息的接口里,对这三种产品来说是有一些共同点的,比如都需要查询相关的合同信息,或者都需要查询相关的银行机构信息。在我们老旧的代码里面,这个提交接口是通过前端传过来的产品id判断客户正在使用的是哪一个产品,这里是需要好几个if else来判断的。并且在这段代码里,我们获取到是在使用哪个产品后,再才会去调用相关的api去获取合同信息、银行机构信息(比如说判断出来是产品a,就去调用a的相关查询方法,b就去调用b的相关查询)。这样的话,我们假如后续进行产品迭代,新上架了一个新的理财产品d后,除了写这个新产品d的相关查询逻辑外,还要在这个接口里面修改代码,多加个if else,在if里面再写个调用d的查询逻辑。这样不符合软件开发的开闭原则,这些abc都是各自独立的产品。但是他们的编辑和查询在这个接口里面耦合了。问题就是写新代码的时候稍微不注意可能会影响到之前的代码。
怎么重构的,重构前后,代码的结构发生了怎样的变化?
我们定义了一个产品策略IProductStrategy的interface,这个抽象接口定义了一些每个产品共有的类似的一些策略,比如说由于每个产品都有一些类似的查询相关的合同信息,或者都需要查询相关的银行机构信息这些逻辑。对每一个具体的产品abc对应的策略类,都会去实现这个抽象产品策略interface,重写这些抽象方法,比如说查询相关的合同信息的方法,这样abc产品的查询合同的逻辑就在他们各自的策略类里了,到时候在我们的业务接口里,就可以直接通过IProductStrategy来调用方法(之前是a点b点c点)了,这里就用到了策略模式。只有这个还不够,我们还需要通过前端传来的id来判断具体使用哪一个产品策略,这里我们使用到了工厂模式,定义一个策略工厂类StrategyFactory,在这个工厂里的方法里,我们可以通过前端传来的产品id,返回一个具体的产品策略,为了进一步降低耦合,在这之前可以定义一个产品策略枚举类,保存产品策略的class对象。在产品策略工厂类里,就可以通过产品id,再结合这个枚举类,直接返回一个具体的产品策略了。
这样的话,在业务代码里就只需要两步操作了,第一步是传入id通过策略工厂得到具体产品策略对象,用IProductStrategy接收,第二步调用IProductStrategy的相关方法,就能调用具体产品策略子类的逻辑了。此外,以后迭代更新的时候,添加新产品,只需要写新产品的策略类和更新产品策略枚举类就行了,业务代码不用动,符合软件开发的开闭原则,易于扩展和维护了。
public ResultVO queryTicket(QueryTicketParam param) {
IProvideStrategy provideStrategy = strategyFactory.getProvideStrategy(param.getcId(),param.getType());
return provideStrategy.queryTicketList(param);
}
2.6.2 工厂模式和策略模式的运用
模式选择:
为什么选择了工厂模式和策略模式?还有没有考虑过其他的设计模式?
因为每中产品的逻辑相似,只是具体的实现不同,并且有多层if else,因此首先就相当了策略模式来重构代码。获取对应策略就用的工厂模式,这两种设计模式也是软件开发时常用的设计模式。
在你的项目中,工厂模式和策略模式分别解决了什么具体问题?
模式实现:
你能详细解释一下你是如何实现工厂模式的吗?具体是怎么创建对象的?
在使用策略模式时,你是如何定义不同的策略以及它们的执行逻辑的?
2.7 基于自定义注解+jackson序列化器优雅实现不同业务数据的脱敏处理。
使用自定义注解+jackson序列化优雅实现数据脱敏
2.7.1 你的数据脱敏方案有什么优点?
通过自定义注解,我们可以将脱敏逻辑与业务逻辑分离,使得代码更加模块化。这样做后,我们只需要在需要被脱敏的字段上加上注解就行,不需要动业务代码。此外还具有扩展性,添加新的脱敏策略时,也不需要动业务代码。很优雅,也能提高开发效率。
2.7.2 能详细说明一下这个方案是怎么实现的吗?
- 首先我们的自定义注解是这样设计的,这个注解上还标注了
@JacksonAnnotationsInside
和@JsonSerialize
,这样的话我们的自定义注解就有了json序列化的功能,然后它还有一个属性,这个属性是个枚举类,定义了不同的脱敏策略应对不同的业务字段。- 这个自定义注解有json序列化功能后,由于有不同的脱敏策略,因此我们要自定义一个序列化器,这个自定义序列化器需要继承
JsonSerializer
,让它能够有序列化的功能,然后还需要实现ContextualSerializer
,重写createContextual
方法,这样就能根据当前上下文创建一个应用于当前业务数据的脱敏策略。比如给手机号字段添加注解时,这个自定义序列化器应用的就是手机号的序列化方式,如果是邮箱,就应用邮箱的序列化策略。
2.7.3 你的脱敏策略是怎么设计的?
我的脱敏策略是使用的一个枚举类,这个枚举类保存的是Function接口,这个Function接口输入一个字符串,返回一个结果字符串,具体的逻辑,我是应用的Hutool类中的一个脱敏工具类,这个工具类对于手机号,邮箱,密码,中文名等都有对应的脱敏实现。这样的话,在自定义的脱敏序列化器里,就可以应用枚举类里对应的Function接口完成相应内容的序列化了。
2.7.4 ContextualSerializer在实现中起到了什么作用?
如果只继承
JsonSerializer
类的话,这个自定义序列化器就只有序列化的功能,不能根据注解里的参数来动态指定序列化方式。实现了这个接口的话,我们在自定义序列化器里定义一个表示脱敏策略的属性,就能根据注解里的参数给这个属性设置对应的脱敏策略,这样在自定义序列化器的serialize
方法里就能应用这个脱敏策略进行字段的序列化了。
2.8 第三方sdk接入,如短信接口对接、微信小程序js-sdk对接、内部项目接口对接、银行业务接口对接等。
2.8.1 第三方SDK接入的具体细节
具体场景:
你能描述一下你在项目中接入的第三方SDK有哪些具体的功能需求吗?
在接入这些SDK的过程中,遇到了哪些主要的技术挑战?
技术细节:
你是如何选择和评估这些第三方SDK的?考虑了哪些因素?
在接入过程中,你是如何确保安全性(如数据传输加密、敏感信息保护等)的?
2.8.2 接口对接的具体实现
对接流程:
以短信接口对接为例,你能详细描述一下对接的整个流程吗?
在对接微信小程序js-sdk时,你是如何处理权限校验和数据安全的?
接口设计:
在对接内部项目接口时,你是如何设计接口规范的?如何确保接口的一致性和可扩展性?
对接银行业务接口时,你采用了哪些标准协议或框架?
2.8.1 安全性和可靠性
安全措施:
在对接第三方服务时,你是如何保障数据传输的安全性的?采用了哪些加密技术?
对于敏感数据(如银行账户信息),你是如何处理的?
可靠性保障:
在对接过程中,你是如何处理网络不稳定或第三方服务中断的情况的?
你是否设计了相应的容错机制或备份方案?
2.8.1 测试与验证
测试策略:
在对接完成后,你是如何进行测试的?采用了哪些测试方法?
你是否进行了压力测试或性能测试?结果如何?
问题排查:
在测试过程中,发现了哪些问题?你是如何解决的?
2.8.1 团队协作与沟通
团队合作:
在对接过程中,你是如何与团队成员协作的?如何协调各方的需求?
你是否与其他部门(如产品、运营等)有过沟通?如何确保各方需求得到满足?