亿级流量点赞系统-第七章
1. 前期回顾
项目源码地址:https://github.com/ruogu-coder/ruogu-like
在前六章,做了很多优化,目前点赞方式有:
- Mysql 点赞 + Redis 缓存
- Mysql +Redis Lua 脚本实现
- Mysql + Redis + Caffine 实现多级缓存
- Mysql + redis + Pulsar
Mysql 可替换成 TiDB 分布式数据库
2. 本期目标
使用 Jmeter 进行各个接口的压力测试
3. 环境搭建
3.1. 下载 Jmeter
官网地址:https://jmeter.apache.org/download_jmeter.cgi
下载完成后解压目录,如下图
进入 bin 目录修改jmeter.properties

将语言改为中文
▼java复制代码language=zh_CN

然后双击 jmeter.bat 脚本
打开后如下图

3.2. 创建线程组


3.3. 创建 HTTP 信息头管理



3.4. 添加 HTTP 请求头信息

▼plain复制代码Content-Type:application/json

3.5. 配置压测结果展示

3.6. 获取更多用户登录 token
在 test 目录下的创建 test 类
▼java复制代码package com.ruogu.thumb; import cn.hutool.core.util.RandomUtil; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.ruogu.thumb.common.exception.enums.GlobalErrorCodeConstants; import com.ruogu.thumb.common.pojo.CommonResult; import com.ruogu.thumb.model.dto.user.UserLoginReqDTO; import com.ruogu.thumb.model.dto.user.UserRegisterReqDTO; import com.ruogu.thumb.model.entity.User; import com.ruogu.thumb.service.UserService; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import java.io.File; import java.io.FileWriter; import java.io.PrintWriter; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /*** *@author ruogu *@create 2025/4/25 12:57 **/ @SpringBootTest @AutoConfigureMockMvc class RuoguLikeApplicationTest { @Resource private UserService userService; @Resource private MockMvc mockMvc; }
- 批量添加用户
▼java复制代码@Test void addUser() throws Exception { int initCount = 5000; Set<String> hashSet = new HashSet<>(5000); for (int i = 0; i < initCount; i++) { // 生成随机用户账号 String userAccount = RandomUtil.randomString(8); while (hashSet.contains(userAccount)){ userAccount = RandomUtil.randomString(8); } hashSet.add(userAccount); // 固定密码和确认密码 String userPassword = "123456789"; // 构建请求体 UserRegisterReqDTO userRegisterReqDTO = new UserRegisterReqDTO(); userRegisterReqDTO.setUserAccount(userAccount); userRegisterReqDTO.setUserPassword(userPassword); userRegisterReqDTO.setCheckPassword(userPassword); // 将请求体转换为 JSON 字符串 String requestBody = new ObjectMapper().writeValueAsString(userRegisterReqDTO); // 构建 MockHttpServletRequestBuilder MockHttpServletRequestBuilder requestBuilder = post("/user/register") .contentType(MediaType.APPLICATION_JSON) .content(requestBody); // 执行请求并获取响应 ResultActions resultActions = mockMvc.perform(requestBuilder); // 解析响应体 MvcResult mvcResult = resultActions.andReturn(); String responseBody = mvcResult.getResponse().getContentAsString(); CommonResult<Long> commonResult = new ObjectMapper().readValue(responseBody, new TypeReference<CommonResult<Long>>() {}); // 断言响应结果为成功,并且返回值不为空 assertThat(commonResult.getCode()).isEqualTo(GlobalErrorCodeConstants.SUCCESS.getCode()); assertThat(commonResult.getData()).isNotNull(); } }

- 批量获取用户登录的 token 并保存成文件
▼java复制代码@Test void testLoginAndExportSessionToCsv() throws Exception { // 获取所有用户列表 List<User> list = userService.list(); // 确保用户列表不为空 assertThat(list).isNotEmpty(); // 使用 try-with-resources 确保资源正确关闭 try (PrintWriter writer = new PrintWriter(new FileWriter("token_output.csv", true))) { // 判断文件是否为空,如果是第一次写入,则添加表头 if (new File("token_output.csv").length() == 0) { writer.println("userId,token,timestamp"); } // 遍历用户列表 for (User user : list) { long testUserId = user.getId(); UserLoginReqDTO userLoginReqDTO = new UserLoginReqDTO(); userLoginReqDTO.setUserAccount(user.getUserAccount()); userLoginReqDTO.setUserPassword("123456789"); if(user.getUserAccount().equals("admin")){ userLoginReqDTO.setUserPassword("12345678"); } // 将请求体转换为 JSON 字符串 String requestBody = new ObjectMapper().writeValueAsString(userLoginReqDTO); // 构建登录请求 MvcResult result = mockMvc.perform(post("/user/login") .content(requestBody) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // 断言响应状态码为 200 .andReturn(); // 获取 Set-Cookie 头 List<String> setCookieHeaders = result.getResponse().getHeaders("Set-Cookie"); assertThat(setCookieHeaders).isNotEmpty(); // 断言 Set-Cookie 头不为空 Optional<String> tokenValue = extractCookieValue(setCookieHeaders, "ruogu-like-token"); // 确保提取到值 String token = tokenValue.orElseThrow(() -> new RuntimeException("No ruogu-like-token found in response")); // 写入 CSV 文件 writer.printf("%d,%s,%s%n", testUserId, token, LocalDateTime.now()); // 输出日志 System.out.println("✅ 写入 CSV:" + testUserId + " -> " + token); } } } public static Optional<String> extractCookieValue(List<String> setCookieHeaders, String cookieName) { return setCookieHeaders.stream() .filter(cookie -> cookie.startsWith(cookieName + "=")) // 过滤包含指定 Cookie 名称的项 .map(cookie -> cookie.split(";")[0]) // 提取 Cookie 名称=值 部分 .map(cookie -> cookie.split("=")[1]) // 提取 Cookie 值部分 .findFirst(); // 返回第一个匹配项,如果未找到则返回 Optional.empty() }


4. 测试实现
4.1. Mysql 点赞 + Redis 缓存
- 回退版本
我的基础实现,是在增加前面这个一起已经实现,不过增加 Redis 支持是将登录完善,所以直接退回到这一期
重新执行添加用户和 token 的 csv 文件 这里使用的是单机 mysql 而不是 TiDB

- 添加 cookie 管理器



- 添加动态 csv 变量


测试结果:

平均值:8916
最慢响应:19239
TPS/s:478/sec
异常:0.42%
4.2. Mysql +Redis Lua 脚本 + 定时任务
将点赞数据通过定时任务写入,点赞信息全部维护在 Redis 中
清空 redis 点赞数据和 mysql 的点赞数据再次测试

平均值:1277
最慢响应:1864
TPS/s:2659/sec
异常:0%
4.3. TIDB + Redis Lua 脚本 + Pulsar
这里需要重新生成下 Token

平均值:14427
最慢响应:66158
TPS/s:275/sec
异常:1.5%
我这个测试出来还没有 Redis + 定时任务 的 TPS 高,Redis 和 mysql 都是本地安装的而 TiDB 和 Pulsar 都是虚拟机中 docker 搭建,感觉和这个应该是有关系的
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
