三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

MySQL性能优化与事务深度解析:从索引设计到分布式事务实战

MySQL性能优化与事务深度解析:从索引设计到分布式事务实战

1. 面试官视角下的MySQL考察核心

最近帮朋友公司面了几个后端开发,发现一个挺有意思的现象:简历上但凡写了“熟悉MySQL”的候选人,十有八九会被问到SQL优化和事务。但能真正把这两块讲清楚、讲透,甚至能结合自己项目里的真实案例聊出点门道的,少之又少。大多数人要么是背八股文,要么就是停留在“加索引”、“事务有ACID”这种表层概念上。

这其实挺可惜的。因为对于绝大多数业务系统来说,数据库就是那个最核心、最吃资源、也最容易出问题的“承重墙”。面试官问这两个问题,本质上是在考察你:第一,有没有从“写SQL”进化到“设计SQL”的思维,能不能预判和规避性能瓶颈;第二,对数据一致性的理解有多深,能不能在复杂的业务场景下,设计出正确、可靠的数据访问逻辑。今天,我就结合自己这些年做项目和面试别人的经验,把MySQL面试里关于SQL优化和事务的高频考点、深度追问以及背后的“为什么”掰开揉碎了讲一讲。这不仅仅是应付面试,更是你日常开发中实实在在能用的“硬通货”。

2. SQL优化:从“能用”到“高效”的思维跃迁

很多人一提到SQL优化,脑子里蹦出来的第一个词就是“索引”。这没错,索引是优化的基石,但如果你只停留在“加索引”这一步,那离真正的“高效”还差得远。面试官想听的,是一个系统性的、有层次的优化思路。

2.1 理解执行计划:优化前的“体检报告”

在动手改任何SQL之前,你必须先知道它现在为什么慢。EXPLAIN就是你的听诊器和X光机。但看执行计划不是扫一眼“type”是ALL还是index就完事了,你需要关注一个完整的链条。

核心字段解读与关联分析:

  1. type(访问类型):这是性能的第一道门槛。从好到坏大致是:system>const>eq_ref>ref>range>index>ALL。面试常问:“refeq_ref有什么区别?” 简单说,eq_ref是通过主键或唯一索引进行等值匹配,最多返回一条记录(比如A.id = B.id,且B.id是主键)。ref则是使用普通索引进行等值匹配,可能返回多条记录。如果看到ALL(全表扫描),那基本就是优化重点。

  2. key(实际使用的索引):这里显示MySQL最终决定使用哪个索引来查找数据。如果这一列为NULL,即使你建了索引也可能没用到,需要结合possible_keys(可能用到的索引)来分析为什么优化器“放弃”了你的索引。

  3. rows(预估扫描行数):这是一个非常关键的指标。它表示MySQL认为执行该查询需要扫描多少行数据。这个数字越接近实际需要返回的行数,说明索引选择性越好,查询效率越高。如果rows值巨大,但实际返回数据很少,通常意味着索引没建对或者SQL写法有问题。

  4. Extra(额外信息):这里藏着魔鬼细节。

    • Using filesort:表示MySQL无法利用索引完成排序,需要额外的排序步骤。对于ORDER BYGROUP BY操作,出现这个就要警惕了。
    • Using temporary:表示需要创建临时表来处理查询,常见于复杂的GROUP BYDISTINCT。临时表可能在内存中,也可能在磁盘上,后者性能损耗极大。
    • Using index:这是一个好信号,表示查询使用了“覆盖索引”,即所需数据直接从索引树中获取,无需回表。
    • Using where:表示在存储引擎层检索行后,还需要在Server层进行过滤。

一个实战排查案例:假设有一条慢查询:SELECT * FROM orders WHERE user_id = 100 AND status = ‘completed’ ORDER BY create_time DESC LIMIT 10;EXPLAIN后,你发现typeindex(走了某个索引扫描),key显示用的是idx_user_id,但Extra里有Using filesort。这说明什么?说明索引idx_user_id帮助快速找到了user_id=100的所有订单,但无法满足status过滤和create_time排序。MySQL需要先把所有user_id=100的记录捞出来(可能很多),在内存或磁盘里按create_time排序,再过滤status,最后取10条。正确的优化方向应该是建立复合索引(user_id, status, create_time)。这样索引可以同时满足查询条件(user_id,status)和排序需求(create_time),实现“索引覆盖”,避免回表和文件排序。

2.2 索引设计与使用的“避坑指南”

知道了怎么看问题,接下来就是如何正确设计和利用索引。这里有几个容易踩坑的点。

1. 最左前缀原则不只是“顺序”大家都知道复合索引要遵循最左前缀。但更深一层是:这个原则也适用于ORDER BYGROUP BY。如果你的查询是WHERE a = ? ORDER BY b, c,那么索引(a, b, c)就是完美的。但如果ORDER BYb DESC, c ASC,在MySQL 8.0之前,这个索引可能无法完全避免排序(因为排序方向不一致)。再比如,WHERE a > ? ORDER BY a, b,索引(a, b)可以同时用于范围查询和排序;但WHERE a = ? ORDER BY b, c,索引(a, b, c)可以;如果是WHERE a = ? AND b > ? ORDER BY c,那么索引(a, b, c)中的c用于排序就会中断,因为b是范围查询。

2. 索引选择性:不是所有字段都值得建索引索引选择性 = 不重复的索引值数量 / 表总记录数。选择性越高,索引过滤能力越强。像“性别”这种只有两三个值的字段,选择性极低,建索引通常意义不大,优化器很可能直接忽略它。高选择性字段(如用户ID、手机号、邮箱)才是索引的优质候选。对于低选择性但又经常需要联合查询的字段,可以考虑将其放在复合索引的后面。

3. 隐式类型转换与函数操作导致的索引失效这是实战中高频的坑。比如表里user_idvarchar类型,但你写WHERE user_id = 123(整数),MySQL会做隐式类型转换,导致索引失效。同样,WHERE DATE(create_time) = ‘2023-10-01’WHERE LEFT(name, 3) = ‘abc’,因为对索引字段使用了函数,索引也无法使用。正确的写法是WHERE create_time >= ‘2023-10-01’ AND create_time < ‘2023-10-02’

4. 联合索引的字段顺序“兵法”如何决定复合索引(A, B, C)中字段的顺序?一个实用的口诀是:“等值查询放最前,范围排序往后靠,高频查询要优先”。

  • 等值查询字段(如WHERE a=1 AND b=2)应该放在最左边。
  • 范围查询字段(如WHERE a>1)和排序字段ORDER BY b)应该放在等值字段之后。因为范围查询后面的索引列无法被利用。
  • 考虑字段的查询频率和选择性。将最常用作查询条件的、选择性高的字段尽量靠左。

2.3 超越索引:语句编写与架构层面的优化

当索引优化到一定程度后,性能瓶颈可能出现在SQL写法本身或架构设计上。

1. 避免使用SELECT *这是老生常谈,但至关重要。SELECT *会带来两个问题:一是网络传输和内存开销大;二是无法使用“覆盖索引”,必然导致回表查询。务必只取需要的字段。

2. 分页查询的深度优化LIMIT 10000, 10这种深度分页为什么慢?因为MySQL需要先扫描并丢弃前面的10000条记录。优化方法:

  • 延迟关联:先通过覆盖索引查出主键ID,再回表查询。
    SELECT * FROM orders a INNER JOIN (SELECT id FROM orders WHERE user_id=100 ORDER BY create_time DESC LIMIT 10000, 10) b ON a.id = b.id;
  • 记录上次查询位置:如果业务允许,记录上一页最后一条记录的ID或时间戳,使用WHERE id > last_id LIMIT 10。这是性能最好的方式。

3. 关联查询的优化

  • 小表驱动大表:这是JOIN的基本原则。确保LEFT JOIN的左表是小表,INNER JOIN时MySQL优化器通常会自己选择,但你可以用STRAIGHT_JOIN强制顺序。
  • 利用索引:确保ONWHERE子句中的关联字段有索引。
  • 子查询谨慎使用:特别是WHERE ... IN (SELECT ...)这种,MySQL 5.6以前可能会对外部查询的每一行都执行一次子查询(DEPENDENT SUBQUERY),性能极差。通常可以改写为JOIN

4. 事务与锁的粒度控制在默认的REPEATABLE READ隔离级别下,长时间运行的事务或范围更新(UPDATE ... WHERE status=‘pending’)可能会持有大量的锁,阻塞其他会话。尽量让事务短小精悍,尽快提交释放锁。对于大批量更新,可以考虑分批进行。

3. MySQL事务:ACID不只是四个字母

事务是保证数据一致性的基石。面试时,你不能只背出ACID(原子性、一致性、隔离性、持久性),更要能说清楚它们是如何实现的,以及在各种隔离级别下会引发什么问题。

3.1 事务隔离级别的本质与实现机制

隔离级别解决的是并发事务执行时可能出现的读现象问题。MySQL的InnoDB引擎默认级别是REPEATABLE READ,但它的实现和标准SQL有些不同。

隔离级别脏读不可重复读幻读实现机制简述
READ UNCOMMITTED可能可能可能几乎不加锁,读最新数据,包括未提交的。
READ COMMITTED不可能可能可能每条SQL执行时生成独立的ReadView,读已提交的最新快照。
REPEATABLE READ不可能不可能可能(InnoDB通过MVCC已解决大部分)事务开始时生成一个ReadView,整个事务期间都用它来读取数据。通过Next-Key Lock解决幻读。
SERIALIZABLE不可能不可能不可能所有读操作都加共享锁,读写互斥,完全串行。

重点聊聊InnoDB的MVCC和ReadView:这是理解READ COMMITTEDREPEATABLE READ区别的关键。InnoDB每行数据都有两个隐藏字段:trx_id(最近一次修改它的事务ID)和roll_pointer(指向undo log中旧版本数据的指针)。这就构成了一条版本链。

  • READ COMMITTED级别,每次执行SELECT语句时,都会生成一个新的ReadView。这个ReadView包含当前活跃(未提交)的事务ID列表。读取时,会沿着版本链找到第一个trx_id小于ReadView中最小活跃ID(即已提交)的数据版本。因此,它能读到其他事务最新已提交的数据。
  • REPEATABLE READ级别,只在第一次执行SELECT语句时生成一个ReadView,并在整个事务期间复用。因此,它每次读到的都是同一个快照版本的数据,从而实现了“可重复读”。

关于幻读的“解决”与“未解决”:标准SQL中,REPEATABLE READ无法解决幻读。但InnoDB通过Next-Key Lock(记录锁+间隙锁)的机制,在REPEATABLE READ级别下很大程度上防止了幻读的发生。例如,SELECT * FROM t WHERE id > 10 FOR UPDATE;这条语句不仅会锁住id>10的所有现有记录,还会锁住(10, +∞)这个间隙,阻止其他事务插入id>10的新记录,从而避免了幻读。但注意,如果是普通的SELECT ...(非锁定读),依然可能读到事务开始后其他事务插入并提交的数据(因为MVCC的ReadView机制),这算是一种“快照读”上的幻读,但对数据一致性通常无影响。

3.2 锁机制详解:悲观锁与实战选择

当MVCC无法满足需求时(比如需要基于最新数据做计算并更新),我们就需要用到锁。

1. 行级锁的种类:

  • 记录锁(Record Lock):锁住索引上的一条具体记录。
  • 间隙锁(Gap Lock):锁住索引记录之间的间隙,防止其他事务在这个间隙插入新记录。间隙锁是REPEATABLE READ隔离级别特有的,为了解决幻读问题。
  • 临键锁(Next-Key Lock):记录锁和间隙锁的组合,锁住一条记录及其前面的间隙。这是InnoDB默认的行锁算法。

2. 悲观锁的使用场景:

  • SELECT ... FOR UPDATE:这是排他锁(X锁)。用于在读取数据时就锁定它,防止其他事务读写。典型场景是“查询并更新库存”:
    BEGIN; SELECT stock FROM product WHERE id = 1 FOR UPDATE; -- 锁定id=1的行 -- 在应用层判断stock > 0 UPDATE product SET stock = stock - 1 WHERE id = 1; COMMIT;
    如果不加FOR UPDATE,在SELECTUPDATE之间,其他事务可能也读到同样的库存并完成扣减,导致超卖。
  • SELECT ... LOCK IN SHARE MODE:这是共享锁(S锁)。允许其他事务也加共享锁读,但阻止任何事务加排他锁写。适用于需要确保读取期间数据不被更改,但允许并发读的场景。

3. 死锁的产生与避免:当两个或以上事务互相等待对方释放锁时,就产生死锁。InnoDB有死锁检测机制,会主动回滚其中一个代价最小的事务。 常见死锁场景:两个事务以不同的顺序更新多行数据。例如:

  • 事务A:UPDATE t SET ... WHERE id = 1;UPDATE t SET ... WHERE id = 2;
  • 事务B:UPDATE t SET ... WHERE id = 2;UPDATE t SET ... WHERE id = 1;避免死锁的最佳实践:以固定的顺序访问多行数据。如果业务上无法保证,可以尝试降低隔离级别(如READ COMMITTED会减少间隙锁的使用),或者设置合理的锁等待超时时间innodb_lock_wait_timeout

3.3 Spring事务管理:声明式事务的陷阱与技巧

现在Java后端开发几乎离不开Spring,其声明式事务(@Transactional)用起来方便,但坑也不少。

1. 事务不生效的常见原因:

  • 方法非public修饰:Spring AOP代理要求目标方法必须是public
  • 自调用问题:同一个类中,一个没有@Transactional的方法A调用了有@Transactional的方法B,事务不会生效。因为代理对象调用方法B时,走的是目标对象内部调用,绕过了代理。解决方法:注入自己的代理(@Autowired自己)、将方法B拆到另一个Service中,或使用AspectJ模式。
  • 异常被“吃掉”:默认情况下,@Transactional只在遇到RuntimeExceptionError时回滚。如果你在方法里捕获了异常并处理了,但没有重新抛出,事务就不会回滚。可以使用@Transactional(rollbackFor = Exception.class)来指定所有异常都回滚。
  • 数据库引擎不支持:比如使用MyISAM引擎。

2. 事务传播行为(Propagation)的选用:这是面试高频点。常用的有:

  • REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。适用于大多数情况。
  • REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务,并挂起当前事务(如果存在)。新事务独立提交或回滚。适用于需要独立记录的日志操作,即使主业务失败,日志也必须保存。
  • NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则行为同REQUIRED。嵌套事务是外部事务的一部分,只有外部事务提交时,嵌套事务才会提交;外部事务回滚,嵌套事务必然回滚。但嵌套事务自己可以单独回滚而不影响外部事务。这是一个保存点(Savepoint)的实现。适用于可独立回滚的子流程。
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则把当前事务挂起。
  • NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

3. 读写分离与事务一致性在读写分离架构中,写主库,读从库。这里有个经典问题:在一个@Transactional写方法中,如果先写后读,读到的可能还是旧数据(因为读可能被路由到从库,而主从同步有延迟)。解决方案:

  • 强制后续读操作走主库。可以使用Spring的@Transactional(readOnly = false)标记,或使用自定义注解/切面,在事务上下文中设置数据源路由键为“主库”。
  • 如果业务能接受短暂延迟,可以考虑使用“最终一致性”方案,而不是在同一个事务中强求实时读到。

4. 分布式事务与复杂场景应对

当系统从单库扩展到微服务、分库分表时,本地事务(ACID)就不够用了,进入了分布式事务(CAP/BASE理论)的领域。面试官常会问:“你们项目里分布式事务怎么处理的?” 你需要有清晰的思路。

4.1 常见分布式事务解决方案对比

没有银弹,只有权衡。下面是一个简单的对比:

方案核心思想一致性性能/复杂度适用场景
2PC/XA两阶段提交,由事务管理器协调。强一致性能差,阻塞严重,实现复杂。老牌数据库支持,传统单体应用跨库。
TCCTry-Confirm-Cancel,业务侵入性强。最终一致性能较好,但业务代码复杂,需要实现三个接口。对一致性要求高,且能容忍一定开发复杂度的金融、交易核心。
本地消息表利用本地事务和消息队列。最终一致实现简单,依赖消息队列可靠性。跨服务、对实时性要求不极高的场景,如积分发放、通知。
最大努力通知定期重试,直到成功。最终一致实现简单,可能延迟大。对一致性要求最低的场景,如结果通知。
Saga长事务拆分为多个本地事务,每个事务有补偿操作。最终一致复杂度在状态机和补偿逻辑。业务流程长、步骤多的场景,如订单-库存-物流。

以“下单扣库存”为例看TCC:

  1. Try阶段:冻结库存(stock = stock - 1, frozen_stock = frozen_stock + 1)。尝试预留资源。
  2. Confirm阶段:确认冻结,真正扣减(frozen_stock = frozen_stock - 1)。如果Try成功,Confirm必须成功。
  3. Cancel阶段:取消冻结(stock = stock + 1, frozen_stock = frozen_stock - 1)。释放Try阶段预留的资源。

4.2 Seata AT模式原理浅析

Seata的AT(Auto Transaction)模式是目前比较流行的“无侵入”分布式事务解决方案,它是对2PC的改良。

工作流程:

  1. 一阶段
    • 业务SQL执行前,Seata拦截器解析SQL,生成查询前置镜像(before image)。
    • 执行业务SQL。
    • 业务SQL执行后,生成查询后置镜像(after image)和行锁(通过SELECT ... FOR UPDATE)。
    • 将前后镜像、行锁信息、业务SQL本身,一并存入undo_log表。
    • 本地事务提交。注意,这里一阶段就提交了本地事务,释放了数据库连接和锁(除了Seata全局锁),性能比XA好。
  2. 二阶段提交
    • 如果所有分支事务的一阶段都成功,TM通知TC。
    • TC通知所有RM异步删除对应的undo_log记录即可。非常快。
  3. 二阶段回滚
    • 如果任何一个分支事务一阶段失败,TM通知TC。
    • TC通知所有成功的RM进行回滚。RM收到回滚指令后,查找undo_log中的前置镜像,生成反向补偿SQL(如UPDATE ... SET stock = ? WHERE id = ?)并执行,然后删除undo_log

它的核心优势是“无侵入”,开发者几乎像写本地事务一样写代码。但它的局限性在于:1) 必须是支持本地ACID事务的关系型数据库。2) SQL要有“幂等性”和“可回滚”的前提(即更新操作要有明确的WHERE条件,能定位到具体数据行)。对于INSERT ... ON DUPLICATE KEY UPDATE这类复杂SQL,支持可能不完善。

4.3 最终一致性的务实选择:本地消息表

对于很多业务场景,强一致性带来的性能损耗和复杂度是不可接受的,最终一致性是更务实的选择。本地消息表是一个经典且可靠的模式。

实现步骤:

  1. 业务服务在执行本地事务的同时,向同一数据库的“消息表”插入一条消息记录,状态为“待发送”。这是关键,它保证了业务操作和消息记录的原子性。
  2. 本地事务提交。
  3. 有一个独立的“消息发送者”定时轮询消息表,取出“待发送”的消息,投递给消息中间件(如RocketMQ、Kafka)。
  4. 消息中间件确保消息被下游消费者成功消费(可能需要消费者回复ack)。
  5. 消息发送者收到MQ的确认后,将消息表状态更新为“已发送”。
  6. 下游消费者消费消息,执行自己的业务逻辑。如果失败,消息中间件会重试。

这个方案的可靠性在于:只要本地事务成功,消息就一定在库里,最终会被发出。它依赖于消息中间件的高可靠投递和消费者的幂等处理。这是很多互联网公司处理跨服务数据一致性的首选方案,比如订单成功后发券、扣减积分等。

5. 面试实战:如何回答高频问题与展示深度

最后,我们来模拟一下面试场景,看看如何把上面的知识组织成有深度的回答。

问题一:“一条SQL执行很慢,你如何排查和优化?”

标准回答(展示系统性思维):“我会遵循一个从外到内、从观察到行动的排查链路。首先,我会确认这是偶发性慢还是持续性慢。如果是偶发的,可能跟当时数据库的锁竞争、缓冲区状态有关;如果是持续的,就从SQL本身入手。 第一步,使用EXPLAIN或者EXPLAIN FORMAT=JSON查看执行计划。我会重点关注type字段看访问类型,最好是consteq_refref,最差是ALL全表扫描。然后看key字段确认是否用上了预期的索引,rows字段预估扫描行数是否巨大,Extra字段里有没有Using filesortUsing temporary这种危险信号。 第二步,根据EXPLAIN的结果分析原因。如果没走索引,可能是索引没建、索引失效(比如对字段做了函数计算、发生了隐式类型转换),或者优化器选错了索引(可以用FORCE INDEX提示,但更要分析为什么选错,是不是统计信息不准)。如果有Using filesort,说明ORDER BYGROUP BY没用上索引排序,需要考虑建立合适的复合索引。 第三步,优化。如果是索引问题,就设计或调整索引,遵循最左前缀、高选择性字段优先的原则。如果是SQL写法问题,比如用了SELECT *、深度分页LIMIT M,N太大、或者嵌套子查询,就重写SQL,比如用延迟关联优化分页,用JOIN代替子查询。 第四步,如果单条SQL已经优化到极限,但还是很慢,就要看上下文了。是不是在事务里,持有锁时间太长?是不是表数据量太大,需要考虑历史数据归档或分库分表?是不是数据库服务器资源(CPU、IO、内存)已经到瓶颈了? 整个过程,我会结合具体的业务场景和数据量来权衡,比如加索引要考虑写操作的成本,分页优化要考虑产品交互是否允许。”

问题二:“详细说一下MySQL的事务隔离级别和MVCC原理。”

深度回答(展示底层理解):“MySQL的InnoDB引擎默认隔离级别是REPEATABLE READ,但它通过MVCC(多版本并发控制)机制,在大部分情况下避免了幻读,这比标准SQL的REPEATABLE READ更强。 MVCC的核心是undo logReadView。每一行记录都有隐藏的trx_idroll_pointertrx_id记录最近更新它的事务ID,roll_pointer指向它在undo log中的旧版本,形成一个版本链。ReadView是事务在读取时生成的一个一致性视图,关键属性有m_ids(当前活跃事务ID列表)、min_trx_id(最小活跃ID)、max_trx_id(下一个将分配的事务ID)等。 在READ COMMITTED级别,每次SELECT都会生成一个新的ReadView。读数据时,会从最新版本开始,顺着版本链找到第一个trx_id小于min_trx_id(即已提交)的版本。所以它能读到其他事务最新提交的结果。 而在REPEATABLE READ级别,只在第一次SELECT时生成一个ReadView,后续读取都复用这个视图。所以它读到的始终是事务开始时的那个快照,实现了可重复读。 对于写操作(UPDATE/DELETE),InnoDB总是读取最新的已提交数据版本来进行更新,否则就会丢失更新。这就是为什么在RR级别下,一个事务里先读后写,可能发现数据‘变了’(因为读是快照读,写是当前读)。 至于幻读,InnoDB通过Next-Key Lock(记录锁+间隙锁)来防止。比如SELECT ... FOR UPDATE,不仅锁住存在的记录,还会锁住记录之间的间隙,阻止其他事务插入,从而在‘当前读’的层面上解决了幻读。但如果是普通的快照读,由于MVCC,依然可能看到新插入的数据,不过这通常不影响业务逻辑的一致性。”

问题三:“你们项目中如何保证分布式事务的一致性?”

务实回答(展示方案选型能力):“这要看具体的业务场景和对一致性的要求。我们没有追求一刀切的方案。 对于核心的、强一致性的交易链路,比如‘支付成功后同时更新订单状态和扣减库存’,我们采用了TCC模式。因为这对资金和库存的准确性要求极高,业务上也能清晰地定义出Try(冻结)、Confirm(确认)、Cancel(解冻)三个阶段。虽然开发量稍大,但数据最可靠。 对于大量的最终一致性场景,比如‘订单创建成功后发送短信通知、给用户增加积分’,我们用的是基于消息队列的最终一致性方案,具体是‘本地消息表’。下单服务在本地事务中完成订单创建,同时在同一事务里向本地消息表插入一条‘待发送’的消息。然后有异步作业轮询这个消息表,将消息可靠地投递到RocketMQ。积分服务和通知服务订阅这些消息并处理。这样保证了只要订单成功,消息最终一定会被发出,下游服务通过幂等性来保证哪怕重复收到消息也不会错。 对于一些对实时性要求不高、甚至可以接受少量丢失的场景,比如运营统计数据同步,我们会用‘最大努力通知’,简单记录然后异步重试几次。 选型的核心权衡点就是:业务上对一致性的要求到底有多强?能否接受短暂延迟?团队对复杂度的承受能力如何?通常,我们会优先考虑基于消息的最终一致性,在必须强一致的地方才用TCC,尽量避免使用对性能影响大的2PC。”

把这些点串起来,你给面试官的印象就不会是一个只会背概念的候选人,而是一个有实战经验、有思考深度、能解决实际问题的工程师。数据库的知识浩如烟海,但抓住“性能”和“一致”这两个核心,从原理到实践,从本地到分布式,层层深入,就能建立起自己的知识体系,在面试和工作中都游刃有余。

← 返回列表