Skip to content

吊打面试官—项目话术

一.自我介绍 泉州信息工程学院

2016.9-2020.6 19 年出来实习 全日制 两年半

面试官你好,我叫 xxx,我来自福建莆田,现居住上海浦东新区。我的上一份工作是在 泉州市 成塾企业 管理有限公司,我担任的职位是 Java 后端开发,主要的工作内容就是在指定的时间内完成自己的代码,并配合小组其他成员进行调试。我近期做的一个项目是有关于新闻类的项目,该项目是基于 SpringBoot + SpringCloud 微服务架构开发的,在这项目中还用到了 MybatisPlus、MongoDB、RabbitMQ、Redis、阿里云 OSS 这些技术,我在该项目中负责的是自媒体发表文章、文章的自动审核、文章的定时发布等等功能

该项目有三个服务器端,一个管理端,一个面向作者的用户端,一个面向用户的 app 端。先从应用层通过 **ngnix **反向代理到网关层,由网关层进行统一的登录和权限校验,然后将请求路由到对应的微服务服务器上。项目的微服务采用的是 **nacos **进行统一的注册和管理,微服务间采用 **feign **进行远程调用

**福建 优及客网络****科技 股份有限公司:**泉州晋江市企业运营中心 6 座 1506

成立于 2018 年,国家级高新技术企业、双软认证企业

计算机软硬件的开发;网站建设与维护

泉州市 成塾企业 管理有限公司:泉州晋江市滨江商务区企业运营中心总部大楼 111

阿里巴巴旗下钉钉合作伙伴,为海量企业用户在企业办公、管理提效、业务增长、数据分析等方面提供应用产品和服务。计算机软、硬件的研发、销售、技术咨询、技术服务

小组人数

我们这个研发小组,一共 16 个人,后台 java 一共 5 个,前端两个,UI 两个,测试两个

然后还有项目经理、架构师、产品、运维

接口文档是由谁来提供,什么格式?

后端提供的,不过前期根据原型有和前端讨论过,后端引入 swagger 框架 +knife4j 框架生成的接口文档

开发周期是多久,项目是否上线,

6 个月

从项目立项后,就会讨论需求、研究原型、设计数据库、分任务,设计接口这一段大概用了 1 个月左右,具体开发 2 个多月,然后修改 bug 和需求调整用了很长一段时间

开发流程

一般我们项目都是先立项,决定要做什么项目,产品会梳理需求,产生需求说明和原型

架构师根据原型设计架构、数据库,然后给我们分派任务,规划项目开发周期。具体开发时,我们现在都是按照前后端分离的模式来的,前端和后端一起根据业务原型 梳理业务需求根据业务需求定义接口文档前后端并行开发后端基于接口文档提供对应的接口实现, 单元测试 POSTMAN 前端基于接口文档生成 mock 数据,进行前端的开发,按照 mock 数据进行测试 功能完成后进行前后端联调 前端把访问地址改成后端服务的地址,进行真实数据的测试

如果联调有问题,谁的问题谁去改 在进行联调功能测试,测试完毕即可发布到测试服务器,由测试人员进行测试

到时就改 bug 在测 bug 搞定后上线

上线后客户量是多少

我们项目现在刚运营不久,注册量有 10 多万吧, QPS 不是很高 100 左右, 不过在用 jmeter 测试时,接口 QPS 能做到 1000 多,TPS 没有专门测试过。

一、项目简介(开发时期 7 个月)(安远新闻)

我近期参与的项目是一个小说文章类项目。随着智能手机的普及,人们更加习惯于用手机来阅读小说书籍。在当前该软件汇集了全网最新最热门的小说资源,各种不同类型题材的小说都能够在这里找到,用户可以根据自己平时阅读的喜好来选择。

二、参与的业务模块

我在这个项目主要参与了自媒体用户对文章的增删改查,定时对审核通过的文章进行发布时间设定,admin 端对自动审核失败驳回的自媒体文章进行人工审核 最后是用户手机号登录验证模块。

三、应用技术

SpringBoot、SpringCloud、MybatisPlus、MongoDB、RabbitMQ、Redis、阿里云 OSS

四、实现流程

1、整体架构

   首先我们的项目是有三个服务器端,一个admin端,也就是管理端,一个面向作者的用户端,一个面向用户的app端。先从应用层通过 **ngnix **反向代理到网关层,由网关层进行统一的登录和权限校验,然后将请求路由到对应的微服务服务器上。项目的微服务采用的是 **nacos **进行统一的注册和管理,微服务间采用 **feign **进行远程调用。数据存储层主要用到的是 **mysql **数据库,用于存储核心数据,另外我们采用 **redis **进行热点文章,粉丝关注等数据的存储,针对点赞评论阅读等行为的数据我们采用 **mongoDB **进行数据的存储。微服务中的配置都会统一存放在nacos中, 所有微服务提供的服务地址也会存储在nacos中服务与服务之间使用Feign的Http客户端调用,feign整合了Ribbon负载均衡器,Hystrix熔断器, 另外还用到了alibaba的seata处理分布式事务,xxljob处理分布式调度,fastdfs处理分布式的存储, 数据方面:核心数据存到了mysql中 , 用redis做的缓存, 用到了es做搜索,还有mongodb、rabbitMQ等软件

首先呢,先从整体来分析一下这个业务的流程。根据需求,app 端用户在实名认证成功后,将会在作者数据库的用户表(wm_user 表)中插入一条数据,同时也会在文章数据库的作者表(ap_author 表)中插入一条数据,这样作者用户就开通成功了。作为作者用户就可以进行发布小说的操作,发布后通过 **RabbitMQ **发消息完成自动审核,自动审核不确定的情况下由人工干预审核。审核通过的话再通过 **RabbitMQ **完成定时发布小说。文章内的图片我们选择保存到 阿里云 OSS 中去,并且为了方便 app 端用户查看小说文章,我们利用 freemarker 实现了页面静态化,并存储到 **Minio **中。

2.项目模块讲解

一。自媒体发布文章的流程,

(版本一)首先在作者端发表小说时,需要在标题与文章主体中输入内容,而文章主体是可以选择输入文字或者插入图片的,当主体内容完成后,用户还可以选择封面图,小说的封面可以是无图、单图、多图或者自动的形式,并且自动生成封面是需要根据主体内容中的图片数量来决定的。所以根据这个需求,数据库定义了一个专门存储图片数量的字段(wm_news 表的 type 字段):0 为无图、1 为单图、2 为多图、-1 为自动生成。当前端所有内容输入完成后,点击发布即进入后台代码逻辑。

(版本二)在自媒体端的发表文章模块,编辑文章标题 内容 标签 选择频道 自动发布时间 还有封面图片 封装成一个大的 json 调用后台的发布文章接口,后台对数据进行处理 比如: 添加文章信息,抽取内容 和 封面中所涉及的所有图片路径 和 素材库的图片做对比,然后设置好关联关系, 如果是自动生成封面的话 还会根据内容中包含图片的格式,按照规则自动生成封面图片,文章保存成功后 会通过 kafka 向 admin 微服务发送一条待审核文章信息。admin 微服务收到这个消息后就会自动调用自动审核的方法

二。小说自动审核处理

**发布文章后,会通过 RabbitMQ 发消息进行小说的自动审核处理,首先我们自己定义了一系列的敏感词,然后使用 DFA 算法,对小说的标题和内容进行初步审核,该审核通过后,会调用阿里云文本内容审核和图片审核的接口,进一步审核小说。云接口审核是否通过是根据调用后返回的具体信息中的一个字段 "suggestion",该字段有三种取值,"pass" 为通过,"block" 为不通过,"review" 为需要再次审核,再次审核则是人工进行审核,这些都是文章的状态,所以我们在设计时,赋予了文章多种状态,以便适应各种情况,分别为草稿(0),待审核(1),审核失败(2),自动审核通过&待发布(8),待人工审核(3),人工审核通过(4),发布(9)。**若自动审核成功则修改文章状态为自动审核通过,可以若有 block 则状态为审核失败,若有 review 则状态改为待人工审核。

MQ 是怎么使用的?(自动审核)

当自媒体用户提交发布文章之后,会发消息给 RabbitMQ 提交审核如果审核通过 判断发布时间 是否小于等于当前时间 如果小于等于 直接发消息通知 文章微服务 发布文章如果未到发布时间,将消息发送到 RabbitMQ 的死信队列 并设置消息失效时间

盲猜你会问:

如何确认消息的可靠性?发消息使用 mq 的好处是什么?

我们通过自定义配置类实现 InitializingBean,SpringBean 的声明周期接口,代表完成 bean 装配后执行初始化方法。注入rabbitTemplate对象设置发送确认回调方法:rabbitTemplate.setConfirmCallback,保证消息能发送到交换机。设置消息返还回调方法:rabbitTemplate.setReturnCallback,确认消息路由到队列;

使用 mq 帮助我们解除了不同业务之间的耦合性,同时也避免了可能由于审核文章失败导致发布文章失败的级联失败问题。

使用 RabbitMq 保证消息可靠性需要注意什么?

需要我们在配置文件中手动开启,消息发送确认机制和消息返还机制。同时为了避免无限重试,我们需要配置最大充实次数,和时间因子,避免无限重试和给我们解决问题时间。

项目是如何集成阿里云文本图片检测的?

导入阿里云文本,图片扫描的依赖,自定义配置类@ComponentScan("com.heima.aliyun") 当前包下的两个工具类,通过 spring 的自动装配机制,创建 META-INF 下面的 spring.factories 文件,写上全限定类名

说一下 DFA 算法?

通过 Map 集合的方式,最外面一层为第一个字,里面嵌套了 isEnd 字段,如果值为 0 说明不是敏感词,如果值为 1 说明为组成了敏感词;相同的 key 唯一,基于 hash 结构搜索的效率很快;比传统的字符串比较效率快很多。

有没有遇到什么 bug?

因为文章拼接文本,需要进行我们自定义基于 DFA 算法敏感词过滤,拼接的过程中需要拼接特殊字符,不然可能会出现误判的情况。比如:你今天很美国家很伟大。如果美国是敏感词,就出现了误判

三。文章定时发布

该项目中发布的文章是支持定时发布的,我们发布文章时可以指定某一个时间点发布,项目中我们引用了 xxl-job 框架实现分布式调度任务,具体使用需要单独部署调度中心,在调度中心中创建执行器和调度任务,在调度任务中定义 cron 日期表达式 每分钟执行一次 ( 0 0/1 * * * ?) , 在 admin 微服务中引入 xxljob 配置 对应的定时方法中 去文章库查询所有审核通过 (4 或 8) 且 发布时间小于等于当前时间的待发布文章,然后调用文章发布的方法发布文章

admin 微服务会监听指定主题的消息,在消息中会得到待审核的文章 ID admin 端使用 Feign 远程获取文章的具体信息,检查文章的各种状态 (0 草稿 1 待审 2 审核失败 3 人工审核 4 人工审核通过 8 通过待发布 9 发布),如果是 审核通过的需要判断下发布时间是否小于等于当前时间,如果是的话直接发布就可以了, 如果是待审核需要对文章进行审核操作,审核

对于资讯类项目很关键,我们选用的是阿里云内容安全服务,可以检测文本、图片、视频等等, 根据阿里云的 API 检测的结果大体上分三种,1. 通过 2. 需人工审核 3. 违规,根据不同的结果修改文章的审核状态,我们也自己定义了敏感词库处理我们自己微服务的敏感词,自定义的敏感词我们是通过实现 DFA 算法实现的敏感词过滤,敏感词的列表缓存到了 Redis 中。 如果阿里云和自定义的敏感词全部审核通过了,那么修改文章审核通过状态 8 如果到达的发布时间,就直接发布文章 发布文章要把文章的信息同步到 article 库中,app 端对 article 的相关信息都是查询 article 库, 文章在这里也被拆分成了多张表

比如: 文章基本信息表,文章内容表,文章配置表

而且这个库我们还使用 mysql 主从的方式作为了优化,后台会通过 aop 智能的切换主库 从库

***问题 1:在发布小说的模块中,我遇到了一个问题,由于发布小说服务需要远程调用其他微服务,当发布文章失败,当前微服务的事务可以回滚,但是被远程调用的服务没法回滚,造成数据的不同步,最后我们采用了阿里巴巴的分布式事务解决方案 seata,解决了分布式架构下的事务问题。

四。手机号登录验证

用户手机号登录验证,首先要考虑到如何防止被恶意拦截传入任意手机号码,恶意刷短信验证码接口?需要验证用户信息和手机号是在当前系统用户中真实存在才给目标手机号发送短信验证码,其次也可以通过某些标识 (如请求头) 来判断是我们自己的应用发来的请求而不是第三方恶意拦截后发来的请求。再次考虑如果防止同一用户一直发验证码,占用资源,我们的上游发送短信接口是设定有发送次数限制的,比如每个手机号每天只能发送 10 次。最后考虑到短信验证码通常比较短,如何防止爆破,我们可以利用 Redis 的 decr 指令,校验短信验证码只能校验设定的次数


这个 Redis 是怎么使用的?

在我们用户端,先引好 Redis 的依赖,在我们用户端,在 nacos、中配置 redis 的配置然后进行发布,例如:database(库,默认为 0),host(redis 的地址),port(端口号),password(密码),lettuce(lettuce 连接池配置,max-active:连接池最大连接数 8,max-wait:-1ms,max-idle:5,min- idle:0,timeout:20000ms,连接超时时间),然后发布。再在我们用户模块那边加上@Atuowired 注解,用 redisTemplate 进行操作,实际操作是比较简单的,就是里面实现代码的逻辑弄清楚就行了


技术介绍

【1】freemarker:一款模块引擎,一种基于模板和要改变的数据, 并用来生成输出文本 (HTML 网页,电子邮件,配置文件,源代码等) 的通用工具,模板 + 数据 = 静态文件;常用的还有Jsp、Freemarker、Thymeleaf 、Velocity。

  • 语法:Hello 命令,${......} 会用真实的值替代
  • 指令
    • 集合指令:list 和 map
    • if 指令(true 显示什么内容,false 显示什么内容)、运算符(算数运算符,比较运算符)、空值处理、内建函数

**【2】**阿里云 OSS:可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种非结构化数据文件。

使用:创建存储空间,新建一个 Bucket。

3、热门小说的缓存的业务流程

  1. 首先从整体来看,实现该模块大致需要 4 步:

(1)首先需要筛选出小说列表中近 5 天热度较高的文章,在每个频道的首页展示;

(2)其次根据用户的行为(阅读、点赞、评论、收藏)实时计算热点小说

(3)然后定时更新小说的热度值,根据热度值替换之前缓存的小说数据

(4)最后重新查询热点小说列表

  1. 在这个需求中,遇到了一个问题,首先是什么时候去统计最近五天的小说,还有就是怎么保证小说热度的实时更新?我之前考虑过使用延时队列去实现,但是没办法保证时效性,还有就是用户的大量行为会不停对数据库进行操作,导致效率问题。最终我的解决办法是将数据先全部存入到 redis 中,最后统一从对象中获取值,更新到数据库中。将五天热门小说缓存到 redis 中,数据类型为 String,key 为业务前缀(hot_article_first_page_),value 为小说详情对象,将用户行为也存到 redis 中,数据类型为 list,key 为业务前缀 + 频道 id,value 为实时的行为数据,来缓解数据库的压力,设计一个对象专门用来存储用户行为的操作数量,更新到数据库中,这样就解决了数据频繁的增加的问题。时效性的问题我们考虑使用了分布式任务调度框架 xxl-job,设置每天凌晨一点查询近五天热度较高的小说;并且每隔 10s 统计一次用户行为数量并更新小说热度值,用到两次 xxljob,这样就解决了以上问题。
  2. 小说分值定时计算
  • 查询前 5 天的(已上架、未删除)文章数据,计算所查询的所有文章的热度值,根据权重(阅读:1,点赞:3,评论:5,收藏:8)计算分值,为每一个频道缓存热点较高的 30 条小说文章,若为推荐频道则缓存所有小说热度值排名前 30 的小说
  • 分组聚合处理小说行为,按照文章 id 进行分组,遍历 map<文章 id,该文章所有行为集合>
  1. 实时采集文章行为
  • 因为之前已经有了一个定时任务负责采集近期的热点小说数据,但是当天也会有用户对文章进行操作(阅读点赞等),所以需要利用 MQ 进行监控,一旦有用户对某个小说进行了操作,就需要用 MQ 发消息,不可能一个操作就去修改热度,这样效率太低,所以将文章行为保存到 redis(list 结构:可重复且有序),再对数据做后续处理。
  • 行为微服务,用户阅读或点赞了某一篇文章(目前实现这两个功能),发送消息给 rabbitMQ
  • 文章微服务,接收行为消息,使用 redis 中 list 结构存储实时的行为数据
  1. 定时更新热度值
  • 文章微服务,接收聚合之后的消,计算文章分值(当日分值计算方式,在原有权重的基础上再 *3)
  • 根据当前文章的频道 id 查询缓存中的数据
  • 当前文章分值与缓存中的数据比较,如果当前分值大于某一条缓存中的数据,则直接替换
  • 新数据重新设置到缓存中
  • 更新数据库文章的行为数量
  1. 查询热点小说列表
  • 判断是否是首页
  • 是首页,选择是推荐,tag 值为 all,从所有缓存中筛选出分值最高的 30 条数据返回
  • 是首页,选择是具体的频道,tag 是具体的数字,从缓存中获取对应的频道中的数据返回
  • 不是,则查询数据库中的数据

问题 1:基于 redis.lua* 脚本优化代码

代码实现细节问题

  1. 从 redis 队列取数据,可以使用 pop 命令 取数据并删除数据,但一次只能取出 1 个 。如果用 while 循环多次取出,性能较低。采用 redis 的管道命令 Pipeline 可以一次性执行多个命令 既保证执行的性能,又可以保证命令有序执行。
  2. Redis Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。但不会删除数据,配合 Ltrim 使用
  3. Redis Ltrim 对一个列表进行修剪 (trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。下标 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
  4. Redis Llen 查询当前队列的长度

管道命令:可以一次性发送多个命令到 redis,并可以配合事务实现命令原子性。不过代码繁琐;且不能根据 redis 的返回结果进行后续命令的处理

适用场景:redis 优化的时候,大量的数据,频繁操作 redis,假如 10000 个命令,操作一万次命令,管道命令可以把一万个命令整体执行才返回,降低 IO。

最终使用 redis.lua 脚本:整个脚本一次性发给 redis,redis 一次性的解析,直接保证了原子性;还可以根据上一个命令的值做下一个命令的处理。管道命令做不到,它想要先拿到 len 的长度,做查询范围和截取的处理,做不到,因为它得不到 len 的返回值。

***问题 2

点赞小说时产生的高并发:用压力测试工具测试,发现一秒发送 100 条请求,最终文章的点赞数只有 92。多线程环境下,好多客户端一起去加评论点赞数量,假如当前点赞量是 50,多个线程同时来点赞,修改后最终结果 +1=51,数据越来越不准确。


实际上****要实现一个高可用的分布式锁需要满足很多条件

  1. 互斥性:和我们本地锁一样互斥性是最基本的,但是分布式锁需要保证在不同节点的不同线程互斥
  2. 可重入性:同一个节点上的同一个线程如果获取了锁之后,那么也可以再次获取这个锁
  3. 锁超时:和本地锁一样支持锁超时,防止死锁。(redis expiretime)
  4. 锁的续约:当程序响应时间过长时,能够续约锁的时间
  5. 高效(高可用):加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级
  6. 支持阻塞和非阻塞:和 ReentrantLock 一样支持 Lock 和 tryLock 以及 tryLock(long timeout),一直等和返回一个 false
  7. 支持公平锁和非公平锁:公平锁的意思是按照请求加锁的顺序获取锁,非公平锁就相反是无序的,这个一般来说实现的比较少

  1. 三种解决思路:
    1. 基于数据库实现:数据库的悲观锁 select 后加 for update(行锁)实现
      1. 问题:写入的并发太大
    2. 基于 zookeeper 实现:cp(强一致性),基于 zookeeper 的文件系统及 watch 监听机制
      1. 文件节点分为四种类型,持久节点、持久顺序节点、临时节点、临时顺序节点
      2. 两种实现:
        1. 多个客户端,同时尝试 创建指定目录节点,哪个客户端创建成功,说明得到锁;没得到锁的客户端,通过 watch 监听所指定目录节点,拿到锁的客户端执行代码完毕后,删除指定节点,其它客户端收到通知 继续获取锁
          1. 缺点:惊群效应, 高并发时会出现多个客户端同时监听节点,释放时需要通知多个节点,影响 zookeeper 的性能
        2. 多个客户端,同时创建 临时 有序的目录节点:lock00001、lock00002、lock00003;顺序编号小的获取到锁,先执行,未获取到锁的客户端,只监听上一个编号的目录节点,当锁释放时, 只有下一编号的客户端收到通知;性能比较好,且实现了有序性。特点: 一致性
          1. 缺点:和 redis 比性能弱一些,实现更复杂些
    3. 基于 redis 实现(推荐):基于 redis 的单线程模型 +setnx
      1. 高可用分布式锁需要满足的特点:互斥性、可重入、锁超时、锁的自动续约、公平和非公平、阻塞和非阻塞、高可用
      2. 原理:主要基于 Lua 脚本 (Lua 脚本可以保证多个 redis 命令的原子性)-->redis 的命令是单线程的,但是多个客户端访问 redis,A 客户端去执行命令 1、2,B 客户端也去执行命令 4、5,在 redis 执行命令穿插了其他命令,例如判断某个 key 是否存在,存在更改某些数据,还没判断就给删了,造成数据混乱,lua 脚本可以保证多个 redis 命令的原子性
        1. 判断指定锁 key 是否存在:如果不存在-->通过 hash 结构加锁: hset 锁名称、客户端 id(UUID+ 线程 id)、锁的次数 1、设置失效时间: 默认 30s
        2. 如果存在-->判断是否是当前客户端加的锁:如果是 在锁的次数上加 1 执行重入锁;如果不是 直接返回剩余的失效时间
        3. 加锁成功后,会开启一个后台调度线程,调度线程每隔 10s 执行一次 (watch dog 看门狗机制),继续续约 30 秒;重置当前客户端的锁的失效时间 作用实现了 锁的自动续约;客户端宕机,调度线程也会宕机,时间一到锁就失效了
        4. 通过 lua 脚本解锁:判断锁 key 是否存在:如果存在, 判断是否是当前客户端加的锁;如果是,将锁的 次数 -1;判断 剩余次数 是否大于 0;如果大于 0 说明未完全解锁 重置失效时间并返回;如果小于等于 0 说明完全解锁 删除锁的 key del key

为什么选择基于 redis 的方案

  1. redis 完全基于内存,性能比 zookeeper 高很多
  2. 另外基于 Redission 框架的实现已经相对非常高可用,而且 api 简单很容易整合到项目中
  3. 而且 redis 也是大部分公司的选择,因为 redis 在其它的使用场景也很多,公司往往都会搭建 redis 集群

也存在问题:无论是哨兵模式还是集群模式,都有 master 节点和 salve 节点,对于写的操作,会加入 master 节点,然后 master 节点同步到 salve 节点。会有一致性的问题,假如两个客户端进来,A 和 B 客户端,A 和 B 都要加锁,A 刚创建锁,还没同步,master 宕机,salve 成为主节点,但 key 在 master 里,B 客户端来判断 key 存不存在,显然 key 是不存在的,这样 A 和 B 都加了锁,假如 A 和 B 都在访问一个受保护的资源,造成隐患。还有就是并发比较高,一大堆线程等待锁,redis 是自旋锁的方式在不断尝试获取锁,造成 cpu 的消耗。

最近更新