1. 导入项目
1.1 架构方案设计
1.1.1 业务模块拆解
角色拆解
社区系统的用户角色权限:管理员>用户>读者。
读者:阅读文章;
用户:编辑发布修改文章。
管理员:整个系统运营管理,如标签、分类管理、文章审核等。
基于以上分析,用户可以分为:
普通用户:社区的注册用户,围绕文章主体展开其覆盖的业务功能点;
管理员:作为官方角色,负责整个社区的生态运营。
业务拆解
整个社区系统按照业务边界进行初始化分:用户、文章、评论、专栏、消息通知;
1.1.2 模块交互设计
整体交互设计
登录交互设计
消息通知方案
1.1.3 整体架构方案
1.2 项目本地编译运行
1.2.1 空文件右键GitBash加载源码
1.2.2 修改配置文件 yml
paicoding-web/src/main/resources-env/dev目录下的配置文件application-dal.yml
修改MySQL和Redis的用户名和密码 redis不能是localhost只能是192.168.203.128
运行后出现命令行过长的提示
1.2.3 运行
knife4j
1.2.4 别人的代码clone后push到自己Gitee
1.3 项目部署
Gitee git clone
Github git clone
2. 基础篇
2.1 项目的MVC分层架构
2.2 Redis实现用户活跃排行榜
用户活跃度的排行榜主要基于Redis的ZSET数据结构来实现。
幂等 = 干过一次就不要再干第二次。就是“同样的操作只会生效一次”,比如今天登录加过 5 分了,就不能再加同样的 5 分。
- 对于上面的流程,使用redis的zrevrange方法,倒叙展示前30名更方便。
- 昨天收藏今天取消收藏,业务失效,是否算存在bug?
不算,如果用户上线取消了收藏发现活跃度降低,不符合实际要求,因为用户活跃度排行榜的存在就是为了增加用户黏性。
2.3 Redis实现作者白名单
作者在白名单里,文章直接上线无需审核;不在白名单里,文章进入审核流程,审核通过后再上线
目前本项目白名单成员是通过管理员手动添加,并非自动添加。自动添加的思路:
2.4 Redis实现计数的业务场景
2.5 项目中缓存的使用
2.5.1 Redis缓存
2.5.2 Caffeine+SpringCache本地缓存
2.5.3 缓存更新策略
Spring Cache 注解:配合 Redis/Caffeine 做方法级缓存
2.6 项目中事务的使用
我们的技术派项目当然也用到了事务,最能体现这一点就是发布文章,它包含:
●文章表写入
●文章详情表写入
●文章标签
●阅读记录
声明式事务:以 @Transactional注解修饰方法的方式
- 修饰位置
在方法上添加注解
在类上添加注解:表示这个类的所有公共方法,都支持事务- roolbackfor: 不指定具体的异常时,默认只有运行时异常才会触发事务回滚
- 声明式:基于AOP方式实现,代码更简洁、使用更方便,理解更容易
编程式事务:声明式事务有一个最明显的约束:最小粒度为方法级别,当我们希望在一个方法内的部分代码块进行事务约束时,这个时候就可以用编程式事务了
- @Autowired private TransactionTemplate transactionTemplate;
- 编程式:非常灵活,完全由用户自己控制,可以最小粒度的控制事务范围
事务不生效的场景:
- 数据库引擎
MySQL 的 MyISAM 引擎就不支持事务,Innodb 支持事务- 类内部访问(非直接调用不生效)
非直接访问带注解标记的方法 B,而是通过普通方法 A,然后由 A 访问 B私有方法
在私有方法上添加 @Transaction 注解也不会生效,私有方法外部不能访问,只能内部访问,所以也不生效。异常不匹配
注解默认只处理运行时异常。如果没有抛出运行时异常,或不是 rollback 指定抛出的异常,就不生效。没有被 Spring 管理的类
类不是由 Spring 管理,所以不会生成对应的代理类,事务当然也不会生效。
3. 进阶篇
3.1 前台微信公众号自动登录
整个登录是基于个人微信公众号来实现的。作为一个文章分享社区,登录是基本的功能点;很多的功能都要求登录之后才能继续,比如发文、点赞、评论等。
首先,后端需要和前端构建一个半长连接,当用户向公众号发送验证码之后,微信公众平台会将用户发送的信息转发给服务端,通过验证码来识别请求登录的用户身份,找到对应的半长连接,实现用户的自动登录跳转。
我们项目中实现了一套基于微信公众号的扫码登录系统,整个流程主要包含以下几个步骤:
前端初始化 :用户访问登录页面时,前端会与后端建立一个半长连接,这是一种单向的长连接机制,允许服务器向客户端推送消息。
验证码生成 :后端收到连接请求后,会为用户生成一个唯一的验证码,并将这个验证码与 用户的设备ID和SSE 连接一起存储在两个缓存中:
1️⃣ verifyCodeCache :存储验证码到SSE连接的映射。后续在登录时,就可以通过验证码找到对应的 SseEmitter,从而实现登录。
2️⃣deviceCodeCache :存储设备ID到验证码的映射。验证码与设备ID绑定,防止盗用验证码展示 :后端通过SSE连接将验证码推送给前端,前端展示给用户。
用户扫码操作 :用户通过微信扫描页面上的公众号二维码,关注公众号并发送验证码
微信服务器回调 :当用户在微信中发送验证码时,微信服务器会向我们配置的回调URL发送一个POST请求,包含用户发送的消息内容。
验证与登录 :后端接收到回调请求后,会解析XML格式的消息内容,提取验证码,然后进行验证。如果验证码有效(即在缓存中找到了对应的SSE连接),系统会:
1️⃣会根据用户ID生成一个会话标识(sessionid),用于标识用户的登录状态;
2️⃣通过之前建立的SSE连接将会话信息推送给前端;
3️⃣前端接收到登录成功的消息后,保存会话信息并刷新页面,完成登录,关闭SSE连接,并从缓存中移除验证码,这样可以确保验证码只能使用一次,提高安全性。安全机制 :整个流程中,我们实现了多重安全保障:
验证码有5分钟的有效期
SSE连接有15分钟的超时时间
验证码与设备ID绑定,防止盗用
验证码一次性使用,登录成功后立即失效技术上,我们主要使用了Spring的 SseEmitter 实现服务器推送,使用Guava Cache管理验证码和连接的映射关系,通过微信公众号的消息回调机制实现用户验证。
优点:对于用户而言登录方式简单,无需记忆密码、用户名,有微信即可
缺点:企业公众号可以实现扫码之后直接自动登录,无需输入验证码。个人公众号不支持自定义二维码参数,因此还需要输入验证码这一步骤,操作麻烦了一点
微信公众平台配置
因为个人公众号能使用的微信公众平台功能较少,主要就是接收用户的发送信息,所以需要的配置也不多,直接登录后台,开启服务器相关配置。
微信公众平台接入验证服务器
这段代码的作用是验证你的服务器。当你在微信公众平台配置服务器URL时,微信会发送一个GET请求到这个URL,请求中包含一个 echostr 参数。你的服务器需要原样返回这个参数值,以证明这个URL确实是你控制的。
微信消息回调处理
除此之外,需要接收微信公众平台的回调,注意微信公众号采用的是 xml 进行通讯。
半长连接的建立
- SSE(Server-Sent Events)是服务器单向推送数据给浏览器的一种机制,也是一种“长连接”方式,但比 WebSocket 更轻量,适合做实时推送但不要求客户端反向通信的场景
- 使用场景:实时通知、消息提醒、系统日志推送、秒杀倒计时、天气推送等
"半长连接映射关系是指在我们的微信公众号登录系统中,使用 SSE 技术建立的一种单向长连接通信机制。与WebSocket这种双工长连接不同,SSE是一种单工的长连接,只允许服务器向客户端推送消息,客户端不能通过这个连接向服务器发送消息。
具体来说,我们的系统维护了两个关键的映射缓存:
1.验证码到SSE连接的映射( verifyCodeCache )
2.设备ID到验证码的映射( deviceCodeCache )
当用户通过微信公众号输入验证码后,系统会通过验证码找到对应的SSE连接,然后通过这个连接向前端推送登录成功的消息和会话信息,从而实现自动登录。
后台用户名+密码登录
当前项目,针对前台和管理员的后台登录,设置了两套不同的玩法;前者是基于微信公众号,后者则是传统的用户名+密码;虽然登录方式不同,但底层的原理一致;
3.2 消息队列RabbitMQ
当我们对文章进行评论点赞、收藏、评论或者管理员用户发送系统消息的时候,那么对应的文章用户就可以实时的来接收到其消息。
消息队列模式
点对点模式(Point to Point):一个消息只能被一个消费者处理。
发布/订阅模式(Pub/Sub):一个消息可以被多个消费者同时处理。
3.2.1 配置与初始化
- 配置文件 : application-rabbitmq.yml 包含RabbitMQ的连接信息
- 自动配置类 : RabbitMqAutoConfig 负责初始化RabbitMQ连接池并启动消息消费进程。
条件加载:这个类只有在配置文件中开启rabbitmq.switchFlag的情况下才会加载。
初始化连接池:根据配置文件中的信息初始化 RabbitMQ 连接池。
异步启动消息消费:启动了一个异步线程去消费消息。
每次使用 RabbitMQ 时都去 New 一个连接,导致并发起不来,所以这次我们就给 RabbitMQ 加一个连接池。
3.2.2 连接管理
- 连接池 : RabbitmqConnectionPool 实现了连接池管理,用于复用连接对象,避免频繁创建和销毁连接。
- 连接类 : RabbitmqConnection 封装了RabbitMQ的连接创建和关闭操作
3.2.3 服务接口与实现
- 服务接口 : RabbitmqService 定义了消息发布和消费的方法
- 服务实现 : RabbitmqServiceImpl 实现了消息的发布和消费
- RabbitMQ 发送消息:从连接池拿到连接 -> 创建通道 -> 声明交换机 -> 发送消息 -> 将连接归还连接池。
- RabbitMQ 消费消息:从连接池拿到连接 -> 创建通道 -> 确定消息队列 -> 绑定队列到交换机 -> 接受并消费消息 -> 将连接归还连接池。
4. 使用场景
- 点赞操作 :在 UserFootServiceImpl 中,当用户点赞时,系统会通过RabbitMQ发送消息
- 消息处理 :在 RabbitmqServiceImpl 的消费者中,接收到消息后保存到数据库
3.3 Mysql/Redis缓存一致性
常见的更新缓存策略是
- 先删缓存,再更新数据库,这时候另一个线程可能刚好查询了数据库的旧数据并写入缓存,导致缓存又变成了旧数据,数据回滚。而且数据库操作更费时,很可能有其他线程进来。
- 先更新缓存,在更新数据库,万一 DB 挂了,你把数据写到缓存,DB 无数据,这个是灾难性的。
- 先更新数据库,在更新缓存,会让无效写操作变多,让缓存只在需要的时候去更新。
- 第一种方式:“先更新数据库再删除缓存”,更适合低并发系统。
1️⃣A 删除缓存是为了让后续请求走数据库并刷新缓存。
2️⃣但由于 B 在 A 删除前把旧数据提前写了进去,A 删除的就是那个旧值,等于白删了
3️⃣如果是低并发,这个概率非常小,基本不会遇到上面的问题,因为更新数据库的时间更久,删除缓存的时间短,所以可以使用“先更新数据库,再删缓存”的写法。- 第二种方式:“延迟双删”,即“先删除缓存,再更新数据库,延迟一段时间后再次删除缓存”,更安全更适合高并发系统
- 先写 MySQL,通过 Binlog,异步更新 Redis
对于异地容灾、数据汇总等,建议会用这种方式,比如 binlog + kafka,数据的一致性也可以达到秒级;纯粹的高并发场景,不建议用这种方案,比如抢购、秒杀等。
- 实时一致性方案:采用“先写 MySQL,再删除 Redis”的策略,这种情况虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。
- 最终一致性方案:采用“先写 MySQL,通过 Binlog,异步更新 Redis”,可以通过 Binlog,结合消息队列异步更新 Redis,是最终一致性的最优解。
因为项目对实时性要求高,所以采用延迟双删,先写 MySQL,再删除 Redis 的方式。
3.4 Mysql/Redis缓存一致性之canal
- 上篇文章中实战为了保证数据的实时性,采用了“先写数据库再删除缓存”的方式。
- 本文不追求数据实时性只追求最终一致性来实现;利用了“先写数据库,通过Binlog来异步更新缓存”方案。
3.5 Canal实现MySQL和ES同步
MySQL如何利用Canal中间件将数据同步至ES
- 用 Canal 订阅 MySQL binlog 做增量实时投递到 ES,辅以定时全量校验;
- 或用应用内事件/消息队列触发写 ES,再配合定时任务兜底。这样兼顾实时性与一致性
3.6 ES实现查询
3.6.1 整体设计思路
- 我们项目采用了双重搜索策略的设计。
- 既支持Elasticsearch的高级全文检索,也支持MySQL的基础搜索作为兜底方案。
- 这样设计的好处是,当ES服务正常时用户享受强大的搜索功能,当ES不可用时系统自动降级到MySQL搜索,保证搜索功能始终可用。
3.6.2 配置和初始化
首先在配置文件中,我们通过elasticsearch.open这个开关来控制是否启用ES功能。配置包含了连接信息、认证、超时设置、连接池等参数。
我们使用了Spring的条件注解@ConditionalOnProperty,只有当elasticsearch.open=true时才会创建ES客户端Bean。这样在开发环境没有ES时可以直接关闭,非常灵活。
3.6.3 核心查询实现
面试官您好,关于ES核心查询实现,我来详细介绍一下:
查询的核心逻辑在
ArticleReadServiceImpl中。当用户搜索时,系统会根据配置选择不同的搜索策略:
- ES搜索流程(ES启用):
- 使用
MultiMatchQuery在文章的title和short_title字段中进行搜索。这种查询方式的优势是支持多字段同时匹配,并且ES会根据匹配程度进行相关性评分排序。- 接着通过
RestHighLevelClient发送请求到ES服务器,我们配置了连接池来提高连接复用率。- ES返回匹配的文章ID列表后,系统再用这些ID从MySQL查询完整的文章信息。
- 为了提升用户体验,搜索提示功能限制返回10条结果,并且使用AJAX异步调用,不会阻塞用户的其他操作。
- 降级机制:
- 如果ES未启用或查询失败,系统会自动降级到MySQL的LIKE查询,确保搜索功能始终可用。
3.6.4 数据存储策略
我们采用了分离存储的设计:
- ES主要存储用于搜索的关键字段如标题和短标题,
- 而MySQL存储文章的完整信息包括内容、作者、统计数据等。
- 查询时先从ES获取匹配的ID,再从MySQL获取完整数据。这样既发挥了ES的搜索优势,又保证了数据的完整性。
项目配置
- 目前我们使用的是单节点ES部署,配置了详细的连接参数包括超时时间、连接池大小等。
- 通过配置开关可以灵活控制ES功能的启用,这样在开发环境或ES维护时可以直接使用MySQL搜索。
这种设计既保证了搜索的高性能,又确保了系统的高可用性。