如何实现博客点赞多请求的依赖执行与错误回滚?
博客点赞功能的事务性实现与优化方案
问题需求
现有博客点赞功能包含三个操作阶段:
- 将目标文章ID添加到点赞用户的
likedPosts列表 - 将点赞用户ID添加到目标文章的
likedPeople列表 - 向文章作者发送点赞通知
需要满足以下约束:
- 若阶段2执行失败,阶段1和3必须完全不执行(或回滚已执行的操作)
- 若检测到用户已点赞过该文章,立即终止所有操作并回滚已做修改
- 保证三个操作的原子性:要么全部成功,要么全部失败
现有代码
module.exports.like = (req, res) => { try{ const userReq = {body: {set: {likedPosts: req.body.post_id}, unset: {}}, params: {userId: req.body.user_id}}; const postReq = {body: {set: {likedPeople: req.body.user_id}, unset: {}}, params: {postId: req.body.post_id}}; const notifReq = {body: {to: req.body.poster_id, fromPost: req.body.post_id, content: "*title* Baslikli postunuz *magnitude* begeni aldi!", magnitude: 1, title: "Icerigin begenildi", img: "https://images.emojiterra.com/google/android-10/512px/1f44d.png", kind: "postLikes"}} console.log(notifReq); userController.pushUser(userReq, {send: _ => {console.log(_)}, status: _ => {}}); postController.pushPost(postReq, {send: _ => {console.log(_)}, status: _ => {}}); notiController.newNotif(notifReq, {send: _ => {console.log(_)}, status: _ => {}}); res.status(200).send("Success!"); }catch(err){ console.log("Wrong parameters") res.status(500).send(err); } }
解决方案与实现架构
核心思路:使用数据库事务保证原子性
所有涉及数据库修改的操作(阶段1、2、3)必须包裹在一个事务中,事务会自动处理失败回滚;同时前置添加重复点赞校验,提前终止无效操作。
具体实现步骤
前置重复点赞校验
先查询用户的likedPosts或文章的likedPeople,确认用户未点赞过该文章:- 推荐给
user.likedPosts和post.likedPeople字段添加索引,提升查询效率 - 若检测到已点赞,直接返回
409 Conflict状态码,终止流程
- 推荐给
改造控制器为Promise风格
原代码使用回调式控制器,无法同步处理异步操作的成功/失败状态,必须将pushUser、pushPost、newNotif改造为返回Promise的方法(或用util.promisify包装回调)事务包裹所有操作
根据你使用的数据库类型实现事务:- MongoDB:需要启用副本集,使用
session对象包裹所有数据库操作 - SQL(MySQL/PostgreSQL):使用数据库原生事务机制
- MongoDB:需要启用副本集,使用
错误捕获与回滚
事务中任何一步操作失败,都会触发整个事务回滚,自动撤销已执行的所有修改
改造后的示例代码
const { promisify } = require('util'); // 假设控制器方法已改造为Promise,或用promisify包装回调 const pushUser = promisify(userController.pushUser); const pushPost = promisify(postController.pushPost); const newNotif = promisify(notiController.newNotif); module.exports.like = async (req, res) => { try { // 1. 输入参数校验 const { user_id, post_id, poster_id } = req.body; if (!user_id || !post_id || !poster_id) { return res.status(400).send("缺少必要参数"); } // 2. 前置校验:检查用户是否已点赞 const existingLike = await User.findById(user_id).select('likedPosts'); if (existingLike.likedPosts.includes(post_id)) { return res.status(409).send("你已点赞过该文章"); } // 3. 启动数据库事务(以MongoDB为例) const session = await mongoose.startSession(); session.startTransaction(); try { // 构造请求参数 const userReq = { body: { set: { likedPosts: post_id }, unset: {} }, params: { userId: user_id }, session }; const postReq = { body: { set: { likedPeople: user_id }, unset: {} }, params: { postId: post_id }, session }; const notifReq = { body: { to: poster_id, fromPost: post_id, content: "*title* 标题的文章获得了*magnitude*个赞!", magnitude: 1, title: "你的内容被点赞了", img: "https://images.emojiterra.com/google/android-10/512px/1f44d.png", kind: "postLikes" }, session }; // 按顺序执行事务内操作 await pushUser(userReq); await pushPost(postReq); await newNotif(notifReq); // 提交事务 await session.commitTransaction(); res.status(200).send("点赞成功!"); } catch (transactionErr) { // 回滚事务 await session.abortTransaction(); throw transactionErr; } finally { session.endSession(); } } catch (err) { console.error("点赞操作失败:", err); // 根据错误类型返回对应状态码 if (err.message.includes("已点赞")) { res.status(409).send(err.message); } else if (err.message.includes("参数")) { res.status(400).send(err.message); } else { res.status(500).send("服务器内部错误"); } } };
优化建议
- 参数校验增强:使用Joi、Yup等校验库,对输入参数的类型、格式做严格验证,避免非法数据进入业务逻辑
- 索引优化:给
user.likedPosts和post.likedPeople添加数组索引,大幅提升重复点赞校验的查询速度 - 通知异步化(可选):若通知操作非核心业务(即使通知失败也不影响点赞逻辑),可将通知放入消息队列异步处理,提升接口响应速度;但需保证点赞成功后才发送通知
- 错误日志细化:记录错误的具体信息(如用户ID、文章ID、错误栈),便于排查问题
- 避免直接操作req.body:解构req.body后使用变量,代码更清晰且便于校验
内容的提问来源于stack exchange,提问作者Bot Ellis
相关产品推荐
相关产品推荐

