前言
可乐他们团队最近在做一个文章社区平台,由于人手不够,后端部分也是由前端同学来实现,使用的是 nest
。
今天他接到了一个需求,就是在用户点开文章详情的时候,把阅读量 +1
,这里不需要判断用户是否阅读过,无脑 +1
就行。
它心想:这么简单,这不是跟 1+1
一样么。
往期文章
仓库地址
- 切图仔做全栈:React&Nest.js 社区平台(一)——基础架构与邮箱注册、JWT 登录实现
- 切图仔做全栈:React&Nest.js社区平台(二)——👋手把手实现优雅的鉴权机制
- React&Nest.js全栈社区平台(三)——🐘对象存储是什么?为什么要用它?
- React&Nest.js社区平台(四)——✏️文章发布与管理实战
- React&Nest.js全栈社区平台(五)——👋封装通用分页Service实现文章流与详情
- 领导问我:为什么一个点赞功能你做了五天?
初版实现
我们用的 orm
框架是 typeorm
,然后他看了一眼官方文档,下面是一个更新的例子。
文章表里有一个字段 views
,表示该文章的阅读量。那我是不是把这篇文章的 views
取出来,然后 +1
,再塞回去就可以了呢?
啪一下,就写出了下面这样的代码:
async addView(articleId: number) {
const entity = await this.articleRepository.findOne({
where: {
id: articleId },
select: ['views', 'id'],
});
entity.views = entity.views + 1;
await this.articleRepository.save(entity);
}
然后就美滋滋的提测,继续摸鱼去了。
并发bug
不出意外的话意外就要发生了,测试下午就找到了可乐,说这个实现有 bug
,具体复现是在并发压测的时候。
这里用一个 node
脚本来模拟一下并发的请求:
import axios from "axios";
const axiosInstance = axios.create({
withCredentials: true,
headers: {
Cookie: "your cookie",
},
});
for (let i = 0; i < 10; i++) {
axiosInstance.get("http://localhost:3000/api/articles/getArticleInfo?id=2");
}
本来应该阅读量加 10
的,结果只加了 1
。
可乐当时脑瓜子嗡嗡的,这还能有 bug
?
具体来说,这是 mysql
处理并发的机制—— MVCC
,它有几种默认的隔离级别。默认的隔离级别是可重复读,在可重复读的隔离级别下,会出现以下这种情况:
也就是说,当多个客户端同时读取相同的文章实体,然后分别对其浏览次数进行增加,并尝试保存回数据库,这有可能前面的提交会被后提交的操作覆盖,导致阅读量的的更新丢失。
update…set
最简单的解决方案就是不要取出来再更新,而是使用 mysql
的 update...set
语句,它本身自带的锁可以帮我们规避掉这种问题。
await this.articleRepository.query(
'UPDATE articles SET views = views + 1 WHERE id = ?',
[articleId],
);
这一次再用测试脚本去跑的时候,发现是没有问题的了,加的次数是对的。