跳至正文

折腾数据库的一点心得

上个月接了个小项目,要给一个社区论坛做数据迁移。原本以为就是导个表、改个配置的事,结果硬生生折腾了三天。那几天我每天对着MySQL和PostgreSQL来回倒腾,最后还把Redis给扯进来了。今天就把这些踩坑经历写下来,希望对正在跟数据库较劲的朋友有点帮助。

先说第一个坑:字符集编码问题。我那个论坛原来的数据库是MySQL 5.7,用的utf8mb3。新服务器我图省事直接装了MySQL 8.0.28,默认字符集是utf8mb4。导数据的时候,mysqldump出来的SQL文件里有几个表情符号,比如用户昵称里带了个笑脸。结果导入的时候就报错:“Incorrect string value: ‘\xF0\x9F\x98\x81’ for column ‘nickname’”。这错误我一眼就认出来了,4字节的UTF-8字符被截断了。解决办法其实不复杂,在mysqldump的时候加上–default-character-set=utf8mb4,然后导入前先在目标库执行SET NAMES utf8mb4。但有个细节容易忽略:MySQL 8.0的默认排序规则是utf8mb4_0900_ai_ci,而5.7用的是utf8mb4_general_ci。如果表里已经有数据,排序规则不一致会导致JOIN查询变慢。我后来统一改成了utf8mb4_general_ci,因为我的业务场景不需要区分大小写,而且这个排序规则兼容性更好。

第二个坑是索引设计。论坛帖子表有个搜索功能,用户按发帖时间、点赞数、评论数排序。原表就三个单列索引,数据量到50万条的时候,排序查询直接卡了三四秒。EXPLAIN一看,全部走了文件排序(Using filesort)。我一开始想建复合索引,但业务逻辑里有两种排序组合:按时间倒序+点赞数倒序,按点赞数倒序+评论数倒序。复合索引必须从左到右匹配,不能同时满足两种需求。查了MySQL官方文档,发现8.0.13之后支持了降序索引,试了一下:CREATE INDEX idx_time_likes ON posts(post_time DESC, likes DESC)。这样排序查询直接走索引,不用额外排序了,查询时间降到了0.02秒。但注意,只有InnoDB引擎支持降序索引,而且如果索引列是VARCHAR类型,降序索引不会自动排序NULL值,需要额外处理。

第三个坑跟Redis有关。论坛有个热门话题排行榜,每天刷新一次。我最开始用MySQL的ORDER BY + LIMIT直接查,但数据量大了之后,这个查询锁表时间太长,导致其他写操作排队。后来改成Redis的Sorted Set,把话题ID作为member,热度值作为score。每天凌晨用定时任务从MySQL计算好热度,批量写入Redis。代码大概是:ZADD hot_topics 100 topic_1 200 topic_2。然后前台直接ZREVRANGE hot_topics 0 9 WITHSCORES就能拿到前十名。QPS从原来的50直接飙到2000,基本不占数据库连接。但有个坑:Redis的Sorted Set如果member重复,会更新score,但不会删除旧记录。我一开始没注意,导致同一个话题在排行榜里出现两次。解决方法是每次更新前先ZREM移除旧记录,再ZADD添加新记录。

最后说说连接池配置。我用的是HikariCP,Java项目里默认配置是maximumPoolSize=10。但论坛中午高峰期并发量上到500,连接池很快被占满,后续请求都在排队等待,接口响应时间从100ms飙升到5秒。我一开始以为加大连接池就行,直接设成100。结果MySQL那边连接数暴涨,CPU直接飙到90%。查了HikariCP的官方文档,里面说连接池大小并不是越大越好,最佳实践是:poolSize = (coreCount * 2) + effectiveSpindleCount。我的服务器是4核,算下来大概10-12个连接就够。问题的根源其实不是连接数不够,而是查询太慢占用了连接。后来优化了几个慢查询,把maximumPoolSize调回15,响应时间反而降到了200ms。连接池不是治慢查询的药,该加索引还得加索引。

折腾完这一圈,我总结了几条经验。字符集和排序规则一定要在项目初期就定好,不然后期改起来要命。索引设计要结合具体的查询模式,别为了省事就建一堆单列索引。缓存层能扛的流量就扛,别让数据库硬抗。连接池参数别瞎调,先查慢查询日志,把慢查询干掉比加连接池管用。数据库这玩意儿,看着简单,但每个细节都可能让你加班到凌晨。下次再有人跟我说“就导个数据”,我得先问问字符集、索引、缓存、连接池都搞定没。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注