论坛项目梳理

上大选课交流论坛:
目的是为了给同学们提供一个课程评价和交流的平台。主要功能包括登陆注册、发帖评论、点赞、按热度排序等功能。使用redis来进行用户信息查询的优化;基于kafka消息队列实现点赞评论后进行系统通知;定时任务对帖子热度进行计算和刷新。

一、拦截器Interceptor

1. 什么是拦截器

在AOP中用于请求进入Controller之前,通过拦截器进行拦截,然后在Controller之前或之后加入某些操作。
拦截器起作用的位置分为3个部分:

  • 请求进入Controller之前,通过拦截器执行代码逻辑【preHandle】
  • Controller执行之后(只是Controller执行完毕,视图还没有开始渲染,viewResolver之前),通过拦截器执行代码逻辑【postHandle】
  • Controller完全执行完毕(整个请求全部结束),通过拦截器执行代码逻辑【afterCompletion】

2. 项目中如何体现

想要自己配置一个拦截器,就必须用到HandlerInterceptorWebMvcConfigurer这两个接口。

首先,定义LoginTicketInterceptor,该类继承并实现了HandlerInterceptor

  • 重写preHandle方法:用户在登录后会以ticket为key,以及实际的ticket为value创建一个cookie存入浏览器。因此从请求request中获取cookie,从而拿到登陆凭证ticket。从redis中根据ticket获取对应的真正的登陆凭证,判断登陆凭证是否过期,从而决定是否拦截。对于放行的用户,要在本次请求中持有该用户的信息(ThreadLocal)。
    在这里插入图片描述在这里插入图片描述
  • 重写postHandle方法:将用户信息从threadlocal中取出来保存在modelAndView中,以便后续ViewResolver处理以及视图渲染。
    在这里插入图片描述
  • 重写afterCompletion方法:在整个请求完全结束的时候,清除掉threadlocal中该用户的信息。
    在这里插入图片描述
    也就是说拦截器的作用是拦截请求,拿到请求中cookie里的ticket键,redis获取用户凭证,校验用户是否可放行,并让threadlocal持有用户信息。还要负责用户信息清理等。

其次,定义配置类,该类继承WebMvcConfigurer,并将该类注入spring容器(@Configuration)

  • 对除静态资源外的所有路径进行拦截
    在这里插入图片描述

二、ThreadLocal

1. 什么是ThreadLocal

todo
InheritableThreadLocal 的使用:https://www.nowcoder.com/discuss/514704377869885440
InheritableThreadLocal 是 ThreadLocal 的一个子类,它允许子线程继承父线程的线程本地变量。

2. 为什么使用ThreadLocal

通过使用 ThreadLocal,每个线程都可以在各自的线程范围内存储和访问自己的上下文信息,而不会干扰其他线程的数据。这种线程隔离性使得 ThreadLocal 成为传递线程上下文信息的一种有效方式。
主要是为了保证线程安全性。

3. 项目中如何体现

创建一个 ThreadLocal 对象来存储用户信息。这里的new ThreadLocal表明在每个线程中均保存独立的用户信息实例User,也就是每个线程中保存的用户信息是互不相干的。
在这里插入图片描述

三、AOP

1、什么是AOP

todo
spring AOP & AspectJ

2、为什么使用AOP

我们需要在业务中记录日志。例如:“【某ip】在【什么时间】访问了【什么页面】。”
由于日志不属于业务需求,它属于系统需求,因此不应该耦合在业务代码中。
其次,如果我们使用OOP(面向对象编程)的思想,将日志封装成bean去调用,会使得代码过于重复。
综上所述,需要使用AOP。

3、一些AOP术语

Target是我们已经开发好的业务逻辑的一个一个bean,我们称之为目标对象。
目标对象上有很多地方可以被织入代码,可以被织入代码的地方我们统称为连接点joinpoint。

4、项目中如何体现

AOP解决统一处理系统需求的方式是将代码定义到一个额外的bean,叫切面组件Aspect,我们项目中即为下图的ServiceLogAspect。这个组件在程序运行之前就需要被框架织入到某些连接点。切面组件的pointcut声明织入到哪个位置,通知Advice方法声明切面要处理什么样的逻辑。

下图中:

  • @Aspect注解声明该类为切面组件
  • 使用@Component注解将该组件注入spring
  • @PointCut定义切点的具体位置
  • @Before代码前置通知,在开头织入程序
    在这里插入图片描述
    通过上述代码,我们就可以实现在切点位置执行相应的操作。更具体而言,在@Pointcut定义的位置,@Before会发挥作用先于切点代码执行。

四、redis

1、如何保持redis和数据库的一致性

项目中需要考虑该问题的位置:优化项目时,将用户信息缓存在了redis中。
如何解决:采用旁路缓存模式(先更新数据库再删缓存)。
为什么不选择先删除缓存再更新数据库的主要原因:有可能导致请求因缓存缺失而访问数据库,给数据库带来压力

如何解决旁路缓存模式下数据不一致问题?

  • 对于删除缓存失败导致的数据不一致:重试机制确保删除成功
  • 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值:延迟双删

重试机制:
把要删除的缓存值或要更新的数据库值暂存在消息队列中。如果应用没有成功删除缓存值或者没有成功更新数据库值,那么消息队列中会一直存在这个数值,直到删除或更新操作完成后才会被去除。类似于CAS思想。

延迟双删:
假设线程 A 删除缓存值后,还没有来得及更新数据库(比如说有网络延迟),线程 B 就开始读取数据了,那么这个时候,线程 B 会发现缓存缺失,就只能去数据库读取。这会带来两个问题:

  • 线程 B 读取到了旧值;
  • 线程 B 是在缓存缺失的情况下读取的数据库,所以,它还会把旧值写入缓存,这可能会导致其他线程从缓存中读到旧值。

等到线程 B 从数据库读取完数据、更新了缓存后,线程 A 才开始更新数据库,此时,缓存中的数据是旧值,而数据库中的是最新值,两者就不一致了。

这种情况我们就用延迟双删来解决(解决的是数据的最终不一致性):
A删除缓存,并且更新数据库后,sleep一段时间,然后删除缓存。
为什么要sleep一段时间?目的是为了让B读取数据库并将读到的旧值更新至缓存。

为什么并发请求下,先更新数据库再删除缓存不需要专门的应对方案来保证数据一致性?
因为在这种情况下,当线程A数据库更新成功,尚未删除缓存时,线程B虽然会读取到缓存中的旧值,但是并不会影响到缓存和数据库之间的最终一致性。

操作顺序是否有并发请求潜在问题现象应对方案
先删除缓存,再更新数据库
缓存删除成功,但数据库更新失败从数据库中读取到旧值重试机制
删除缓存后,尚未更新数据库,有并发读请求并发请求从数据库中读到旧值,并更新到缓存,导致后续请求都读到旧值延迟双删
先更新数据库,再删除缓存
数据库更新成功,但是缓存删除失败从缓存中读取到旧值重试机制
数据库更新成功后,尚未删除缓存,有并发读请求并发请求从缓存中读到旧值等待缓存删除完成,期间会有不一致数据短暂存在

2、项目中如何体现

缓存点赞和关注:

  1. 缓存用户点赞数。
    使用Set, key为点赞对象【帖子/评论】,value为点赞用户Id
    使用String,key为用户id,value为获得的点赞数量
    在这里插入图片描述
    用户点赞时,需要对用户获赞数String和实体获赞集合zSet进行修改。所以这个位置需要使用redis中的事务支持
    具体而言,我们重写了execute方法,使得点赞相关的几个操作可以成为一个原子性操作。
    在这里插入图片描述
  2. 缓存用户的关注列表和粉丝列表。
    使用zset,key为被关注者,set保存关注者以及关注时间为score,使用zCard获得粉丝数量。利用reverseRange的时间戳反向排序,按关注时间加载粉丝列表。
    follower:entityType:entityId -> zset(userId, now) 粉丝
    followee:userId:entityType -> zset(entityId, now) 以当前关注的时间进行排序

优化登录:

  1. redis缓存用户信息。
  2. redis缓存验证码。
  3. redis缓存登陆凭证。

随手记

  1. ticket保存在redis中,可能会由于存在大量的数据导致内存溢出,所以可以通过xxljob设置一个定时任务,定时对于redis中的无用ticket进行清理。更详细地说,需要定时去访问全部或者部分redis数据,对数据进行检查,只保留同一个用户id下的最新有效ticket。

难点和挑战

在开发这个项目过程中,我遇到了以下一些困难,以及我是如何解决它们的:

  1. 技术选型和搭建环境的困难:
    在开始项目之前,选择适合的技术栈和工具是一个重要的决策。我花了一些时间来研究和评估各种技术选择,并最终确定了Spring Boot、MyBatis、MySQL、Redis和Kafka。然后,我花了一些时间来搭建整个开发环境,包括配置和连接数据库、安装和配置Kafka、Redis等。通过查阅文档和参考示例,我成功地搭建了开发环境,并确保各个组件正常运行。

  2. 数据库设计和数据模型的困难:
    在设计数据库时,我遇到了一些挑战,主要是如何合理地设计表结构和建立各种关系。我认真研究了应用的需求和数据之间的关联关系,并进行了一些反复的迭代和讨论,以确保数据模型的合理性和可扩展性。基于这个理解,我成功地设计了数据库模式,并使用MyBatis进行数据库操作。

  3. Redis缓存和数据同步的困难:
    引入Redis作为缓存数据库是为了提升网站性能。在实现用户凭证、点赞和关注等功能时,我遇到了一些困难,主要是如何正确地使用Redis来存取数据,并保持数据的一致性。我仔细研究了Redis的数据结构和相关的操作命令,并使用Redis的事务和分布式锁等机制来确保数据的完整性和一致性。

  4. Kafka消息队列的配置和集成的困难:
    使用Kafka实现系统通知功能是一个新的挑战。我首先学习了Kafka的基本概念和工作原理,并在项目中集成了Kafka。我遇到了一些配置和生产/消费消息的问题,在解决时,我仔细检查了配置文件和代码,确保Kafka的各个组件正确运行,并实现了生产者和消费者的消息通信。

  5. 定时任务的配置和调度的困难:
    对于热帖排行榜的实现,我使用了定时任务来定期计算帖子的分数,并在页面上展示。我遇到了一些配置和调度的问题,比如如何配置定时任务、如何设置任务的调度周期等。通过学习和查阅相关资料,我成功地配置了定时任务,并根据需要进行了调整,确保数据的准确性和展示的及时性。

在解决这些困难的过程中,我主要通过以下方法来解决:

  • 深入学习和调查相关文档和资料,包括官方文档、社区论坛和博客等。
  • 参考示例代码和项目案例,学习其他开发者的经验和做法。
  • 与团队成员进行讨论和沟通,分享问题和解决方案。
  • 进行实验和测试,不断调试和优化代码,确保功能的正确性和性能的可靠性。
  • 寻求导师或同行的指导和建议,及时解决遇到的问题。

通过克服这些困难,我成功地完成了这个项目,并获得了宝贵的经验和技能。同时,我也意识到,在实际开发过程中,遇到问题是正常的,关键是要善于学习和探索,寻找解决方案,并与团队合作,共同克服困难,取得成功。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值