【天机学堂】面试总结

写在前面,首先要将天机学堂包装一下,智慧教育平台,暂时就想到这个。天机学堂文档

1.包装简历

在这里插入图片描述

2.简单介绍一下你的项目

智慧教育平台是一个基于微服务架构的生产级在线教育项目,我们的核心用户是面向15-22岁的C端的学生和非学历职业技能培训平台。智慧教育平台是一个B2C类型(企业对消费者的电子商务模式,一般以网络零售业为主,主要借助于Internet开展在线销售活动)的教育网站,因此分为管理端和用户端(PC网站)。用户端包括:学习中心(例如学习记录,学习计划等),个人中心(例如信息管理和登录等),交易中心(购物车,支付和退款等),管理端包括:课程管理,用户管理,交易管理,营销活动,学习管理。我主要负责的是点赞功能,用户学习模块中的学习进度管理,用户积分系统中的排行榜管理,优惠券管理。该项目技术包括:SSM+SpringBoot+Redis+MySQL+MyBatisPlus+RabbitMQ+xxl-job等。
在这里插入图片描述

3.你在开发中参与了哪些功能开发让你觉得比较有挑战性?

  • 我参与了整个学习中心的功能开发,其中有很多的学习辅助功能都很有特色。比如视频播放的进度记录。我们网站的课程是以录播视频为主,为了提供用户的学习体验,需要实现视频的续播功能。这个功能实现逻辑本身不复杂,但是我们产品提出的要求比较高:首先续播时间误差要控制在30秒以内。然后要做到用户突然断开或者切换设备,都可以继续上一次播放。要达到这个目的,我们必须把播放进度保存在服务端。其次要做到续播误差不超过30秒,那播放进度的记录频率就需要比较高。我们会在前端每隔15秒发起一次心跳请求,提交最新的播放进度,记录到服务端。这样用户下一次续播时直接读取服务端的播放进度,就可以将时间误差控制在15秒左右。
  • 现在面试官可能会追问,如果把播放数据存入到数据库,当用户量比较大时,前端每隔15秒就要提交一次请求,如果直接写入数据库,会对数据库造成的压力很大。请问你们是怎么解决的?我们当时也考虑到了这样的情况,我们思考了整个流程发现有问题没有解决。
    • 每个用户提交的播放记录数据,只有在视频关闭之前的那个提交才有效,其他的视频播放效率是无效的。
      • 解决办法:我们当时定义了一个延时任务类,将播放记录加入到延时队列里,每个任务倒计时20秒,20秒过后就会弹出,我们当时是这样想的一个逻辑,当用户在播放视频时,前端每隔15秒会提交一次请求,我们将前端提交的请求存到缓存中,然后再提交延时任务,延时任务每次出来的播放记录都会与缓存中的播放记录做一个比较,如果不相同,则播放记录在一直提交,如果相同则未提交,也就是视频暂停了。(我们使用JDK自带的DelayQueue,因为这种方案使用成本最低,而且不依赖任何第三方服务,减少了网络交互。但缺点也很明显,就是需要占用JVM内存,在数据量非常大的情况下可能会有问题。但考虑到任务存储时间比较短(只有20秒),因此也可以接收。)
    • 每个用户是同步提交的视频播放记录,所以整个播放记录的缓存业务的响应时间就是每一次数据库写业务的响应时间之和,并发能力肯定不会太好。
      • 解决办法:我们利用MQ将同步业务编程异步,我们将每个用户的播放记录先缓存到Redis中,然后定义一个定时任务,将这些数据合并写到数据库中。

4. 你能讲一讲项目中课程管理是怎么设计的,有哪些接口?

  • 作为一个在线教育项目,为了让学员有一个更好的学习体验,促进学员,帮助学员学习,课表管理非常重要。因为这个项目是我和同学一起做的,他把搜索课程和报名课程都写好了。我拿到课程管理的任务后,先按照分析产品原型,设计数据结构,然后实现功能接口和测试+前后端联调。
    • 第一步是分析产品原型:根据需求,分析业务流程,设计业务接口,通过需求分析到有加入课表、分页查询课表、查询学习进度、查询指定课程学习状态、根据id查询某课程学习状态。
    • 第二步是设计数据结构,通过第一步的分析产品原型的过程中,其实我们就已经知道数据库需要哪些字段,根据需求和原型图画出ER图,再设计数据表。
    • 第三步是实现接口:根据接口设计(yapi)来实现功能,一般来说我们不能直接在dev分支上直接开发,会创建一个新的feature分支,用这个分支来实现课程管理功能,实现和测试完后,再合并到dev分支,删除这个分支。
    • 第四步进行测试:测试我们使用的是Postman和Swagger两个工具来测的,我主要用的Postman。根据请求方式,请求路径和请求参数格式,通过数据库查询数据,进行测试。
    • 第五步进行前后端联调,一般是通过postman测试完没问题,然后再通过前后端联调测试。
  • 如果给一个类似的场景题,也是根据这个过程来设计。

5.我看你简历个人贡献写了学习进度管理,那请你讲一下是怎么设计的?

学习进度统计的问题在在线教育、视频播放领域是一个非常常见的问题。学习进度管理关联到两个业务,学习计划和学习进度统计。

  • 学习计划:当我们买了课程后,就会有一个创建学习计划的按钮,例如选择某个课程后,创建每周学习章节数,然后有一个预计学习完成时间(这是仿照扇贝单词软件的背单词计划设计的)。页面就有了本周进度,本周计划这些显示。
  • 学习进度统计:那学习进度统计,就是方便我们下一次点进来能够直接浏览到我们上次观看的时间点。我们设计了两种小节,一种是视频,一种是考试,考试提交完了,那么就表示学完了,而视频的话我们不需要要求用户一定要播放进度达到100%,而我们是这样设计的,只要当前是第一次学习且视频播放进度超过50%则显示已学习状态。(如果面试官,问怎么记录视频播放进度超过50%,那么你就回答上面第三个问题)

6.回答系统你是怎么设计的?

回答系统的整个流程是这样的:在某个课程的某个章节下学员提出的问题,老师和其他学员可以进行回答(并且可以进行多次回答),老师和其他学员可以对某个回答进行评论,并且对评论进行回复也是评论(可以进行多次评论),老师在管理端管理问题、回答和评论的状态。
追问:你能讲一讲管理端和用户端分别要实现哪些接口吗?

  • 用户端:对问题的带条件过滤的分页查询,提问的增删查改。对问题的回答和评论有,根据id查询问题详情(我们要点击某个问题进行查看,是一个因此的接口),分页查询问题下的所有回答,分页查询回答下的评论,点赞和取消点赞某个回答和评论,回答某个提问、评论他人回答。
  • 管理端:和用户端相同的接口(有一些独有的字段,问题是否被查看,问题是否被隐藏),另外还有管理端分页查询问题列表,隐藏或显示指定问题。

7. 我看你的项目里点赞数据是用Redis存的,为什么会选择用Redis呢,怎么做持久化的?

点赞它是一个高并发写的过程,因为可能会在同一时间段内大量的用户在点赞和取消赞的过程,如果这些请求都打在MySQL上,给MySQL带来了很大的压力,可能会使其宕机。因此对于高并发写的常见优化有,优化SQL和代码、变同步写为异步写、合并写请求。而使用MQ异步是用来解耦的,数据库的写次数并没有减少,因此我们这里采用了合并写。合并写操作是有使用场景的(对中间的N次写操作不明感的情况下),而刚好用户只关注最终的点赞结果。
合并写有哪些注意的点?

  • 1.数据如何缓存
    这里有两个数据需要缓存:用户是否点赞,某业务的点赞总次数(某个回答,笔记,课程等)
    • 用户是否点赞:我们可以选用Redis里面的set,将业务类型+业务id作为key,value缓存每一个用户的id的set集合
      可以使用以下命令来完成点赞功能:
      判断用户是否点赞(判断bizId集合中有无userId):sismember bizId userId
      点赞,如果返回1则代表点赞成功,返回0则代表点赞失败(value是否push成功):sadd bizId userId
      取消赞:就是删除一个元素:srem bizId userId
      统计点赞数(求bizId这个集合元素的个数):scard bizId
      然后这一部分数据我们可以直接存到缓存,不用持久化到数据库,因为每个业务只有一个数据,数据量很小的,然后当Redis宕机了也不用担心,有AOF提供的数据可靠性。如果实在不够,再结合MySQL做持久化。
      我们在判断用户是否点赞使用的是Redis里面的sisremember命令,但是我们对多次调用这个命令,就需要向Redis发起多次Redis请求,就会给网络带宽带来非常大的压力,所以我们使用了Redis里面的管道(Pipelining)来提高查询速度。
    • 某业务的点赞次数(必须持久化)
      这里我们选用了SortedSet方法,利用key作为业务类型,member作为业务Id,score作为点赞数。
  • 2.缓存何时写入数据库
    我们使用定时任务来做持久化,每隔20秒遍历每个业务做持久化,由于业务过多,我们每次处理30个业务。

这里会追问,为什么要用zset,而不用list和set呢?

  • 首先list不具备去重的能力,因为将业务id和点赞数一并存到list里面,如果业务在一分钟内被点过多次赞,那么就会存入多个相同的业务id,而我们只需要最后的一次。
  • 而用set的话,因为set里只放业务id,而在通过业务id去通过MQ来查询,这样做的话会经过多次网络通信从而给系统网络增加负担。zset可以同时保存业务id和最新点赞数,就少了多次查询的操作。
  • 但并不是zset就是完美的,只是综合考虑下选择了zset,因为zset底层是使用的哈希结构+跳表实现的,会占用额外的内存,当然我们在做定时任务时,每次查询都会把Zset删除。

8.你的签到功能使用的BitMap,为什么会使用这个结构,怎么存储的。

我刚开始是想的使用数据库表来存,但是一个用户每一次签到,用一张表来存就是一条记录,如果一个用户一年签到了100次,而网站有100万用户,就会产生1亿条记录。随着用户的增多,时间的推移,这张表占用的空间将会越来越大。然后我想的是每个用户记录每天是否签到,就是01数据,那么有没有只保存01数据的结构,而如果只保存01数据(数据量小)用redis处理起来很快,redis里面有一种天然的保存01数据的结构BitMap。
1.怎么签到:使用setbit key offset value,将地offset 的bit位改为value数据,例如第三天签到,就改为setbit bm 2 1.(这里用的是setbit第1天在最左边,当天的数据在最后一位)
2.如何查询签到记录:使用bitfield key get encoding offset, 从offset的bit位开始读取,u2,就是读取两个bit位。例如我想要第一天到第三天的签到记录,就用bitfield bm get u3 0。
然后我们设计的有积分签到奖励,每天签到有1个积分,每连续签到7天得10的积分。这些积分就用到后面的排行榜功能。如果说统计一个区间里有多少天的签到数,可以使用bitfield,但是如果统计当前有多少连续的签到数,就不能使用bitfield命令,只能去模拟,使用当前的二进制去不断的右移与1做与运算,如果等于1表示当前有签到,直到为0就没有签到。

9.你是怎么设计积分功能的,有那些奖励?

首先刚刚说到的签到,每次签到+1积分,而每连续签到7天就会加10的积分奖励。写课程评价+10积分,写问答 +5积分(每日上限20积分)。这里我们用的MQ,当签到功能写完后,就会将积分和当前用户通过MQ发送给处理方,而业务类型就是mq的类型。学习系统监听到mq做处理,会先去判断是否积分是否超上限(也就是在数据库中查询今天的数据)。

10.你使用Redis来保存签到数据,如果Redis宕机了怎么办?

其实我们可以给Redis添加数据持久化机制,比如使用AOF持久化机制(保存每一条插入语句命令,AOF命令是默认关闭的,要在redis.conf配置文件里开启)。这样宕机后也丢失的数量不多,可以接受。当然Redis持久化还有RDB(数据快照),把内存中的所有数据记录到磁盘中,当Redis宕机重启后,从磁盘中读取快照文件,恢复数据。我们这里使用的是AOF,因为数据完整性更高。
或者我们可以搭建Redis主从集群,再结合Redis哨兵。主节点会把数据持续的同步给从节点,宕机后会有哨兵从从节点中选取主节点,基本不用担心数据丢失问题。
当然,如果对于数据的安全性要求非常高。肯定还是要结合传统数据库来实现,但是为了解决签到数据量较大的问题,我们可能就需要对数据做分表处理了。或者及时将历史数据存档。
总的来说,签到数据使用Redis的BitMap无论是安全性还是数据内存占用情况,都是可以接受的。但是具体是选择Redis还是数据库方案,最终还是要看公司的要求来选择。

11. 答::你在项目中负责积分排行榜功能,说说看你的排行榜怎么设计实现的?

我们的排行榜功能分为两部分:一个是当前赛季排行榜,一个是历史排行榜。
因为我们的产品设计是每个月为一个赛季,月初清零积分记录,这样学员就有持续的动力去学习。这就有了赛季的概念,因此也就有了当前赛季榜单和历史榜单的区分,其实现思路也不一样。
首先说当前赛季榜单,我们采用了Redis的SortedSet来实现。member是用户id,score就是当月积分总值。每当用户产生积分行为的时候,获取积分时,就会更新score值。这样Redis就会自动形成榜单了。非常方便且高效。
然后再说历史榜单,历史榜单肯定是保存到数据库了。不过由于数据过多,所以需要对数据做水平拆分,我们目前的思路是按照赛季来拆分,也就是每一个赛季的榜单单独一张表。这样做有几个好处:

  • 拆分数据时比较自然,无需做额外处理
  • 查询数据时往往都是按照赛季来查询,这样一次只需要查一张表,不存在跨表查询问题
    因此我们就不需要用到分库分表的插件了,直接在业务层利用MybatisPlus就可以实现动态表名,动态插入了。简单高效。
    我们会利用一个定时任务在每月初生成上赛季的榜单表,然后再用一个定时任务读取Redis中的上赛季榜单数据,持久化到数据库中。最后再有一个定时任务清理Redis中的历史数据。
    这里要说明一下,这里三个任务是有关联的,之所以让任务分开定义,是为了避免任务耦合。这样在部分任务失败时,可以单独重试,无需所有任务从头重试。(这里我们设置的子任务id是,将创建榜单的子任务id设置为持久化Redis上赛季榜单,再将持久化Redis上赛季榜单的子任务id设置为清除上赛季榜单)
    当然,最终我们肯定要确保这三个任务的执行顺序,一定是依次执行的。

12.你使用Redis的SortedSet来保存榜单数据,如果用户量非常多怎么办?

首先Redis的SortedSet底层利用了跳表机制,性能还是非常不错的。即便有百万级别的用户量,利用SortedSet也没什么问题,性能上也能得到保证。在我们的项目用户量下,完全足够。
当系统用户量规模达到数千万,乃至数亿时,我们可以采用分治的思想,将用户数据按照积分范围划分为多个桶。
然后为每个桶创建一个SortedSet类型的key,这样就可以将数据分散,减少单个KEY的数据规模了。
而要计算排名时,只需要按照范围查询出用户积分所在的桶,再累加分值范围比他高的桶的用户数量即可。依然非常简单、高效。

13.你们使用历史榜单采用的定时任务框架是XXL-JOB,和SpringTask比起来有什么优点?处理数百万的榜单数据时任务是如何分片的?你们是如何确保多个任务依次执行的呢?

SpringTask存在一些问题:

  • 当微服务多实例部署时,定时任务会被执行多次。而事实上我们只需要这个任务被执行一次即可。
  • 我们除了要定时创建表,还要定时持久化Redis数据到数据库,我们希望这多个定时任务能够按照顺序依次执行,SpringTask无法控制任务顺序

XXL-JOB自带任务分片广播机制,每一个任务执行器都能通过API得到自己的分片编号、总分片数量。在做榜单数据批处理时,我们是按照分页查询的方式:

  • 每个执行器的读取的起始页都是自己的分片编号+1,例如第一个执行器,其起始页就是1,第二个执行器,其起始页就是2,以此类推
  • 然后不是逐页查询,而是有一个页的跨度,跨度值就是分片总数量。例如分了3片,那么跨度就是3
    此时,第一个分片处理的数据就是第1、4、7、10、13等几页数据,第二个分片处理的就是第2、5、8、11、14等页的数据,第三个分片处理的就是第3、6、9、12、15等页的数据。
    这样就能确保所有数据都会被处理,而且每一个执行器都执行的是不同的数据了。
    最后,要确保多个任务的执行顺序,可以利用XXL-JOB中的子任务功能。比如有任务A、B、C,要按照字母顺序依次执行,我们就可以将C设置为B的子任务,再将B设置为A的子任务。然后给A设置一个触发器。
    这样,当A触发时,就会依次执行这三个任务了。

持续更新:正在复习过程中,尽量做到每日一更,面试遇到的项目问题也会更新在这里面。

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星空皓月

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值