西城区好的网站建设多少钱,做化妆品网站怎样,郑州网站高端设计,网站建设合同附件产品结构图 Nginx实现代理
问#xff1a;我们在本机的host文件中配置了域名映射#xff0c;都是同一个服务器。我们只需要输入对应的域名就可以到对应的界面#xff0c;这是怎么实现的#xff1f;
答#xff1a;主要就是通过Nginx反向代理来实现的#xff0c;Nginx会先… 产品结构图 Nginx实现代理
问我们在本机的host文件中配置了域名映射都是同一个服务器。我们只需要输入对应的域名就可以到对应的界面这是怎么实现的
答主要就是通过Nginx反向代理来实现的Nginx会先通过域名去匹配对应的端口主要就是通过配置监听端口再通过配置的服务器地址找到服务器最终访问服务器对应的端口。
一个server就是一个虚拟主机。并且每个虚拟主机都是在监听80端口在没有输入端口的时候就默认就是走80端口。通过server_name也就是域名做匹配最终进行代理。
项目编写时出现的小问题
1.在对Long类型的数据进行比较的时候如果数值在-128~127时则可以使用判断是否相同。在源码中使用-128~127的缓存数组在该区间的Long数据都是相同的而不在该区间时则是通过new来创建的不能使用来判断是否相等。所以在判断Long数值的相同时使用equal
2.返回类型后存储类型为枚举的时候的存储方式。 JsonValue //在使用枚举类型的数据进行返回时我们可能只是需要枚举中的某个属性此时在枚举中被JsonVlaue修饰的属性就会作为返回类型用于返回前端时需要配合jackJson使用EnumValue //当枚举类型的数据存储数据库的时候此时在枚举中被EnumValue修饰的属性就会作为存储类型用于数据库的存储需要配合mybatis使用
JsonValue在使用枚举类型的数据进行返回时我们可能只是需要枚举中的某个属性此时在枚举中被JsonVlaue修饰的属性就会作为返回类型用于返回前端时需要配合jackJson使用 EnumValue//当枚举类型的数据存储数据库的时候此时在枚举中被EnumValue修饰的属性就会作为存储类型用于数据库的存储需要配合mybatis使用
3.在个人课表中的幂等性的处理方式通过对用户id和课程的id做联合索引并且将该索引设置为唯一索引这样就可以保证在做课程加入的时候一个用户只能有一个对应的课程最终解决幂等性问题。
4.在项目中每次进入个人空间及需要userId的地方是如何获取userId的
在项目中我们主要是通过token来进行校验的我们会做一个全局拦截器去解析该token通过JWT最终获取到userId为了能够传递下去不局限于当前方法我们可以将userId存储到ThreadLocal中最终后续的方法都可以获取到UserId。 视频观看模块合并写请求
问在你开发中有没有比较有挑战性的模块呢
答有一个辅助功能我印象比较深刻视频的播放位置的处理我们的规则就是续播位置的误差需要控制在30秒内并且切换设备也需要有这个续播功能本质上就是观看的位置储存到服务端数据库的一系列操作因为播放视频为用户的主要操作所以需要考虑高并发的优化。.....那我给您介绍一下这个优化方案。.....
提交信息记录的优化方案高并发优化方案 未优化前
提交的记录分为两种类型考试和视频。
考试的话我们就直接新增记录即可并将修改课表的已学章节数 1(因为规则就是只能考试一次)。
视频的话前端每个15秒就会提交一次主要就是记录当前的观看位置去不断地修改记录的观看位置看完的规则就是大于50%我们通过前端传来的观看位置和视频总时间进行判断最终修改课表数据中的已学章节数 1只有第一次看完会修改课表数据。
我们需要判断是否存在记录以及每次提交都需要修改记录的观看位置涉及到大量的数据库操作接口会就一直处于高并发的场景。
优化后
高并发的优化方案可以从集群数量服务的熔断限流及减少DB操作进行优化。
使用redis缓存rabbitMq来进行优化(缓存 异步任务线程池)。
使用redis的hash结构来存储临时观看位置大key储存lessonId小key储存节的idvalue储存amount及看完标识。
在判断是否存在记录的时候先去缓存中查询是否存在然后再去DB中查询并添加缓存。用缓存查询解决判断
为了防止多次的操作数据库修改观看位置时先将储存到redis中的观看位置进行修改使用延迟队列发送延迟20秒的消息数据主要就是观看的位置在每次监听到消息的时候。判断消息的观看位置和缓存中的是否相同lessonId 节id来确定缓存中观看位置数据如果相同说明用户当前20秒内没有观看则去更新数据库并清除缓存。如果不相同说明用户还在观看则不进行操作。
在后续执行操作db的时候为了提高速度我们还使用了线程池使用submit方法进行提交任务在监听方法中监听到任务后使用多线程执行任务我们会创建一个静态的线程池,实现多线程操作数据库加快db的操作,减少io的消耗时间。因为涉及到的io操作比较多所以将核心线程数设为2N1(N为电脑的核数)
问题回答及评论模块 表的设计: 总共两张表分别是问题表和 回答记录表。
问题表只要就是存储问题的基本信息 标题内容 回答的数量。用户id课程id外键就是回答的id。
回答表存储回答和评论的基本信息内容是否点赞回复问题的id 回复回答的id用户id。其中 通过判断问题id和回答id来判断是 回答还是评论。 在展示界面的设计就是先展示问题的对应课程的 问题列表。点击对应问题的回答信息就会展示回答列表点击对应的回答的讨论就会展示讨论的信息。通过上级问题id上级的回答id来确定返回的讨论和回答数据。一些具体的数据需要使用feign接口查询就比如用户的信息 点赞模块使用set结构的redis进行优化 并且使用xxl-job执行定时任务
问介绍一下点赞模块吧
点赞的业务包括对问答的点赞对笔记的点赞所以我们直接将点赞设置一个独立的服务。
我们考虑到此模块可能会存在高并发的场景所以在原先涉及大量的DB操作旧方案上通过redis的数据结构和定时任务来优化。面试官感兴趣就介绍优化方案
表的设计:点赞记录表。
点赞记录表: uerId, 讨论或回答id点赞目标类型(回答或笔记)点赞时间。 新的点赞方案 优化前的方案就是直接保存点赞记录到db MQ修改对应回答或讨论的点赞数。
优化后
将点赞的记录用set结构存储到redis中key存储目标id就是回答或笔记的idvalue存储用户的id在每次提交点赞或取消点赞的时候对set新增或删除的操作并且取统计目标的点赞数通过scard方法就可以获取点赞数。在做点赞记录是否存在判断的时候使用isMember方法进行判断
将点赞的数量存储到zset结构中key存储目标类型(回答或笔记)member存储目标idscore存储点赞的数量。
通过xxl-job定时任务每隔20秒发送消息到mq实现到zset结构中取30条数据,使用popmin方法获取目标id和点赞数量做批量的修改操作。
减少了大量的DB操作。
因为点赞的模块请求量本来就比较小就没有考虑对xxl-job搭建集群。(选答)
题外话在改优化完成之后我就在想像这种小体量的请求直接走数据库其实会快很多因为DB操作也没有那么多但是为了考虑高并发场景所以还是进行优化比较好。只能说有利有弊吧。可以跟面试官陈述这样比较真实 问redis中具体使用什么数据结构
将点赞的记录用set结构存储到redis中key存储目标id就是回答或笔记的idvalue存储用户的id。
将点赞的数量存储到zset结构中key存储目标类型(回答或笔记)member存储目标idscore存储点赞的数量。
问在set结构中为什么不使用userid作为key使用目标id作为value如果用户量庞大的话会不会出现BigKey的问题?
这个方案也是可以的因为此模块不会涉及到大V的点赞操作因此就认为点赞的个数为1000以内就不会有BigKey的问题。并且我们需要统计每个目标的点赞数量当目标id作为key的时候正好可以使用scard方法来获取个数比较方便。而Redis的SET结构会在头信息中保存元素数量因此SCARD直接读取该值时间复杂度为O(1)。
问那你使用zset的作用是什么呢 我们使用set结构存储点赞的记录为配合xxl-job的使用我们需要使用zset来记录点赞数发生变化的目标id和点赞数量这样才能确定需要修改的目标id和点赞数。
问那为什么不使用List结构呢而是使用zset结构
如果我们使用List结构那么在每次统计目标id的点赞个数的时候就会存储因此改目标的点赞个数到list中其实我们只需要最后一次的点赞数但是使用list进行批量修改的话就可能会多很多DB操作。但zset一定最好吗我是不怎么认为毕竟zset的底层实现为:哈希结构 跳表需要格外的空间占用。
签到及积分模块
在就是选型的时候我们就考虑使用mysql来存储签到的记录当是签到涉及每个人每天的签到情况这样表的数据就会十分的庞大为了解决这个问题我们想到使用redis的bitMap来解决此问题因为存储是二进制的文件并且每一个月才会有一条数据比较节省空间。在该模块中比较特别的就是查询连续签到的天数主要就是通过 (与运输) 右移来查询的。
问你的项目中用过redis中的什么结构呢
用得还是比较多的就比如String HashsetzsetlistbitMap等等。
1.Hash 在做分布式锁物流项目中的支付模块防止多次支付临时存储视频的观看位置解决高db操作的问题。
2.set 保证即可的幂等性防止重复消费(物流项目中运单防止被车辆多次消费)作为缓存存储点赞记录(点赞模块减少db操作)
3.zset保存对应业务的点赞个数配合xxl-job发送消息到mq进行批量的修改操作。点赞模块,减少多余的db操作
4.list在等等转运单将两地网点的id拼接作为keyvalue就是存储对应位置的运单id通过定时任务让车辆转载运单并通过set解决幂等性的问题。物流模块车辆转载流程
5.bitMap保存当月的签到记录通过二进制文件进行存储。签到模块减少db的大小
问你使用redis保存签到记录的话如果redis宕机了怎么办呢?
主要就是要保证redis的高可用性。
1.我们可以给redis设置持久化机制包括使用 AOFRDB。
2.搭建redis的集群设置哨兵等等。
3.对于数据的安全性和正确性要求较高的数据还是需要使用传统的数据库进行存储。
总的来说redis的bitmap存储签到的信息的安全性和内存的占用的情况都是可以接受的但是具体的技术选型还是需要根据要求取选择。
积分模块
项目中规定写问题回答的时候可以 3积分,编写笔记 可以 4分每天都是最高20积分。这些积分可用于做排行榜。在增加不同的类型的积分的时候通过mq的router key来添加不同的类型积分。
排行榜模块
问说说你积分排行榜功能的设计和实现吧
主要就是通过用户的每个月的积分做排行在月初的时候会清零。将每个月作为一个赛季我们的功能主要就是本月的排名和历史赛季排名。 本月的排名会通过redis的zset结构进行存储key存储年月member存储userIdscope存储积分值可以通过ZREVRANGE,ZRANK方法获得排序和排名后的数据。因为底层是基于跳表的结构实现的所以效率很高。
历史排名的话就是要存储到数据库中都是如果将每个赛季的数据都存到一张表里的话数据就会很庞大为了解决这个问题我们可以使用水平分表这里的分表主要就是基于每个赛季建立独立的表使用赛季id作为后缀生成独立的表这里我们没有使用share-jdbc而是通过mybaits做创建表的操作。在我们每次查询赛季的时候就不会有跨表的行为。在数据的插入和查询的时候使用mybtis-plus的插件设置动态表名来实现数据的插入和获取。实现步骤就是:DynamicTableNameInnterInterceptor,在map中创建tableNameHandler类重写dynamicTableName方法将就表名拼接成新名的表名最终动态修改表名
通过xxl-job执行三个任务创建对应赛季表插入本赛季的排名数据删除zset中的缓存记录。
这三个任务是相互关联的分成三个任务的的目的就是减少任务的耦合度在某个任务失败的时候单独重新执行即可。无需重新执行全部任务当然前提就是要保证任务执行的顺序不能变。如果不按顺序你可能先删除缓再做插入操作这样就会出现插入空数据这明显不合理 public interface TableNameHandler {/*** 生成动态表名** param sql 当前执行 SQL* param tableName 表名* return String*/String dynamicTableName(String sql, String tableName);
}
问排行榜使用zsetsortedSet进行储存那用户数据量非常多的时候该怎么办呢
sortSet的底层使用的是跳表的结构排序效率是很高的我们项目的用户体量在十万左右即使是百万级的用户量性能依旧是非常好的。
如果用户量真的非常大的话我们的优化方案就是使用分治和桶排序的思想将用户按积分的范围分成多个桶。就比如 0100100200 ......在后续获取排名的时候也是很方便的当前桶中用户的排名 大于该桶对应范围的其他桶数据个数的总和 就是该用户的排名这样效率也是很高的。
问在执行定时任务的时候你们是使用什么框架来实现的呢处理百万排名数据的时候是怎么解决的呢然后保证多任务的顺序性呢
1.在我们生成历史排名的时候就会顺序执行三个任务使用xxl-job框架来实现的。
2.在处理海量任务的时候就会使用xxl-job的分片广播策略因为排名的数据会做分页我们可以使用api获取xxl-job的索引和总数通过对页码取模的方式来确定执行任务的xxl-job节点。最终对应任务会被执行。
在物流项目的运单消费中也是通过xxl-job的分片广播来执行消费任务的通过运单id取模。为了提高用户的体验防止同用户的运单装配到不同的车上【用户就需要多次不同时间收快递】要添加分布式锁
3.为了保证任务的顺序性可以使用子任务来保证顺序。a(子任务:b)-b(子任务:c)-c。在任务管理中设置即可 优惠劵模块使用异步实现优惠劵领取
优惠劵兑换码模块
问你们优惠劵有兑换码的方式兑换是吧那你聊聊实现的流程吧
为了提高安全性我们最优的值就是使用自增序列号通过reids的bitmap就可以判断改兑换码是否兑换过直接使用自增序列号会有爆刷的风险因此我们还需要使用一些加密的算法。1~9 A~Z不要0,o,i,l
我们会将序列号转为32位并且每四位转化为10进制这样就可以获得8为的数值对数值进行按位加权。我们准备16组的权数组此就结果就是签名。
随机生成4位新鲜值用于随机的取一个权数组。签名的后14位 4位新鲜值 32位的自增序列号就过程了50位的数据按5位计算字符最终生成兑换码基于Base32生成10位的兑换码。
兑换码的校验就是该生成的相反操作。
整个过程没有涉及db操作所以在效率上非常高。
使用incrBy来记录自增的最大位置。 问在你的项目中有使用过线程池吗
在优惠券模块使用了线程池在我们对优惠卷发放的时候如果优惠卷是兑换码兑换的时候不仅需要修改优惠卷的状态也要生成对应数量的兑换码因为兑换码的数量较多如果在发放的时候生成兑换码的话会很耗时因此使用线程池使用线程异步的生成兑换码在效率上就得到了大大的提升。 问你的线程池参数是怎么设置的呢
线程池设置的参数主要就是核心线程数最大线程数救急线程数救急线程的时间单位阻塞线程拒绝策略。
因为优惠卷发放并不是高频的场景所以在核心线程数就设置为2这里使用线程池主要就是异步的执行兑换卷的生成减少整个接口的耗时。将阻塞队列容量设置为200当核心线程和阻塞队列都无法应对任务的时候就会使用救急线程去执行。
优惠劵领取模块
问在你的优惠劵模块中你是如何解决优惠劵超领的问题呢超卖问题
超卖的的主要原因就是多线程并发访问导致的事务的数据未提交导致其他的事务也做了新增的操作最终导致超卖的问题。
主要的解决方案就是使用悲观锁或者乐观锁。
悲观锁的效率比较低就没有考虑了mysql中的乐观锁主要也是通过版本号字段的匹配来解决超卖的问题。这也会导致失败率较高在发放优惠劵的时候没有到达最后一张时也只会有一个线程执行成功效率上比较低。
所以我们在解决超卖是直接在sql的条件中加 当前发放的数量要小于总数量条件来解决并发的问题。大大提高了成功率 问那你在这模块中开发过程中有遇到什么难题吗 当我们使用jmert去使用同一个用户并发的抢一张卷时优惠劵的领取数量超过了限制的数量在整个代码实现过程中没有保证原子性。在这个方法中主要就是修改优惠劵的发放数创建用户卷修改兑换码的状态因为这个普通方法是抽取出来的直接加事务并使用锁 因为此方法的事务是独立的在相同的userId获取优惠劵的时候呢此方法都执行完了此事务完成不受外部方法的事务回滚的影响。最终导致超过限制。不同的userId会去做限制的判断内部事务会直接回滚 开始的时候使用synchronzied代码可以通过userId来上锁保证相同用户每次只能由一个线程执行但是呢userId时Long类型的,当数据不在-128~127时每次都是新对象又想到用toString方法都是其底层还是创建一个新的对象所以我们需要使用常量池中的数据来做校验也就是调用.toString.intern方法。
但是经过测试还是存在相同的问题经过分析发现如果是先开启事务在获取的锁的话在上一个事务db操作完解锁但是未提交此时下一个事务获取锁进行db操作最终提交出现了两次的db操作。这就导致问题还存在的原因。所以解决方案就是缩小事务的范围先获取锁在开启事务这样就避免了问题。 问只使用syncheronzied来加锁吗实现悲观锁有没有其他的方式呢
在开始的时候确实使用synchronized来实现锁的但是其是在一个jvm中实现的在微服务项目中肯定是不够用的。因为优惠劵会搭建集群(集群下的jvm是独立的会导致锁失效)我们需要考虑微服务下锁失效的问题。在项目中我们使用redisson来实现分布式锁。最开始的时候我们使用可以可重入锁也就是lock方法但是呢这个方法是死的。后续需要使用其他锁的时候我们还需要修改代码。
解决方案自定义注解 aop 工厂模式 策略模式 SPEL 来实现的。通过添加自定义注解来实现上锁事务的优先级最低所以会先上锁在开启事务
1.自定义注解主要就是存储用户的配置。包括锁的名字获取锁的等待时间过期时间锁的类型失败策略。 2.aop:主要就是通过自定义注解作为切点使用环绕通知来上锁。通过切点.proceed方法来控制执行的顺序
3.工厂模式在工厂类中map属性EnumMap,底层是简单的数组,key存的就是锁类型的枚举value存储的是方法。在工厂创建的时候就会对map进行插入操作。在工厂方法中通过锁类型的枚举从map获取并执行方法最终返回对应类型的锁。 4.策略模式在枚举类中会有一个上锁的接口并且在枚举中会有多个类实现该方法每个类都是一种策略。通过自定义注解中设置的策略执行对应的上锁方法。 5. 通过SPEL来动态的设置上锁的名字主要就是根据userId来设置名字SPEL是找的现成的代码所以不是很了解。名字的格式:lock:coupon:#{userId}
最终保证动态的控制分布式锁的类型及失败策略。
在物流项目中使用的分布式锁也是基于redisson实现的锁的底层实现就是基于hash结构来实现的大key也就是锁的名字在物流项目中使用名字就是交易单id订单id的拼接小key就是线程idvalue就是锁重入的次数。而本项目中的大key使用的就是userId。只是做了格式化 问有了解过事务失效的场景吗有遇到过吗你是怎么解决的呢
事务失效的场景
1.在是事务方法中捕获到异常未主动的抛出。
2.事务方法为非public方法。
3.事务的传播性行为错误。
4.事务设置为的回滚异常类不匹配。
5.非事务方法调用事务方法。
在我编写优惠劵模块的时候就出现非事务方法调用事务方法导致索引失效。
因为在代码开中有重复的数据库操作所以我们封装了一个通用的方法主要就是领卷后的添加记录修改优惠劵发放数量的操作但是修改优惠劵老是不生效后来就发现是事务失效了。
解决方法
导入aop依赖 - 开启aop注解开启暴露代理-获取代理类调用对应的事务方法。 优惠劵智能推荐模块
问说说优惠劵智能推荐的流程吧
主要的流程就是对查询到的用户进行进行初筛-细筛-优惠劵排列组合-计算每种组合的优惠金额-选择最优优惠。 1.先查询该用户的所有优惠劵集合。
2.初筛先不考虑优惠劵的使用范围将门槛金额大于课程价格和的优惠劵直接过滤掉。
3.细筛每个优惠劵可能会限定范围查询出每个优惠劵能使用的课程集合最终使用Map进行收集。(Mapcoupon, ListLoong)此时的key集合就是可以使用的优惠劵集合。
其实这里的初筛和细筛可以合并在一起主要是为了增加可读性。
4.排列组合生成所有的可用优惠劵的搭配方案。这个就是leetcode的全排序算法这里要注意的就是还要添加只使用一张优惠劵的情况
5.计算每种组合的优惠金额主要就是循环优惠劵组合判断当前的金额是否复合优惠劵的最低门槛并按优惠劵的金额计算折扣价格按照课程价在总价格中的比例计算各个课程的折扣价在下次计算中金额前需要扣除当前的折扣价。
6.选择最优优惠主要就是要考虑多个组合的折扣价格相同情况此时就优先考虑使用优惠劵最少的组合。如果还存在优惠劵数量使用相同的组合的话直接将这多种组合都返回给前端将选择权交给用户。返回的类型就DTO的listDTO中的属性折扣金额优惠劵的id集合每次商品的折扣金额使用map结构 问你项目中有没有使用过设计模式
在优惠劵的折扣计算的时候就使用了策略模式动态的获取不同的折扣类型的策略类这些折扣类主要用于判断是否达到门槛和计算折扣金额。
在使用分布式锁的时候为了能够动态的获取锁的类型及不同的上锁策略就使用工厂模式 策略模式。工厂模式主要就是动态的获取锁的类型 策略模式主要就是动态的获取对应的上锁策略这里就可以引导模式官到到抢卷的模块说说使用分布式锁的原因....
问你们优惠劵规则是怎么实现的
在优惠劵的折扣计算的时候就使用了策略模式动态的获取不同的折扣类型的策略类这些折扣类主要用于判断是否达到门槛和计算折扣金额。 问在项目中有使用过线程池吗
在生成优惠劵兑换码的模块中使用了线程池。引导面试官到发放优惠劵的模块中..... 问如果出现部分商品退款的情况退款金额和优惠劵是怎么处理的呢
在退款金额上呢如果用户选择了部分的商品进行退款的话我们就根据每个商品实际付款金额进行退款主要就是将每个商品的实付金额存储到redis中并将ttl设置为15天通过该结构进行退款使用hash结构,大key存储订单id小key存储课程idvalue存储实付金额主要也满足我们不退回优惠劵的规则。