在线影院项目话术(0.7w精选)

用户登录模块

登录模块主要就是基于springSecurity实现的,在登录后,会提供JWT生成对应的token。

我给您说一下JWT的实现流程吧,其主要分为三个部分,header,body(存放userId),签名。使用header + body基于base64生成签名。最终请求都会去携带该token。(token中不要存放非常重重要的信息,因为安全性不高)

在我们的微服务项目中,请求会先进入网关,网关会配置一个全局拦截器,去解析token,并将userId继续让请求携带到微服务中。

服务中涉及到远程调用,为了保证服务间能够正常的获取userId,我们是使用threadLocal进行存储的。同时给定义一个公共的全局拦截器,在每次进入服务的时候,先判断threadLocal中是否有数据,如果没有就会到请求中获取,最终放行。

片源观看模块

1.视频的播放方式和上传

使用腾讯的VOD来实现的,其有非常全面的视频任务流,我们只需要对任务流进行配置即可使用,包括视频的加密,视频封面图的生成,视频雪碧图的生成,视频的审核。

我们只需要实现视频的上传即可,我们无需视频文件上传到服务端,服务只需要生成对应的授权签名,使用VOD提供的JS的SDK并携带授权起名,提供文件上传工具即可实现在客户端的视频上传。

VOD也提供了视频播放器,无需提供视频的播放地址,而是提供授权签名和文件id给VOD,就可以实现视频的播放,在这个视频的传输过程都是加密的,可以做到防止视频被盗。

 实现方式就前端的操作,引入SDK,生成签名,视频文件上传(返回file类型的数据)。

2.提交观看位置(合并写请求,减少99%的DB操作)

这里的业务需求,就是需要实现用户续看的功能,保证用户切换其他设备的时候也可以续看,并且误差要在30秒之内。

旧方案:在刚开始的时候,就是想直接去操作数据库,前端每隔15秒就提交一次观看位置的请求,因为视频的播放为用户主要的操作,所以就会存在高并发的场景,大量的访问DB最终会压垮数据库。(修改观看记录表)

新方案:合并写请求,我们会发现在这么多DB操作只有最后一次的观看位置有用,使用redis的hash结构,大key存储片源表的id(其关联了片源id和userId),小key存储集的id(一个片源如果是电影的话就1节,反之存在多节),value就存储观看位置mount。在每次请求进来后会修改hash结构中的数据,并且使用mq的死信队列发送一个20秒延迟消息(面试官有问的话,就稍微解释一下),消息中主要就是mount,监听器任务就是判断消息中的mount和redis中的mount是否相同,如果相同就说明20内没有继续观看,此时修改数据库中观看位置。反之则不进行操作。

片源话题及回复模块

在数据库的设计上主要就是两张表,分别是 话题表和回复表。

话题表 主要字段就是标题,内容,回复数,而在回复表中主要即使目标回复id和点赞数。

在展示话题的回复时,通过递归的获取对应的回复集合。

在里面比较难的就是点赞的实现,我给您介绍一下点赞回复/话题模块吧。

点赞回复/话题模块

旧方案:基于点赞记录表做点赞的记录并且每次进行点赞or取消点赞的时候也小修改回复的总点赞数。缺点很明显,恶意的大量取消和点赞的请求出现的时候就会出现大量的DB操作,最终压垮数据库。

新方案:点赞记录不再存储到数据库中,使用redis的set结构进行存储,key存储业务id,value存储点赞的userId集合,因为set的结构其会校验重复点赞,使用zset结构存储各个回复的点赞量。key存储目标业务的类型,member存储业务id,scope存储点赞量。使用xxl-job每隔20秒就会调用popmin从zset中取三十条数据批量的修改点赞总数。(通过业务的类型进行不同表的修改操作)

但是并不是说旧方案是不好的,点赞操作本来就是一个低频的操作,使用数据库进行直接操作其实也是没有问题的,而且速度上反而更快,所以只能说有利有弊。

问:在在set结构中为什么不使用userId作为key呢,如果使用userId作为key的话,用户量庞大,会不会出现BigKey的情况呢?

可以使用userId作为key,但是呢,这里使用业务id作为key的话在获取业务对应的点赞量时比较方便,通过scard方法就可获取中点赞数,因为在set结构中头部会存储value的总数量,其时间复杂度为O(1)。在我们的项目中没有大V,所以我们默认总点赞数不会超过1000,所以就没有考虑BigKey的情况。

问:在点赞量的存储上为什么不使用redis的list结构呢?

如果是使用list结构的话,那在每次修改点赞状态的时候都会就新的点赞数存储到list中,但是在list中只有最后一次点赞量是有用的,增加了不少的DB操作,所以使用redis的zset结构配合xxl-job进行批量修改操作。

签到积分模块

通过签到获取积分,积分可用于抵扣金额,我们规定1积分抵0.01元。连续的签到可以格外获得积分。我们这里规定连续签到7天后开始每天2积分,14后每天3积分,在月初连续签到会重置。  

在设计的时候,先是想到mysql进行存储签到的记录,但是呢,随着用户体量的增大和时间的关系,就会浪费大量的存储空间。所以想到使用redis的BitMap(位图,String类型)结构进行存储,每个月对应一个bitMap,因为其使用的是二进制进行存储的,在签到的判断上与1做异或判断就可以获取今天的签到情况。(主要就是通过redisTemplate实现的)

在连续签到遍历的做 ^1(与1异或) 并 >>>1(数值右移一位)操作,最终就可以获取类型签到的天数,最终添加对应数额的积分。

积分主要就是一张表,用于存储积分的情况,在后续使用积分抵扣就会操作该数据库。

问:你的项目中使用过redis的什么结构呢?

1.set结构:在物流项目中,主要就解决幂等性的问题,在消费运单中防止运单被多次消费。

在电影院项目中用来存储每个业务对应点赞用户的集合。

2.list结构:在物流项目中,作为用来存储运单的队列。key就是当前网点和下一网点的拼接,value就是对应起始和下一网点的运单集合。

3.hash结构:主要就是redisson的实现方式,大key存储的就是要上锁的业务id,小key就是线程id,value就是锁重入的次数。在电影院项目中,用来存储用户在集里的观看位置,解决合并写请求的问题。

4.bitmap结构:在电影院项目中,使用bitmap解决签到的问题,降低存储内存。

问:你将点赞的用户信息和签到的信息全部存储到redis中,不怕数据丢失吗?

点赞的数据和签到的数据不是非常重要的数据,所以没有存储到mysql中。

为了防止redis出问题,主要的解决方法就要:

1.手动设置redis的持久化类型就比如:AOF,RDB。

2.为redis搭建主从模式(一般是一种三从),包括使用哨兵模式,防止redis宕机。

3.如果对数据的完整性和正确性要求比较高的话,还是需要使用mysql进行存储。就比如那种银行项目等等,所以还是要根据具体的业务场景进行选择。

热门片源榜模块

热门排行榜主要就是给用户更多的观影发现,其分为:单月热门榜和历史热门榜。

主要就是使用redis的set结构记录每个片源的观看次数,并且使用使用zset结构记录每种片源的总观看数,类似点赞模块的设计模型。不同的就是在每月出会重置set和zset。

zset提供了ZREVRANGE,ZRANK可以很方便的获取排序后的数据,也就是对应的排名。key就是当月,member就是片源id,scope就是热度(我们会对该排序进行分页)

在历史热门榜的设计上,我们不考虑将所有的历史排行榜存到一张表中,这样后续在出现上效率会比较低。我们使用水平分表,这里没有使用jdbc-share插件,在分表上,每个月都单独的做一张排行表,在后续的查询上也不会有跨表的行为,使用年月作为表的后缀。

主要是通过三个定时任务在每个月月初的时候执行,分别是创建热门榜表,插入数据,清除zset缓存。

在创建表上主要就是通过mysql的insert标签及mybtisplus设置动态表名。(主要就是从写dynamicTableName来实现的)

这三个任务是必须按照顺序执行,分成多个任务的主要原因就是解耦合。在某个任务失败的时候无需从头执行,但是前提是要保证任务的执行顺序不能变。

问:热门榜使用zset结构,如果在电影数量庞大的时候,该怎么办呢?

zset底层使用跳表和哈希表实现的,所以在排序序列上效率是很高的,即使十几万乃至百万的电影数量,在效率上还是很高的。

如果真的需要进行优化的话,可以使用 分治和桶排序的思想来实现。将不同热度范围的片源放到不同的桶中,就比如 0~100,101~200... 

在后续计算排名的时候,就是大于该分为的所有桶中数量的总和 + 当前桶的排名即可。

问:在执行定时任务的时候,使用什么技术实现的,在处理百万的排名数据上是怎么设计的呢?你是怎么控制任务的执行顺序呢?

定时任务上使用xxl-job来实现的,在处理排名数据存储到DB的时候使用 分片广播策略来实现的。

主要就是通过页码取模的方式来让对应的节点执行定时任务。(可以从xxljobhelper获取获取总节点数和当前节点的索引数)

xxl-job支持设置子任务,所以在保证任务的顺序上按照执行的顺序设置子任务id即可。

代金卷模块

兑换码的生成 

我们的代金卷主要分为两种类型,手动领取及兑换码领取。优惠劵发放的时候兑换码优惠劵会多一步生成兑换码的操作。

兑换码的生成是自己手动实现的,我们需要生成十位的字符串,这些的字符分为 1~9,A~Z排除相同形状的字符总共是32位置字符。

为了提高安全性,想到使用自增id作为序列号,提高redis的Bitmap判断兑换码是否被兑换,为了防止序列号被爆刷,我们需要使用加密算法。

将序列号转换为32位的二进制并且每4为转为十进制,就可以得到8位数,按位加权,得到签名。

还会准备4位的新鲜值,用于随机的从16个权数组中取一个权数,最后将 签名的后16位+新鲜值+32位置的序列号,每5位转为字符,就可以得到兑换码。后续校验就是反向操作。

问:在项目中有使用过线程池吗?(可以在介绍兑换码的时候引导出来)

由于生成的兑换码较大,如果使用主线程取执行的话就太耗时了,在兑换码优惠劵发放的时候,会使用线程池创建异步的线程取执行兑换码的生成。主要就是提高效率。
线程池的创建上,主要的参数就是:核心线程数,总线程数,阻塞队列,救急线程数等等。在项目中的核心线程数设置为2,兑换码的生成本来就是一个低频的操作,所以没有设置很多,如果阻塞队列都装不下的时候,就会创建救急线程池去执行任务。

代金卷的领取(超卖问题)

1.超领问题的解决 

代金卷的领取无非就是在高并发的场景下会有一个超领的问题,也就是常说的超卖问题。 这里我们没有使用传统的基于版本号的乐观锁,因为版本号锁每次只能有一个线程领取成功,在代金卷充足的时候,效率很低下。我们的先想到的解决方案就是修改sql语句中的条件语句设置为 已发放数量 < 中数量。主要就可以实时的获取数据库中的信息。

2.同用户并发领卷超领

在代金卷中,都是设置用户的限领量,同用户并发领卷的时候,领卷的数量大于限量。这个过程会去查询领取的数量再操作数据库,该方法没有加锁,导致没有原子性。(方法抽取成独立的方法)

是所以使用sychronized对userId进行加锁,再写该方法的时候遇到了上锁失败和锁粒度不够的问题。

上锁失败:因为userId是Long类型不能直接使用==进行匹配(底层是缓存数组的问题),我们想到使用toString,但是能toString也是new一个String,最终就使用 userId.toString.intern方法。(获取常量池中的数据)

锁粒度不够:先开启事务在上锁的话超领的问题还是在,所以需要上锁在开启事务。

问:只要sychronized吗,如果服务搭建集群的话,怎么办?

因为sychronized是基于一个jvm的吗,所以搭建集群的话,还是会导致锁失效的问题。

我们需要使用分布式锁,项目中我们使用的是redisson实现的分布式锁,在项目中我们主要使用就是可重入锁(lock)。但是呢,项目后期优化可能不局限于使用该锁,所以我就实现了动态获取锁及不同的上锁策略。(需要配置redis的基本数据)

实现方式:自定义注解 + aop + 工厂模式 + 策略模式 + SPEL。

1.自定义注解的属性主要就是锁的名字,锁的类型(对应枚举),策略(对应枚举)。使用@interface创建。

2.aop主要就是使用环绕通知,切点就是自定义注解。(使用切面.proceed方法来控制方法执行的顺序)

3.工厂模式主要就是类中的enumMap属性,key就是枚举类型,value就是redisson获取对应类型锁的函数。通过提供锁枚举获取对应的锁。

4.策略模式主要就是类中会有一个策略接口,不同的策略内部类都会实现该接口。在里面的策略就包括 快速失败,无限重试,超时后失败等等。

5.SPEL就是动态的做锁名字的拼接。

问:那你对redisson的底层实现有了解过吗?

redisson实现锁的功能主要就是:锁的重入性,阻塞重试机制,ttl重置机制。

锁的重入性:主要就是使用redis的hash结构实现的,大key存储锁的名字,小key存储线程的id,value存储锁重入的次数。

阻塞重试机制:主要通过redis的发布与订阅模式实现的,获取锁失败的线程会去订阅解锁频道,当获取锁的成功的线程完成解锁操作的时候,就会在该频道发送通知,失败的线程就会去重新获取锁,循环此过程直到获取锁成功或者超过等待时间。

ttl重置机制:底层主要就是通过watch dag来实现的,watch dag就是一个定时任务,每过10s就会判断线程是否解锁,如果没有就重置锁的ttl为30s。

问:那redisson是如何保证数据的原子性呢?

redisson的底层主要就是通过Lua基本实现的,Lua基本代码具有天然的原子性。

代金卷智能推荐

代金卷的智能推荐的步骤:查询数据库,初筛,细筛,排列组合,选择最优方案。

1.初筛:计算出购物车中所有片源的总金额和查询出该用户所有代金卷,将没达到门槛的优惠劵注解过滤掉。

2.细筛:此时要考虑代金卷的使用范围,片源有不同的分类(就比如:国内,国外等等)我们主要通过遍历查询查询出可以使用优惠劵的片源。已map进行存储。

3.排列组合:就是对map的keyset进行排列组合。(这里参考的算法就是leetcode的全排序算法)

4.通过遍历组合计算出对应的抵扣价格,及按百分比技术每个片源的抵扣金,将最优解存放在返回给前端。还会返回前端一个按抵扣金排序好的组合列表,通过给用户不同的代金卷组合方式。需要特殊处理的就是:在金额相同时,优先选择用卷最少的。

如果用户选择积分抵扣就会总金额就会再减去 积分数 * 0.01的金额。

当然在这个过程中我们也采用到策略模式,因为有不同的折扣类型,通过不同的折扣类型创建不同的策略类,在这些策略类中实现了判断门槛和计算折扣金额的方法。(在支付成功后会在redis中使用hash结构存储折扣的详情,大key就是订单id,小key就是片源id,value就是折扣的价格)

支付模块 

此项目的支付方式主要就是:支付宝。

我们会创建一张支付模板表,属性主要就是:appId,公钥,私钥,商户id,支付标签等等。(后续可能会有不同的支付方式)

通过工厂模式 + 自定义注解实现,通过设置支付标签,获取对应的支付类。

因为项目支付在网页中,是所以使用的是扫码支付,在调用支付FaceToFace().preCreate()就会生成支付链接,通过前端的Qrcode工具生成支付二维码。通过设置异步回调通知地址setNotifyUrl方法用来判断支付是否成功。

问:点击支付按钮的时候,是怎么防止多次支付的呢?

前端在进入下单界面时就会先使用雪花算法生成对应的orderId作为唯一标识。在后续进行生成订单支付的时候对orderId进行判断,就可以避免多次支付。

问:退款你是怎么实现的呢?退款后代金卷是怎么处理的呢?

项目中是支持部分片源退款的操作,如果是部分片源退款的话,我们就会从redis的hash结构中获取用户对该片源的实付金额进行退款,因为其他的片源还是进行了优惠,所以代金卷不会退回。

如果如果是全部退款(订单中回存储片源的id集合用来判断是否是全额退款),代金卷则会退给用户。

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值