面试文案的

自我介绍

面试官好,我的名字叫做王文,我是华中师范大学人工智能教育学部电子信息专业研二的研究生,在校期间通过了大学英语四六级考试,参加过蓝桥杯程序设计大赛获得省奖,专业成绩一直位于专业前列

寻味外卖

寻味点评

短信登陆

在手机号、密码登录的基础上做的一个扩展,整合了阿里云短信服务,实现过程:用户输入手机号并点击发送验证码,后端获取到用户的手机号并调用阿里云短信服务向对应手机号发送验证码(有效期限制),用户再用手机号和验证码登录,后端获取阿里云生成的验证码和用户输入的验证码进行校验,并校验手机号,通过校验即表示用户登录成功,否在登录失败

共享Session

考虑到后面服务器肯可能集群部署来面对高并发的大量请求,这里就有一个问题:session存在于服务器上的,用于存储用户已经登录的状态,每次用户发送请求过来,根据cookie携带sessionID就能找到对应的session,从中取出用户,从而证明该次请求是由已经登录过的用户发送的,但是集群模式下,假如莫用户的第一次请求被分配A服务器,用户被存储到A服务器上的session中,但第二次请求被负载均衡分配给了B服务器处理,但B服务器上找不到存储了用户的session,导致服务端错误的认为了该用户是未登录,解决方式有两种:

  • session复制:将每个服务器都存储一份保持了用户的session(效率低下,空间浪费)
  • 用中间件Redis保存登录过的用户,因为中间件Redis对所有服务器都是可见的(本质:模拟session运行机制):选用是redis里面的hash结构,用户登录后,生成UUID随机串作为token也作为key,将描述用户的键值对信息作为value存到Redis中并设置有效期(30分钟),并向前端返回生成的token,让对应用户以后每次发送请求都携带这个token(前端的实现),这样下次该用户发送请求时,无论请求被负载均衡分配到哪个服务器,服务器拿这个token在redis中查找即可验证用户是否登录了

商户缓存

前端首页展示的商户数据,是很热点的数据,果断将其放入Redis缓存中,也将商铺类型表中的数据放入了缓存中,查就先从Redis中查,查不到再到数据库中查,再存入Redis中,但也涉及到很多问题

  • 删除还是更新缓存?

    • 更新:更新数据库就顺便更新缓存,无效更新操作较多:较长时间段内都没人来读,期间更新很多次缓存无意义
    • **删除:**更新数据库时就删除对应的缓存,要查找时再重建缓存
  • 如何保证缓存和数据库两个操作的同成功同失败?

    • 由于是单体项目:采用事务机制
  • 先删缓存再操作数据库还是先操作数据库再删除缓存?

    • 先删缓存再操作数据库:A线程请求删除了缓存,正准备更新数据库,此时B线程获得了CPU资源,请求查询数据,由于缓存中没有就去数据库中查,并重建了缓存,但重建的是数据库的旧数据,此时A线程得到了执行权,执行更新数据库数据后线程结束,此时就发生了缓存中的数据和数据库中的数据不一致
    • **先操作数据库再删除缓存:**A线程请求查询已过期的缓存,转为查询数据库,查询到后正准备重建缓存时,此时线程B获得了CPU资源,更新了数据库,并删除缓存(此时其实并没有缓存),然后A线程得到了执行权,将之前查到的旧数据写入了缓存,此时就发生了缓存中的数据和数据库中的数据不一致

**分析:**第一种情况:由于线程A的第二个操作是更新数据库,耗时长,容易线程B的重建缓存的钻空子,而第二种:线程A的第二个操作是耗时短的重建缓存,不容易被线程B的耗时长的更新数据库操作钻空子,所以采用第二种方案更优

最终缓存商户信息的方案(单体项目用事务保证数据库操作和缓存操作的原子性):

  • 根据ID查商户时,缓存未命中就查数据库,再重建缓存并设置超时时间
  • 根据ID修改商户时,先修改数据库,再删除缓存

使用缓存会产生的问题:

  • 缓存穿透:请求的数据缓存和数据库中都不存在,缓存也没法重建,每次请求都会打到数据库

    • **缓存空对象:**重建null对象到缓存中,短时间过期(简单,额外内存消耗)
    • 布隆过滤器:实现复杂、不完全可靠(预测数据不存在时数据肯定不存在,预测数据存在时数据不一定存在)
  • 缓存雪崩:大量缓存同时过期或的Redis服务器宕机,导致大量请求打到数据库

    • 不同的key设置不同的TTL
    • Redis搞集群或者哨兵部署,增加Redis的高可用性
  • 缓存击穿:被高并发访问且缓存重建业务较复杂的热点Key过期,大量请求会瞬间打到数据库

    • 互斥锁(使用setnx命令作为互斥锁,不存在的key才会set成功,存在的key会返回nil,del对应的key表示释放锁):线程A查询商户数据,缓存中未命中,查数据库前要先获取互斥锁,查完数据再做缓存重建,重建完释放锁并返回结果,期间线程B想查商户数据,先查缓存未命中,再准备查数据库时发现获取互斥锁失败,会不断的休眠一会再重试查缓存再获取互斥锁,直到缓存命中(特点:实现简单,保证强一致性,但性能低下)

    在这里插入图片描述

    • 逻辑过期(商户类中添加逻辑过期字段,缓存中并不设置TTL,开启缓存重建的线程时用到了线程池:JUC包下的Executors.newFixedThreadPool()):线程A查数据,缓存命中,但发现缓存中value中存储的时间已过期,尝试获取互斥锁,获取成功后开启另一个线程进行查数据库并缓存重建,重建完成后释放锁,线程A无需等待,返回旧数据即可,在此期间其他线程想查数据,发现数据已经逻辑过期,同样会尝试获取互斥锁,获取失败就直接返回旧数据,缓存重建完成后,再有线程想查数据,直接就可以返回最新的数据(特点:性能高,保证可用性,但不保证一致性,实现较复杂)

    在这里插入图片描述

    在这里插入图片描述

优惠卷秒杀

一般流程:提交优惠券ID查询优惠券信息,校验是否在秒杀时段范围内,查询库存是否充足,充足就扣减库存并创建订单(某用户买了某张优惠券),最后返回订单ID(项目中涉及到优惠券表和秒杀优惠券表,秒杀优惠券表关联了优惠券表,并记录了库存信息、开始、结束秒杀时间)

在这里插入图片描述

**超卖问题:**请求A查询库存,发现库存为1,请求B这时也来查询库存,库存也为1,然后请求A让数据库减1,然而之前B查询到的仍然是1,也继续让库存减1,导致超卖。解决方案有:悲观锁、乐观锁

悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。例如Synchronized、Lock都属于悲观锁,但性能不好

乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据。如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常,实现乐观锁主要有以下两种方法:

  • 版本号法:初始查一次数据,记录版本号,扣减库存时再次用where条件查询数据库中数据的版本号并于之前的比对,一致则正常扣减,否则就重试请求

    在这里插入图片描述

  • CAS(Compare And Swap:比较替换)法:版本号法的简化,利用库存量代替版本号,初始查的库存要和扣减时where条件查的库存一致才正常扣减,否在重试

一人一单

一般流程:根据前面的流程,只不过经过了秒杀时间和库存校验后还需要根据优惠券ID和用户ID去查询是否已经有订单了(该用户已经买了该优惠券),没下过单再去扣库存,添加订单。但是有并发安全问题:

在这里插入图片描述

单体项目加锁即可,但如果后期部署成集群服务器,多个JVM是互相隔离的,所以需要分布式锁,让集群中每个服务器上的进程都共享

在这里插入图片描述

**有可能发生的问题:**线程A获取锁后执行自己的业务阻塞了,超时释放后还阻塞着,此时线程B就能够获取锁成功,执行自己的业务时,此时线程A阻塞完后,就直接去释放锁,但释放的是线程B的锁

**解决:**设置锁的时候要存上一个线程标识(随机生成UUID),执行完业务不能直接去释放锁,而是释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致,一致直接释放锁,不一致不释放锁

**还有可能发生的问题:**执行完业务,先获取锁中的线程标识,发现是与当前线程标识一致,于是去释放锁,但是此时恰好阻塞了,锁超时释放后还阻塞着,同样B线程获取锁成功后执行业务,但此时线程A阻塞结束,直接去释放锁了,结果把B的锁的释放了,**问题出在:**判断和删除两个操作没有保证原子性。

**解决:**利用LUA脚本,将多条redis命令(释放锁的流程)编写到一起,保证原子性,脚本中包括:

  • 在获取锁时存入线程标示(可以用UUID表示)
  • 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
  • 如果一致则释放锁
  • 如果不一致则不释放锁

P64…

达人探店

发布探店笔记:首页点击+号加入发布笔记页面,新增一条blog表中的记录(上传照片:上传到nginx目录下+发布笔记,还包含关联的商户)

  • 查看笔记(简单的CRUD)

点赞他人发布的笔记:对于某篇笔记,同一个用户只能点赞一次,如果当前用户点过赞了,点赞按钮高亮(前端已实现,判断字段Blog类的isLike属性)

  • 实现:修改Blog类的isLike的属性即可,考虑到要记录当前blog被哪些用户点过赞,且不能被重复的用户点赞,所以选用redis中的Set集合:点赞前先获取笔记对应的点赞Set,判断当前用户是否点过赞了,没点过就将当前用户存到Set中,数据库表中的点赞数+1,若点过了,将数据库表中的点赞数-1。查询blog时还要从Set中判断是否点过赞,再为isLike属性赋值

点赞排行榜:笔记详情页显示了点赞数和哪些用户点赞了,但是是无序的,需要将点赞的按先后时间顺序的Top5显示出来,选用SortSet集合,每个元素有一个score,利用zscore命令,若返回了分数代表元素存在,否则代表元素不存在,根据分数查找Top5:zrange key 0 4

  • 将点赞用户存入某笔记的zset中时,应该带上分数,用当前时间的时间戳(天然自增)表示分数
  • 查询点赞用户接口:利用zrange 命令返回Top5的点赞数据,解析出用户ID,查询出用户,封装到集合里返回

好友关注

关注和取关:MySQL实现,有一张中间表tb_follow,一条记录就可以记录某个用户关注了哪一个用户

  • 关注和取关接口:新增或删除tb_follow中的一条记录
  • 判断是否关注:查询tb_follow中是否有当前用户ID和目标用户ID关联的记录

共同关注:点击目标用户可以进入到目标用户主页,里面的共同关注模块就可以看到当前用户和目标用户的共同关注。利用redis的Set集合,是可以求交集的:给每个用户设置一个key,利用set结构存储关注该用户的人,利用intersect求对应两个set的交集即为共同关注,所以之前的接口也需要改,关注和取关都要对应从set中添加元素或移除元素

关注推送:不做内容筛选,让某用户关注的目标用户发布的内容都能被该用户看得到

  • **拉模式:**所有用户都有一个发件箱,用户所有发布的笔记都存到他自己的收件箱里,当某一个用户打开关注列表页面时,就把其所有关注的用户的发件箱里的邮件都拉到该用户的收件箱(按时间戳排序)里

    在这里插入图片描述

    • 优点:笔记只保存一份,节约内存
    • 缺点:每次查看关注页都要去拉取所有关注,延迟高(某用户关注了几万人,拉取的时候就会很慢)
  • **推模式:**无收件箱的概念,某用户若发布笔记,就直接推到其粉丝的收件箱里(每个粉丝都推一份),粉丝去查看关注页的时候就可以直接取数据

    在这里插入图片描述

    • 优点:时延低
    • 缺点:内存占用大(某大V,几千万的粉丝,往每个粉丝收件箱都要发一份,内存占用很大)
  • **可以说成是项目不满意要改进的点:推拉结合:**将普通用户和大V区分开,大V有发件箱,普通用户无发件箱,将粉丝也分为普通粉丝和活跃粉丝

    • 具体讲就是大V发内容,往自己的发件箱里存一份,直接推给活跃粉丝收件箱一份,普通粉丝要读取的时候再临时拉取大V发件箱里的内容
    • 普通用户发内容直接推给粉丝的收件箱

    在这里插入图片描述

  • 当前项目使用推模式实现关注推送:

    • 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱,每个用户都有一个SortSet的收件箱存储关注的人推过来的内容,并且用时间戳作为score值将推送内容默认按时间排序
    • 用户查看关注列表时直接自己的收件箱中取出按时间排好序的推送内容即可(分页查询用zrange命令)

附件商户

  • **GEO数据结构:**手动导入商户的经纬度坐标,利用Redis里面额GEO数据结构来存储,经纬度对应的member存入店铺ID,同一种类型商户存到同一个GEO的key中

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RrdWAX8V-1682839017954)(C:\Users\wangw\AppData\Roaming\Typora\typora-user-images\image-20230429170652412.png)]

  • **附近商户:**首页点击某种类型的商户后进入商户列表页,实现默认展示按距离排序并显示距离,前端请求queryShopByType传过来的是商户类型、经、纬度,要求返回商户List

    • 判断是否需要根据坐标查
    • 查询Redis,按距离排序,并带上距离
    • 解析出id
    • 从数据库中查询出shop(字段中有个非必须的distance字段,专门为了封装距离),封装成List返回

用户签到

利用每月签到固定次数送积分,优惠券等机制促进用户经常活跃在APP上,假如用数据库中的某张表(记录签到的用户,签到时间…)记录签到,那么每个用户的每次签到都会生成一条记录,数据量太大二零

  • **BitMap:**底层基于String类型实现,用每个位置上的数表示签到情况(1或0),30个位置(4字节)就能表示一个用户的一个月的签到情况,非常节省空间,本质就是把每个bit位对应某月的每一天,形成映射关系,这就是BitMap

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XeqnHBp6-1682839017956)(C:\Users\wangw\AppData\Roaming\Typora\typora-user-images\image-20230429175046091.png)]

签到功能:将每个用户当前年当前月作为一个key,算出当前是这个月第几天,利用Redis将对应BitMap的对应位置设置为1

签到统计:统计当前月总签到次数,找到对应用户的当前年当前月对应的BitMap,利用BitCount统计所有1的数量即为当前月签到的次数

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值