1. 使用 Redis 优化登录模块
需求:
- 使用 Redis 代替传统的 session 来临时存储验证码。
为什么?
1) 性能优势
- 高并发支持:Redis 是一个基于内存的数据库,具有极高的读写性能。相比传统的 session 存储(如基于服务器内存或数据库),Redis 能够更好地支持高并发场景。特别是在有大量用户请求验证码的情况下,Redis 能确保响应速度和系统的稳定性。
- 低延迟:由于 Redis 操作是纯内存的,读取和写入验证码的速度非常快,可以显著减少用户在请求验证码时的等待时间,提升用户体验。
2) 分布式环境的支持
- 跨服务器共享:在分布式架构中,应用通常部署在多个服务器上。传统的 session 是基于服务器内存的,用户的请求必须保持在同一个服务器(Session Stickiness),否则会导致无法获取之前存储的 session 数据。使用 Redis 后,由于 Redis 是一个独立的服务,可以被所有服务器共享访问,避免了 Session Stickiness 问题,并简化了分布式架构的设计。
- 统一管理:通过 Redis,所有服务器都可以访问同一个数据源,这使得验证码的存储和验证更加统一和便捷,尤其是在用户频繁切换服务器的情况下。
3) 自动过期管理
- 过期时间的控制:Redis 原生支持键的过期时间设置。在存储验证码时,可以直接为验证码设置一个短期的过期时间(例如 5 分钟)。当验证码过期后,Redis 会自动删除对应的数据,无需额外的清理逻辑。这不仅简化了代码逻辑,还能有效地防止旧验证码的滥用。
- 内存管理:Redis 的过期策略不仅能保证验证码的有效性,还能通过自动删除过期的验证码释放内存,从而节省资源并提升性能。
4) 安全性
- 隔离性更强:Redis 将验证码存储在独立的缓存系统中,与应用服务器内存分离,能够减少某些类型的安全风险,如通过劫持 session 进行攻击。同时,由于 Redis 是一个独立的服务,具有更灵活的权限控制和加密机制,可以进一步增强数据的安全性。
- 防止重复请求:Redis 可以利用其原子性操作(如 SETNX 命令)防止同一用户短时间内重复请求验证码,进一步提升系统的安全性和稳定性。
5) 扩展性和维护性
- 方便扩展:使用 Redis 后,验证码的存储可以轻松扩展到 Redis 集群中,处理更大量级的请求。Redis 集群能够自动进行数据分片和故障转移,使系统具有更好的扩展性和高可用性。
- 简化维护:Redis 提供了丰富的运维工具和监控指标,可以方便地对验证码的存储和访问情况进行监控和调整,提升系统的可维护性。
详细解释:
- 验证码存储:将验证码存储在 Redis 中,可以利用其快速的读写能力和自动过期功能来管理验证码的生命周期。例如,用户请求验证码时,将其存储在 Redis 中,并设置一个短暂的过期时间(如 5 分钟)。验证时,从 Redis 中取出验证码进行比对。
- 用户登录凭证存储:当用户登录成功后,生成一个唯一的登录凭证(如 JWT 或自定义 token),并将其与用户信息一同存储在 Redis 中。通过设置过期时间,可以自动管理用户的登录状态。客户端每次请求时,附带该凭证来验证身份,从而实现无状态的用户管理。
在我们的项目中很多地方都用到了Redis , Redis在我们的项目中主要有三个作用 :
- 使用Redis做热点数据缓存/接口数据缓存
- 使用Redis存储一些业务数据 , 例如 : 验证码 , 用户信息 , 用户行为数据 , 数据计算结果 , 排行榜数据等
- 使用Redis实现分布式锁 , 解决并发环境下的资源竞争问题
Redis中的数据类型有很多 , 例如 :
- string:最基本的数据类型,二进制安全的字符串,最大512M
- list:按照添加顺序保持顺序的字符串列表
- set:无序的字符串集合,不存在重复的元素
- sorted set:已排序的字符串集合
- hash:key-value对的一种集合
2. 使用拦截器拦截用户请求,将用户信息绑定在 ThreadLocal 上
需求:
- 拦截用户的每个请求,将用户信息绑定在 ThreadLocal 上,以便在请求的生命周期内进行用户行为分析。
详细解释:
- 拦截器的实现:在 Spring MVC 中,可以通过实现 HandlerInterceptor 接口来定义拦截器。拦截器会在请求到达控制器之前执行,将当前请求的用户信息(如从 Redis 中获取的用户信息)放入 ThreadLocal 中。
- 用户行为分析:通过将用户信息存储在 ThreadLocal 中,控制器或服务层代码可以方便地访问用户信息,并进行行为分析,例如记录用户的访问路径、操作记录等。ThreadLocal 确保了用户信息在当前线程中的安全性和独立性。
3. 使用 Redis 实现点赞,粉丝列表,网站行为分析等功能
需求:
- 利用 Redis 实现用户的点赞操作、粉丝列表管理,以及网站的行为分析。
详细解释:
- 点赞功能:利用 Redis 的集合或有序集合(Sorted Set)实现用户点赞功能。例如,每个帖子的点赞数可以存储为一个集合,集合中的元素是用户 ID,这样既能快速统计点赞数,也能判断某用户是否已点赞。
- 粉丝列表:可以通过 Redis 的集合数据结构存储每个用户的粉丝列表和关注列表。用户关注或取消关注时,更新对应的集合。
- 网站行为分析:通过 Redis 的数据结构(如计数器、列表、散列等),可以实时统计和分析用户行为,如页面访问量(PV)、独立访客数(UV)、热帖点击量等。
4. 使用 Kafka 实现系统通知
需求:
- 使用 Kafka 实现用户在收到点赞、关注、评论、加精、置顶等操作后触发的系统通知。
详细解释:
- Kafka 集成:在用户执行点赞、关注、评论等操作时,创建相应的事件(如点赞事件、评论事件等),并将这些事件发布到 Kafka 的指定 Topic 中。
- 消费者处理:系统会有专门的消费者订阅这些 Topic,并处理接收到的消息。处理逻辑可以包括生成通知、发送消息给用户,或者更新相关的统计数据。
- 异步处理:Kafka 的引入使得这些事件处理可以异步进行,不影响用户的实时操作体验。
自媒体用户发布文章成功之后需要进行文章的审核 , 审核通过之后才会发布到APP端供用户查看 , 审核功能因为耗时较久 , 长时间阻塞会影响用户体验 , 而且长时间阻塞会严重影响系统的吞吐量,所以为了实现功能之间的解耦 , 提升用户体验 , 我们可以抽取一个独立的审核服务 , 文章发布成功之后自媒体服务通过MQ通知审核服务进行文章审核 。
为什么要选择使用Kafka作为消息中间件?
- 因为我们后期会使用MQ进行行为数据采集 , 对于消息的吞吐量要求更高
- 因为后期会进行文章的实时推荐 , 会使用到一些实时流计算技术 , Kafka提供这么一个技术 Kafka Stream , 开发成本和运维成本会更低一些
- 为了提高读写硬盘的速度,Kafka就是使用顺序读写。规避了磁盘寻址 , 因此效率非常高。
- Kafka 中消息先被写入页缓存,由操作系统负责刷盘任务 , 这是 Kafka 实现高吞吐的重要因素之一
使用场景:大数据处理、日志采集、事件流处理、实时分析等。
优点:
- 高吞吐量、低延迟。
- 强大的持久化能力。
- 可以处理海量数据,支持横向扩展。
缺点:
- 对于消息传递的实时性要求特别高的场景(如低延迟微服务间通信),可能不如 RabbitMQ 等合适。
- 相对复杂的部署和管理。
5. 定义敏感词前缀树(TrieTree),实现敏感词过滤算法
需求:
- 使用 TrieTree 数据结构定义敏感词库,并实现讨论区的敏感词过滤算法。
详细解释:
- TrieTree 实现:构建一个前缀树,每个节点代表一个字符,路径上所有节点组成一个敏感词。通过 TrieTree 可以高效地匹配和过滤敏感词。
- 过滤算法:在用户提交内容时,利用 TrieTree 进行敏感词匹配。遍历内容字符,通过 TrieTree 检查是否存在敏感词匹配,一旦匹配则进行替换或屏蔽。
6. 分布式 Session 问题
为了保证用户每次请求不用重新输入账号密码,保存用户的登录状态,就会有session和cookie这样的机制,去保存用户登录信息,但是在分布式部署的时候就会存在session共享的一个问题。现在网站基本是多台服务器分布式部署的,如果将用户信息存到session中,而session是存到服务器上,在分布式环境下,由于各个服务器主机之间的信息并不共享,将用户信息存到服务器1上,同一个用户的下一个请求过来的时候,由于nginx的负载均衡策略,去请求了服务器2,就找不到之前的session了。
1) 粘性 Session(Sticky Session)
- 粘性 Session 是一种简单的负载均衡策略,即将同一个用户的请求始终分发到同一台服务器上。通过负载均衡器(如 Nginx)来实现,同一个客户端 IP 地址的请求总是被路由到同一个后端服务器上。
2) Session 同步(Session Replication)
- 当一台服务器创建或更新 Session 时,会将该 Session 数据同步到其他服务器上,确保所有服务器都拥有相同的 Session 数据。
3)Session 服务器(Session Server)
- 将 Session 管理集中化,所有服务器都将 Session 数据存储到专门的 Session 服务器上。
5) Session 数据存储在数据库
- 将 Session 数据存储在数据库中,所有服务器都可以通过查询数据库来获取和修改 Session 数据。
- 常用于需要持久化会话数据的场景。
6) Session 数据存储在 Redis
- 将 Session 数据存储在 Redis 中,所有服务器都可以通过 Redis 访问和操作 Session 数据。
- Redis 是一个高性能的键值对存储系统,支持内存存储和持久化。
粘性 Session(Sticky Session) | - 实现简单 | - 负载均衡效果差,可能导致部分服务器过载 |
Session 同步(Session Replication) | - 保证了 Session 数据的高可用性和一致性 | - 网络和服务器的开销较大,特别是在高并发场景下 |
Session 服务器 | - Session 数据集中管理,便于维护 | - 存在单点故障风险,依赖于 Session 服务器的高可用性 |
Session 数据存储在数据库 | - Session 数据持久化,可靠性高 | - 性能较低,数据库的读写速度可能无法满足高并发场景 |
Session 数据存储在 Redis | - 高性能,Redis 读写速度快,适合高并发场景 | - 依赖外部 Redis 服务,增加了系统复杂度 |
在分布式环境下,最常用且有效的解决方案是将 Session 数据存储在 Redis 中。它兼具高性能、扩展性和分布式支持,是目前很多大型网站的首选方案。相比之下,粘性 Session 和 Session 同步虽然简单,但在扩展性和可靠性方面存在明显不足,而专用 Session 服务器的维护和安全性则需要更多的管理资源。最终的选择应根据具体的业务需求、系统架构和运维能力来确定。
7.用户表 登录
type:用户类型,定义了用户的角色,如:
- 0:普通用户。
- 1:超级管理员。
- 2:版主。
status:用户状态,定义了用户账户的激活状态:
- 0:未激活。
- 1:已激活。
逻辑:
- 注册:
- 用户提交注册信息后,系统生成盐值,使用密码哈希算法对密码加盐处理并存储到数据库中。
- 系统生成激活码,通过邮件发送给用户以完成账户激活。
- 将用户信息(如用户名、邮箱等)存储到数据库的用户表中。
- 登录:
- 用户提交用户名和密码,系统根据用户名查询对应的密码哈希和盐值。
- 使用相同的哈希算法和盐值处理用户提交的密码,并与数据库中的密码哈希进行比对。
- 如果验证通过,生成 Session 或 JWT 返回给客户端,并将用户信息存储到 Redis 以便快速访问。
- 实现登录限制和分布式锁,防止暴力破解和并发问题。
8 Kaptcha生成验证码
V1 :将用户登录凭证ticket存到mysql的login_ticket表中
登陆成功的时候生成登录凭证,生成Loginticket往数据库login_ticket存,并且被设置为cookie,下次用户登录的时候会带上这个ticket,ticket是个随机的UUID字符串,有过期的时间expired和有效的状态status。
用login_ticket存储用户的登录信息,每次请求会随着cookie带到服务端,服务端只要与数据库比对携带的ticket,就可以通过表中的used_id字段查到用户的信息。用户退出时将status更改为0即可。
V2: 使用Redis优化登录模块,使用Redis存储验证码
- 验证码需要频繁的访问与刷新,对性能要求比较高
- 验证码不需要永久保存,通常在很短的时间后就会失效(redis设置失效时间)
- 分布式部署的时候,存在Session共享的问题(之前验证码是存到session里面,使用redis避免session共享问题)
owner : 由于此时用户还未登录,owner为临时生成的凭证,存到cookie中发送给客户端。登录的时候从cookie中取值构造redisKey,再从redis中取值。并与用户输入的验证码进行比对。
最初是直接将验证码字符串存到session当中,每次都是从session中获取验证码字符串的值在进行判断在分布式系统中,多个服务器会处理用户请求。如果生成验证码的请求由服务器 A 处理,而用户登录请求由服务器 B 处理,服务器 B 无法访问服务器 A 中的 Session 数据。这会导致服务器 B 无法获取到正确的验证码,进而无法正确验证用户的输入。
传统方案:登录凭证存储在 MySQL 中
在传统方案中,用户的登录凭证(LoginTicket)存储在 MySQL 数据库中。每次请求时,服务器需要从用户的 Cookie 中提取 ticket,然后从 MySQL 中查询对应的 LoginTicket 记录,以验证用户的身份。由于请求频率高,频繁的数据库访问会增加 MySQL 的负载,降低系统性能。
改进方案:登录凭证存储在 Redis 中
将 LoginTicket 存储在 Redis 中,而不再存储在 MySQL 中,从而减少对 MySQL 的访问频率,提高系统的响应速度。
存储结构:Key: "ticket:" + ticket — 这是 Redis 中的键,其中 ticket 是实际的登录凭证字符串。Value: LoginTicket 对象被序列化为 JSON 字符串存储在 Redis 中。
登录时存储登录凭证:
当用户成功登录后,生成一个 LoginTicket 对象,并将其序列化为 JSON 字符串存储在 Redis 中。
验证时从 Redis 读取登录凭证:
- 在处理每次请求时,从用户的 Cookie 中获取 ticket,然后使用该 ticket 作为 Redis 的键去查询对应的 LoginTicket。
登录凭证的业务层service:给定ticket字符串从redis中检索LoginTicket对象;根据用户浏览器cookie携带的ticket查询用户登录凭证LoginTicket。
9 拦截器 Handle Interceptor
使用拦截器Interceptor来拦截所有的http用户请求,用于检查用户的登录状态,判断请求中的cookie是否存在有效的ticket(用户已登录),如果有的话就从 Redis 中查询用户信息并将用户的信息写入ThreadLocal变量中。在本次请求中持有用户,将每个线程的ThreadLocal都存到一个叫做hostHolder的实例中,根据这个实例就可以在本次请求中全局任意的位置获取用户信息。
LoginTicketInterceptor:通过拦截 HTTP 请求实现了用户身份验证和授权功能,确保用户凭证有效性,并将用户信息安全地传递给请求处理流程。在请求完成后,它会清理所有与该请求相关的数据,以防止数据泄漏和线程污染。这种设计方式确保了系统的安全性和高效性。
ThreadLocal: 的作用是用于存储和管理当前请求线程的用户信息,以保证在多线程环境下每个线程都能独立、安全地访问自己的用户数据,确保数据隔离和安全性。比如说我查看帖子详情页面,去做评论或者回复,就可以直接从ThreadLocal中取到用户的信息,进行编码,而无需重复查询数据库或缓存。
10 AJAX异步发帖+TRIETREE过滤敏感词
实时性和用户体验:使用 AJAX 进行异步发帖,用户可以在不刷新页面的情况下发布帖子,提升了用户体验。服务器在后台异步处理发帖请求,包括敏感词过滤,这种操作对用户是透明的。
高效性:TrieTree 结构非常高效,能够在较短的时间内完成敏感词的过滤操作,适合处理大量用户请求的场景。
安全性:使用 ThreadLocal 存储用户信息,确保每个请求线程独立操作,避免了多线程环境下的安全问题。同时,过滤敏感词可以防止不当内容的传播,符合法律法规和平台规范。
SensitiveWordFilter.java实现类:TrieTree 是一种树形数据结构,常用于字符串处理,例如快速检索、自动补全和敏感词过滤。对于敏感词过滤,TrieTree 的作用是高效地存储和查找敏感词列表,从而快速检查和替换文本中的敏感词。
11 评论+私信功能
- 评论(Comment)表:
Id自增、唯一标识每一条评论,用户id,评论类型(1对帖子2对评论),被评论的实体目标id,被评论的用户id,内容,评论状态(0正常1隐藏);创建时间。
Service服务层:主要负责评论相关的所有业务逻辑处理、数据验证、数据过滤、数据持久化等。
- 添加评论:接收来自控制器层的评论对象,进行数据验证、敏感词过滤,然后调用数据库交互层(CommentMapper)将评论保存到数据库中。如果评论对象是对帖子的评论,还会更新帖子的评论数量。
- 查询评论列表:根据帖子或评论的 ID 查询与之相关的所有评论,并支持分页查询。
- 查询评论数量:统计某个实体(帖子或评论)下的所有评论数量。
- 查询用户的所有回复:根据用户 ID 查询该用户的所有评论或回复,支持分页。
- 根据 ID 查询单条评论:提供根据评论 ID 查询单条评论的功能,用于显示评论详情或其他用途。
Controller服务器层:主要负责接收和处理客户端的 HTTP 请求,将请求转发到相应的服务层来完成业务逻辑,然后将结果返回给客户端。
设置评论信息(id,status,time)-添加评论(addComment)-触发评论事件(创建event,topic,判断目标,调用EventProducer.handleEvent,异步发送事件到kafka消息队列中异步处理)
KAFKA:分布式流处理平台,常用于日志收集、消息传递、实时数据流处理等场景。在这段代码中,Kafka 被用作消息队列,用于异步处理事件通知和数据更新。使用 Redis 记录可能影响帖子分数的帖子ID。每当有新的评论(尤其是评论目标是帖子时),该帖子可能需要重新计算热度或分数
为什么使用 Kafka?
- 异步处理: 使用 Kafka 可以将一些耗时的操作(如通知用户、更新搜索引擎索引等)异步化处理,提高系统的响应速度。
- 解耦: Kafka 作为消息中间件,可以解耦不同服务之间的依赖。例如,评论操作与通知服务之间通过 Kafka 消息传递,无需直接依赖调用。
- 高吞吐量和扩展性: Kafka 设计用于高吞吐量的消息传递,适合处理大量并发事件的场景。
处理用户请求:
- 接收用户通过浏览器发送的 HTTP 请求(如 POST、GET 请求)。
- 解析请求参数(如评论的内容、评论目标的 ID 和类型等),将这些参数封装成 Java 对象(如 Comment 对象)。
调用服务层方法:
- 调用 CommentService 中的方法来执行业务逻辑。
返回响应给客户端:
将 CommentService 的处理结果返回给客户端(浏览器)。
- 帖子(DiscussPost)表:
帖子唯一ID,发布用户ID,标题VARCHAR,内容,类型(0普通1置顶),状态(0正常1精华2拉黑)创建时间,评论数,热度值
- 私信(Meaasge)表:
唯一的会话ID,发送用户ID,接收用户ID,a与b间的对话,状态(0未读1已读),创建时间。
MessageController.java:查看私信列表;私信详情,发送私信(封装),标记已读(在查看私信详情时,获取所有未读消息的 ID),获取未读消息列表,消息分页“PageInfo对象”…
如何确保数据的一致性和完整性?
利用数据库事务管理确保数据的一致性(例如,发送私信操作需要同时更新多个表时,可以将这些操作放在一个事务中)。通过 MessageService 中的方法调用确保业务逻辑的完整性。使用数据验证和校验(如检查用户是否存在、私信内容是否为空等)来确保输入数据的正确性。
如何处理异常情况?例如目标用户不存在或数据库操作失败。
目标用户不存在的情况:在 sendLetter 方法中,检查 target 是否为空,如果为空,则返回一个 JSON 格式的错误响应。数据库操作失败的情况:可以使用全局异常处理器(@ControllerAdvice 和 @ExceptionHandler)来捕获并处理这些异常,给出用户友好的提示信息。
12 SpringMVC执行流程
- 客户端发送请求
用户在浏览器中输入一个 URL 或点击一个链接,向服务器发送一个 HTTP 请求。 DispatcherServlet
接收请求
Spring MVC 的核心是一个前端控制器DispatcherServlet
。所有的请求首先会被DispatcherServlet
接收。DispatcherServlet
是 Spring MVC 的中央调度器,它根据请求的内容决定由哪个控制器来处理。- 处理器映射(Handler Mapping)定位控制器
DispatcherServlet
使用HandlerMapping
(处理器映射器)来确定哪个控制器(Controller
)应该处理当前的请求。HandlerMapping
通过请求 URL、HTTP 方法等信息找到相应的控制器方法。 - 调用控制器方法(Handler Adapter 执行处理器)
一旦找到合适的控制器,DispatcherServlet
使用HandlerAdapter
(处理器适配器)来调用控制器中的具体处理方法。控制器方法包含了业务逻辑,比如查询数据库、处理表单数据等。 - 控制器返回视图名和模型数据
控制器方法执行完毕后,会返回一个ModelAndView
对象。ModelAndView
包含了视图的逻辑名称和模型数据(需要展示的数据)。 - 视图解析器(View Resolver)解析逻辑视图名
DispatcherServlet
通过ViewResolver
(视图解析器)将逻辑视图名解析为具体的视图实现(例如 JSP、Thymeleaf 模板)。ViewResolver
根据视图名和一些配置(如前缀、后缀)来确定具体的视图文件。 - 渲染视图
视图负责将模型数据渲染为 HTML 页面。Spring MVC 支持多种视图技术(如 JSP、Thymeleaf、Freemarker 等)。视图渲染完成后生成一个完整的 HTML 响应。 DispatcherServlet
返回响应
渲染后的视图返回给DispatcherServlet
,然后DispatcherServlet
将最终的 HTML 响应发送给客户端(浏览器)。
13 如何应用AOP
在我们的项目中,我们使用了 Spring AOP 来实现对 Service 层方法的统一日志记录功能。AOP(面向切面编程)是一种编程思想,它能够帮助我们在不改变业务代码的情况下,统一处理一些跨越多个模块的通用逻辑,比如日志记录、异常处理等。在这个项目中,我们定义了一个切面类 ServiceLogAspect,通过 @Aspect 注解将其标记为切面类。我们使用 @Pointcut 注解定义了一个切入点,用于拦截所有位于 com.nowcoder.community.service 包下的所有方法。然后,使用 @Before 注解定义了一个前置通知,确保在每个 Service 方法执行之前记录日志。通过这种方式,我们可以自动记录用户的 IP 地址、访问的时间以及访问的具体方法。这种日志记录的方式不仅减少了重复代码,还让日志逻辑更加集中和易于维护。
14 点赞功能
Redis 的 Set 数据结构特别适合用于存储用户点赞的数据,因为它支持高效的添加、删除、和判断元素是否存在等操作。没有专门的实体类来表示点赞(like),而是通过 Redis 和业务逻辑直接在 Controller 和 Service 层来处理点赞功能,这也是一种常见的设计模式。尤其是在轻量级、性能要求高的系统中,使用 Redis 来管理点赞状态,可以避免频繁的数据库操作,从而提高系统的响应速度。
|
- Redis 数据结构设计
Key: like:entity:entityType:entityId
(点赞相关;实体;类型1POST2COMMENT;实体标识符ID)
Value: set(userId)(表示点赞的用户ID集合)
- 操作示例
点赞: 将用户 ID 添加到 Redis Set 中。
取消点赞: 从 Redis Set 中移除用户 ID。(判断userId在不在set集合中,就可以判断用户有否点过赞,如果已经点过赞了,就将用户从集合中删除)
查询点赞数量: 获取 Set 的大小。
查询点赞状态: 检查 Set 中是否包含用户 ID。
- 根据业务需求、性能要求和数据特性,可以考虑以下几点来选择方案:
- 高并发场景: 如果应用程序需要处理大量的并发点赞请求,使用 Redis 是更好的选择。Redis 在内存中操作,性能高、响应快,适合频繁的读写操作。
- 数据持久性要求高: 如果对数据持久性有严格要求(如点赞数据需要长期保存,并能在系统重启或崩溃后恢复),MySQL 是更合适的选择。
- 数据量大且需要持久存储: 如果系统需要存储大量历史数据,并且这些数据不需要频繁访问,MySQL 是更好的选择,因为它存储在磁盘上,支持大数据量存储。
- 需要快速开发和原型设计: 如果你想快速实现一个点赞功能原型,可以使用 Redis,因为 Redis API 简单直观,易于开发和调试。
- 扩展性和一致性: 如果应用场景需要支持更复杂的功能扩展(如点赞、点踩、用户互动等)且要求强一致性,MySQL 方案更适合。
- 在查询某人对某实体的点赞状态时,用可以用boolean作为返回值,但项目中使用int(支持业务扩展,可以支持是否点踩)
步骤(service):处理具体的业务逻辑和事件触发
功能: 处理用户的点赞和取消点赞操作,同时更新相关实体的点赞数和实体作者的总获赞数。
事务性操作: 使用 RedisTemplate 的编程式事务(SessionCallback)来确保点赞或取消点赞的原子性。
点赞/取消点赞逻辑:
- 首先,获取两个 Redis Key:
entityLikeKey: 存储某个实体(帖子或评论)的点赞用户的集合。
userLikeKey: 存储某个用户(实体的作者)总的被点赞数量。
- 判断用户是否已点赞: 使用isMember(entityLikeKey, userId) 判断用户是否已对某个实体点赞。由于点赞和更新获赞数是两个相关的操作,需要保证同时成功或同时失败,因此使用编程式事务来实现这一功能。确保了操作的原子性。
- 如果用户已经点赞,再次操作即为取消点赞:
使用 opsForSet().remove() 从实体的 Set 中移除用户 ID。
使用 opsForValue().decrement() 减少实体作者的总获赞数。
如果用户未点赞:
使用 opsForSet().add() 将用户 ID 添加到实体的 Set 中。
使用 opsForValue().increment() 增加实体作者的总获赞数。
- 查询实体点赞数量
使用 Redis 的 Set 数据结构的 size() 方法来获取集合中的元素数量
- 查询用户对实体的点赞状态
使用 Redis 的 Set 的 isMember() 方法,判断用户 ID 是否在该实体的点赞集合中。如果在,返回 1(已点赞);否则,返回 0(未点赞)。
- 查询用户获得的总点赞数
使用 Redis 的 String 数据结构的 get() 方法来获取用户的总获赞数。
步骤(controller): 负责接受请求和返回响应
1类和依赖注入
2点赞操作: 查询点赞数量和状态: 封装返回结果:(map)
3触发点赞事件: 如果点赞状态为 1(表示用户点赞成功),则创建一个Event 对象,并通过 eventProducer 触发事件。事件对象的内容: 包含了事件的主题(点赞)、用户 ID、实体类型、实体 ID、实体用户 ID 和额外数据(帖子 ID)。
4记录影响帖子分数: 如果点赞的实体是帖子类型,则将帖子 ID 添加到一个用于计算帖子分数的 Redis Set 中。
高效的事件处理: 使用事件机制(EventProducer)来异步处理点赞之后的业务逻辑,如通知被点赞的用户、更新动态等,这种设计有助于提高系统的响应速度和用户体验。
灵活的设计: 通过 Redis 记录帖子分数的变化,使得点赞功能不仅支持基本的点赞操作,还能够扩展支持更多的业务场景,如热门帖子的动态排名。
15 关注功能
使用 Redis 的 ZSet 数据结构来存储和管理用户之间的关注关系。通过这个服务类,系统可以高效地实现关注、取消关注、查询关注列表和粉丝列表等功能。
步骤(service):处理具体的业务逻辑和事件触发
1 依赖注入,
2 用户关注功能 (toFollow
方法)
使用 ZSet:
- followeeKey 存储当前用户关注的对象(用户、帖子等),ZSet 的分数(score)为关注时间戳。
- followerKey 存储当前对象的粉丝列表,ZSet 的分数(score)同样为关注时间戳。
事务操作:
- 通过 redisOperations.multi() 开启事务,确保关注操作的原子性(要么全部成功,要么全部失败)。
- 最后通过 redisOperations.exec() 提交事务。
3 用户取消关注功能 (unFollow
方法):一样,用remove移除zset中的元素
4 查询用户关注的实体数量 (findFolloweeCount
方法)
使用 ZSet
的 zCard
方法获取集合的元素数量。
5 查询实体的粉丝数量 (findFollowerCount
方法)
使用 ZSet
的 zCard
方法获取集合的元素数量。
6 查询用户是否已关注该实体 (hasFollowed
方法)
通过 ZSet
的 score
方法,检查 followeeKey
中是否有该实体 ID
7 查询用户关注列表 (findFollowees
方法)
使用 ZSet
的 reverseRange
方法进行分页查询,获取目标 ID 列表。
8 查询用户的粉丝列表 (findFollowers
方法)
与查询关注列表类似,但操作的 ZSet 是 followerKey。
步骤(controller): 负责接受请求和返回响应
1 类和依赖注入
2 关注方法(toFollow 方法)
处理用户的关注请求。
- 获取当前登录用户信息。
- 调用 FollowService 的 toFollow 方法执行关注操作。
- 触发一个关注事件(Event),通知其他系统组件(如消息推送等)。
- 返回一个 JSON 格式的响应,表示关注成功。
3 取消关注方法(unFollow 方法):同上
4. 获取关注列表方法(getfollowees 方法)
使用 PageInfo 设置分页信息。
调用 FollowService 的 findFollowees 方法查询关注列表。
5. 获取粉丝列表方法(getfollowers 方法)
16 Elastic Search 实现网站的搜索功能
搜索功能:Elasticsearch 被用作全文搜索引擎,用于存储和检索帖子数据。当用户发布新帖子时,系统需要将该帖子保存到 Elasticsearch 中。
CRUD服务:
将帖子保存到Elasticsearch服务器SAVE
从 Elasticsearch 服务器中删除帖子DELETE
简要总结searchDiscussPostByCondition功能:
- 全文搜索: 根据用户输入的 keyword(关键词),在帖子标题和内容
- 排序: 按照帖子类型、得分、创建时间等字段进行排序,使得搜索结果按照指定的优先级显示。(type-score-createTime顺序),withSort 方法 是 NativeSearchQueryBuilder 类的一个方法,用于向查询中添加排序规则。
- 分页: 根据用户请求的页码(current)和每页显示的数量(size),返回对应页的搜索结果,支持分页浏览。
- 高亮显示: 对搜索结果中匹配的关键词进行高亮显示(用 <em> 标签包裹),以便在前端界面上突出显示搜索关键词。
主要步骤:
- 构建搜索查询(包含关键词搜索、排序、分页、高亮设置)。
- 执行查询并获取结果。
- 处理查询结果,将搜索到的帖子数据(包括高亮显示)封装成分页对象返回。
发布事件(将发帖或者更改帖子的事件存到kafka中,消费事件并将帖子存到es服务器中):
发布帖子时,将帖子异步的提交到Elasticsearch服务器
增加评论的时候,将帖子异步的提交到Elasticsearch服务
在kafka消费组件中增加一个方法,消费帖子发布事件
17 UV,DAU数据统计
使用了 Redis 提供的两种数据结构:HyperLogLog 和 Bitmap,用于实现网站数据的统计功能。这些数据结构能够有效地处理大量数据,具有良好的性能和较小的存储空间。
HyperLogLog(超级日志) 是一种概率性数据结构,用于估算集合的基数(即不同元素的个数)。它特别适用于需要对大量数据进行去重统计的场景。适合统计独立访客(UV,Unique Visitor),即不同 IP 的访问量。
- 独立访客,需要通过用户IP排重统计数据(可以统计游客,在拦截器中实现)
- 每次访问都要进行统计
- HyperLogLog,性能好,且存储空间小,是一种近似算法
|
Bitmap(位图)不是一种独立的数据结构,而是将数据位存储在字符串中的一种技术。在 Redis 中,Bitmap 实际上是对二进制位的操作,可以理解为对大字符串的位操作。
适合存储大量连续的布尔值(True/False)数据,如签到、用户登录状态等。
- 日活跃用户,需要通过用户ID排重统计数据(排除游客,需要精确)
- 访问一次,则认为其活跃
- Bitmap,性能好,且可以统计精确的结果,精确统计每个位置上的值
|
使用场景:
- UV 统计: 适用于所有用户的请求(包括未登录用户)。每当有新的请求进来时,无论用户是否登录,都需要记录 IP 地址用于 UV 统计。
- DAU 统计: 适用于已登录用户的请求。只有当用户登录后,才会统计其活跃状态,因此需要检查用户信息是否存在。
计算 UV 和 DAU:
calculateUv(Date start, Date end) 方法:
功能: 统计指定日期范围内的独立访客数量(UV)。
实现:
验证输入的 start 和 end 日期参数是否为空。
使用 Calendar 类遍历日期范围内的每一天,生成对应的 Redis 键(每日的 UV 统计键)。
使用 HyperLogLog 的 union 操作合并多个键的 UV 数据,存储到新的 Redis 键中(rangeUvKey)。
通过 redisTemplate.opsForHyperLogLog().size(rangeUvKey) 获取合并后 UV 统计结果并返回。
calculateDau(Date start, Date end) 方法:
功能: 统计指定日期范围内的日活跃用户数量(DAU)。
实现:
验证输入的 start 和 end 日期参数是否为空。
使用 Calendar 类遍历日期范围内的每一天,生成对应的 Redis 键(每日的 DAU 统计键)。
使用 Redis 的 bitOp 操作进行多个 Bitmap 键的按位 OR 操作,将结果存储到新的 Redis 键(rangeDauKey)。
通过 redisConnection.bitCount(rangeDauKey.getBytes()) 计算合并后的 DAU 统计结果并返回long。
18 Spring Quartz实现定时热帖排行
热帖排行功能需要定期从数据库中查询最热门的帖子并进行更新展示。使用定时任务的形式来实现,JDK自带的ScheduledExecutorService以及Spring自带的ThreadPoolTaskScheduler都可以实现定时任务的功能,但是,这些工具在分布式环境中有一个缺陷:它们是基于内存的,Scheduler是基于内存,服务器1和服务器2的上的Scheduler代码是相同的,会定时的做同样的事情。本项目采用的是Quartz,因为quartz实现定时任务的参数是存到数据库中的,所以不会出现重复代码的问题,多个服务器实例可以共享相同的任务数据。
帖子排行的计算公式:
**log(精华分 + 评论数 10 + 点赞数 2 + 收藏数 * 2)+ (发布时间 - 纪元)
将分数发生变换的帖子丢到Redis缓存中去,每10分钟计算一下帖子的分数。
需求:综合分析帖子的获赞数,评论数,是否加精等指标,计算帖子分数,实现热帖排行,并且定期更新热帖榜
19 使用数据库(MySQL)和Redis的场景
1. 使用数据库(MySQL)的场景
数据库通常用于存储需要持久化的数据,这些数据需要长期保存,并具有较强的事务性和一致性要求。在您的项目中,以下场景会使用到MySQL数据库:
发帖(Discuss Post): 帖子信息(如标题、内容、作者、创建时间等)需要被持久化存储,以便能够长期保存和管理。所有的帖子内容都会存储在数据库的帖子表中。
评论(Comment): 评论是用户对帖子或其他评论的反馈,这些信息需要被持久保存,以便后续查看和管理。评论数据通常会存储在单独的评论表中。
用户关注(Follow): 用户之间的关注关系需要被存储在数据库中,以便能够持久化保存用户的社交关系网络。这些关系数据通常会存储在用户关注表中。
用户信息和认证(User Information and Authentication): 用户的基本信息(如用户名、密码、邮箱等)和用户认证相关的数据(如权限、状态等)都需要持久化保存,确保数据的安全性和一致性。
帖子点赞和状态(Like and Post Status): 帖子的状态信息(如是否置顶、加精、删除等)需要持久化,以便系统可以长期保存和管理这些状态信息。
2. 使用Redis的场景
Redis通常用于存储需要快速访问的临时数据,或者用于实现某些特定的高效数据结构。Redis适合存储那些对实时性要求高、访问频率高,但对持久化要求不强的数据。在您的项目中,以下场景会使用到Redis:
点赞(Like): 点赞操作通常会频繁发生,并且需要快速响应。使用Redis存储点赞数可以大幅提升访问速度,并减少对数据库的读写压力。Redis中的数据可以通过定时任务或者其他机制同步回数据库,保证数据的最终一致性。
消息通知(Message Notification): Redis可以用来暂存用户的即时消息通知,如点赞通知、评论通知等。因为这些数据需要高频访问和快速响应,而Redis的性能非常适合这种场景。
热帖排行(Hot Post Ranking): 热帖排行是根据帖子的互动数据(如评论、点赞等)进行实时计算的。使用Redis可以暂存这些需要频繁计算的数据,并利用Redis的排序集合(Sorted Set)来高效计算和维护热帖的排名。
高级数据统计(如DAU,Daily Active User): 日活跃用户统计等高频率的数据分析通常会使用Redis。Redis可以利用位图(Bitmap)等数据结构来高效地存储和统计用户的活跃状态,提供快速的数据分析结果。
会话管理和缓存(Session Management and Caching): Redis常用于管理用户会话和缓存数据,比如用户登录信息、页面缓存等。这样可以提升系统的响应速度,并减少数据库的负载。
总结
MySQL:用于需要持久化存储的数据,如用户信息、帖子内容、评论、关注关系等。这些数据需要长期保存,并支持复杂的查询和事务处理。
Redis:用于需要快速访问和高频操作的数据,如点赞计数、消息通知、热帖排行等。这些数据通常不需要持久化,或者可以通过后台任务定期同步到数据库。
附录:
在 Java 项目中,按照功能模块划分代码通常会将相关类组织到不同的包(package)中。
1. job 包
- 用途:存放与定时任务相关的配置和实现类。通过这些类,项目可以执行各种周期性任务,比如发送邮件、生成验证码、缓存清理等。
- 内容解释:
- KaptchaConfig.java:配置验证码相关的设置。
- MailClientConfig.java:配置邮件客户端的相关设置,用于发送邮件任务。
- QuartzConfig.java:配置 Quartz 任务调度框架,用于定时任务管理。
- RedisConfig.java:配置 Redis,可能用于缓存管理或其他定时任务相关的功能。
- SecurityConfig.java:配置安全相关的设置,比如用户认证和授权。
- Swagger2Config.java:配置 Swagger,用于生成 API 文档。
- ThreadPoolConfig.java:配置线程池,优化任务执行的并发处理能力。
- WebMvcConfig.java:配置 Spring MVC 的一些全局设置,如跨域、拦截器等。
- WkConfig.java:具体作用不明,可能是项目中特定功能的配置。
2. constant 包
- 用途:存放常量类,这些类定义了项目中使用的各种常量值,例如状态码、消息类型、配置参数等。
- 内容解释:
- ActivationStatus.java:定义用户激活状态的常量。
- CommentEntityConstant.java:定义与评论实体相关的常量。
- LoginConstant.java:定义登录相关的常量,例如错误信息、状态码等。
- MessageConstant.java:定义消息相关的常量,可能用于系统通知或消息服务。
- ResultEnum.java:定义枚举类型的结果常量,表示操作的不同结果状态。
- SysEmailConstant.java:定义系统邮件相关的常量,如邮件模板、标题等。
- SystemConstant.java:定义系统级别的常量。
3. controller 包
- 用途:存放控制器类,这些类用于处理用户请求,将请求分发到相应的服务层进行处理,并返回响应结果。
- 内容解释:
- CommentController.java:处理与评论相关的请求。
- DataController.java:处理与数据统计或分析相关的请求。
- DiscussPostController.java:处理与讨论帖(帖子)相关的请求。
- FollowController.java:处理用户关注相关的请求。
- IndexController.java:处理主页相关的请求,通常用于展示网站首页。
- LikeController.java:处理点赞相关的请求。
- MessageController.java:处理消息相关的请求,可能涉及系统通知或私信功能。
- SearchController.java:处理搜索功能相关的请求。
- ShareController.java:处理内容分享相关的请求。
- UserController.java:处理用户信息和用户操作相关的请求,如注册、登录、用户资料管理等。
1. dao 包
- 作用:DAO(Data Access Object)包用于存放与数据访问相关的类,主要负责与数据库的交互。
- CommentMapper.java:
- 负责定义对评论表的数据库操作方法,例如添加、删除、查询评论等操作。
- DiscussPostMapper.java:
- 负责定义对帖子表的数据库操作方法,例如发布帖子、查询帖子列表、删除帖子等。
- LoginTicketMapper.java:
- 负责定义对登录票据(Session)的数据库操作方法,用于用户登录状态的管理。
- MessageMapper.java:
- 负责定义对消息表的数据库操作方法,例如发送消息、查询用户的消息列表等。
- UserMapper.java:
- 负责定义对用户表的数据库操作方法,例如用户的注册、查询、更新等操作。
2. entity 包
- 作用:entity 包用于存放实体类,这些类通常对应数据库中的表结构,表示应用中的核心数据对象。
- Comment.java:
- 代表评论表的实体类,定义了评论的属性,如评论内容、评论时间、评论人等。
- DiscussPost.java:
- 代表帖子表的实体类,定义了帖子的信息,如标题、内容、作者、创建时间等。
- Event.java:
- 代表事件的实体类,可能用于定义系统中的特定事件(如用户行为、系统通知等)。
- LoginTicket.java:
- 代表登录票据的实体类,定义了用户的登录状态,如用户 ID、登录凭证、有效期等。
- Message.java:
- 代表消息表的实体类,定义了消息的内容、发送者、接收者、发送时间等。
- ReplyInfo.java:
- 代表回复信息的实体类,可能用于定义某个实体(如评论或帖子)的回复结构。
- User.java:
- 代表用户表的实体类,定义了用户的基本信息,如用户名、密码、邮箱、注册时间等。
5. interceptor 包
- 作用:interceptor 包用于存放拦截器类。拦截器通常用于拦截和处理进入控制器之前的请求,可以用于权限校验、日志记录等。
- 内容解释:
- DataInterceptor.java、LoginInterceptor.java、LoginTicketInterceptor.java、MessageInterceptor.java:
- 这些类定义了不同类型的拦截器,用于在请求处理的不同阶段进行拦截和处理。
- 例如,LoginInterceptor.java 用于拦截未登录用户的请求,强制用户进行登录操作。
- DataInterceptor.java、LoginInterceptor.java、LoginTicketInterceptor.java、MessageInterceptor.java:
6. service 包
- 作用:service 包用于存放服务类,服务类封装了应用的业务逻辑,并负责调用 DAO 层来处理数据操作。
- CommentService.java:
- 封装了与评论相关的业务逻辑,如添加评论、删除评论、查询评论列表等。
- DataService.java:
- 封装了与数据统计或分析相关的业务逻辑,可能用于数据报表生成或分析服务。
- DiscussPostService.java:
- 封装了与帖子相关的业务逻辑,如发布帖子、查询热门帖子、删除帖子等。
- FollowService.java:
- 封装了与用户关注功能相关的业务逻辑,如关注用户、取消关注、查询关注列表等。
- LikeService.java:
- 封装了与点赞功能相关的业务逻辑,如点赞、取消点赞、查询点赞状态和数量等。
- LoginTicketService.java:
- 封装了与登录票据相关的业务逻辑,如创建登录票据、验证票据、注销票据等。
- MessageService.java:
- 封装了与消息相关的业务逻辑,如发送消息、查询消息、标记已读等。
- UserService.java:
- 封装了与用户相关的业务逻辑,如用户注册、登录、修改资料、查询用户信息等。