1、吃透分库分表组件和大营销项目
2、简历里相关的八股
3、hot100算法
面试官你好,我叫崔中闯,一名网络工程专业的大四学生,在校期间成绩始终保持专业前三,大学期间曾获得过校一等奖学金,专利授权奖金,软著奖金,在校期间发布过四项软著并已拿到著作权,一项专利已授权,参加过多个创新创业比赛,并在团队中作为技术研发人员开发项目并申请软件著作权为团队项目做支撑,课业方面,多次作为小组长带领团队共同研发课程项目,接触过数据可视化echart图表,three.js开源3D图形库,前端语言html三件套,vue,react,数据库方面用过mysql、oracle、neo4j图数据库,后端接触过python,java语言并基于这两个语言都开发了项目并申请软著,工作经历方面,参加过两端实习,第一段科大讯飞的实习生作为负责人带领三个人的小团队共同研发公司的项目《家庭传承云平台》,第二段实习是杭州亚信公司,参与中国移动的稽核业务开发,项目经验方面,除了公司项目之外我还做了一个大乐透抽奖业务项目和分库分表组件项目,以上就是我的个人情况介绍
实习经历摘要:
在实习期间,我参与了一个重要的数据处理项目,负责实现一个高效的数据分流统计系统。该项目涉及将大量数据自动化地从总表分流到六大业务类型的分表中,以满足不同业务需求。
主要实现过程:
设计并实现了一个抽象基类
BaseSample
,一个auditSample类用于统一获取总表中的数据存入List<Object> records,再定义了抽象方法saveRecords
,该方法用于将数据记录列表保存到指定的业务类型表中。根据六大业务类型,创建了相应的子类,每个子类重写了
saveRecords
方法,实现了具体的数据插入SQL语句,确保数据正确地插入到对应的业务表。开发了自动化类,通过实例化具体的业务类型类并调用其
auditSample
方法,实现了从总表中提取数据并自动分流到相应分表的功能。引入了事务管理机制,通过
DBUtils
类的startTransaction
、commitTransaction
和rollBackTransaction
方法,确保数据库操作的原子性和一致性。
实现成果:
成功实现了一个自动化的数据分流系统,提高了数据处理的效率和准确性。
通过抽象工厂模式的应用,增强了代码的可维护性和可扩展性。
通过事务管理,确保了数据库操作的安全性和数据的一致性。
项目介绍
大乐透项目是我对稀土掘金平台的抽奖业务进行深度分析后开发的一个采用微服务架构前后端分离的抽奖平台,前端采用react,后端框架是SpringBoot,数据库采用mysql和redis,其中用到的分布式技术栈有dubbo、nacos、rabbitmq、ElasticSearch、canal、xxl-job
关于这个项目的核心是抽奖流程的设计和库存扣减处理,抽奖前的⼈群判断采用责任链模式依次执行黑名单规则、权重规则、默认规则、之后进入抽奖的中和后,这两部的流程是相对复杂的,需要判断用户抽奖了几次,对于不同次会限定是否能获得某个奖品,同时还有库存的扣减,如果库存不足或者不满足n次抽奖得到某个奖品,则会进行兜底。那么这就是一个树规则的交叉流程,所以使用了组合模式构建一颗规则树,并通过数据库表的动态配置决定在抽奖前完成后,后续的流程要如何进行。扣减库存这块采用的是Redis decr原子操作+setnx加锁兜底,decr操作保证库存是一个一个的进行扣减,扣减完之后给扣减过的库存进行setnx加锁并设置过期时间操作,这样下次再重复扣减该库存就会不被允许,保证不会出现超卖情况。另外抽奖的算法选择也是一大亮点,我设置了一个概率范围的阈值,当概率范围较小时采用O(1)空间换时间的算法,提前将概率计算出来做成概率表存入redis,后续抽奖时直接查找即可,当概率范围较大时采用O(log(n))的时间换空间算法,log(n)的时间复杂度算法又分for循环、二分查找、多线程查找三种,也是根据概率范围的大小来选择,这样的设计保证了抽奖的快速响应
目前我是将这个项目的技术栈都部署在了服务器上,可直接访问,用grafana作为可视化面板和普罗米修斯作为数据源来实现抽奖系统的监控
⚔
mq主要用于
监听活动sku库存消耗为0
监听积分账户调整成功消息,进行交易商品发货
监听用户行为返利消息
监听用户奖品发送消息,发奖
XXL-JOB主要用于
扫描数据库中的task表来发送任务信息给MQ
更新活动sku库存(从Redis队列里更新到数据库)
更新奖品消耗库存(从Redis队列里更新到数据库)
分库分表具体实现,这块需要加强一下
分库分表组件的执行流程:
1、先配置抽奖系统的yml文件,配置多个数据源(就是多个数据库:db01, db02)
2、自定义一个注解,用于AOP切面拦截,拦截后切换数据源,看该分配到哪个库哪个表
3、然后在组件里面实现的切面拦截:数据库路由计算、扰动函数加强散列、计算库表索引、设置到 ThreadLocal 传递数据源,
4、切换数据源
简单技术问题:
什么是 AOP?在 DB-Router 中如何应用 AOP?
(AOP:Aspect-Oriented Programming)面向切面编程,是一种设计模式,它允许程序员在不修改原有代码的情况下,对程序进行拓展和修改,Spring中AOP用于实现横切逻辑,如日志记录、权限校验、异常处理等。
在DB-Router中通过自定义注解的方式,拦截被切面的方法,进行数据库路由,拦截后读取方法中的入参字段,经过扰动函数计算得出库索引和表索引存入ThreadLocal中。
AbstractRoutingDataSource 是什么?它的作用是什么?
AbstractRoutingDataSource
是 Spring 框架中DataSource
抽象类的一个实现,用于实现数据源的动态路由。使用AbstractRoutingDataSource类中的
determineCurrentLookupKey()
方法来实现数据源的动态路由,根据存入ThreadLocal中的DBKey来确定当前的数据源。这样可以提高代码的可维护性和灵活性,同时也可以减少代码的重复。ThreadLocal 是什么?在 DB-Router 中是如何使用的?
从AOP切面后取得自定义注解中的入参字段并通过反射获取到值,计算出最终路由的结果库索引和表索引,存入ThreadLocal,在后面切换数据源时取出库表索引替换原有数据源。
什么是哈希散列?在 DB-Router 中为什么选择了哈希散列算法?
哈希散列能够提供数据的均匀分布、简单高效、易于扩容和灵活定制等优点
SAC 测试是什么?在 DB-Router 中如何应用 SAC 测试?
中等技术问题:
什么是 MyBatis Plugin?在 DB-Router 中如何应用 MyBatis Plugin 实现动态变更表信息?
DB-Router 基于 MyBatis 插件拦截机制,获取 SQL 信息,并进行修改操作。主要涉及MyBatis源码类;StatementHandler - 语句处理器、MetaObject - 元对象、MappedStatement 映射语句对象。
分库分表的散列算法有哪些,各自的优缺点是什么?
在 DB-Router 中如何支持个性化的分库分表控制?请结合具体实例说明。
DB-Router 可以实现不同的分库分表策略,在策略中可以提供不同的散列算法或者允许一个字段分库,另外一个字段分表的设计。
在 DB-Router 中如何实现扩展监控、扫描、策略等规则?
DB-Router 可以与监控服务配合,在路由执行时,在方法上添加监控配置信息。一般是一个key。这里通过key与对应的方法名称组合出唯一值,来监控路由方法的执行。包括耗时、异常等信息。
什么是雪崩测试?在 DB-Router 中如何进行雪崩测试?
预防XSS攻击
输入验证和净化:对用户输入进行严格验证和净化,确保输入内容不包含恶意脚本代码。使用白名单策略,只允许符合预期格式的输入。
输出编码:对输出数据进行适当的编码,以防止浏览器将其解释为代码。例如,HTML编码、JavaScript编码和URL编码可以有效防止恶意脚本的执行。
使用安全的API和库:使用已知安全的API和库,避免直接操作DOM或执行动态生成的代码。许多现代框架和库提供了内置的XSS保护功能。
设置内容安全策略(CSP):通过HTTP头来告诉浏览器只允许加载特定来源的资源,这可以阻止攻击者注入恶意脚本。
HTTP-only Cookie:禁止JavaScript读取某些敏感Cookie,攻击者完成XSS注入后也无法窃取此Cookie。
预防CSRF攻击
使用Token验证:服务器生成一个唯一的Token,并存储在用户的Session中,每次请求都需要携带这个Token,服务器验证Token的有效性。
验证Referer头部:服务器检查请求的Referer是否来自本站,如果不是则拒绝请求。
设置HttpOnly和Secure标志的Cookie:HttpOnly防止JavaScript脚本读取Cookie,Secure防止Cookie在非HTTPS连接中被发送,从而降低CSRF攻击的风险。
验证码:在执行敏感操作时给用户的设备发送一个验证码,黑客拿不到验证码也就无法发动攻击。
预防SQL注入攻击
使用参数化查询:参数化查询将输入数据与SQL语句分离,并将其视为参数进行处理,从而避免了注入攻击。
输入验证和过滤:有效的输入验证和过滤是防范SQL注入攻击的关键。应该始终对用户输入进行验证和过滤,只接受符合预期格式的数据。
使用存储过程:存储过程可以限制SQL注入攻击的影响,因为它们通常不允许直接的SQL命令执行,而是通过预定义的代码路径执行。
最小权限原则:为数据库连接或用户账户分配仅够完成其任务所需的最小权限。
定期更新和维护数据库软件:定期更新数据库管理系统和应用程序,以确保它们具有最新的安全补丁。
Java
介绍一下Java的三大特性
1. 封装:封装是将数据(属性)和操作数据的方法(行为)捆绑在一起,并隐藏对象的内部实现细节,仅通过公共接口对外交互。
2. 继承:继承允许一个类(子类)继承另一个类(父类)的属性和方法,支持代码复用,并能添加或覆盖自己的属性和方法。
3. 多态:多态使得同一个方法调用可以有不同的行为,这取决于对象的实际类型,它增强了程序的灵活性和扩展性。
java集合
集合接口有两个Collection、Map,由此延伸出所有的实现类list、set、queue、map
StringBuffer和StringBuilder
StringBuilder是线程不安全的,性能方面StringBuilder>StringBuffer
重载和重写
重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理
重写是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变(遵循两同两小一大)
MySql
mysql分组查询
group by 如果需要条件加上having 。。。
数据库有哪些关键字? 删除操作都有哪些?
drop删表删数据、truncate一次性删数据不删表、delete逐行删数据不删表
索引分类
以数据结构分类
B+Tree索引、Hash索引、Full-text索引
以物理存储分类
聚簇索引、二级索引
以字段特性分类
主键索引、唯一索引、前缀索引
以字段个数分类
单列索引、联合索引
优化索引的方法
前缀索引优化、覆盖索引优化、主键索引最好是自增的、防止索引失效
什么时候索引会失效
当使用左或左右模糊匹配(like %xx或like %xx%),都会造成索引失效
当在查询条件中对索引做了计算、函数、类型转换操作,会导致索引失效
联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则会导致索引失效
在where子句中,如果在or前的条件列是索引列,而在or后的条件列不是索引列,那么索引会失效
为了更好的利用索引,索引列要设置为not null 约束
什么时候不需要创建索引
where条件,group by, order by 里用不到的字段
字段中存在大量重复数据,不需要创建索引
表数据太少,不需要创建索引
经常更新的字段不用创建索引
什么时候适用索引
字段有唯一性限制的,比如商品编码
经常用于where查询条件的字段
经常用于group by 和 order by 的字段
为什么Mysql InnoDB 选择 B+tree作为索引的数据结构
B + Tree vs B Tree:
存储相同数量级别的情况下,B+Tree树高比 B Tree低,磁盘I/O次数更少
B+Tree叶子节点用双向链表串起来,适合范围查询,B Tree无法做到这点
B + Tree vs 二叉树:
随着数据量的增加,二叉树的树高会越来越高,磁盘I/O次数也会更多,B+Tree在千万级别的数据量下,高度依然维持在3~4层左右,也就是说一次数据查询操作只需要做3~4次磁盘I/O操作就能查询到目标数据
B + Tree vs Hash:
虽然Hash的等值查询效率很高,但无法做范围查询
MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
事务
读未提交,四大事务隔离级别都存在
读已提交,解决了脏读
可重复读,解决了脏读,不可重复读
串行化,解决了四大隔离级别
InnoDB默认是可重复读,但其也可以解决幻读的情况,针对快照读由MVCC来保证不出现幻读情况,当前读由Next-Key lock锁机制来保证不幻读
MVCC关键是read view,创建read view有四个关键点,min_trx_id,max_trx_id,m_ids,create_trx_id,
create_trx_id:创建该read view的事务的事务id
m_ids:已启动但未提交的事务id集合
min_trx_id:m_ids中最小的事务id
max_trx_id:创建read view时当前数据库应该给下一个事务的id,即全局事务中最大的事务id+1
根据这几个元素来判断并发事务数据是否对当前事务可见
通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
锁
全局锁、表级锁(表锁、元数据锁、意向锁、AUTO-INC锁)、行级锁(record lock、gap lock、next-key lock)
Redis
redis基本数据结构
String、Hash、List、Set、Zset
Redisson通过提供丰富的分布式数据结构和服务,极大地简化了Java应用中Redis的使用,使得开发者可以更容易地构建高性能的分布式系统。
Spring
介绍一下Spring、SpringMVC和SpringBoot
Spring 是一个开源的 Java 框架,用于简化企业级应用程序的开发。它的核心功能主要是IoC和AOP
Spring MVC 是 Spring 的一个子框架,专门用于构建 Web 应用程序。
Spring Boot 是 Spring 的一个扩展,旨在简化 Spring 应用的初始搭建和开发过程。它通过自动配置和“约定优于配置”的原则,使得开发者可以快速启动和运行 Spring 应用,而无需大量配置。
springmvc流程
Spring MVC执行流程简而言之,就是用户请求经过DispatcherServlet中央处理器,由HandlerMapping处理器映射器找到对应的Controller处理请求,然后通过View Resolver视图解析器渲染视图并返回响应。
aop定义及相关注解
aop面向切面编程,核心思想是将这些横切关注点从业务逻辑中分离出来,形成独立的切面,切面包含了横切关注点的代码,当程序执行到特定的切点时,切面的代码就会被执行,从而实现了与业务逻辑的解耦
@Aspect声明切面类、@Pointcut定义切入点、@Before声明前置通知、@After声明后置通知、@AfterReturning声明返回后通知、@Around声明环绕通知、@AfterThrowing声明异常抛出后通知、@Order指定切面执行顺序
IoC的实现原理
IoC的实现原理主要依赖于三个基本要素:依赖注入(DI)、容器(Container)和配置文件(或注解)。
IoC容器其实就是一个大工厂,它用来管理我们所有的对象以及依赖关系。原理就是通过Java的反射技术来实现的,通过反射我们可以获取类的所有信息(成员变量、类名等等),再通过配置文件(xml)或者注解来描述类与类之间的关系。这样我们就可以通过这些配置信息和反射技术来构建出对应的对象和依赖关系了。
MQ
RabbitTemplate是Spring AMQP中的核心组件之一,它提供了一组方法来与RabbitMQ进行交互,简化了RabbitMQ的消息发送和接收操作。
mq重复消费
指同一条消息被消费者多次消费,重复消费可能会导致数据不一致性和错误,在我的大乐透抽奖项目中会采用数据库唯一索引做兜底,
设计模式
在我的项目中运用到的设计模式有工厂模式、责任链模式、规则树模型、模板模式等
设计模式分为创建型、结构型、行为型
责任链模式:行为型设计模式,其核心思想是将多个对象以链式结构连接起来,让请求沿着这条链传递,直到有一个对象处理该请求为止。
规则决策树模型:将规则树、树节点、树节点连线存入数据库,以便后续对这个树的调整和配置
工厂模式的优缺点
优点:
解耦:工厂模式可以将对象的创建和使用过程分割开来,降低了类与类之间的耦合度。
减少代码量,易于维护:通过工厂模式,对象创建的具体逻辑被隐藏起来,交给工厂统一管理,减少了代码量,方便日常维护。
灵活性和扩展性:工厂模式使代码更加灵活,易于维护和扩展。
降低系统复杂度:工厂模式可以隐藏对象的创建细节,使得客户端代码更加简洁。
遵循开闭原则:工厂模式允许在不修改现有代码的情况下引入新的产品类,提高了系统的可扩展性。
单一职责原则:每个工厂只负责一种产品,而不是由一个工厂去生成所有商品。
缺点:
增加系统复杂度:工厂模式引入了新的类和抽象层次,可能会增加系统的复杂度。
增加开发成本:工厂模式需要额外的代码来实现,可能会增加开发成本。
可能降低系统性能:由于需要创建额外的对象,工厂模式可能会降低系统的性能。
违反开闭原则:在简单工厂模式中,增加新产品时需要修改工厂逻辑,违反了开闭原则。
工厂类职责过于集中:工厂类的职责过于集中,增加了其复杂性。
难以支持新种类的产品:抽象工厂模式难以扩展以生产新种类的产品,因为几乎确定了可以被创建的产品集合。
多线程的实现
实现Runnable接口、继承Thread类、使用Executor框架
多线程查询实现
private Integer threadSearch(int rateKey, Map<Map<String, Integer>, Integer> table) {
List<CompletableFuture<Map.Entry<Map<String, Integer>, Integer>>> futures = table.entrySet().stream()
.map(entry -> CompletableFuture.supplyAsync(() -> {
Map<String, Integer> rangeMap = entry.getKey();
for (Map.Entry<String, Integer> rangeEntry : rangeMap.entrySet()) {
int start = Integer.parseInt(rangeEntry.getKey());
int end = rangeEntry.getValue();
if (rateKey >= start && rateKey <= end) {
return entry;
}
}
return null;
}, threadPoolExecutor))
.collect(Collectors.toList());
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
try {
// 等待所有异步任务完成,同时返回第一个匹配的结果
allFutures.join();
for (CompletableFuture<Map.Entry<Map<String, Integer>, Integer>> future : futures) {
Map.Entry<Map<String, Integer>, Integer> result = future.getNow(null);
if (result != null) {
return result.getValue();
}
}
} catch (CompletionException e) {
e.printStackTrace();
}
return null;
}
多线程交替打印
public class AlternatePrinting {
private final Object lock = new Object();
private int turn = 0; // 用于控制打印顺序
public void printNumbers(int n, final int id) {
for (int i = 1; i <= n; i++) {
synchronized (lock) {
while (turn != id) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Thread " + id + " prints: " + i);
turn = (turn + 1) % 2; // 切换到另一个线程
lock.notifyAll(); // 唤醒所有等待的线程
}
}
}
public static void main(String[] args) {
AlternatePrinting ap = new AlternatePrinting();
Thread t1 = new Thread(() -> ap.printNumbers(10, 0));
Thread t2 = new Thread(() -> ap.printNumbers(10, 1));
t1.start();
t2.start();
}
}