项目介绍:
我这个项目是基于微服务架构的生产级在线教育项目,面向成年人的非学历职业技能培训平台。分为两部分,学生端:其核心业务主体就是学员,所有业务围绕着学员的展开。管理端:其核心业务主体包括老师、管理员、其他员工,核心业务围绕着老师展开。在这个项目中我负责的模块点赞功能,用户学习下面的学习进度,用户积分排行榜,优惠劵管理。
项目中登陆认证流程?
用户登陆成功之后后端会给前端返回一个token
接下来用户发起的每个请求都会携带token
每个请求到达后台都会先经过网关,在网关中我们配置了过滤器,用来获取token,并解析,拿到用户id
接着会把用户id放到请求头中,转发给微服务
每个微服务都会有一个拦截器,在拦截器中会获取请求头中的用户id,并且把用户id存入ThreadLocal中
将来在业务代码中需要用到登陆用户,直接从ThreadLocal中获取即可
你讲讲你做的那个学习进度管理
我们这个学习进度关联了两个业务一个是学习计划,一个是学习计划进度统计
学习计划:就是当用户购买课程之后,在学习中心里面就可以点击课程有个创建学习计划的按钮,用户可以选择每周学几节,这样就会有个预计学习完时间(这个我们参考百词斩的)。页面就有了本周的进度
学习进度统计:就是每次用户看视频的时候都可以看到上次看到的进度。同时我们设计了视频和考试,考试提交完了就表示学完了。视频方面我们是当视频进度超过60的时候,就显示以学习完。
你在开发中参与了哪些功能开发让你觉得比较有挑战性?
那我就说一下我做的那个学习服务里面的一个视频进度记录。首先我们这个视频是使用录播的方式,我们需求是实现这个视频的续播。这个功能逻辑业务不复杂,但是对于视频续播的误差控制30秒以内,实现用户退出可以继续上一次的播放。我们前端每隔15秒会发起一次心跳请求,提交这个播放进度,我们数据库也设计了播放进度这个字段。但是如果每次请求都发放数据库,用户多了对数据库的压力就会很大。因为其实只有视频关闭之前的的那个提交才有效,其他的提交是无效的。因此我们采用了合并写请求的方案,当用户提交播放进度时会先缓存在Redis中,后续再将数据保存到数据库即可,由于播放进度会不断覆盖,只保留最后一次即可。然后我的为了确定用户提交的播放记录是否变化,我们还将播放记录保存为一个延迟任务,等待超过一个提交周期(20s)后检查播放进度。这个延迟任务我们采用的是DelayQueue,因为这个是jdk自带的,成本比较低。

同时我们也考虑了每个用户是同步提交的视频播放记录,所以整个播放记录的缓存业务的响应时间就是每一次数据库写业务的响应时间之和,并发能力肯定不会太好。我们利用MQ将同步业务编程异步,我们将每个用户的播放记录先缓存到Redis中,然后定义一个定时任务,将这些数据合并写到数据库中
我看你的项目里点赞数据是用Redis存的,为什么会选择用Redis呢,怎么做持久化的?
首先点赞它是一个高并发写的过程,因为可能会在同一时间段内大量的用户在点赞和取消赞的过程,如果这些请求都打在MySQL上,给MySQL带来了很大的压力,可能会使其宕机所以我们采用的是redis
1.点赞记录:我们可以选用Redis里面的zset结构,将业务类型+业务id作为key,value缓存每一个用户的id的set集合
可以使用以下命令来完成点赞功能:
判断用户是否点赞(判断bizId集合中有无userId):sismember bizId userId
点赞,如果返回1则代表点赞成功,返回0则代表点赞失败:sadd bizId userId
取消赞:就是删除一个元素:srem bizId userId
统计点赞数(求bizId这个集合元素的个数):scard bizId
然后这一部分数据我们可以直接存到缓存,不用持久化到数据库,因为每个业务只有一个数据,数据量很小的,然后当Redis宕机了也不用担心,有AOF提供的数据可靠性。如果实在不够,再结合MySQL做持久化。
2.某业务的点赞次数(必须持久化)
这里我们选用了SortedSet方法,利用key作为业务类型,member作为业务Id,score作为点赞数。
缓存何时写入数据库
我们使用定时任务来做持久化,每隔20秒遍历每个业务做持久化,由于业务过多,我们每次处理30个业务。
你的签到功能使用的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就没有签到。
你在项目中负责积分排行榜功能,说说看你们排行榜怎么设计实现的?
首先我们的排行榜功能分为两部分:一个是当前赛季排行榜,一个是历史排行榜。
我们赛季是每一个月一个赛季;
1.赛季榜单
首先说当前赛季榜单,我们采用了Redis的SortedSet来实现。member是用户id,score就是当月总积分值。每当用户产生积分行为的时候,获取积分时,就会更新score值。这样Redis就会自动形成榜单了。非常方便且高效
2.历史榜单
我们会利用一个定时任务在每月初生成上赛季的榜单表,然后再用一个定时任务读取Redis中的上赛季榜单数据,持久化到数据库中。最后再有一个定时任务清理Redis中的历史数据。。不过由于数据过多,所以需要对数据做水平拆分,我们目前的思路是按照赛季来拆分,也就是每一个赛季的榜单单独一张表。同时因为每个赛季的数据库id不一样,我们采用mybatisPlus来实现动态表名。
优惠卷的兑换码生成
生成兑换码。需要支持20亿以上的唯一兑换码,长度不超过10,只能包含字母数字,并且要保证生成和校验算法的高效。
这里我们使用了自增序列来实现的。因为自增序列号可以借助于BitMap验证兑换状态,完全不用查询数据库,效率非常高。
要满足20亿的兑换码需求,只需要31个bit位就够了,也就是在Integer的取值范围内,非常节省空间。我们就按32位来算,支持42亿数据规模。
不过,仅仅使用自增序列还不够,因为容易被人猜到爆刷。所以我们设计了一个按位加权求和的加密验签算法。

签名算法

签名:32位的自增序列,可以每4位一组,转为10进制,这样就有8个数字。提前准备一个长度为8的加权数组,作为秘钥。对自增序列的8个数字按位加权求和,得到的结果作为签名。
密钥:当然,考虑到秘钥的安全性,我们也可以准备多组加权数组,比如准备16组。然后生成兑换码时随机生成一个4位的新鲜值,取值范围刚好是0~15,新鲜值是几,我们就取第几组加权数组作为秘钥。然后把新鲜值、自增序列拼接后按位加权求和,得到签名。
兑换码:最后把签名值的后14位、新鲜值(4位)、自增序列(32位)拼接,得到一个50位二进制数,然后与一个较大的质数做异或运算加以混淆,再基于Base32进行转码,得到的兑换码恰好10位,符合要求。
在开发中碰到过什么疑难问题,最后是怎么解决的?
在领取优惠卷的时候因为我们领取的整个业务外面加了事务,而加锁的是其中的限领数量校验的部分。因此业务结束时,会先释放锁,然后等整个业务结束,才会提交事务。这就导致我们测试的时候还有出现超领。为了解决这个问题,我们将事务的范围缩小,保证了事务先提交,再释放锁,最终线程安全问题不再发生了。
1758

被折叠的 条评论
为什么被折叠?



