1. 先搞清楚GORM的慢到底慢在哪里做Go后端开发的朋友应该都有这种感觉项目一开始数据量小GORM写起来真爽链式调用一气呵成结构体映射自动帮你扫清所有重复的SQL。等到线上数据涨到几十万、上百万行接口开始动不动就超时的时候第一反应往往是把锅甩给ORM性能差或者直接下结论GORM不能用了得换SQL。我在实际项目里踩过不少这类坑想先说一个结论GORM的性能瓶颈绝大多数时候不在ORM本身而在你使用它的姿势。GORM确实有反射带来的额外开销但那点开销在数据库查询、网络传输、序列化面前通常可以忽略。真正让你接口变慢的是N1查询、全字段SELECT、连接池耗尽、缺失索引让查询走全表扫描、以及批量写入时一条条插这些看起来无害的写法。1.1 默认行为里的隐形开销GORM默认的Find会把结构体所有字段都查出来。假设你的用户表有30个字段其中包含一个bio这种大文本字段列表页其实只需要id、nickname、avatar三列。这时候db.Find(users)生成的SQL是SELECT * FROM users多传输的那些字段就是纯浪费。数据量一上来每行多传几十字节百万行的差距就是几十MB的网络流量。解决办法很简单用Select显式指定字段var users []User db.Select(id, nickname, avatar).Find(users)如果是查统计数据、报表类场景直接定义一个只包含所需字段的结构体能避免GORM在映射时生成大量不必要的结构体字段处理逻辑。另一个容易被忽略的开销是日志。GORM默认Logger在LogMode(Warn)下会把慢SQL打出来但如果你的Logger级别设置为Info每个SQL都会输出到标准输出高并发下光是打日志的IO就够喝一壶。我见过一个同事排查了半天线上性能问题最后发现是日志量太大把磁盘IO吃满了。线上环境建议用LogMode(Warn)或者Silent只保留慢SQL日志就够排障用了。1.2 连接池参数别用默认值这是我觉得GORM项目里最值得先做的一步把连接池参数显示设置出来。GORM用database/sql作为底层连接管理如果不手动设置默认连接池的上限是无限个空闲连接数但MaxOpenConns在MySQL驱动下默认是00意味着不限制。不限制听着是好事实际在高并发下会导致数据库被打到连接数上限然后出现too many connections错误。我用的配置一般是这样sqlDB, err : db.DB() if err ! nil { panic(err) } sqlDB.SetMaxOpenConns(200) sqlDB.SetMaxIdleConns(50) sqlDB.SetConnMaxLifetime(time.Hour) sqlDB.SetConnMaxIdleTime(30 * time.Minute)几个参数的经验值说明一下MaxOpenConns200是我在多数中等规模业务下的起步值具体要看数据库实例的最大连接数限制一般不超过数据库max_connections的70%。如果多个服务实例共享同一个数据库还要均分。MaxIdleConns50左右够用活跃连接不会频繁创建销毁。这个值设太低高峰期会频繁新建连接握手开销不小。ConnMaxLifetime建议设置MySQL默认的wait_timeout是8小时连接空闲超过这个时间会被服务端断开客户端可能拿到一个失效连接。设为1小时能有效避免invalid connection错误。这个值不是越大越好也不是越小越好太小会导致连接频繁重建。ConnMaxIdleTime空闲连接超过30分钟就回收防止一堆闲置连接占着数据库资源。注意SetConnMaxLifetime是连接从创建到销毁的最大存活时间SetConnMaxIdleTime是连接在空闲池里允许空闲的最长时间。两者别搞混很多项目只设置了前者忽略了后者结果高峰期过去后一堆空闲连接还挂着。1.3 PrepareStmt与预编译SQL的取舍GORM从2.0开始支持PrepareStmt选项开启后底层会使用预编译语句db, err : gorm.Open(mysql.Open(dsn), gorm.Config{ PrepareStmt: true, })预编译SQL的好处是数据库只需要解析一次SQL后续执行直接复用执行计划在高频执行相同模板SQL的场景下能明显降低数据库端的CPU开销。同时参数化查询天然防SQL注入。但PrepareStmt不是无脑开的。它默认会缓存1000个预编译语句可以通过PrepareStmt配置里的缓存大小调节如果你的业务SQL模板非常多比如动态拼接的查询条件组合爆炸缓存的预编译语句可能会被频繁淘汰重建反而增加开销。而且开启PrepareStmt后连接池需要更多的数据库端预处理语句槽位如果数据库有max_prepared_stmt_count限制可能出现预处理语句数量超限的报错。我的建议是高频固定查询多的项目开动态查询多的项目别开。如果你不确定自己的业务属于哪种可以先开着观察一两天看数据库端prepared_stmt_count指标和接口延迟有没有变化。我在一个订单查询项目里实测过开启后P99延迟从220ms降到180ms左右效果还是很明显的。2. Preload、Joins与子查询关联查询的三种武器2.1 N1问题是怎么被Preload解决的N1查询是ORM使用者最经典的性能灾难。场景是这样的你要查10个用户以及他们各自的最新订单第一反应可能是var users []User db.Find(users) for i : range users { db.Where(user_id ?, users[i].ID).Order(created_at DESC).First(users[i].LastOrder) }这样会产生1次用户查询加上10次订单查询一共11条SQL。如果用户有1000个就是1001条SQL。每条SQL就算只要1ms加上网络往返也要1秒以上这还没算数据库端解析执行的负载。GORM的Preload就是来解决这个问题的var users []User db.Preload(LastOrder).Find(users)它会拆成两条SQL先查用户再根据用户ID列表一次性查订单然后在内存里完成关联组装。注意Preload并不是用JOIN而是分两次查询再在内存里做关联这是ORM处理关联关系的常见策略好处是避免大规模JOIN带来的数据冗余传输坏处是如果关联表的数据量特别大这两次查询的返回体积也可能失控。Preload还能嵌套db.Preload(LastOrder.Items).Preload(Profile).Find(users)这一下会生成三条SQLusers、orders、order_items层级关系GORM会自己处理。2.2 Preload条件过滤的坑Preload支持带条件的加载但有个细节很多人会搞错// 错误示范这样写会在所有用户的订单里找statuspaid的 db.Preload(Orders, status ?, paid).Find(users) // 正确思路如果你想要的是每个用户的已支付订单上面的写法就是对的 // 但如果你想要的是只查有已支付订单的用户这个写法是错的需要配合别的条件上面的写法本身没错Preload的第二个参数就是给关联查询加的条件结果是你拿到所有用户每个用户的Orders字段只包含statuspaid的订单。这就是左连接条件过滤的效果而不是只返回有已支付订单的用户。如果你希望只查有过已支付订单的用户那要用InnerJoins或者直接在Find的where里加Has条件var users []User db.Preload(Orders, status ?, paid). Where(EXISTS (SELECT 1 FROM orders WHERE orders.user_id users.id AND status paid)). Find(users)还有个小坑Preload条件的字段必须跟关联表对应如果你在关联条件里用了主表的字段做判断会变成跨表引用SQL可能执行不报错但结果不对排查起来很头疼。我习惯在写完Preload后开LogMode(Info)看一眼生成的SQL再上线。2.3 Joins查询聚合统计的正确选择Preload适合要返回完整关联对象的场景但如果你只需要关联表的几个聚合字段比如查每个用户的订单总数、总金额再用Preload就显得臃肿了。这时候Joins加Scan到自定义结构体是更好的选择type UserOrderStat struct { ID int64 Name string OrderCnt int64 TotalAmt float64 } var stats []UserOrderStat db.Model(User{}). Select(users.id, users.name, COUNT(orders.id) AS order_cnt, COALESCE(SUM(orders.amount), 0) AS total_amt). Joins(LEFT JOIN orders ON orders.user_id users.id). Where(users.status ?, active). Group(users.id). Having(COUNT(orders.id) ?, 5). Scan(stats)这里有两个关键点Model(User{})指定主表是usersJoins里的表名要跟订单表实际表名一致。Scan(stats)把SQL查询结果映射到自定义结构体上不需要定义完整映射关系GORM会把带AS别名的列映射到结构体字段。结构体字段名不一定要跟数据库列名完全一致只要有合适的别名映射就行。Joins还有一个隐式好处聚合、排序、过滤都可以在数据库端完成返回的数据量远小于加载所有关联对象再在Go里聚合的方案网络传输和内存使用都会小很多。2.4 子查询让复杂条件更优雅GORM 2.0对子查询支持得已经很好了。比如查订单金额高于平均金额的用户var orders []Order db.Where(amount (?), db.Model(Order{}).Select(AVG(amount))).Find(orders)再比如查最近7天有下单的用户var users []User db.Where(id IN (?), db.Model(Order{}).Select(DISTINCT user_id).Where(created_at ?, sevenDaysAgo), ).Find(users)子查询的优势在于数据库优化器可以整体评估SQL的执行计划有时候能比多条SQL加内存组合更高效。而且子查询避免了把中间结果集拉到应用层数据量大时分页、过滤都在数据库端完成。经验如果一条查询SQL超过3层嵌套子查询或者多个子查询之间共享相同子查询建议拆成两条SQL在应用层做组合。过度追求一条SQL解决所有问题在高并发下反而可能让单条SQL执行时间过长锁竞争和IO放大都很难受。我见过一条800行的SQL把CPU打到100%的案例排查半天最后拆成两条各50行的SQL性能反而好了。3. 动态查询与Scopes复杂条件的工程化写法3.1 结构化条件构建的三种方式日常开发里最头疼的是查询条件不确定的列表接口用户可能传了关键词、时间范围、状态、价格区间可能什么都没传。最简单粗暴的写法是用if拼接但如果你像我一样接手过几千行的列表查询代码就会明白这种写法维护起来多痛苦。GORM的Where支持三种传参方式各有适合的场景// 方式一map适合等值条件零值也会被处理 db.Where(map[string]interface{}{ name: 张三, status: 1, }).Find(users) // 方式二struct适合已知非零值条件注意零值会被忽略 db.Where(User{Name: 张三, Status: 1}).Find(users) // 方式三原生SQL片段适合复杂条件 db.Where(age ? AND city ?, 18, 北京).Find(users)关键区别在于结构体方式会忽略零值字段。比如User{Status: 0}并不会生成status 0的条件因为GORM无法区分用户没传status和用户传了status0。用map方式就没有这个问题map里显式写了status: 0就会生成对应条件。所以如果接口条件里包含0、空字符串这种合法值优先用map。3.2 Scopes封装可复用的查询片段动态查询的进阶玩法是Scopes。它本质上是一个接收*gorm.DB并返回*gorm.DB的函数可以把一段查询逻辑封装成组件多个接口复用也可以组合使用。举个例子一个电商后台经常要查不同状态下的订单还经常要限制时间范围func StatusScope(status int) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { if status 0 { return db.Where(status ?, status) } return db } } func TimeRangeScope(start, end time.Time) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { if !start.IsZero() { db db.Where(created_at ?, start) } if !end.IsZero() { db db.Where(created_at ?, end) } return db } } db.Scopes(StatusScope(2), TimeRangeScope(start, end)).Find(orders)Scopes的好处不仅仅是复用。它让查询代码变得声明式新同事看代码时一眼就能明白接口的过滤维度。而且Scopes可以嵌套调用比如func OrderListScopes(params OrderQueryParams) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Scopes(StatusScope(params.Status), TimeRangeScope(params.Start, params.End)) } }不过要提醒一下Scopes里的逻辑也会被执行两次GORM内部会先调用一次Scopes用于生成COUNT查询然后再调用一次用于实际查询所以Scopes里面不要放有副作用的操作比如修改外部变量、计数等。这是我在实际项目里踩过的坑——在Scope里给一个外部map写入统计数据测试的时候发现数据被写了两遍排查半天才发现是这条底层逻辑。3.3 分页与游标大数据量下的两种分页姿态经典分页写法是Limit Offsetdb.Order(id DESC).Offset(offset).Limit(pageSize).Find(articles)Offset实现简单但深层分页有性能问题。MySQL里LIMIT 100000, 20还是要扫描前100000行再丢弃数据量大了之后越翻越慢。替代方案是游标分页keyset pagination适合那种下拉加载更多的列表场景// 记住上一页最后一条的id下一页带上这个条件 db.Where(id ?, lastID).Order(id DESC).Limit(pageSize).Find(articles)游标分页的SQL可以直接走主键索引不需要扫描前面那些已经翻过的行性能稳定得多。缺点是它不支持随意跳页而且如果你要按多个字段排序游标条件要写成复合条件稍微复杂一点// 按created_at和id复合排序时的游标写法 db.Where((created_at ? OR (created_at ? AND id ?)), lastCreatedAt, lastCreatedAt, lastID). Order(created_at DESC). Order(id DESC). Limit(pageSize). Find(articles)我个人的经验是后台管理类系统需要跳页老老实实用OffsetC端App的列表几乎都是加载更多直接用游标分页接口延迟稳定性会好非常多。有一个真实项目订单列表从第100页开始接口耗时从80ms涨到1.2s改成游标分页后稳定在60ms左右效果立竿见影。4. 写入优化从一条条插到批量处理4.1 CreateInBatches批量插入的正确姿势业务里经常遇到导入、同步、批处理这类场景一次要写几万条数据。如果循环调Create每条都是一次单独的INSERT事务数据库连接往返、日志、事务提交的开销全部放大写入速度惨不忍睹。GORM提供了CreateInBatchesvar users []User // 假设这个slice有几万条 db.CreateInBatches(users, 500)它做的事情是把数据按指定大小分批每500条作为一个批次在一次事务里执行批量INSERT。500是我常用的批次大小实际可以根据字段数量和单条数据大小调整。一般来说单批次数据量控制在1MB以内比较安全超过这个阈值MySQL的max_allowed_packet可能会报错。如果只是用Create传入一个大sliceGORM默认会一次性把所有数据拼成一条INSERT语句这在数据量大的时候同样可能撑爆max_allowed_packet。所以要么显式用CreateInBatches要么在Create前先手动分片。除了批次大小还有个优化点关闭耗时操作。批量写入时如果不需要更新updated_at或created_at这些时间戳或者不需要执行钩子Hooks可以通过SkipHooks和UpdateColumn等手段减少不必要的处理。例如db.Session(gorm.Session{SkipHooks: true, CreateBatchSize: 500}).Create(users)4.2 Upsert实现幂等写入同步数据场景经常遇到这行数据存在就更新不存在就插入。自己写判断逻辑要先查一遍再决定性能差还容易有并发问题。GORM的clause.OnConflict直接映射了MySQL的ON DUPLICATE KEY UPDATEimport gorm.io/gorm/clause db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: email}}, DoUpdates: clause.AssignmentColumns([]string{name, age}), }).Create(user)这段的意思是如果email这个唯一键冲突了就更新name和age字段。这里有几个常见误区Columns指定的必须是数据库里实际存在的唯一索引或主键否则冲突判断不会生效。DoUpdates不一定要更新所有列。只更新业务上需要变动的列可以减少不必要的写操作。如果需要让冲突时某个字段自动加1比如计数可以用clause.Expr比如DoUpdates: clause.AssignmentColumns([]string{login_count})配合SQL表达式实现login_count login_count 1这种更新直接用clause.Expr更灵活db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: email}}, DoUpdates: clause.Assignments(map[string]interface{}{ login_count: gorm.Expr(login_count 1), updated_at: gorm.NowExpr(), }), }).Create(user)Upsert配合批量CreateInBatches基本就是同步类需求的黄金组合一次请求把几千条数据同步进库重复执行也不会产生重复数据效率和幂等性都有了。4.3 事务边界与批量更新的性能控制GORM的Transaction方法把事务边界包装得很干净err : db.Transaction(func(tx *gorm.DB) error { if err : tx.Create(order).Error; err ! nil { return err } if err : tx.Model(product{}).Where(id ?, order.ProductID). Update(stock, gorm.Expr(stock - ?, order.Quantity)).Error; err ! nil { return err } return nil })注意两个要点事务里不要做耗时操作比如HTTP调用、复杂计算。MySQL的InnoDB在事务未提交前会持有行锁事务持续时间越长锁等待越多并发性能越差。我之前有个同事在事务里调了第三方支付接口支付接口响应3秒这3秒内数据库行锁一直不释放一个秒杀活动直接把数据库锁死教训很深刻。事务里的查询尤其是FOR UPDATE的尽量精确命中索引减少锁的粒度。GORM里可以用Clauses(clause.Locking{Strength: UPDATE})实现SELECT ... FOR UPDATE但前提是where条件能走索引否则锁的是全表。批量更新场景比如把所有过期订单标记为已取消如果一次性更新几万条单条SQL可能执行很久并产生大量binlog。可以考虑分批更新每批1000条中间加一点间隔避免长期占用表锁。GORM没有内置的UpdateInBatches需要自己写循环for offset : 0; ; offset batchSize { result : db.Model(Order{}). Where(expired_at ? AND status ?, now, pending). Limit(batchSize). Update(status, cancelled) if result.Error ! nil { return result.Error } if result.RowsAffected int64(batchSize) { break } }5. 索引配合与执行计划从SQL层面压榨查询性能5.1 先让GORM帮你找出慢SQL优化查询性能第一步永远是找到慢的SQL而不是凭感觉猜。GORM的Logger支持自定义慢SQL阈值db, err : gorm.Open(mysql.Open(dsn), gorm.Config{ Logger: logger.New( log.New(os.Stdout, \r\n, log.LstdFlags), logger.Config{ SlowThreshold: time.Second, LogLevel: logger.SlowLog, IgnoreRecordNotFoundError: true, }, ), })设置SlowThreshold: time.Second超过1秒的SQL都会打出来。线上把这个日志收集到日志系统按SQL模板分组统计基本就能定位到Top N慢查询。定位到慢SQL之后最有效的动作是让数据库告诉你它为什么慢EXPLAIN SELECT * FROM orders WHERE user_id 123 AND status paid ORDER BY created_at DESC LIMIT 20;看**type字段ALL表示全表扫描ref/range/index表示走了索引、rows字段预估扫描行数、key**字段实际使用的索引。GORM本身不提供EXPLAIN能力你可以打开GORM的日志把SQL复制出来放到数据库客户端里执行EXPLAIN或者用db.DryRun()把SQL打印出来去分析stmt : db.Session(gorm.Session{DryRun: true}). Where(status ?, paid). Find(Order{}).Statement fmt.Println(stmt.SQL.String())5.2 联合索引设计与查询字段顺序慢SQL定位到之后十有八九是索引没设计好。以订单查询为例常见条件是WHERE user_id ? AND status ? ORDER BY created_at DESC如果只建了user_id单列索引数据库在过滤完user_id后还需要回表再过滤status、再排序效率一般。正确做法是建联合索引ALTER TABLE orders ADD INDEX idx_user_status_created (user_id, status, created_at);**联合索引的字段顺序遵循等值条件在前范围条件在后排序字段放最后**的核心原则。user_id和status是等值条件created_at既做范围过滤又做排序。把它放最后索引本身已经有序ORDER BY created_at DESC甚至可能直接用索引逆序扫描避免文件排序filesort。这里要特别提醒一个GORM相关的坑GORM的链式调用条件顺序不等于SQL里的where顺序尤其当你用了Scopes组合条件时最终SQL的where子句顺序是GORM拼接出来的。不过好在where条件的书写顺序对索引选择基本没影响数据库优化器会自己调整真正有影响的是条件里的字段组合。所以重点还是确保所有等值过滤字段都在联合索引里并且按选择性高低排序。索引设计还有一个维度是覆盖索引。如果你的查询只需要user_id、status、created_at、amount这几个字段而联合索引恰好包含它们那数据库可以只扫描索引而不回表性能提升非常可观。我在一个报表查询场景里通过把查询字段收窄、建覆盖索引把一个原来要跑3秒的聚合查询优化到了300ms以内而代码本身一行没改。5.3 该用原生SQL的时候别硬撑GORM虽然强大但有些场景它的表达力确实有限。典型的有复杂的窗口函数递归CTE多表联合的复杂聚合透视表之类数据库特有的函数如MySQL的JSON_TABLE硬用GORM API拼这些查询代码可读性极差还容易出现性能问题。GORM提供了Raw方法type Row struct { Category string Total float64 Rank int64 } db.Raw( SELECT category, total, RANK() OVER (ORDER BY total DESC) AS rank FROM ( SELECT category, SUM(amount) AS total FROM orders WHERE created_at ? GROUP BY category ) t , startTime).Scan(rows)Raw加Scan的好处是映射逻辑依然由GORM处理你保留了ORM在结构体绑定上的便利同时SQL完全可控。我见过一些团队因为不想写SQL强行用GORM拼复杂查询最后SQL字符串拼接得像天书一样调试成本极高还不如直接用Raw写清晰的SQL。经验判断该不该用Raw就看这段查询逻辑GORM表达出来后业务同学能不能一眼看懂。看不懂就用RawSQL本身就是一种通用语言没什么不好意思用的。6. 实测对比一个列表接口从2.8秒到180毫秒6.1 一个典型的优化链路复盘下面用一个我自己经手的案例说明整套优化怎么落地。背景是一个管理后台的订单列表接口数据量约80万行分为按用户昵称模糊搜索 按订单状态过滤 按时间范围过滤 分页四个维度。初始实现是这样的链路先用db.Find(orders)查出当前页订单每单再Preload(User)加载用户信息Preload里做用户昵称模糊匹配实际SQL是WHERE nickname LIKE %关键词%走了全表扫描接口分页用Offset(100000).Limit(20)深层翻页时扫描量巨大状态和时间范围没有联合索引支撑。压测结果单接口并发50P99延迟2.8秒数据库CPU持续90%以上。优化动作依次做了四步第一步加了联合索引ALTER TABLE orders ADD INDEX idx_user_status_created (user_id, status, created_at);这一步让基础过滤和排序走了索引接口P99从2.8秒降到1.2秒。第二步把Preload改成Joins聚合查询原来查的是订单列表加用户信息实际页面只需要用户的昵称不需要完整用户对象。把Preload(User)改成Joins(LEFT JOIN users ON users.id orders.user_id)然后Select(orders.*, users.nickname AS user_nickname)一次查询拿到所有数据避免了一次全表用户查询。这一步让P99降到了500ms左右。第三步解决昵称模糊搜索。LIKE %关键词%的写法天生无法走索引在80万行订单表里做这种匹配无论怎么建索引都没办法根治。但这个接口的业务形态是管理员搜索指定用户订单不是全表模糊匹配。我改成了先查users表拿到匹配关键词的user_id列表再用WHERE user_id IN (...)过滤订单。我把用户表的数据量控制在较小范围5万以内先做用户表模糊搜索很快然后订单表走user_id索引过滤效果完全不同。这一步P99到了300ms左右。第四步分页改为游标分页。管理后台因为要跳页不能完全用游标我做了折中第一页用Limit翻页用Where(id ?, lastID)同时保留跳页入口但提示仅支持前100页。这一步最终P99稳定在180ms数据库CPU降到20%。6.2 优化完成后的一些正确认知回过头看这次优化代码改动量并不大但每一步都建立在先定位再动手的基础上。如果一上来就重写逻辑、换数据库大概率白忙活。有几个认知我觉得值得分享ORM不是性能问题的根源SQL质量才是。GORM把SQL生成过程自动化了但你对索引、执行计划的理解依然决定了最终查询质量。先看执行计划和慢日志再动手优化。我见过太多人直接猜是不是该加缓存结果缓存加了仍然慢因为没有解决根本的SQL问题。批量写入优先考虑CreateInBatches和Upsert这能解决绝大多数写入性能焦虑。连接池参数值得花五分钟配置。绝大多数项目的性能问题里连接管理占的比重比想象中高。最后再说一个小技巧GORM的Session用法在优化中很实用。有时候你在一个接口里反复使用同一套只读查询条件可以用Session搞一个带条件的session复用activeOrderDB : db.Session(gorm.Session{}).Where(status ?, active) // 后续基于activeOrderDB继续查询条件会自动带上 activeOrderDB.Where(amount ?, 100).Find(orders) activeOrderDB.Where(created_at ?, dayAgo).Find(orders)但注意Session创建出来不是完全独立的默认情况下它对查询结果的缓存处理跟普通链式调用一致别指望通过它减少SQL执行次数它只是帮你复用条件构建逻辑省的是你写代码的重复劳动不是数据库的负载。关于GORM性能优化我一直的态度是先把GORM用对再谈换掉GORM。绝大多数业务系统里GORM远没到性能瓶颈的地步真正瓶颈往往是你对数据库本身的理解。把连接池调好、把查询列收窄、把N1消灭、把索引设计对这四件事做了大部分性能问题已经解决了。剩下的再用缓存、读写分离、分库分表这些手段去解决那是另一套话题了。
阅读完成 · 觉得有帮助?