ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

动态社区发布视频加载慢?从索引到异步化全面优化指南

动态社区发布视频加载慢?从索引到异步化全面优化指南 在实际的社区类产品里“动态太多导致发布视频加载慢”是一个很典型的性能问题。很多团队收到用户反馈后第一反应是清理历史动态甚至有人会开玩笑说“朋友请删掉一些动态吧”。但真正的问题是为什么动态数据量变大之后不只是列表加载变慢连发布视频操作也会变慢这两条链路之间存在哪些共享资源如果只清数据而不优化链路过一段时间问题还会再次出现。这篇文章以一个常见的动态社区为例从“发布视频”和“加载动态”两条链路出发梳理索引、缓存、异步化、对象存储、数据归档等优化手段最终目标是让系统在动态数据持续增长时依然能保持较快的发布和加载体验。文章会提供建表 SQL、慢查询排查命令、Spring Boot 异步示例、对象存储接入示例以及一份可以直接用于上线前检查的清单。1. 先把“发布视频加载慢”拆成两条链路“发布视频加载慢”这句话其实包含了两个方向一个是用户点击发布后等待时间过长另一个是动态列表里视频加载变慢。它们看起来是一个问题但在数据库、存储、网络层面会互相影响。把两条链路拆开才能知道优化应该做在哪一层。1.1 发布视频的完整时序发布视频不是“上传一个文件插入一条记录”这么简单。从用户点击“发布”到其他用户看到动态中间通常包括以下步骤用户选择本地视频客户端检查视频格式、大小、时长。客户端将视频上传到后端或对象存储。如果直传对象存储后端只负责签发上传凭证。后端接收上传完成回调校验视频是否可访问获取视频元信息。后端生成封面图、缩略图并触发视频转码任务。如果转码和截帧放在同步链路里用户会一直等待如果异步处理则先返回“发布中”状态。后端向动态主表插入一条新动态记录向视频资源表写入文件的存储路径。后端刷新相关用户的时间线缓存。前端收到成功响应发布完成。用户感受到的“加载慢”可能卡在上述任意一步。比如上传占满了带宽后端 ffmpeg 同步转码或者插入动态表时因为索引维护太多而变慢。1.2 动态列表加载的完整时序用户进入动态流页面时前端会发起列表请求。后端的处理链路通常包括校验登录状态确认用户身份。查询当前用户可看的动态范围例如好友动态或关注人动态。根据用户 ID 查询动态主表按创建时间倒序分页。查询每条动态对应的用户昵称、头像、图片、视频资源。拼接视频地址、封面地址、作者信息。返回 JSON 给前端渲染。这个流程如果没有缓存每个请求都会实时查询 MySQL。动态表达到百万行后一个缺少有效索引的分页查询就可能把数据库 CPU 打高接口响应时间从几十毫秒变成几秒。1.3 两条链路在哪里互相影响两条链路表面上是独立的但共享三类底层资源数据库连接池数据库 CPU 和磁盘 IO应用服务器带宽、对象存储和 CDN 带宽当列表接口产生大量慢查询时数据库连接会被占满发布接口在获取连接时会等待导致用户点击“发布”后迟迟没有响应。动态主表上的冗余索引越多列表查询可能稍快一些但每次发布插入都要维护所有二级索引插入速度会明显下降。这就是“动态太多导致发布视频慢”的隐藏原因之一优化没有从整条链路去考虑。链路主要操作数据量增长后的典型表现发布视频上传、转码、写动态表、刷新缓存上传慢、写库慢、超时加载动态查询动态、关联用户和资源、分页列表响应慢、数据库连接紧张2. 先确认瓶颈不要急着删数据先把问题量化收到用户反馈后最忌讳直接删除数据或盲目加缓存。应该先用监控工具和日志确认瓶颈到底在哪里再决定优化方案。2.1 从数据库连接池开始检查如果发布和列表接口都变慢首先要看数据库连接是否已经耗尽。进入 MySQL 命令行执行SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Max_used_connections; SHOW FULL PROCESSLIST;重点看两点Threads_connected是否接近max_connections。如果接近说明连接池已经饱和。SHOW FULL PROCESSLIST中是否大量线程处于Query状态并且执行的 SQL 都是查询t_post表的语句。如果大量查询集中在同一张动态表并且执行时间超过 1 秒基本可以判断是动态列表查询拖垮了数据库进而影响发布接口获取连接。2.2 用慢查询日志定位耗时的 SQL临时打开慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;注意这个方式只适合在测试环境快速验证。生产环境应该把参数写入 MySQL 配置文件避免重启失效。开启后观察日志典型慢查询如下# Query_time: 3.800000 Lock_time: 0.000000 Rows_sent: 20 Rows_examined: 500000 SELECT id, user_id, content, type, status, created_at FROM t_post WHERE user_id 123 ORDER BY created_at DESC LIMIT 20;关键是Rows_examined: 500000说明查询扫描了 50 万行。继续用执行计划确认EXPLAIN SELECT id, user_id, content, type, status, created_at FROM t_post WHERE user_id 123 ORDER BY created_at DESC LIMIT 20;如果type列是ALL说明发生了全表扫描如果Extra列出现Using filesort说明排序字段没有和索引匹配。这些都是动态列表变慢的直接原因。2.3 最小数据模型示例为了后续说明索引和归档方案这里给出一个最小数据模型。实际业务可能还有社区 ID、好友关系、话题标签等字段但核心结构是相通的。CREATE TABLE t_user ( id bigint NOT NULL AUTO_INCREMENT, nickname varchar(64) NOT NULL, avatar_url varchar(255) DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_post ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, content text, type tinyint NOT NULL DEFAULT 1 COMMENT 1 图文 2 视频, status tinyint NOT NULL DEFAULT 0 COMMENT 0 发布中 1 成功 2 失败, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id_created_at (user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_video ( id bigint NOT NULL AUTO_INCREMENT, post_id bigint NOT NULL, object_key varchar(255) NOT NULL COMMENT 对象存储 key, cover_key varchar(255) DEFAULT NULL, duration int DEFAULT NULL COMMENT 时长单位秒, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_post_id (post_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;t_post上的idx_user_id_created_at复合索引是为了支持“查询某个用户的动态并按创建时间倒序”这个核心场景。t_video表通过post_id关联动态主表。2.4 常见瓶颈速查表在开始优化前可以先对照下面的速查表判断大致方向问题现象可能原因排查方式解决方向点击发布后一直转圈上传走应用服务器、同步转码、数据库连接池满看应用日志、上传耗时、连接池监控对象存储直传、异步转码、优化连接池视频上传完成但发布不成功后端同步转码、事务范围过大观察接口响应时间、事务日志引入消息队列快速返回“发布中”动态列表加载越来越慢全表扫描、深分页、N1 查询慢查询日志、EXPLAIN加索引、游标分页、批量查询数据库 CPU 高大量慢查询、缓存击穿监控 CPU、processlist优化 SQL、加缓存、限流发布成功后别人看不到缓存未更新、数据异步任务失败查缓存、查数据库记录手动刷新缓存、查看消费者日志3. 优化方向一索引优化既要查得快也不能让写入变慢索引是动态列表查询最直接的优化手段但索引不是越多越好。动态表既要支持查询又要支持发布写入索引设计必须平衡。3.1 先理解 InnoDB 索引如何影响列表查询和写入InnoDB 的主键索引是聚簇索引叶子节点存放整行数据。二级索引的叶子节点存放的是主键值。查询时如果使用二级索引通常会先找到主键再回表拿完整行数据。如果动态表没有合适索引WHERE user_id ? ORDER BY created_at DESC就会全表扫描。有了(user_id, created_at)复合索引MySQL 可以先根据user_id快速定位用户再按created_at顺序读取这样既过滤了用户又避免了额外的排序操作。3.2 动态流查询的索引设计针对“查看某个用户的动态列表”场景建议建立复合索引ALTER TABLE t_post ADD INDEX idx_user_created (user_id, created_at DESC);MySQL 8.0 支持降序索引如果使用旧版本也可以先按升序建索引再在 SQL 层处理顺序。重要的是让过滤字段和排序字段同时出现在索引中。执行计划检查示例EXPLAIN SELECT id, user_id, content, type, status, created_at FROM t_post WHERE user_id 123 ORDER BY created_at DESC, id DESC LIMIT 20;如果索引生效type列应该是ref或rangeExtra列不应出现Using filesort。3.3 发布写入为什么会被索引拖慢插入一条动态记录时InnoDB 不仅要写主键索引还要写所有二级索引。如果t_post表上为每个列都建了索引比如content前缀索引、type单独索引、status单独索引那么每次发布插入都要额外写多棵 B 树。数据量大之后这种写入成本会被放大。检查表上的索引SHOW INDEX FROM t_post;删除无用索引ALTER TABLE t_post DROP INDEX idx_status;这里要强调索引设计不是一劳永逸而是在业务查询稳定后根据EXPLAIN结果和慢查询日志反复调优。不要为了“看起来环境里每个查询都有索引”而把所有列都加上索引那样会让发布写入变慢。3.4 索引失效的常见写法即使建了索引错误写法也会让索引失效。下面列出动态场景里容易出现的几种对索引列使用函数例如WHERE YEAR(created_at) 2025应改为created_at 2025-01-01 AND created_at 2026-01-01。隐式类型转换例如user_id是bigint但查询写成WHERE user_id 123可能导致索引无法高效使用。前缀模糊查询例如WHERE content LIKE %关键词%普通索引无法加速需要考虑全文索引或搜索引擎。使用OR连接多个范围条件时优化器可能放弃索引可以拆成多条 SQL 或使用UNION合并结果。每次发现列表变慢都应该先看执行计划确认索引是否真的被使用再决定下一步做什么。4. 优化方向二把发布视频从同步流程改成异步流程发布视频慢的另一个常见根因是同步流程太长。用户点击发布后后端既要做上传、又要做 ffmpeg 转码、截封面、写库、刷新缓存整个链路串行执行用户只能一直等待。4.1 为什么同步发布会让用户感觉“加载慢”同步发布链路耗时等于上传耗时加上转码耗时加上写库耗时。视频转码和封面截取是 CPU 密集型操作一段 30 秒的视频转码可能耗时几秒甚至更久。如果同时有多个用户发布应用服务器线程被大量占用最终会表现为发布接口响应越来越慢。核心思路是把必须同步做的事情压缩到最小例如参数校验、文件路径生成、插入“发布中”状态把耗时任务交给消息队列和后台消费者异步处理。4.2 引入消息队列后的最小发布时序引入 RabbitMQ 后发布接口只负责两件事写入status 0的动态记录发送一条异步消息然后立即返回。Spring Boot 项目引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-amqp/artifactId /dependency配置连接信息spring: rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest发送消息Service public class PublishService { private final RabbitTemplate rabbitTemplate; public PublishService(RabbitTemplate rabbitTemplate) { this.rabbitTemplate rabbitTemplate; } public Long publishVideo(VideoPublishRequest request) { // 1. 校验视频元信息 // 2. 插入动态记录status 0 Post post new Post(); post.setUserId(request.getUserId()); post.setType(2); post.setStatus(0); postDao.insert(post); // 3. 发送异步消息 VideoPublishMessage message new VideoPublishMessage(); message.setPostId(post.getId()); message.setVideoKey(request.getVideoKey()); rabbitTemplate.convertAndSend(video.exchange, video.publish, message); return post.getId(); } }消费异步任务Component public class VideoPublishConsumer { RabbitListener(queues video.publish.queue) public void onMessage(VideoPublishMessage message) { // 1. 获取视频元信息 // 2. 截图封面、转码 // 3. 写入 t_video // 4. 更新 t_post.status 1 // 5. 刷新动态缓存 } }示例省略了异常处理和重试机制。生产环境必须配置消息确认、消费失败重试、死信队列否则消息丢失会导致动态永远停留在“发布中”。4.3 状态机与前端反馈异步发布后前端不会立刻拿到最终结果因此需要状态字段配合。可以用数字表示0发布中1发布成功2发布失败前端在拿到动态 ID 后可以先展示“处理中”的占位卡片再轮询状态接口GET /v1/posts/{postId}/status响应{ postId: 123, status: 0 }轮询间隔建议在 3 到 5 秒避免请求太频繁。生产环境也可以使用 WebSocket 或 Server-Sent Events把状态变更主动推送给客户端。4.4 消息队列积压时怎么排查异步化之后可能出现的瓶颈会转移到消息队列。查看 RabbitMQ 队列积压情况rabbitmqctl list_queues name messages messages_ready messages_unacknowledged如果messages_ready持续上涨说明消费者处理速度跟不上。常见原因有消费者实例数量不够。转码服务本身成为瓶颈。消费逻辑抛异常消息被不断重试。prefetch设置过小消费者同时处理的消息太少。排查时先看消费者日志确认是资源不足还是代码异常再决定扩容消费者、优化转码流程还是修复重试逻辑。5. 优化方向三动态列表缓存与分页改造发布链路优化完成后下一个重点是动态列表加载。动态列表是高频读接口不能每次都实时查询数据库并执行大量关联。5.1 列表接口常见的 N1 查询问题很多动态列表慢的根因不是单条 SQL 慢而是循环查询造成 N1 次数据库请求。错误写法示例ListPost posts postDao.selectPage(...); for (Post post : posts) { User user userDao.selectById(post.getUserId()); // N 次查询 Video video videoDao.selectByPostId(post.getId()); // N 次查询 }每页 20 条动态至少会产生 40 次额外查询。优化方式是批量查询ListPost posts postDao.selectPage(...); ListLong userIds posts.stream() .map(Post::getUserId) .distinct() .toList(); MapLong, User userMap userDao.selectByIds(userIds); ListLong postIds posts.stream() .map(Post::getId) .toList(); MapLong, Video videoMap videoDao.selectByPostIds(postIds);先用一条 SQL 查出所有需要的用户和视频再在内存中组装结果。IN查询的数量如果很大需要分批查询避免超过数据库参数限制。5.2 用 Redis 缓存动态 ID 列表动态列表的缓存策略不建议直接缓存完整对象因为动态内容变化后缓存更新非常麻烦。更稳妥的方式是只缓存动态 ID 列表再根据 ID 批量查询详情。Redis Key 设计feed:user:{userId}写入流程ListLong postIds redisTemplate.opsForList().range(feed:user: userId, 0, -1); if (postIds null || postIds.isEmpty()) { postIds postDao.selectLatestIds(userId, 500); redisTemplate.opsForList().rightPushAll(feed:user: userId, postIds); redisTemplate.expire(feed:user: userId, Duration.ofMinutes(5)); }发布新动态后最简单有效的做法是直接删除该用户的缓存 Key让下一次请求重新加载。不要尝试在并发环境下同时修改列表和新增数据容易产生短时间不一致。这种缓存策略适合读多写少的动态流场景。缓存过期时间不要太长5 分钟到 15 分钟之间比较合适。5.3 游标分页替代页码分页页码分页在数据量小的时候很直观但数据量大后会出现深分页问题。OFFSET 1000000 LIMIT 20会扫描前面一百万行消耗大量数据库 IO。游标分页通过(created_at, id)组合条件定位让扫描范围始终控制在一个小范围内。SQL 示例SELECT id, user_id, content, type, status, created_at FROM t_post WHERE user_id 123 AND (created_at, id) (2025-01-01 00:00:00, 999999) ORDER BY created_at DESC, id DESC LIMIT 20;前提是表上存在(user_id, created_at, id)相关索引。游标值不要直接暴露数据库时间字符串可以返回一个拼接后的字符串由前端下次请求时原样传回。5.4 视频地址的动态拼接不要在数据库中保存完整的 CDN URL因为域名变更和证书切换都会导致历史数据失效。更合理的做法是保存对象存储的object_key返回给前端时再拼接 CDN 域名。String videoUrl cdnDomain / video.getObjectKey();如果视频是私有读权限需要生成签名 URLString signedUrl ossClient.generatePresignedUrl(bucket, objectKey, expiration).toString();签名 URL 有有效期频繁生成会带来不必要的计算和网络开销。可以在第一次生成后缓存一段时间比如 5 分钟减少请求量。6. 优化方向四媒体文件与业务服务解耦视频文件体积大如果走业务服务器上传和转发会占用大量网络带宽和磁盘空间。把媒体文件独立出来是发布视频性能优化的关键一步。6.1 为什么不能把视频放在业务服务器本地业务服务器本地磁盘有几个明显问题磁盘容量有限视频文件增长快。业务服务重启或扩容时本地文件不好同步。上传和下载同时占用应用服务器带宽影响接口响应。日志、代码、临时文件和业务数据混在一起管理混乱。视频文件应该放到对象存储中业务服务器只负责生成上传凭证、处理回调、写入元数据。6.2 对象存储的最小接入流程以 MinIO 为例生成预签名上传 URL 的代码import io.minio.MinioClient; import io.minio.GetPresignedObjectUrlArgs; import io.minio.http.Method; MinioClient client MinioClient.builder() .endpoint(http://127.0.0.1:9000) .credentials(admin, admin123) .build(); String objectKey video/ userId / System.currentTimeMillis() .mp4; String uploadUrl client.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(media) .object(objectKey) .expiry(600) .build());前端拿到uploadUrl后直接向对象存储发起 PUT 请求后端不承接视频上传流量。上传完成后前端再调用后端接口提交动态元信息例如时长、封面图 key、视频 object key。云厂商对象存储通常也有类似能力例如阿里云 OSS、腾讯云 COS、华为云 OBS接入方式大同小异。重点是架构上保持“客户端上传到存储后端只做协调和元数据管理”。6.3 封面和缩略图处理视频封面和缩略图不能在前端生成也不能在发布接口里同步执行。可以使用 ffmpeg 在异步消费者中截帧ffmpeg -i input.mp4 -ss 00:00:01 -vframes 1 cover.jpg ffmpeg -i input.mp4 -vf scale320:-1 thumb.jpg生产环境可以用容器化的转码 worker也可以使用云厂商提供的视频处理服务。关键点在于转码任务一定不能放在发布请求的同步链路里否则大量用户同时发布时应用服务器 CPU 会被瞬间打满。6.4 前端加载媒体资源的常见优化列表加载不仅依赖后端前端也需要配合优化。动态列表先返回封面图和视频地址但视频默认不自动播放用户点击后再加载真实播放地址。图片和封面使用懒加载减少首屏请求数量。长列表使用虚拟滚动只渲染可视区域内的动态。视频标签使用preloadmetadata避免一次性加载整个视频文件。前后端配合优化后动态列表的流量会明显下降后端压力和用户等待时间也会同步减少。7. 如果真的需要“删动态”也要按归档策略操作回到“朋友请删掉一些动态吧”这句话。如果业务确实需要清理历史数据不能直接在线上大表执行 DELETE必须有归档策略。7.1 直接 DELETE 的风险直接删除大量数据会带来几个问题大事务执行 DELETE 时可能持有锁影响正常写入。删除产生的 binlog 量很大会导致主从同步延迟。物理删除后数据不可恢复一旦误删很难补救。如果没有带条件可能全表删除造成重大事故。所以即使要删也要设计成“分批迁移 延迟清理”。7.2 软删除与归档表一种稳妥方案是动态主表增加deleted字段ALTER TABLE t_post ADD COLUMN deleted tinyint NOT NULL DEFAULT 0 COMMENT 0 正常 1 删除;用户删除动态时先执行软删除UPDATE t_post SET deleted 1 WHERE id ?;列表查询默认过滤已删除记录SELECT id, user_id, content, type, status, created_at FROM t_post WHERE user_id 123 AND deleted 0 ORDER BY created_at DESC LIMIT 20;定期归档时将超过时间阈值的记录先复制到归档表t_post_archive再从主表分批删除。分批删除示例DELETE FROM t_post WHERE created_at 2025-01-01 00:00:00 AND id IN ( SELECT id FROM ( SELECT id FROM t_post WHERE created_at 2025-01-01 00:00:00 LIMIT 1000 ) tmp );这个写法通过子查询先取 1000 条 ID再删除减少单次事务的锁范围。不同 MySQL 版本对DELETE的限制略有差异落地前需要在测试环境验证。7.3 清理后必须做的事清理数据之后有几件事不能省略删除或更新 Redis 缓存中的已归档动态 ID避免用户仍然看到已经删除的内容。低峰期执行OPTIMIZE TABLE t_post;整理表空间。注意该操作可能锁表生产环境需要安排在业务低峰期。检查归档表数据量是否和主表删除前预期一致。延迟清理对象存储中的视频文件避免用户拿着旧的签名 URL 还在下载等过期后再删除更安全。7.4 归档策略参数表参数建议值说明归档时间阈值动态保留 6 个月根据业务实际确定每批删除行数200 到 1000避免锁时间过长执行时间凌晨 2 点到 4 点业务低峰期归档表t_post_archive与主表结构一致保留查询索引缓存过期时间5 分钟避免列表长期不一致8. 常见问题排查与最佳实践清单8.1 常见问题表问题现象可能原因处理建议发布视频按钮一直转圈上传走应用服务器带宽被占满改为对象存储直传动态列表打开很慢全表扫描、无有效索引使用 EXPLAIN 分析并加索引发布接口很快但视频不出现异步任务失败或消息队列积压查看队列积压、消费者日志数据清理后查询仍然很慢统计信息未更新、索引碎片多低峰期执行 OPTIMIZE TABLE同一时间大量用户发布视频应用线程池满、数据库连接满连接池监控、异步化、限流8.2 从现象到根因的排错路径遇到“动态多了之后发布视频变慢”的问题按照下面的顺序排查确认变慢的是发布接口还是列表加载接口或者两者都慢。抓取应用日志观察接口耗时集中在哪一段。查看数据库连接数和慢查询日志。对慢 SQL 执行EXPLAIN确认是否全表扫描。查看消息队列积压量判断异步消费者是否正常。检查对象存储上传耗时和带宽占用。检查前端是否一次性请求了过多媒体资源。排错时不要跳步尤其是在没有日志和监控数据的情况下猜测只会让问题更难定位。8.3 发布前性能检查清单每次发布版本前可以对照下面清单动态主表索引是否和核心查询匹配是否删除了不必要的二级索引避免拖慢写入发布流程是否包含同步转码、封面生成等耗时操作消息队列是否已接入并且有积压监控列表接口是否仍然存在 N1 查询分页是否还是OFFSET深分页视频文件是否已经迁移到对象存储并配置 CDNRedis 缓存是否设置过期时间和失效策略数据库连接池大小是否根据并发情况调整定时归档任务是否安排在业务低峰期8.4 学习环境与生产环境的差异维度学习/演示环境生产环境数据库单机 MySQL手动执行 SQL一主多从、监控、备份恢复文件存储MinIO 单节点云对象存储或分布式存储跨可用区容灾消息队列RabbitMQ 默认配置集群、镜像队列、消息追踪、死信队列缓存Redis 单机哨兵或 Cluster缓存击穿保护发布流程同步写库演示分阶段异步、状态机、审计日志数据清理手动执行 DELETE定时任务、归档表、灰度分批回到最初的问题用户说“朋友请删掉一些动态吧”更像是对系统变慢的不满。真正有价值的思路是先不要把责任推给数据量而是把发布和加载的链路拆开结合索引、异步化、缓存、对象存储和归档让系统在数据增长时依然稳定。架构改造最忌讳一步跨太大。建议先定位到具体瓶颈发现列表查询慢就先优化索引或分页发现发布流程里 ffmpeg 同步转码卡住用户就先异步化。每一步完成后都要用监控数据验证效果。对于刚接触这类问题的开发者可以从最小示例开始建一张动态表、写一个发布接口、模拟大量数据然后用EXPLAIN观察查询计划再动手加索引或引入 Redis 缓存。把这条链路走通后对“数据量变大之后系统变慢”的理解会比只看理论深入很多。
返回列表