Dunwu Blog

大道至简,知易行难

主流数据库对比

数据类型

::: info 扩展阅读

:::

类型 Elasticsearch MongoDB
整数型 byteshortintegerlongunsigned_long int、long
浮点型 floatdouble double、decimal
布尔型 boolean bool
字符串型 keywordtext string
二进制型 binary binData
时间类型 date date、timestamp
组合类型 objectnested object、array
特殊类型 nullipversion null、regex、objectId、javascript

CRUD

::: info 扩展阅读

:::

操作 Elasticsearch MongoDB
PUT <index>/_doc/<id>
PUT <index>/_create/<id>
POST <index>/_doc
db.collection.insertOne()
db.collection.insertMany()
DELETE <index>/_doc/<id> db.collection.deleteOne()
db.collection.deleteMany()
POST <index>/_update/<id> db.collection.updateOne()
db.collection.updateMany()
db.collection.replaceOne()
GET <index>/_doc/<id> db.collection.find()
批处理 _bulk_mget_msearch db.collection.insertMany()
db.collection.bulkWrite()

聚合

::: info 扩展阅读

:::

综合对比

RDBM Elasticsearch MongoDB
WHERE query $match
GROUP BYHAVING Bucket(桶聚合) $group$match
SELECT field $project
ORDER BY order $sort
LIMIT size $limit
SUM() sum $sum
COUNT() value_count $count
JOIN $lookup
SELECT INTO NEW_TABLE $out
MERGE INTO TABLE $merge
UNION ALL $unionWith

Elasticsearch 提供了极其丰富的聚合能力。

MongoDB 提供了丰富的聚合能力。

Elasticsearch 聚合

在 ES 中,不仅仅是普通搜索,相关性计算(评分)和聚合计算也是先在每个 shard 的本地进行计算,再由 coordinate node 进行汇总。由于分片的本地计算是独立的,只能基于数据子集来进行计算,所以难免出现数据偏差。

要解决聚合准确性问题,有两个解决方案:

  • 解决方案 1:当数据量不大的情况下,设置主分片数为 1,这意味着在数据全集上进行聚合。但这种方案不太现实。
  • 解决方案 2:设置 shard_size 参数,将计算数据范围变大,牺牲整体性能,提高精准度。shard_size 的默认值是 size * 1.5 + 10

Elasticsearch 将聚合分为三类:

MongoDB 聚合

MongoDB 使用 db.collection.aggregate() 方法分 阶段 进行聚合计算。

存储

逻辑存储

RDBM Elasticsearch MongoDB
database database
table index collection
row document document
column field field
index index

物理存储

MongoDB:MongoDB 的物理存储机制和 MySQL 较为相近。

  • 文件级存储: 一个 MongoDB 实例可以包含多个数据库,每个数据库对应一组 .wt 文件,集合和索引分散在这些文件中。
    • collection-*.wt: 存储集合数据的文件。
    • index-*.wt: 存储索引数据的文件。
    • WiredTiger.wt: 一个元数据文件,跟踪所有其他文件。
    • WiredTiger.lock: 锁文件,标识该数据目录正在被使用。
    • journal/: 预写事务日志目录。
  • 内存优先: 几乎所有操作都在解压后的缓存中进行,延迟写入磁盘。
    • 工作方式: 它缓存的是解压后的数据和索引的页(Page)。查询首先在缓存中查找,如果找不到(cache miss),才会从磁盘读取对应的页,解压后加载到缓存中。
    • 页面淘汰: 使用 LRU (Least Recently Used) 算法淘汰最久未使用的页。
  • 磁盘管理
    • **记录 (Record)**: 对应一个 BSON 文档及其头部信息。
    • **页 (Page)**: 磁盘 IO 的基本单位。一个页包含多个记录(文档)或索引项。
    • **区域 (Extent)**: 一组连续的页,分配给特定的集合或索引。
      • 当集合需要更多空间时,WiredTiger 会分配一个新的 Extent 给它。
      • 这种预分配策略有助于减少碎片和提高写入性能。
  • 持久化
    • oplog:服务层的逻辑日志,类似 MySQL 服务层的 binlog,用于主从同步,恢复数据。
    • Journal:WiredTiger 存储引擎的物理日志,类似 InnoDB 的 Redo Log,都是预写日志(Write-Ahead Log, WAL)的实现。
    • Checkpoint:MySQL 和 MongoDB 都会定期将内存中的修改批量、一致地写入磁盘文件,减少随机 IO。需要故障恢复时,也都是基于最后一个 Checkpoint,逐一重放操作,以恢复数据。

索引

Elasticsearch MongoDB
索引数据结构 字典树(FST) B+树
索引类型 倒排索引 单字段索引,复合索引,多键索引,全文搜索,地理空间索引,哈希索引
索引优化 覆盖索引、最左匹配原则

事务

复制

架构对比

特性 MySQL (以 InnoDB 集群为例) Elasticsearch MongoDB
复制单元 数据库 (Database) 索引 (Index)分片 集合 (Collection)
核心架构模型 **主从复制 (Master-Slave)**:主负责读写,从只负责读 **对等节点 (Peer-to-Peer)**:无中心主节点。任何节点都可接收请求并路由 **副本集 (Replica Set)**:主负责读写,从只负责读
节点角色 Primary & Replica:角色清晰固定 所有节点对等:但可配置专属角色(如 Master-eligible, Data, Ingest, Coordinating) Primary, Secondary, Arbiter:角色清晰,内置自动故障转移(通过心跳和选举)
数据同步方式 基于 Binlog 的逻辑复制:主节点将写操作记录到 Binlog,从节点拉取 Binlog,并重放(Replay)SQL 语句 基于 Translog 的段同步:主分片处理写请求,并将操作同步到副本分片 基于 Oplog 的逻辑复制:主节点将写操作记录到 Oplog,从节点异步拉取并重放这些操作
一致性模型 强一致性(默认):从节点默认异步复制,但可配置为半同步(至少一个从节点确认)以实现强一致性 最终一致性:默认异步复制,支持通过写入 consistency 参数来来控制写操作的一致性级别;通过 preference 参数来控制读一致性 最终一致性(默认):读写关注(Write Concern & Read Concern)可灵活配置,从最终一致到强一致(如 {w: "majority"}
自动故障转移 依赖外部组件:如 Group Replication 或 InnoDB Cluster 提供内置选主。传统主从依赖外部工具(MHA, Orchestrator) 内置:由主节点管理集群状态,并在节点失败时重新分配分片 内置:副本集成员通过心跳检测,自动触发选举产生新的主节点

相同点

  1. 核心目标一致:三者都为实现高可用(HA)灾难恢复(DR) 而设计,防止单点故障导致服务中断。
  2. 数据冗余:都是通过将数据复制到多个节点来实现数据冗余。
  3. 读写分离:都支持将读请求分发到副本节点,从而提升系统的整体读吞吐量。
  4. 异步复制为基:默认的复制方式都是异步的,以优先保证主节点的写入性能。
  5. 日志驱动:依赖于一种预写日志(WAL) 的变体来驱动复制:
    • MySQL → Binlog 和 Redo Log(InnoDB 引擎)
    • Elasticsearch → Translog
    • MongoDB → Oplog 和 Journal(WiredTiger 引擎)
  6. 提供一致性配置: 三者都提供了配置参数,允许用户在性能一致性之间进行权衡。

不同点

维度 MySQL Elasticsearch MongoDB 利弊分析
架构哲学 中心化、主从分明 去中心化、对等网络 中心化、内置自治 ES 的架构无单点瓶颈,更易于水平扩展。MySQL/MongoDB 的单一主节点简化了数据一致性管理,但主节点可能成为瓶颈和单点故障(需通过选主解决)。
配置与管理 相对复杂:传统主从配置繁琐;现代组复制/InnoDB 集群简化了操作,但依然较重 非常简单:开箱即用。节点加入集群后自动分配数据,运维成本极低 非常简单:副本集配置简单,内置自动化程度高,运维友好 ES & MongoDB 在易用性上胜出,MySQL 的复制生态更庞大但也更复杂
一致性控制 最强最灵活:支持全局事务(XA)、半同步复制,能轻松实现跨节点的强一致性 最弱:主要为搜索场景设计,偏向最终一致性。虽支持仲裁,但不像关系型数据库那样严格 灵活可调:通过读写关注可在最终一致和强一致之间平滑切换,适应多种场景 MySQL 是金融等强一致性场景的首选。MongoDB 提供了很好的灵活性。ES 不适合强一致性事务场景。
扩展性 读扩展性好,写扩展性差:可以通过添加只读副本来扩展读能力,但写操作始终只能在主节点上进行 读写扩展性极佳:通过分片将数据分散,读写都可以在多个分片上并行进行,真正实现水平扩展 读写扩展性好:结合分片集群,可以将数据分散到多个分片(每个分片是一个副本集),实现写的水平扩展。读扩展通过副本集本身实现 ESMongoDB(分片集群) 在应对海量数据和高并发写入方面天生优于 MySQL。MySQL 的写扩展需要通过应用层分库分表,复杂度高
延迟与性能 复制延迟可能导致从节点读到的数据是旧的 搜索性能极高,但数据同步延迟可能比数据库更高 复制延迟通常较低,Oplog 操作日志效率很高 ES 为搜索性能优化,可能牺牲部分实时性。MySQL/MongoDB 更注重数据的实时同步。
适用场景 强一致性、复杂事务的 OLTP 应用:如金融系统、电商核心交易系统 搜索、日志分析、OLAP:如商品检索、日志平台、大数据分析 灵活模型、高吞吐的 Web 应用:如内容管理系统、用户画像、实时分析 复制机制的设计直接反映了其目标场景。MySQL 为交易而生,ES 为搜索而生,MongoDB 为灵活扩展的现代应用而生。

小结

数据库 复制机制优势 复制机制劣势 典型使用场景
MySQL 强一致性保证,事务支持完备,生态成熟。 写扩展性困难,架构复杂,运维成本较高。 银行系统、会计软件、任何需要严格 ACID 事务的场景。
Elasticsearch 真正的水平扩展,读写性能极高,容错和恢复自动化程度极高,运维简单。 最终一致性,不支持事务,不保证数据的实时性。 全文搜索引擎、日志和指标分析、应用程序搜索。
MongoDB 扩展性良好(读和写),灵活性高(一致性可调),运维简单,内置自动故障转移。 默认最终一致性,多文档事务性能有损耗(相比 MySQL)。 物联网、内容管理、移动应用、实时分析。

如何选择:

  • 如果应用核心是「交易」和「强一致性」:选择 MySQL。它的复制机制为数据安全性和一致性提供了最坚实的基础。
  • 如果应用核心是「搜索」和「大数据分析」:选择 Elasticsearch。它的分布式对等架构为海量数据的查询和分析提供了无与伦比的性能和扩展性。
  • 如果应用需要「灵活的数据模型」、「快速迭代」和「水平扩展」,同时需要一定的一致性控制:选择 MongoDB。它在扩展性、一致性和易用性之间取得了最佳平衡。

分区

核心概念对比

特性维度 MySQL Elasticsearch MongoDB
分区目的 水平扩展写入能力,管理超大表 水平扩展读写能力,实现分布式计算 水平扩展读写能力,支持海量数据增长
分区/分片单元 表 (Table) 分片 (Shard)
一个独立的Lucene索引,是数据移动的基本单位。
块 (Chunk)
一个分片键值范围的连续数据段。默认大小64MB。
核心架构 需要外部中间件或自定义逻辑 原生集成,对应用完全透明 原生集成,对应用近乎透明
分片键 (Shard Key) **分区键 (Partition Key)**,在表定义时指定。 **路由键 (Routing Key)**,默认是 _id,可自定义。 **分片键 (Shard Key)**,在集合分片时指定,选择至关重要。
分片策略 范围分区 (RANGE)哈希分区 (HASH)KEY 分区 哈希分片 (默认),基于路由键的哈希值。 范围分片 (Ranged)哈希分片 (Hashed)混合分片 (Zoned)
数据分布目标 将数据拆分到不同物理文件, 便于管理和局部优化。并不自动分布到不同服务器 将数据均匀分布到集群所有节点,实现负载均衡和并行处理。 将数据均匀分布到分片集群的所有分片(Shard) 上,每个分片是一个副本集。
查询路由 应用层负责。应用必须知道如何将查询路由到正确的分区。 协调节点负责。应用可连接任意节点,节点自动路由查询。 mongos 路由器负责。应用连接 mongos,由它自动路由和聚合结果。
跨分片查询 极其困难。需要查询所有分区并手动合并结果,性能极差。 原生支持。搜索和聚合查询自动并行化,由协调节点汇总结果。 原生支持。多数查询通过 mongos 自动路由和聚合。但某些操作(如$lookup)受限。
再平衡 (Rebalance) 不支持自动再平衡。需要手动导出/导入数据,操作复杂且耗时。 自动再平衡
节点数变化后,ES 自动在节点间迁移分片,实现负载均衡
集群中的 master 节点负责所有元数据变更和分片分配决策
自动再平衡
当分片间的块数量差异超过某个阈值时触发
配置服务器(Config Server) 管理元数据,并触发平衡器迁移数据块

相同点

  1. 核心目标一致: 三者都为了突破单机硬件(CPU、内存、磁盘)的限制,通过将数据分散到多个节点来实现水平扩展
  2. 基于键值分区: 都要求选择一个或多个字段的值作为依据(分片键/分区键/路由键),通过这个值的哈希或范围来决定数据的具体位置。
  3. 面临类似挑战: 都需要解决跨分片查询数据分布均衡性事务支持(难度高)和集群管理的复杂性。

不同点

维度 MySQL Elasticsearch MongoDB 利弊分析
易用性与集成度 极低 自身分区功能弱,需借助中间件(如 Vitess, ShardingSphere)或应用层自己分库分表。 极高 开箱即用。创建索引时指定分片数即可,集群自动管理数据分布、查询路由和再平衡。 原生支持。需部署 mongos 和配置服务器,但一旦搭建完成,对应用透明。 ES > MongoDB > MySQL。ES 的分布式设计是骨子里的,体验最无缝。MySQL 的分片需要大量的开发和运维投入。
数据均衡与再平衡 手动 需要 DBA 手动干预数据迁移,过程繁琐且易出错。 全自动 是 ES 的核心优势之一。节点增减自动触发分片重平衡,无需人工干预。 自动 平衡器自动在分片间迁移数据块(Chunks) 以保持均衡。 ES 和 MongoDB 的自动再平衡是巨大优势,极大降低了运维成本。MySQL 在这方面几乎是空白的。
查询支持 极差 跨分片查询是噩梦。JOIN、ORDER BY + LIMIT 等操作几乎无法高效进行。 极佳 核心优势。所有搜索和聚合 API 都是为分布式设计,自动并行化,对用户无感。 良好 大多数 CRUD 操作都能被正确路由。但跨分片聚合、$lookup(表连接)性能较差。 ES 作为搜索引擎,在分布式查询上碾压其他两者。MongoDB 支持常见操作,但复杂操作受限。MySQL 的跨分片查询基本不可用。
分片键选择 影响管理,不影响性能 选择主要影响数据归档和管理(如按时间分区删除旧数据)。 影响性能 路由键影响数据分布的均匀性。自定义路由键可优化查询,将相关数据放在同一分片。 至关重要 一旦选择不可更改。直接影响性能、数据分布和扩展性。不合适的键会导致数据热点性能瓶颈 MongoDB 的分片键选择是“一次性”的重大架构决策,责任最大。ES 和 MySQL 相对灵活一些。
事务支持 强(单机) 在单分区内支持完整 ACID。跨分片事务需要借助中间件,复杂度高,性能差。 不支持 ACID 事务。提供部分原子性操作(如脚本更新)。 支持(多文档) 4.0+ 支持跨分片的多文档事务,但性能有损耗,默认有 60 秒超时限制。 MySQL 在单机事务上最强MongoDB 提供了跨分片事务的能力,是一个折中方案。ES 完全不考虑事务,这是为其搜索场景做的取舍。

小结

数据库 分区机制优势 分区机制劣势 典型使用场景
MySQL 单机性能强大,分区可用于数据生命周期管理(如高效删除旧数据)。 分片功能极其薄弱,需要大量外部工作和自定义开发,运维复杂度最高 单表数据量巨大且需要定期归档清理的场景(如日志表、事件表)。真正的水平扩展必须依赖中间件。
Elasticsearch 原生分布式,易用性顶级自动再平衡分布式查询能力无敌 不支持事务,不适合强一致性要求的 OLTP 场景。 搜索、日志、分析等海量数据读多写少的场景。天生为分布式查询而生。
MongoDB 原生分片,自动平衡,对应用透明。支持跨分片事务(有限制)。 分片键选择是永久且关键的,一旦选择错误代价巨大。复杂查询支持不如 ES。 需要水平扩展的 OLTP 类应用,数据模型灵活,读写吞吐量要求高。如游戏、物联网、内容平台。

如何选择:

  1. 如果主要需求是「搜索」和「分析」:选择 Elasticsearch。它的分区和分布式查询是业界的黄金标准,完全无需你操心数据如何分布和查询如何执行。
  2. 如果需要一个「可水平扩展的通用数据库」,用于现代应用:选择 MongoDB。它的分片集群是内置的,提供了良好的扩展性和灵活性,同时还能支持跨分片事务,适合各种 Web 和移动应用。
  3. 如果数据量很大但主要是「单机操作」,或需要「严格的单机事务」:选择 MySQL。可以使用其分区功能来管理大表,但不要指望它原生能提供分布式数据库的能力。真正的分片需要引入复杂的中间件,这通常是最后的选择。

故障恢复

核心机制对比

特性维度 Elasticsearch MongoDB
故障检测核心 Zen Discovery:自定义的节点发现和故障检测协议。主节点(Master-elected)负责监控集群状态。 心跳机制 (Heartbeat) 副本集成员间每2秒发送一次心跳包。
检测指标 节点存活状态、网络分区、分片分配状态 节点存活状态、优先级、Optime(操作时间戳)
故障恢复 重新选主 & 分片重分配
1. 选举新主节点
2. 新主节点将缺失的副本分片提升为主分片,并在其他节点上创建新的副本分片
自动故障转移 (Failover)
1. 剩余节点发起选举
2. 基于节点优先级、Optime 等规则选举出新主节点
选举算法 Bully-like 算法 基于节点ID和集群状态,更简单高效。 Raft 协议变体 在分布式共识和效率之间取得平衡,易于理解。
数据一致性保证 最终一致性 恢复期间可能读取旧数据,同步队列可能导致数据延迟。 最终一致性 -> 强一致可调 默认最终一致,但通过写关注 {w: "majority"} 可保证读己之写和强一致性。
恢复后数据同步 分片同步 新的副本分片从主分片拉取数据进行完整同步。 初始同步 & Oplog 重放 新节点先做全量同步,然后持续重放主节点的 Oplog 以保持数据最新。
运维复杂度 几乎全自动化,对用户透明,运维非常简单。 配置简单,但需要理解选举规则和优先级,运维比ES复杂但比MySQL简单。

相同点

  1. 基于心跳检测: 都依赖于节点间定期发送心跳包来检测对方是否存活。
  2. 自动选主: 在主节点故障时,都具备自动选举新主节点的能力,无需人工干预。
  3. 多数派原则: 都遵循“多数派”(Quorum)原则来避免脑裂(Split-Brain)。即集群必须拥有超过半数的投票节点在线才能正常进行主节点选举和数据写入,否则整个集群会进入只读或不可用状态以保护数据。

不同点

维度 Elasticsearch MongoDB 利弊分析
架构哲学 可用性与分区容错性优先 源自CAP理论的AP系统,优先保证服务可用性和扩展性,接受最终一致性。 灵活可调 在CAP中偏向CP(一致性+分区容错性),但通过读写关注允许应用选择一致性级别。 ES 为搜索性能和可用性牺牲一致性。MongoDB 试图在中间取得平衡。
故障检测粒度 分片级 & 节点级 不仅检测节点,更关注每个分片(数据副本)的状态,粒度更细。 节点级 关注副本集成员节点的状态。 ES 的检测粒度最细,因为它管理的是分片而非整个节点,恢复可以更精细。
恢复速度 非常快 选举速度快,且分片恢复是并行进行的,单个分片故障不影响其他分片。 Raft选举效率高,通常在10秒内完成故障转移。数据同步基于高效的Oplog。 ES 和 MongoDB 的恢复速度通常快于 MySQL,对业务影响更小。
脑裂 通过 minimum_master_nodes 配置防止 需要人工正确配置,配置不当有脑裂风险。7.x 后,由集群自动控制 通过选举规则避免 只有拥有最新数据(最高optime)的节点才可能当选为主,防止数据回退。 ES 需要人工配置保证,MongoDB 通过规则自动保证
数据冲突解决 最后写入获胜 基于版本号或时间戳,可能导致数据丢失。 基于Oplog顺序 复制是单向的(主->从),从根本上避免了写入冲突。 MySQL 和 MongoDB 能很好地避免数据冲突,ES 不擅长处理写入冲突。

小结

数据库 故障恢复优势 故障恢复劣势 典型使用场景
Elasticsearch 恢复自动化程度最高,速度最快,分片级故障隔离,集群扩展和恢复无比流畅。 只有最终一致性,故障期间和恢复后可能读到旧数据,有脑裂配置风险。 日志、监控、搜索等允许数据短暂不一致、但要求高可用和高吞吐的AP场景。
MongoDB 在一致性和可用性之间平衡良好,故障转移快(秒级),配置简单,支持可调一致性。 分片集群的恢复比副本集更复杂,可能会遇到性能抖动(jumbo chunks、平衡器运行)。 现代Web应用、物联网平台等需要高可用灵活数据模型,并能接受最终一致或配置强一致的场景。

如何选择:

  1. 如果业务要求是「数据绝对不能错」,宁可停止服务也要保证一致性:选择 MySQL。它的强一致性模型和基于共识的故障恢复机制为此而生。
  2. 如果业务要求是「服务绝对不能停」,可以接受秒级的数据延迟:选择 Elasticsearch。它的分布式设计和快速恢复能力能最大程度保证服务的可用性和连续性。非常适合可观测性场景。
  3. 如果需要一个「兼顾可用性与一致性」的通用数据库,希望故障恢复快速且对业务透明:选择 MongoDB。它在两者之间取得了最佳实践,故障转移速度快,并且通过读写关注给了开发者灵活选择的权利,适合大多数互联网应用。

Dubbo 面试之应用

简介

【简单】Dubbo 是什么?为什么使用 Dubbo?

Dubbo 是一款高性能、轻量级的开源 Java RPC 框架。

Dubbo 提供了三大核心能力:

  • 面向接口的远程过程调用(RPC):提供高性能的基于代理的远程调用能力,服务以接口为粒度,为开发者屏蔽远程调用底层细节。
  • 智能容错和负载均衡:内置多种负载均衡策略,智能感知下游节点健康状况,显著减少调用延迟,提高系统吞吐量。
  • 服务自动注册和发现:支持多种注册中心服务,服务实例上下线实时感知。

【简单】Dubbo3 有什么新特性?

Dubbo3 的核心新特性:

  • 新通信协议 - Triple - Triple 协议是 Dubbo3 设计的基于 HTTP 的 RPC 通信协议规范。它完全兼容 gRPC 协议,支持 Request-Response、Streaming 流式等通信模型,可同时运行在 HTTP/1 和 HTTP/2 之上
  • 应用级服务发现
    • 接口级服务发现,以接口为粒度将信息注册到注册中心。举例来说,如果有 10 个 RPC Provider,部署在 100 台机器实例上,就要注册 10 * 100 条数据。
    • 应用级服务发现,以应用为粒度将信息注册到注册中心。将信息进行了拆分:接口元数据信息、接口和应用的映射关系维护在元数据中心;应用信息维护在注册中心。这样的好处是,存储的数据量大大减少,则传输数据的 I/O 开销也随之显著减少。
  • Dubbo Mesh - 让 Dubbo 应用能够无缝接入 Istio 等业界主流服务网格产品。

扩展:技术创想 66 | Dubbo3.0 应用级服务注册原理

【简单】Dubbo 的配置方式有哪些?

Dubbo 支持多种配置方式,适用于不同开发场景:

配置方式 优点 缺点 适用场景
XML 结构清晰,易于维护 配置冗长 传统 Spring 项目
Properties 简单轻量 复杂配置不便 小型项目或少量配置
注解 代码简洁,集成方便 灵活性较低 Spring Boot/Cloud 项目
API 高度灵活,动态可控 代码侵入性强 框架集成或动态调整需求

XML 配置

  • 适用场景:传统 Spring 项目,配置直观但较冗长。

  • 示例

    1
    2
    3
    4
    <dubbo:application name="demo-provider"/>
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
    <dubbo:protocol name="dubbo" port="20880"/>
    <dubbo:service interface="com.example.DemoService" ref="demoService"/>

Properties 配置

  • 适用场景:简单项目,配置项较少时使用。

  • 示例application.properties):

    1
    2
    3
    4
    dubbo.application.name=demo-provider
    dubbo.registry.address=zookeeper://127.0.0.1:2181
    dubbo.protocol.name=dubbo
    dubbo.protocol.port=20880

Spring 注解配置

  • 适用场景:Spring Boot/Cloud 项目,简化 XML 配置。

  • 核心注解

    • @Service(暴露服务)
    • @Reference(引用服务)
  • 示例

    1
    2
    3
    4
    5
    @Service  // Dubbo 服务提供者
    public class DemoServiceImpl implements DemoService { ... }

    @Reference // Dubbo 服务消费者
    private DemoService demoService;

API 编程配置

  • 适用场景:动态配置、框架集成等需要灵活控制的场景。

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    ApplicationConfig app = new ApplicationConfig("demo-provider");
    RegistryConfig registry = new RegistryConfig("zookeeper://127.0.0.1:2181");
    ProtocolConfig protocol = new ProtocolConfig("dubbo", 20880);

    ServiceConfig<DemoService> service = new ServiceConfig<>();
    service.setInterface(DemoService.class);
    service.setRef(new DemoServiceImpl());
    service.export(); // 暴露服务

应用

【中等】Dubbo 中如何实现服务端与客户端的版本兼容?

版本和分组

Dubbo 服务中,接口并不能唯一确定一个服务,只有 接口+分组+版本号 的三元组才能唯一确定一个服务

  • 当同一个接口针对不同的业务场景、不同的使用需求或者不同的功能模块等场景,可使用服务分组来区分不同的实现方式。同时,这些不同实现所提供的服务是可并存的,也支持互相调用。
  • 当接口实现需要升级又要保留原有实现的情况下,即出现不兼容升级时,我们可以使用不同版本号进行区分。

下面以官方示例来解释一下如何指定版本。

假设,接口定义如下:

1
2
3
public interface DevelopService {
String invoke(String param);
}

版本 1 实现:

1
2
3
4
5
6
7
8
9
@DubboService(group = "group1", version = "1.0")
public class DevelopProviderServiceV1 implements DevelopService{
@Override
public String invoke(String param) {
StringBuilder s = new StringBuilder();
s.append("ServiceV1 param:").append(param);
return s.toString();
}
}

版本 2 实现:

1
2
3
4
5
6
7
8
9
@DubboService(group = "group2", version = "2.0")
public class DevelopProviderServiceV2 implements DevelopService{
@Override
public String invoke(String param) {
StringBuilder s = new StringBuilder();
s.append("ServiceV2 param:").append(param);
return s.toString();
}
}

跨版本升级

可以按照以下的步骤进行版本迁移:

  1. 在低压力时间段,先部署部分 Provider 新版本
  2. 再将所有 Consumer 升级为新版本
  3. 然后将剩下的一半提供者升级为新版本

当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。

参考用例 https://github.com/apache/dubbo-samples/tree/master/dubbo-samples-version

服务提供者

老版本服务提供者配置:

1
<dubbo:service interface="com.foo.BarService" version="1.0.0" />

新版本服务提供者配置:

1
<dubbo:service interface="com.foo.BarService" version="2.0.0" />

服务消费者

老版本服务消费者配置:

1
<dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />

新版本服务消费者配置:

1
<dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />

不区分版本

如果不需要区分版本,可以按照以下的方式配置:

1
<dubbo:reference id="barService" interface="com.foo.BarService" version="*" />

通过以上描述,可以看到,通过版本号来进行 Dubbo 接口升级实际上较为麻烦。如果接口提供方和消费方分属不同的业务团队,同步发版就更加麻烦了。因此,在实际应用中,更常见的操作是应该尽量充分考虑接口的后向兼容性,确保不会影响旧版本的调用。需要考虑的点如下:

  • 如果方法签名无任何变化,不会影响旧版本的调用。服务提供方可以直接先全量上线。
  • 如果入参、出参上新增属性,不会影响旧版本的调用(当然,对于新增属性的逻辑处理要充分考虑兼容性)。服务提供方可以直接先全量上线,消费方根据需要选择是否后续安排对接。
  • 如果入参、出参上删除或修改属性,会影响旧版本调用,可以新增接口。

扩展阅读:Dubbo 官方文档之版本与分组

【中等】Dubbo 中的分组(Group)是如何使用的?

核心作用

Dubbo 分组通过轻量级的逻辑隔离,在不增加物理部署成本的情况下实现服务治理能力。

  • 服务隔离:逻辑划分不同服务实例
  • 流量控制:实现定向路由和灰度发布

基础配置

1
2
3
4
5
<!-- 服务提供方 -->
<dubbo:service interface="com.example.DemoService" group="group1"/>

<!-- 服务消费方 -->
<dubbo:reference interface="com.example.DemoService" group="group1"/>

典型应用场景

场景 配置示例 作用说明
多版本 group="v1.0" 新旧版本服务共存
多环境 group="prod" 隔离生产/测试环境
灰度发布 group="canary" 定向流量到金丝雀版本

高级配置方式

  • 全局默认分组

    1
    2
    <dubbo:provider group="default-group"/>
    <dubbo:consumer group="default-group"/>
  • 动态分组(通过 RPC 上下文)

    1
    RpcContext.getContext().setAttachment("group", "dynamic-group");

最佳实践

  • 分组命名采用「业务_环境_版本」规范(如:payment_prod_v2)
  • 配合标签路由实现更精细的流量控制
  • 生产环境建议开启分组校验:
    1
    dubbo.provider.group-validation=true

【中等】Dubbo 中如何配置多协议、多注册中心?

有时服务会面对不同用户,支持多协议可以提高服务的兼容性和灵活性。

1
2
3
4
5
6
7
<!-- 声明两种协议 -->
<dubbo:protocol name="dubbo" port="20880"/>
<dubbo:protocol name="rest" port="8080"/>

<!-- 为不同服务指定协议 -->
<dubbo:service interface="com.example.UserService" protocol="dubbo"/>
<dubbo:service interface="com.example.ApiService" protocol="rest"/>

【中等】Dubbo 中如何配置多注册中心?

多注册中心可以提高服务的可用性以及容灾能力,任一中心宕机不影响服务注册和发现。

1
2
3
4
5
6
<!-- 声明两个注册中心 -->
<dubbo:registry id="zookeeper1" address="zookeeper://192.168.1.1:2181"/>
<dubbo:registry id="zookeeper2" address="zookeeper://192.168.1.2:2181"/>

<!-- 服务同时注册到两个中心 -->
<dubbo:service interface="com.example.OrderService" registry="zookeeper1,zookeeper2"/>

要点:

  • 注册中心 ID 需唯一,用逗号分隔可指定多个
  • 消费端无需特殊配置,自动发现所有注册中心的服务

故障排查

【中等】Dubbo 的超时问题如何排查与调优?

核心排查步骤

  1. 明确超时位置

    • 区分是消费端超时(TimeoutException)还是服务端处理超时
    • 检查报错日志中的side标识(consumer/provider)
  2. 关键配置检查

    1
    2
    3
    4
    5
    6
    7
    # 服务端配置
    dubbo.provider.timeout=3000 # 默认服务超时时间
    dubbo.provider.executes=200 # 最大并发执行数

    # 消费端配置
    dubbo.consumer.timeout=1000 # 调用超时时间(优先级更高)
    dubbo.reference.timeout=2000 # 方法级超时配置
  3. 监控指标分析

    • 观察RT(响应时间)分布:P90/P99 是否接近超时阈值
    • 检查TPS与线程池活跃度:是否达到executes限制

常见问题场景

问题类型 典型表现 解决方案
网络抖动 偶发超时,伴随 Connection 异常 增大超时时间+重试机制
服务端阻塞 RT 曲线陡增 优化 SQL/缓存+线程池扩容
消费端配置不合理 特定服务超时 调整方法级 timeout
级联超时 多层服务同时超时 设置合理超时阶梯+熔断降级

调优方案

  1. 分层超时设置

    1
    2
    3
    4
    <!-- 基础服务设置长超时 -->
    <dubbo:reference interface="BaseService" timeout="5000"/>
    <!-- 聚合服务设置短超时 -->
    <dubbo:reference interface="AggregateService" timeout="1000"/>
  2. 动态调整策略

    1
    2
    // 通过 RpcContext 动态设置
    RpcContext.getContext().set("timeout", 2000);
  3. 线程池优化

    1
    2
    3
    4
    5
    dubbo:
    provider:
    threads: 200 # IO 线程数
    threadpool: cached # 弹性线程池
    queues: 0 # 不堆积请求
1
2
3
4
5
6
7
8

4. **熔断降级配合**

```xml
<!-- 结合 Sentinel 实现自动熔断 -->
<dubbo:reference>
<dubbo:method name="query" sentinel="true"/>
</dubbo:reference>

高级排查工具

(1)Arthas 诊断

1
2
# 监控方法执行时间
watch com.example.ServiceImpl * '{params,returnObj}' -x 3 -n 5 -b

(2)全链路追踪

1
2
3
4
5
6
7
8
// 在 Filter 中记录关键节点耗时
long start = System.currentTimeMillis();
try {
return invoker.invoke(inv);
} finally {
log.info("Method {} cost {}ms", inv.getMethodName(),
System.currentTimeMillis() - start);
}

最佳实践建议

  1. 超时公式参考

    1
    理想超时时间 = 平均 RT × 3 + 安全余量 (200~500ms)
  2. 配置优先级原则

    1
    2
    方法级 > 接口级 > 全局配置
    消费端配置 > 服务端配置
  3. 生产环境推荐

    • 所有服务显式声明超时时间
    • 核心服务设置timeout="3000" retries="0"
    • 非核心服务设置timeout="1000" retries="1"

:超时时间不是越长越好,需要平衡用户体验和系统资源占用。建议通过压测确定合理阈值。

【中等】如何在 Dubbo 中优化网络通信性能?

核心优化措施

  1. 序列化优化

    • 优先选用Kryo(高性能)或Protobuf(跨语言)
    • 避免使用 Java 原生序列化
    1
    <dubbo:protocol serialization="kryo"/>
  2. 连接管理

    • 强制启用长连接复用
    1
    2
    3
    4
    5
    dubbo:
    protocol:
    keepalive: true
    consumer:
    connections: 10 # 每个服务维持的连接数
  3. 网络参数调优

    1
    2
    3
    4
    # Netty 参数优化
    io.netty.allocator.type=pooled
    io.netty.noPreferDirect=true
    dubbo.protocol.payload=8388608 # 8MB 最大包

进阶优化手段

优化方向 具体实施 预期收益
数据压缩 启用gzip压缩(>1KB 数据有效) 带宽减少 30%-70%
异步 IO 配置dispatcher=message 吞吐量提升 20%-40%
批量调用 实现BatchInvoker接口 RPS 提升 50%+
EPoll 模式 -Dio.netty.epoll.enabled=true(Linux) 延迟降低 10%-15%

关键配置示例

  1. 服务提供方配置

    1
    2
    3
    4
    5
    6
    7
    8
    @Bean
    public ProtocolConfig protocolConfig() {
    ProtocolConfig config = new ProtocolConfig();
    config.setThreads(200); // IO 线程数
    config.setBufferSize(16384); // 16KB 缓冲区
    config.setAccepts(1000); // 最大连接数
    return config;
    }
1
2
3
4
5
6

2. **消费方超时控制**
```xml
<dubbo:reference timeout="1000">
<dubbo:method name="query" timeout="500"/>
</dubbo:reference>

性能验证指标

  1. 关键监控点

    • 网络吞吐量:netstat -s | grep segments
    • 线程池状态:DubboPREFIX.thread.pool.active.count
    • 序列化耗时:DubboPREFIX.serialize.time
  2. 压测建议

    1
    2
    # 模拟不同数据包大小 (1K/10K/1M)
    jmeter -n -t dubbo_perf.jmx -l result.csv

最佳实践:建议先进行基准测试(1K/10K/100K 数据包),逐步调整参数。典型优化效果:

  • 小包场景:TPS 提升 30%-50%
  • 大包场景:吞吐量提升 2-3 倍
  • 延迟敏感场景:P99 降低 20%-40%

【中等】如何调试 Dubbo 的服务调用失败问题?

快速定位步骤

  1. 错误类型识别

    • TimeoutException:调用超时(网络/服务端阻塞)
    • RpcException:RPC 协议错误(序列化/版本不匹配)
    • NoProviderException:服务未注册/下线
  2. 关键日志检查

    1
    2
    # 查看 Dubbo 错误日志(通常包含错误根源)
    grep -E "Exception|ERROR" dubbo.log

常见问题诊断表

错误现象 可能原因 排查工具
持续 NoProvider 注册中心异常/服务未发布 telnet registryIP 2181
偶发 Timeout 网络抖动/服务端 Full GC ping+jstat -gc PID
序列化失败 参数类型不匹配 Arthas watch参数检查
线程池耗尽 服务端并发过高 dubbo-admin线程池监控

深度排查工具

(1)Arthas 诊断

1
2
3
4
5
# 检查服务提供者状态
watch com.xxx.ServiceImpl * '{params,returnObj,throwExp}' -x 3

# 跟踪调用链路
trace com.alibaba.dubbo.rpc.filter.ExceptionFilter

(2)网络分析

1
2
3
4
5
# 检查网络连通性
tcpping providerIP 20880

# 抓包分析(需 sudo 权限)
tcpdump -i eth0 port 20880 -w dubbo.pcap

(3)注册中心检查

1
2
# Zookeeper 服务列表查询
ls /dubbo/com.xxx.Service/providers

典型解决方案

(1)服务不可用场景

1
2
<!-- 增加重试机制 -->
<dubbo:reference retries="2" cluster="failfast"/>

(2)性能瓶颈场景

1
2
3
4
5
6
dubbo:
provider:
threads: 500 # 扩大线程池
accepts: 1000 # 增加连接数
protocol:
payload: 52428800 # 增大传输包限制 (50MB)

(3)版本冲突场景

1
2
<!-- 明确指定版本 -->
<dubbo:reference version="1.2.0"/>

预防建议

(1)监控配置

1
2
3
# 开启 Dubbo QoS 在线诊断
dubbo.application.qos.enable=true
dubbo.application.qos.port=22222

(2)日志增强

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Activate
public class ErrorLogFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation inv) {
try {
return invoker.invoke(inv);
} catch (Exception e) {
log.error("RPC 失败:{}.{}, 参数:{}",
invoker.getInterface(),
inv.getMethodName(),
Arrays.toString(inv.getArguments()));
throw e;
}
}
}

:建议结合 APM 工具(SkyWalking/Pinpoint)建立全链路监控,80%的调用失败问题可通过监控指标提前预警。

【中等】Dubbo 的序列化异常如何解决?

核心解决步骤

  1. 依赖检查:确保序列化库(如 Kryo/FastJson)版本一致,排除冲突。

  2. 序列化合规性

    • 所有传输类需实现Serializable接口
  • 非序列化字段用transient标记
  1. 版本与配置统一:服务端/客户端使用相同序列化协议(如 Hessian2)

    1
    <dubbo:protocol serialization="kryo"/>
  2. 日志分析:通过错误日志定位具体异常类(如NotSerializableException

高阶优化方案

  • 自定义序列化器:实现ObjectInput/ObjectOutput接口处理特殊对象

  • 性能选型

    协议 性能 稳定性 适用场景
    Kryo ★★★ ★★ 高性能内部调用
    Hessian2 ★★ ★★★ 跨语言兼容场景
  • 调试技巧

    • 显式定义serialVersionUID防版本冲突
    • 抓包对比序列化前后数据一致性

典型报错处理

1
2
3
4
5
// 示例:字段缺失 Serializable 导致的异常
public class User implements Serializable {
private transient Address addr; // 避免序列化
private static final long serialVersionUID = 1L; // 显式声明 UID
}

:生产环境推荐 Hessian2 作为默认协议平衡稳定性与性能,关键服务建议压测验证序列化性能。

【中等】Dubbo 的服务无法发现,可能的原因有哪些?

注册中心→提供者→消费者→网络→版本顺序排查,结合日志与工具快速定位问题。多数情况由配置不一致网络隔离导致。

核心排查方向

问题类型 关键检查点 验证方法
注册中心问题 - 注册中心(Zookeeper/Nacos)是否运行
- 网络连通性(telnet 检测端口)
- 配置地址是否正确
telnet 注册中心 IP 端口
查看注册中心控制台服务列表
服务提供者问题 - @Service/XML 配置是否正确
- 服务启动日志是否有报错
- 是否注册到正确分组/版本
检查 Dubbo 启动日志
netstat -tlnp确认服务端口监听
服务消费者问题 - 引用配置(接口名/版本/组)是否匹配
- 依赖冲突(如 Dubbo 多版本)
- 消费者缓存未更新
对比提供者/消费者配置
清理消费者本地缓存(rm -rf ~/.dubbo/
网络问题 - 防火墙/安全组策略
- DNS 解析问题
- 跨机房网络延迟
ping/traceroute测试
检查 iptables 规则
版本不匹配 - 接口版本号(version)是否一致
- 方法签名变更未同步
对比提供者与消费者的@Reference(version="x.x")

高频问题解决方案

  • 注册中心连接失败

    1
    2
    <!-- 检查配置示例 -->
    <dubbo:registry address="zookeeper://192.168.1.100:2181" timeout="3000"/>

    确保:

    • 地址协议前缀正确(如zookeeper://nacos://
    • 超时时间足够(默认 1000ms 可能太短)
  • 服务未注册成功

    1
    2
    @Service(version = "1.0.0", group = "order") // 提供者注解
    @Reference(version = "1.0.0", group = "order") // 消费者注解

    确保:

    • 版本号(version)和分组(group)完全匹配
    • 接口包路径一致(避免 IDE 自动导入错误包)
  • 消费者缓存脏数据

    1
    2
    3
    # 清理 Dubbo 本地缓存
    rm -rf ~/.dubbo/ # Linux/Mac
    del /s /q %USERPROFILE%\.dubbo # Windows

进阶诊断工具

  • 开启 Dubbo 调试日志

    1
    2
    # application.properties
    logging.level.org.apache.dubbo=DEBUG
    • 观察服务注册/订阅日志
    • 检查Invoker转换异常
  • 使用 Telnet 直连调试

    1
    2
    3
    telnet 服务提供者 IP 20880
    > ls -l # 列出所有服务
    > invoke 接口名。方法名(参数) # 手动测试调用
  • 注册中心控制台

    • ZookeeperzkCli.sh查看/dubbo/接口名/providers节点
    • Nacos:控制台检查服务列表是否可见

预防建议

  • 标准化配置:使用 Maven 属性管理版本号,避免手动配置不一致
    1
    2
    3
    <properties>
    <dubbo.version>2.7.15</dubbo.version>
    </properties>
  • 健康检查:集成 Spring Boot Actuator 监控 Dubbo 服务状态
  • 灰度发布:通过group区分环境(如group="prod"/group="test"

【中等】Dubbo 的服务上线后无法调用,可能的原因有哪些?

网络问题

检查方法:

  • ping 测试网络连通性。
  • telnet/nc 检查端口是否开放(如 Dubbo 默认端口 20880)。
  • traceroute 分析网络路径是否异常。

服务注册失败

排查步骤:

  • 确认注册中心(如 Zookeeper)是否正常运行,使用 zkCli.sh 查看节点。
  • 检查 <dubbo:registry address="..."> 配置是否正确。
  • 查看服务提供者日志,确认是否报注册失败错误。

服务依赖问题

关键点:

  • 确保 Maven 依赖无冲突(特别是 Dubbo 版本)。
  • 关注日志中的 ClassNotFoundExceptionNoClassDefFoundError

消费者配置错误

常见错误:

  • 版本号不一致:<dubbo:reference version="..."> 需与提供者匹配。
  • 分组不一致:检查 group 配置是否一致。

防火墙拦截

解决方案:

  • 开放 Dubbo 服务端口(如 20880)。
  • 检查云服务器安全组或本地防火墙规则(如 iptables)。

代码/配置错误

重点检查:

  • XML 配置:<dubbo:service><dubbo:reference> 等标签参数是否正确。
  • 注解配置:@Service@Reference 是否被 Spring 扫描到。

扩展工具与技巧

  • 注册中心调试:通过 Zookeeper 命令(ls /dubbo/服务名)查看注册情况。
  • Dubbo Admin:使用控制台查看服务状态和调用关系。
  • 日志分析:开启 Dubbo 调试日志(logger.org.apache.dubbo=DEBUG)定位问题。

参考资料

Dubbo 面试之服务治理

服务注册和发现

【中等】什么是服务注册与发现?Dubbo 如何实现?

::: info 什么是服务注册与发现?
:::

服务注册与发现是微服务的核心基础设施,通过解耦服务地址硬编码,实现动态扩缩容故障自动恢复

  • 服务注册(Registration):服务提供者(Provider)启动时,将自己的 IP、端口、接口名 等信息上报到注册中心(如 Zookeeper/Nacos)。举例订单服务启动后,向注册中心注册:"order-service: 192.168.1.100:8080"
  • 服务发现(Discovery):服务消费者(Consumer)从注册中心 拉取可用服务列表,并基于负载均衡策略选择目标实例。举例支付服务需要调用订单服务时,从注册中心获取所有可用的order-service节点列表。

注册阶段

1
2
graph LR
Provider-->|1. 注册地址|Registry(Zookeeper/Nacos)

发现阶段

1
2
3
graph LR
Consumer-->|2. 拉取服务列表|Registry
Consumer-->|3. 调用目标 Provider|Provider

::: info 有哪些常见的注册中心?
:::

主流注册中心对比

类型 代表产品 特点
CP 型 Zookeeper/Consul/Etcd 强一致性,适合金融类业务
AP 型 Eureka 高可用优先,适合互联网场景
混合型 Nacos 可调整副本数、同步策略,来确立偏重

Dubbo 中可以通过 registry 配置来指定注册中心。

1
2
3
4
5
<!-- 服务提供者注册 -->
<dubbo:service interface="com.example.OrderService" ref="orderService" registry="zookeeper://127.0.0.1:2181"/>

<!-- 服务消费者发现 -->
<dubbo:reference id="orderService" interface="com.example.OrderService" registry="zookeeper://127.0.0.1:2181"/>

【简单】Dubbo 支持哪些注册中心?

不同于传统的 Dubbo2,Dubbo3 中定义了三种中心:注册中心、配置中心、元数据中心。配置中心、元数据中心是实现 Dubbo 高阶服务治理能力会依赖的组件,如流量管控规则等,相比于注册中心通常这两个组件的配置是可选的。

配置方式如下:

1
2
3
4
5
6
7
dubbo
registry
address: nacos://localhost:8848
config-center
address: nacos://localhost:8848
metadata-report
address: nacos://localhost:8848

需要注意的是,对于部分注册中心类型(如 Zookeeper、Nacos 等),Dubbo 会默认同时将其用作元数据中心和配置中心(建议保持默认开启状态)。

Dubbo 目前支持的主流注册中心实现包括:

  • Zookeeper
  • Nacos
  • Redis
  • Consul
  • Etcd
  • 更多实现

同时也支持 Kubernetes、Mesh 体系的服务发现,具体请参考 使用教程 - kubernetes 部署

【简单】注册中心挂了可以继续通信吗?

可以。Dubbo 消费者在应用启动时会从注册中心拉取已注册的生产者的地址接口,并缓存在本地。每次调用时,按照本地存储的地址进行调用。

【中等】注册中心是选择 CP 还是 AP?

::: info 什么是 CAP?
:::

在分布式系统领域,有一个著名的 CAP 理论。CAP 定理提出:分布式系统有三个指标,这三个指标不能同时做到:

  • 一致性(Consistency) - 在任何给定时间,网络中的所有节点都具有完全相同(最近)的值。
  • 可用性(Availability) - 对网络的每个请求都会返回响应,但不能保证返回的数据是最新的。
  • 分区容错性(Partition Tolerance) - 即使任意数量的节点出现故障,网络仍会继续运行。

CAP 就是取 Consistency、Availability、Partition Tolerance 的首字母而命名。

在分布式系统中,分区容错性是一个既定的事实:因为分布式系统总会出现各种各样的问题,如由于网络原因而导致节点失联;发生机器故障;机器重启或升级等等。因此,CAP 定理实际上是要在可用性(A)和一致性(C)之间做权衡

::: info 注册中心选 AP 还是 CP?
:::

注册中心作为服务提供者和服务消费者之间沟通的桥梁,它的重要性不言而喻。所以注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。

根据 CAP 理论,三种特性无法同时达成,必须在可用性和一致性之间做取舍。于是,根据不同侧重点,注册中心可以分为 CP 和 AP 两个阵营:

  • CP 型注册中心 - 牺牲可用性来换取数据强一致性,最典型的例子就是 ZooKeeper,etcd,Consul 了。ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。而 etcd 和 Consul 集群内都是通过 Raft 协议来保证强一致性,如果出现脑裂的话, 注册中心也不可用。
  • AP 型注册中心 - 牺牲一致性(只保证最终一致性)来换取可用性,最典型的例子就是 Eureka 了。Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。
  • CP & AP 都支持型注册中心 - Nacos 的内在设计偏向于 CP,即在发生网络分区的情况下优先保证数据的一致性和分区容错性,牺牲一定的可用性。虽然 Nacos 的内在设计偏向于 CP,但通过合理的配置与实践,可以在一定程度上优化其可用性。例如:调整副本数、配置同步策略。更多详情可以参考:Nacos CAP

选择 CP 还是 AP,根据实际需要来定:如果业务场景要求强一致,优先选择 CP 型注册中心;如果业务场景强调可用性,优先选择 AP 型注册中心。

::: info 注册中心选型对比

:::

注册中心 特点 适用场景
Zookeeper CP 系统,强一致性,高延迟 对一致性要求高的传统项目
Nacos AP/CP 可切换,支持动态配置 云原生、微服务架构
Consul 多数据中心,健康检查完善 跨机房服务发现

【中等】Dubbo 的服务自动上线与下线机制是怎样的?

::: info 服务自动上线流程

:::

  • 启动注册
  • 服务提供者启动时,解析配置文件(如 dubbo:service 或注解 @Service),获取服务接口、方法、版本、分组等信息。
    • 将服务元数据(如 IP、端口、接口名)注册到注册中心(如 Zookeeper/Nacos)。
    • 注册中心存储服务信息,形成服务目录(如 Zookeeper 的 /dubbo/{service}/providers 节点)。
  • 消费者发现
    • 消费者启动时,从注册中心拉取服务提供者列表,并建立长连接。
    • 注册中心推送变更通知(如新服务上线),消费者动态更新本地服务列表。

::: info 服务自动下线流程

:::

  • 主动下线
  • 服务提供者正常关闭时,触发 Shutdown Hook,向注册中心发送注销请求。
    • 注册中心删除对应节点,消费者通过事件监听感知服务下线。
  • 被动下线
    • 心跳检测:若提供者宕机,注册中心未收到心跳(如 Zookeeper 的 Session 超时),自动剔除故障节点。
    • 消费者容错:已连接的消费者通过故障处理机制(如 Failover)切换至其他可用节点。

::: info 关键保障机制

:::

  • 心跳保活:默认心跳间隔 60 秒(可调),超时时间建议 3 倍心跳间隔。
  • 重试容错:消费者支持 retries 配置(如 Failover 策略),避免单点故障。
  • 优雅停机:通过 ProtocolConfig.destroy() 确保注销完成后再终止 JVM。

::: info 高级治理能力

:::

  • 版本灰度:通过 version 字段实现多版本共存,逐步下线旧版本。
  • 权重调整:动态修改服务权重(如 Nacos 控制台),实现平滑流量迁移。
  • 无损下线:结合 QOS 命令(offline)或 PreStop Hook,确保流量完全迁移后再下线。

::: info 常见问题排查

:::

  • 服务未注册

    • 检查注册中心地址、网络连通性。
    • 查看提供者日志是否有 RegistryFailedException
  • 僵尸节点

    • 手动清理注册中心残留节点(如 Zookeeper 的 delete /dubbo/{service}/providers/xxx)。
    • 调整心跳超时时间,避免因网络抖动误判。
  • 消费者未感知下线

    • 确认注册中心支持事件推送(如 Nacos 的 notify 机制)。
    • 检查消费者是否配置了 check=false(禁用启动时强依赖检查)。

::: info 最佳实践

:::

  • 生产环境建议
    • 使用 Nacos 作为注册中心,兼顾可用性和动态配置能力。
    • 开启 Dubbo Admin 监控,实时查看服务状态。
  • 停机操作:先通过 telnet 127.0.0.1 20880 执行 invoke offline() 命令,再重启服务。
  • 版本迭代:采用 version="1.0.0"group="canary" 分批次上线,降低风险。

【困难】Dubbo3 的应用级服务发现的工作原理是什么?

【应用级服务发现】是 Dubbo3 引入的新特性,旨在解决大规模微服务架构下 注册中心压力大服务治理效率低 的问题。其核心思想是 以应用为维度注册实例,而非传统接口级注册。

应用级注册通过 注册与元数据分离,将注册中心的数据量降低至常数级,显著提升了大规模微服务架构的可扩展性。其核心创新在于:

  • 注册中心只存应用实例,元数据独立存储;
  • 消费者按需加载接口信息,减少网络开销;
  • 完美兼容旧模式,支持渐进式迁移。

与传统接口级注册的对比

维度 Dubbo2(接口级) Dubbo3(应用级)
注册单位 每个服务接口单独注册(如 com.example.UserService 整个应用的所有接口统一注册(如 user-service-app
注册数据量 随接口数量线性增长(100 接口=100 条注册记录) 恒定(1 个应用=1 条注册记录)
服务发现粒度 接口级别 应用级别(消费者按需拉取接口元数据)

::: info 接口级服务发现工作原理

:::

Dubbo3 以前的版本采用的是接口级服务发现。

interface-data1

Provider 部署的应用中通常会有多个 Service,每个 service 都可能会有其独有的配置。Service 服务发布的过程,其实就是基于这个服务配置生成地址 URL 的过程,生成的地址数据如图所示。

注册中心的地址数据存储结构,以 Service 服务名为数据划分依据,将一个服务下的所有地址数据都作为子节点进行聚合,子节点的内容就是实际可访问的 ip 地址,也就是我们 Dubbo 中 URL,格式就是刚才 Provider 实例生成的。

interface-data2

这里把 URL 地址数据划分成了几份:

  • 实例可访问地址:主要信息包含 ip 和 port,消费端将基于这条数据生成 tcp 网络链接,作为后续 RPC 数据的传输载体。
  • RPC 元数据:元数据用于定义和描述一次 RPC 请求。它可以分为两类:
    • 具体的 RPC 服务信息:如版本号、分组以及方法相关信息;
    • RPC 配置数据:控制 RPC 调用的行为,同步 Provider 进程实例的状态(如超时时间、数据编码的序列化方式等)。
  • 自定义元数据:用户可任意扩展并添加自定义元数据。

综上,有以下结论:

  1. 服务发现聚合的 key 就是 RPC 粒度的服务
  2. 注册中心同步的数据不止包含地址,还包含了各种元数据以及配置
  3. 得益于 1 与 2,Dubbo 实现了支持应用、RPC 服务、方法粒度的服务治理能力

这就是一直以来 Dubbo2 在易用性、服务治理功能性、可扩展性上强于很多服务框架的真正原因。

interface-defect

接口级注册的易用性是有代价的,它限制了整体架构的扩展性,在大规模 Dubbo 集群中尤为凸显。其突出问题如下:

  • 注册中心集群容量会成为瓶颈:由于所有的 URL 地址数据都被发送到注册中心,注册中心的存储容量达到上限,推送效率也随之下降。
  • 消费端资源消耗较大:在消费端,Dubbo2 框架常驻内存已超 40%,每次地址推送带来的 cpu 等资源消耗率也非常高,影响正常的业务调用。

为什么会出现这个问题?举例来说,假设有一个普通的 Dubbo Provider 应用,该应用内部定义有 10 个 RPC Service,应用被部署在 100 个机器实例上。这个应用在集群中产生的数据量将会是 Service 数 * 机器实例数,也就是 10 * 100 = 1000 条数据。数据会从两个维度放大:

  • 从地址角度。100 条唯一的实例地址,被放大 10 倍
  • 从服务角度。10 条唯一的服务元数据,被放大 100 倍

::: info 应用级服务发现工作原理

:::

app-metadataservice

提供者服务注册

  • 启动时,应用(如 user-service-app)将所有服务接口的 元数据(方法列表、协议等)上报至 元数据中心(如 Nacos)。
  • 仅将 应用名+实例 IP+端口 注册到 注册中心(如 ZooKeeper),不包含接口信息

消费者服务发现

  • 订阅目标应用
    • 消费者(如 order-service-app)从注册中心获取目标应用(如 user-service-app)的 实例列表(IP+端口)。
    • 不直接获取接口信息,避免注册中心数据膨胀。
  • 按需拉取元数据
    • 消费者首次调用前,从 元数据中心 拉取目标应用的接口元数据(如 UserService 的方法签名)。
    • 本地缓存元数据,后续调用直接使用。

调用过程

  • 消费者通过 应用名+接口名 定位实例,发起 RPC 调用(协议兼容 Dubbo2)。
  • 负载均衡在应用实例级别进行(如随机选择 user-service-app 的一个实例)。

关键设计优势

  • 注册中心轻量化:实例数从 O(接口数×实例数) 降至 O(实例数),适合万级节点集群。
  • 动态扩容高效:新增接口无需重复注册,只需更新元数据中心。
  • 兼容性保障:支持与 Dubbo2 的接口级注册共存,平滑升级。

扩展:Dubbo3 应用级服务发现设计

负载均衡

【中等】Dubbo 支持哪些负载均衡方式?各有什么利弊?

Dubbo 提供了多种均衡策略,缺省为 weighted random 基于权重的随机负载均衡策略。

具体实现上,Dubbo 提供的是客户端负载均衡,即由 Consumer 通过负载均衡算法得出需要将请求提交到哪个 Provider 实例。

目前 Dubbo 内置了如下负载均衡算法,可通过调整配置项启用。

算法 算法类型 说明
Random 加权随机 默认算法,默认权重相同
RoundRobin 加权轮询 借鉴于 Nginx 的平滑加权轮询算法,默认权重相同,
LeastActive 最少活跃优先 + 加权随机 背后是能者多劳的思想
ShortestResponse 最短响应优先 + 加权随机 更加关注响应速度
ConsistentHash 一致性哈希 确定的入参,确定的提供者,适用于有状态请求
P2C Power of Two Choice 随机选择两个节点后,继续选择“连接数”较小的那个节点。
Adaptive 自适应负载均衡 在 P2C 算法基础上,选择二者中 load 最小的那个节点

Dubbo 的负载均衡配置可以细粒度到服务、方法级别,且 dubbo:servicedubbo:reference 均可配置。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 服务端服务级别 -->
<dubbo:service interface="..." loadbalance="roundrobin" />
<!-- 客户端服务级别 -->
<dubbo:reference interface="..." loadbalance="roundrobin" />
<!-- 服务端方法级别 -->
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
<!-- 客户端方法级别 -->
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>

::: info Random(随机)

:::

  • 加权随机,按权重设置随机概率。
  • 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
  • 缺点:存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

::: info RoundRobin(轮询)

:::

  • 加权轮询,按公约后的权重设置轮询比率,循环调用节点
  • 缺点:同样存在慢的提供者累积请求的问题。

::: info LeastActive(最少活跃优先)

:::

  • 加权最少活跃调用优先,活跃数越低,越优先调用,相同活跃数的进行加权随机。活跃数指调用前后计数差(针对特定提供者:请求发送数 - 响应返回数),表示特定提供者的任务堆积量,活跃数越低,代表该提供者处理能力越强。
  • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大;相对的,处理能力越强的节点,处理更多的请求。

::: info ShortestResponse(最短响应优先)
:::

  • 加权最短响应优先,在最近一个滑动窗口中,响应时间越短,越优先调用。相同响应时间的进行加权随机。
  • 使得响应时间越快的提供者,处理更多的请求。
  • 缺点:可能会造成流量过于集中于高性能节点的问题。

这里的响应时间 = 某个提供者在窗口时间内的平均响应时间,窗口时间默认是 30s。

::: info ConsistentHash(一致性 Hash)
:::

  • 一致性 Hash,相同参数的请求总是发到同一提供者。
  • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
  • 算法参见:Consistent Hashing | WIKIPEDIA
  • 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
  • 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />

::: info P2C
:::

Power of Two Choice 算法简单但是经典,主要思路如下:

  1. 对于每次调用,从可用的 provider 列表中做两次随机选择,选出两个节点 providerA 和 providerB。
  2. 比较 providerA 和 providerB 两个节点,选择其“当前正在处理的连接数”较小的那个节点。

以下是 Dubbo P2C 算法实现提案

::: info Adaptive(自适应)
:::

Adaptive 即自适应负载均衡,是一种能根据后端实例负载自动调整流量分布的算法实现,它总是尝试将请求转发到负载最小的节点。

以下是 Dubbo Adaptive 算法实现提案

扩展:

服务路由

【中等】Dubbo 如何进行服务路由控制?

路由规则

策略类型 配置方式 匹配维度 适用场景
条件路由 DSL 表达式/IP 段匹配 方法名、参数、来源 IP 灰度发布、环境隔离
标签路由 实例元数据标记 逻辑分组(如地域、版本) 金丝雀发布、A/B 测试
脚本路由 Groovy/JS 脚本 复杂业务逻辑 动态分流(如 VIP 用户)

配置方案

条件路由强化

1
2
3
4
# 多条件组合路由(支持&&,||,! 运算符)
conditions:
- 'method=createOrder && userLevel=VIP => 192.168.1.100'
- 'headers.appVersion=3.2.* => region=hangzhou'

动态生效技巧:通过force:false实现软路由,当目标节点不可用时自动降级

标签路由进阶

1
2
// 编程式打标(配合配置中心)
RpcContext.getContext().setAttachment("traffic-tag", "experimental");

最佳实践:建立标签命名规范(如env=prodversion=2.0

架构图

1
2
3
4
5
graph TB
Admin[管理控制台] -->|推送规则| ConfigCenter[配置中心]
ConfigCenter -->|实时同步| Provider[服务节点]
Consumer -->|路由决策| RouterChain[路由链]
RouterChain -->|过滤| LB[负载均衡]

动态管控

  • 规则存储:建议使用 Nacos/Apollo 替代 Zookeeper,支持规则版本回溯
  • 实时生效:消费者节点秒级感知(长连接推送)
  • 兜底策略:本地缓存最后有效规则,避免配置中心不可用

生产级解决方案

场景:跨机房流量调度

1
2
3
4
5
6
7
8
9
# 基于地域标签的路由
tags:
- name: 'region=shanghai'
addresses: ['10.1.1.1-10.1.1.20']
- name: 'region=beijing'
addresses: ['10.2.1.1-10.2.1.20']
rules:
- '=> region=shanghai' # 默认路由
- 'headers.forceZone=beijing => region=beijing'

性能优化

  • 路由缓存:本地缓存路由决策结果(TTL 5s)
  • 预计算:在服务目录更新时提前生成路由快照

异常处理机制

1
2
3
4
5
6
7
8
// 自定义路由失败处理器
public class CustomRouterFailover implements RouterListener {
@Override
public void onRouteFail(Invoker<?> invoker) {
Metrics.counter("route_fail").increment();
// 自动切换备用集群。..
}
}

监控指标设计

指标名称 采集方式 告警阈值
路由命中率 采样统计(每 1min) <95% 触发警告
规则变更频率 配置中心事件监听 >5 次/分钟告警
跨域调用比例 标签路由计数器 超出预设范围报警

与治理功能联动

  • 负载均衡:路由结果作为 LB 的优先权重依据
  • 限流:按路由维度设置独立限流规则
  • 熔断:路由异常节点自动加入熔断黑名单

实施建议

  • 先通过<dubbo:parameter key="router" value="tag" />启用基础路由
  • 使用 Arthas 的watch命令实时观察路由决策过程
  • 在预发布环境进行规则压测(模拟 1000 次/秒规则变更)

【中等】Dubbo 路由是怎样工作的?

以下是 Dubbo 单个路由器的工作过程,路由器接收一个服务的实例地址集合作为输入,基于请求上下文 (Request Context) 和 (Router Rule) 实际的路由规则定义对输入地址进行匹配,所有匹配成功的实例组成一个地址子集,最终地址子集作为输出结果继续交给下一个路由器或者负载均衡组件处理。

Router

通常,在 Dubbo 中,多个路由器组成一条路由链共同协作,前一个路由器的输出作为另一个路由器的输入,经过层层路由规则筛选后,最终生成有效的地址集合。

  • Dubbo 中的每个服务都有一条完全独立的路由链,每个服务的路由链组成可能不通,处理的规则各异,各个服务间互不影响。
  • 对单条路由链而言,即使每次输入的地址集合相同,根据每次请求上下文的不同,生成的地址子集结果也可能不同。

Router

【中等】Dubbo 支持哪些路由方式?分别适用于什么场景?

Dubbo 的路由规则可以基于应用、服务、方法、参数等粒度精准的控制请求分发,根据请求的目标服务、方法以及请求体中的其他附加参数进行匹配,符合匹配条件的请求会进一步的按照特定规则转发到一个地址子集。

Dubbo 支持以下路由规则:

  • 标签路由规则
  • 条件路由规则
  • 脚本路由规则
  • 动态配置规则

::: info 标签路由规则

:::

标签路由通过将某一个服务的实例划分到不同的分组约束具有特定标签的流量只能在指定分组中流转,不同分组为不同的流量场景服务,从而实现流量隔离的目的。标签路由可以作为蓝绿发布、灰度发布等场景能力的基础

标签路由规则是一个非此即彼的流量隔离方案,也就是匹配标签的请求会 100% 转发到有相同标签的实例,没有匹配标签的请求会 100% 转发到其余未匹配的实例。如果您需要按比例的流量调度方案,请参考示例 基于权重的按比例流量路由

标签主要是指对 Provider 端应用实例的分组,目前有两种方式可以完成实例分组,分别是动态规则打标和静态规则打标。动态规则打标可以在运行时动态的圈住一组机器实例,而静态规则打标则需要实例重启后才能生效,其中,动态规则相较于静态规则优先级更高,而当两种规则同时存在且出现冲突时,将以动态规则为准。

::: info 条件路由规则

:::

条件路由与标签路由的工作模式非常相似,也是首先对请求中的参数进行匹配,符合匹配条件的请求将被转发到包含特定实例地址列表的子集。相比于标签路由,条件路由的匹配方式更灵活:

  • 在标签路由中,一旦给某一台或几台机器实例打了标签,则这部分实例就会被立马从通用流量集合中移除,不同标签之间不会再有交集。有点类似下图,地址集合在输入阶段就已经划分明确。

tag-condition-compare

  • 而从条件路由的视角,所有的实例都是一致的,路由过程中不存在分组隔离的问题,每次路由过滤都是基于全量地址中执行

tag-condition-compare

条件路由规则的主体 conditions 主要包含两部分内容:

  • => 之前的为请求参数匹配条件,指定的匹配条件指定的参数将与消费者的请求上下文 (URL)、甚至方法参数进行对比,当消费者满足匹配条件时,对该消费者执行后面的地址子集过滤规则。
  • => 之后的为地址子集过滤条件,指定的过滤条件指定的参数将与**提供者实例地址 (URL) **进行对比,消费者最终只能拿到符合过滤条件的实例列表,从而确保流量只会发送到符合条件的地址子集。
    • 如果匹配条件为空,表示对所有请求生效,如:=> status != staging
    • 如果过滤条件为空,表示禁止来自相应请求的访问,如:application = product =>

::: info 脚本路由规则

:::

脚本路由是最直观的路由方式,同时它也是当前最灵活的路由规则,因为你可以在脚本中定义任意的地址筛选规则。如果我们为某个服务定义一条脚本规则,则后续所有请求都会先执行一遍这个脚本,脚本过滤出来的地址即为请求允许发送到的、有效的地址集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
configVersion: v3.0
key: demo-provider
type: javascript
enabled: true
script: |
(function route(invokers,invocation,context) {
var result = new java.util.ArrayList(invokers.size());
for (i = 0; i < invokers.size(); i ++) {
if ("10.20.3.3".equals(invokers.get(i).getUrl().getHost())) {
result.add(invokers.get(i));
}
}
return result;
} (invokers, invocation, context)); // 表示立即执行方法

::: info 动态配置规则

:::

通过 Dubbo 提供的动态配置规则,可以动态的修改 Dubbo 服务进程的运行时行为,整个过程不需要重启,配置参数实时生效。基于这个强大的功能,基本上所有运行期参数都可以动态调整,比如超时时间、临时开启 Access Log、修改 Tracing 采样率、调整限流降级参数、负载均衡、线程池配置、日志等级、给机器实例动态打标签等。与上文讲到的流量管控规则类似,动态配置规则支持应用、服务两个粒度,也就是说一次可以选择只调整应用中的某一个或几个服务的参数配置。

当然,出于系统稳定性、安全性的考量,有些特定的参数是不允许动态修改的,但除此之外,基本上所有参数都允许动态修改,很多强大的运行态能力都可以通过这个规则实现。通常 URL 地址中的参数均可以修改,这在每个语言实现的参考手册里也记录了一些更详细的说明。

【中等】如何在 Dubbo 中使用直连提供者?

核心配置方式

  • XML 配置
1
2
3
4
<dubbo:reference
id="myService"
interface="com.example.MyService"
url="dubbo://192.168.1.100:20880/com.example.MyService"/>
  • 注解配置
1
2
@Reference(url = "dubbo://192.168.1.100:20880/com.example.MyService")
private MyService myService;

关键特性

特性 说明
协议格式 dubbo://IP:PORT/接口全限定名
绕过注册中心 直接连接指定服务节点,避免注册中心复杂度
即时生效 配置修改后无需重启服务

适用场景

  • 开发调试:快速连接本地/测试环境服务
  • 问题排查:临时绕过注册中心验证服务可用性
  • 单元测试:固定服务端点保证测试稳定性

注意事项

  • 生产环境风险

    • 丧失服务发现能力(无法自动感知节点上下线)
    • 缺少负载均衡和熔断机制
    • 配置维护成本高(需手动管理所有节点地址)
  • 安全建议

    • 限制直连 IP 白名单
    • 避免在配置文件中硬编码地址(建议使用环境变量)
    1
    <dubbo:reference url="dubbo://${PROVIDER_IP}:20880/com.example.MyService"/>

调试技巧

  • 动态切换:通过 JVM 参数临时启用直连
1
-Dcom.example.MyService.url=dubbo://127.0.0.1:20880
  • 组合使用:保留注册中心配置,通过url属性覆盖特定服务
1
2
<dubbo:registry address="nacos://localhost:8848"/>
<dubbo:reference url="dubbo://192.168.1.100:20880" interface="com.example.MyService"/>

最佳实践:建议仅在非生产环境使用直连模式,生产环境应通过注册中心实现服务发现和治理。

流量控制

【中等】Dubbo 中的流量控制策略有哪些?

流量控制策略主要包括:限流、熔断、降级

Dubbo 中,可以通过集成 Hystrix 或 Sentinel 来实现限流、熔断、降级。

【中等】什么是 Dubbo 的 Mock 机制?如何使用?

Dubbo 的 Mock 机制是一种用于服务降级的功能。当远程调用失败或不稳定时,通过 Mock 机制可以返回预先定义的结果,从而保证服务的可用性。

除了 Dubbo,很多分布式框架和微服务架构都会提供类似的服务降级功能。比如 Spring Cloud 里面的 Hystrix,可以通过配置降级策略,在服务异常时返回降级数据。

配置方式

类型 实现方式 示例
全局配置 XML 配置 <dubbo:reference mock="true" interface="com.xx.DemoService"/>
注解配置 @Reference 注解 @Reference(mock = "true") private DemoService demoService;
自定义 Mock 实现 Mock 类

高级配置

  • 强制 Mockmock="force:return empty"(直接返回 Mock 结果)
  • 失败 Mockmock="fail:return null"(仅调用失败时生效)
  • 方法级 Mockmock="return {methodName}"(指定方法 Mock)

典型应用场景

  • 容灾场景:网络故障/服务不可用时的兜底方案
  • 开发测试:解耦依赖服务,提升开发效率
  • 性能保障:非核心服务降级,保障核心链路

注意事项

  • 生产环境建议
    • 配合超时配置使用:<dubbo:reference timeout="3000" mock="true"/>
    • 避免写操作接口使用 Mock
  • 性能影响
    • Mock 类应保持简单逻辑
    • 复杂 Mock 可能成为性能瓶颈

与其他方案对比

方案 触发条件 粒度 实现复杂度
Dubbo Mock 调用失败 方法级
熔断器 错误率阈值 服务级
限流 流量阈值 系统级

故障处理

【中等】如何在 Dubbo 中使用健康检查?

Dubbo 通过 多级健康检查机制 保障服务可用性,开发者可根据业务需求选择默认配置或扩展自定义检查逻辑。

健康检查方式

类型 触发条件 适用场景 配置示例
服务提供者探活 定时心跳检测(默认 3 秒) 快速发现宕机节点 <dubbo:provider heartbeat="5000"/>
注册中心剔除 长连接断开后自动摘除(如 Zookeeper) 防止调用失效节点 无需配置,依赖注册中心能力
接口级检查 自定义HealthCheck接口实现 精细化业务健康状态(如依赖 DB) 实现org.apache.dubbo.health.HealthChecker

关键配置参数

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 服务端配置示例 -->
<dubbo:provider
heartbeat="3000" <!-- 心跳间隔毫秒-->
heartbeat-timeout="60000" <!-- 超时剔除时间 -->
checks="true" <!-- 开启消费者检查(默认 true) -->
/>

<!-- 客户端配置示例 -->
<dubbo:consumer
check="false" <!-- 启动时不强制检查提供者默认为 true 阻塞启动-->
stale-check="true" <!-- 启用陈旧节点检查 -->
/>

自定义健康检查(高级)

步骤

  1. 实现HealthChecker接口:

    1
    2
    3
    4
    5
    6
    public class DbHealthChecker implements HealthChecker {
    @Override
    public boolean isHealthy() {
    return checkDatabaseConnection(); // 自定义检查逻辑
    }
    }
  2. SPI 注册(在META-INF/dubbo/org.apache.dubbo.health.HealthChecker文件添加):

    1
    dbHealth=com.example.DbHealthChecker

监控与运维建议

  • 日志监控:关注HeartbeatFailedEvent告警日志
  • 组合策略
    • 生产环境建议同时启用 心跳检测 + 注册中心剔除
    • 关键服务补充 接口级检查(如数据库/缓存连接)
  • 压测注意:高频心跳可能增加注册中心负载,需调整heartbeat参数平衡敏感度与性能

【中等】Dubbo 有哪些集群容错策略?

在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。

Dubbo 容错

图中节点关系说明:

  • 这里的 InvokerProvider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息
  • Directory 代表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
  • ClusterDirectory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
  • Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等
  • LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选

Dubbo 支持的容错策略:

  • Failover - 失败自动切换。当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。
  • Failfast - 快速失败。只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
  • Failsafe - 失败安全。出现异常时,直接忽略。通常用于写入审计日志等操作。
  • Failback - 失败自动恢复。后台记录失败请求,定时重发。通常用于消息通知操作。
  • Forking - 并行调用多个服务器。只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。
  • Broadcast - 广播调用所有提供者。逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

集群容错配置示例:

1
2
<dubbo:service cluster="failsafe" />
<dubbo:reference cluster="failsafe" />

可观测

【中等】Dubbo 提供了哪些监控能力?

Dubbo 内部维护了多个纬度的可观测指标,并且支持多种方式的可视化监测。可观测性指标从总体上来说分为三个度量纬度:

  • Admin - Admin 控制台可视化展示了集群中的应用、服务、实例及依赖关系,支持流量治理规则下发,同时还提供如服务测试、mock、文档管理等提升研发测试效率的工具。
  • Metrics - Dubbo 统计了一系列的流量指标如 QPS、RT、成功请求数、失败请求数等,还包括一系列的内部组件状态如线程池数、服务健康状态等。
  • Tracing - Dubbo 与业界主流的链路追踪工作做了适配,包括 Skywalking、Zipkin、Jaeger 都支持 Dubbo 服务的链路追踪。
  • Logging - Dubbo 支持多种日志框架适配。以 Java 体系为例,支持包括 Slf4j、Log4j2、Log4j、Logback、Jcl 等,用户可以基于业务需要选择合适的框架;同时 Dubbo 还支持 Access Log 记录请求踪迹。

【中等】Dubbo 的 Monitor 是如何工作的?

Dubbo Monitor 在设计上兼顾低侵入性、高性能、实时性。

核心职责

  • 数据采集:通过MonitorFilter拦截每次服务调用,记录:调用次数、调用时间、耗时、成功数、失败数、异常等信息。
  • 数据上报:采用非阻塞方式上报统计信息到监控中心,避免影响业务性能。
  • 存储和分析:监控中心可以是第三方监控系统,如:Zabbix、Prometheus 或 Dubbo 内置的轻量级实现,持久化存储数据。
  • 可视化:可视化可以选择 Dubbo 提供的轻量级实现 Dubbo Admin,也可以集成第三方可视化 Dashboard。

关键组件

  • MonitorFilter
    • 调用前:记录开始时间。
    • 调用后:计算耗时,封装监控数据(成功/失败状态)。
    • 上报:通过MonitorProtocol异步发送数据。
  • MonitorProtocol:专为监控设计的轻量协议,支持批量上报,降低网络开销。

高可用与扩展

  • 多实例部署:监控中心集群化,避免单点故障。
  • 弹性扩展:采用分布式存储(如 ES、时序数据库)应对高流量。

监控数据流

1
服务调用 → MonitorFilter 拦截 → 记录指标 → 异步上报 → Monitor 中心 → 存储 → 可视化

【困难】如何在 Dubbo 中处理服务调用链路追踪?

核心实现步骤

  1. 集成追踪框架

    • 选择 SkyWalking/Zipkin/Jaeger 等工具
  • 添加对应依赖(如skywalking-agent.jar
  1. 配置 Dubbo 过滤器

    1
    2
    3
    <!-- 全局启用追踪过滤器 -->
    <dubbo:provider filter="tracingFilter"/>
    <dubbo:consumer filter="tracingFilter"/>
  2. 实现追踪过滤器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Activate(group = {PROVIDER, CONSUMER})
    public class TracingFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
    // 1. 从 invocation 获取/生成 TraceID
    String traceId = getOrCreateTraceId(inv);

    // 2. 记录开始时间(RPC 上下文/ThreadLocal)
    long start = System.currentTimeMillis();

    try {
    // 3. 传递追踪上下文(通过 RpcContext)
    RpcContext.getContext().setAttachment("traceId", traceId);

    // 4. 执行实际调用
    return invoker.invoke(inv);
    } finally {
    // 5. 上报追踪数据
    report(traceId, inv, start);
    }
    }
    }

关键设计要点

  1. 上下文传播

    • 通过RpcContext传递TraceIDSpanID
    • 跨服务时自动携带 HTTP Headers/Dubbo Attachments
  2. 异步上报

    1
    2
    3
    4
    // 示例:使用异步线程池上报
    executor.submit(() -> {
    tracer.report(new Span(traceId, duration));
    });
  3. 采样控制

    1
    2
    # 在配置文件中控制采样率
    dubbo.tracing.sample-rate=0.1

主流方案对比

框架 数据存储 可视化 特点
SkyWalking ES/H2 原生 UI 零侵入、APM 集成度高
Zipkin Cassandra/ES Zipkin UI 轻量级、部署简单
Jaeger Cassandra/Kafka Jaeger UI Uber 开源、支持大规模集群

生产建议

  1. 性能优化

    • 采用异步批量上报(如 Jaeger 的 gRPC reporter)
    • 对高频服务启用采样(1%~10%)
  2. 异常处理

    1
    2
    3
    4
    5
    6
    7
    try {
    return invoker.invoke(inv);
    } catch (Exception e) {
    // 标记错误 span
    span.tag("error", e.getMessage());
    throw e;
    }
  3. 扩展功能

    1
    2
    3
    4
    // 添加自定义标签
    span.tag("region", "north-1");
    // 记录业务指标
    span.log("order_amount", 100);

:新版 Dubbo 已内置 Tracing 支持,可通过dubbo-spring-boot-starter快速集成,推荐优先使用官方方案。

参考资料

Java 虚拟机面试二

垃圾收集

【困难】如何判断 Java 对象是否可以被回收?

判断 Java 对象是否可以被回收有两种方法:

  • 引用计数法
  • 可达性分析法

引用计数法

引用计数算法(Reference Counting)的原理是:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

引用计数算法简单高效,但是存在循环引用问题——两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。

1
2
3
4
5
6
7
8
9
10
public class ReferenceCountingGC {
public Object instance = null;

public static void main(String[] args) {
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
}
}

因为循环引用的存在,所以 Java 虚拟机不适用引用计数算法

可达性分析法

通过 GC Roots 作为起始点进行搜索,JVM 将能够到达到的对象视为存活,不可达的对象视为死亡

可作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象(如当前方法局部变量)。
  • 本地方法栈(JNI)中引用的 Native 对象。
  • 方法区中静态属性(static字段)引用的对象。
  • 方法区中常量(final常量)引用的对象。

方法区的回收条件

主要回收废弃常量不再使用的类

不再使用的类定义如下:

  • Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已被回收。
  • 该类对应的 Class 对象无任何地方引用(如反射)。

以上为类卸载必要条件,且全部满足也不一定被卸载

常见内存泄漏场景

内存泄漏的本质是对象无法回收,常见的有以下情况:

  • 静态容器(如 static HashMap)持有对象。
  • 未关闭的资源(如数据库连接、流)。
  • 监听器未注销。
  • 不合理使用 finalize() 导致对象复活。

【中等】为什么不建议使用 finalize()?

finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。finalize() 方法是 Java 提供的对象被垃圾回收前最后的自救机会(在 GC 时被调用一次)。

  • 调用时机:对象被标记为垃圾后、实际回收前,由 JVM 的垃圾回收线程触发(不保证立即执行)。
  • 自救机制:在 finalize() 中重新让对象被引用(如赋值给静态变量),可避免本次回收。
  • 风险
    • 执行时机不确定,可能永远不调用
    • 性能差(延迟回收),易导致内存泄漏

**不要使用 finalize()**!在 Java 9 后,finalize() 直接被标记为 @Deprecated。推荐用 try-with-resources 或显式调用 close() 管理资源。

【中等】Java 对象有哪些引用类型?

在 Java 中,对象的引用类型决定了它们如何被垃圾回收(GC)处理,主要分为 4 种引用类型,按强度从高到低排列如下:

引用类型 回收时机 是否可获取对象(get() 典型用途
强引用 永不回收(除非显式置空) 常规对象
软引用 内存不足时 缓存
弱引用 GC 运行时 避免内存泄漏(如 WeakHashMap
虚引用 GC 运行时 对象回收跟踪(如堆外内存管理)

(1)强引用(Strong Reference)

被强引用关联的对象不会被垃圾收集器回收。

强引用:使用 new 一个新对象的方式来创建强引用。

1
Object obj = new Object(); // 强引用

回收条件:当 obj = null 或超出作用域时,对象变为可回收状态。

(2)软引用(Soft Reference)

被软引用关联的对象,只有在内存不够的情况下才会被回收。

软引用:使用 SoftReference 类来创建软引用。

1
2
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get(); // 可能返回 null(如果被回收)

用途:适合实现缓存(如图片缓存)。

(3)弱引用(Weak Reference)

被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。

使用 WeakReference 类来实现弱引用

1
2
3
WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.gc();
Object obj = weakRef.get(); // 通常返回 null

用途:适合临时缓存(如 WeakHashMap 的键)、避免内存泄漏。

(4)虚引用(Phantom Reference)

虚引用又称为幽灵引用或者幻影引用。

无法通过虚引用获取对象(get() 始终返回 null):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

使用 PhantomReference 来实现虚引用。

1
2
3
4
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
System.gc();
Reference<?> ref = queue.poll(); // 不为 null 说明对象被回收

用途:管理堆外内存(如 NIO 的 DirectByteBuffer)。

对比

  1. 强引用是默认方式,其他引用需显式使用 java.lang.ref 包下的类。
  2. 软引用 vs 弱引用
    • 软引用适合保留缓存直到内存紧张;
    • 弱引用立即释放,避免内存泄漏。
  3. 虚引用的唯一用途是关联 ReferenceQueue,用于对象回收后的通知。

通过合理选择引用类型,可以优化内存管理并避免内存泄漏问题。

【中等】Java 中有哪些垃圾回收算法?

Java 中的垃圾回收(GC)算法主要分为以下几类,每种算法针对不同的场景和内存区域(如年轻代、老年代)进行优化。

垃圾收集的性能指标主要有两点:

  • 停顿时间 - 停顿时间是因为 GC 而导致程序不能工作的时间长度。
  • 吞吐量 - 吞吐量关注在特定的时间周期内一个应用的工作量的最大值。对关注吞吐量的应用来说长暂停时间是可以接受的。由于高吞吐量的应用关注的基准在更长周期时间上,所以快速响应时间不在考虑之内。

以下是核心算法及其特点的概括:

标记-清除算法(Mark-Sweep)

  • 原理
    1. 标记:从 GC Roots 出发,标记所有可达对象。
    2. 清除:遍历堆内存,回收未被标记的对象。
  • 缺点
    • 产生内存碎片(不连续空间),可能导致大对象分配失败。
    • 效率较低(需遍历全堆)。
  • 适用场景:老年代(如 CMS 回收器的初始阶段)。

标记-整理算法(Mark-Compact)

  • 原理
    1. 标记:与标记-清除相同,标记可达对象。
    2. 整理:将存活对象向内存一端移动,清理边界外空间。
  • 优点:避免内存碎片。
  • 缺点:移动对象开销大(需更新引用地址)。
  • 适用场景:适合老年代,对象存活率高(如 Serial Old、Parallel Old 回收器)。

复制算法(Copying)

  • 原理
    • 将内存分为两块(FromTo 空间),每次只使用一块。
    • GC 时将存活对象从 From 复制到 To 空间,并清空 From
  • 优点
    • 无内存碎片。
    • 高效(仅复制存活对象)。
  • 缺点:内存利用率仅 50%(需预留一半空间)。
  • 适用场景:年轻代(如 Serial、ParNew 等回收器),因年轻代对象存活率低。
  • 优化:实际 JVM 将年轻代分为 EdenSurvivor(From/To) 区(比例通常为 8:1:1),通过多次复制到 Survivor 区避免浪费。

分代收集算法(Generational Collection)

分代收集是 JVM 在吞吐量、延迟和内存占用之间找到的经典平衡点,而新一代 GC 则通过更复杂的并发机制尝试突破其限制。

跨代引用处理:使用记忆集Remembered Set)记录老年代对年轻代的引用,避免全堆扫描。

根据对象存活周期将堆分为年轻代老年代,对不同区域采用不同算法:

  • 年轻代:复制算法(因对象朝生夕死,存活率低)。
  • 老年代:标记-清除或标记-整理(因对象存活率高)。
  • 永久代:这部分就是早期 Hotspot JVM 的方法区实现方式了,储存 Java 类元数据、常量池、Intern 字符串缓存。在 JDK 8 之后就不存在永久代这块儿了。

分区算法(Region-Based)

  • 原理:将堆划分为多个独立区域(如 G1 的 Region),优先回收垃圾最多的区域。
  • 优点
    • 控制每次回收的区域数量,减少停顿时间(STW)。
    • 适合大内存应用(如 G1、ZGC、Shenandoah)。

增量算法(Incremental)

  • 目标:减少单次 GC 停顿时间,通过分阶段执行 GC 与用户线程交替运行。
  • 缺点
    • 线程切换开销大,整体吞吐量可能下降。
  • 现代实现:如 CMS 的并发标记阶段。

常见垃圾回收器与算法对应

回收器 新生代算法 老年代算法 特点
Serial 复制 标记-整理 单线程,STW 时间长。
ParNew 复制 标记-整理 Serial 的多线程版。
Parallel Scavenge 复制 标记-整理 吞吐量优先。
CMS - 标记-清除(并发) 低延迟,但内存碎片多。
G1 复制 + 分区 标记-整理 + 分区 兼顾吞吐与延迟,Region 分区。
ZGC/Shenandoah 复制 + 分区 标记-整理 + 分区 亚毫秒级停顿,并发标记/整理。

现代 JVM 趋向于使用分代+分区+并发的复合算法(如 G1),在吞吐量和延迟之间取得平衡。

【中等】Java 中常见的垃圾收集器有哪些?

img

以下是 Java 主要垃圾收集器的详细对比表格,涵盖算法、特点、适用场景和关键参数:

垃圾收集器 分类 算法 目标 适用场景 JDK 版本 启用参数 优缺点
Serial GC 串行 新生代:复制
老年代:标记-整理
简单低开销 单核、客户端应用、小堆 所有版本 -XX:+UseSerialGC ✅ 内存占用小
❌ 全程 STW,延迟高
Parallel Scavenge 并行(吞吐优先) 新生代:复制 高吞吐量 后台计算、多核大堆 JDK 1.4+ -XX:+UseParallelGC ✅ 吞吐量高
❌ 停顿时间较长
Parallel Old 并行(吞吐优先) 老年代:标记-整理 配合 Parallel Scavenge 与 Parallel Scavenge 搭配使用 JDK 6+ -XX:+UseParallelOldGC ✅ 老年代并行回收
❌ 仍以吞吐优先,延迟较高
ParNew 并行 新生代:复制 低停顿(与 CMS 配合) 需与 CMS 搭配的多核环境 JDK 1.4+ -XX:+UseParNewGC ✅ 多线程版 Serial GC
❌ 仅新生代,需搭配 CMS
CMS 并发(低延迟) 老年代:标记-清除 最小化停顿时间 老年代低延迟应用 JDK 1.4-14 -XX:+UseConcMarkSweepGC ✅ 并发收集,低停顿
❌ 内存碎片、并发模式失败风险
G1 分区+并发 标记-整理(分 Region) 平衡吞吐与延迟 大堆(数十 GB)、JDK 8+ 默认 JDK 7+(JDK 9+默认) -XX:+UseG1GC ✅ 可预测停顿、大堆友好
❌ 内存占用略高
ZGC 并发 染色指针+读屏障 亚毫秒级停顿(<10ms) 超大堆(TB 级)、云原生 JDK 11+ -XX:+UseZGC ✅ 极低停顿、堆大小几乎无限制
❌ JDK 11+ 支持,兼容性要求高
Shenandoah 并发 转发指针+读屏障 低延迟(与 ZGC 竞争) Red Hat 系、低延迟大堆 JDK 12+ -XX:+UseShenandoahGC ✅ 并发压缩、无停顿扩展
❌ 非 Oracle 官方默认

关键对比维度

  • 吞吐量:Parallel GC(Parallel Scavenge + Parallel Old)最优。
  • 延迟:ZGC/Shenandoah < G1 < CMS < Parallel GC。
  • 堆大小
    • 小堆(<4GB):Serial GC / Parallel GC。
    • 大堆(4GB~数十 GB):G1。
    • 超大堆(TB 级):ZGC/Shenandoah。
  • 版本兼容性
    • JDK 8:默认 Parallel GC,可选 G1/CMS(CMS 已废弃)。
    • JDK 11+:默认 G1,可选 ZGC/Shenandoah。

选择建议

  • 常规服务端应用:JDK 8 用 G1,JDK 11+ 用 ZGC(若需超低延迟)。
  • 批处理任务Parallel GC(高吞吐优先)。
  • 资源受限环境Serial GC(如嵌入式设备)。
  • 兼容性测试:JDK 11+ 可试用 Shenandoah(非 Oracle 官方构建需注意)。

通过此表格可快速定位适合业务需求的 GC 组合。

【困难】为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?

Java 8 用元空间替代永久代,解决了 PermGen 固定大小易导致内存溢出和垃圾回收效率低的问题。元空间使用本地内存,具备更灵活的内存分配能力,提升了垃圾收集和内存管理的效率。

永久代(PermGen)的主要问题

  • 固定大小限制:永久代大小通过 -XX:MaxPermSize 设定,默认较小(64MB~128MB),易触发 OutOfMemoryError: PermGen space,尤其是动态加载类过多时(如频繁部署的 Web 应用)。
  • 垃圾回收效率低:永久代与老年代共用垃圾回收机制(Full GC 时才会回收),类卸载条件苛刻(需类加载器被回收)。
  • 内存管理不灵活:永久代在 JVM 堆内分配,与对象堆共享内存空间,易导致堆内存碎片化。

元空间(Metaspace)的优势

  • 使用本地内存(Native Memory):元空间直接分配在操作系统的本地内存中,默认无上限(仅受系统物理内存限制),避免 PermGen 大小硬限制问题。可通过 -XX:MaxMetaspaceSize 设置上限(如不设置则动态扩展)。
  • 自动调整大小:元空间可以根据需要自动扩展大小,从而降低了 OOM 的风险。
  • 性能优化:元空间由于在堆外,因此减少了 Full GC 触发频率。避免了频繁回收 PermGen 时的停顿。

永久代 vs. 元空间

特性 永久代(PermGen) 元空间(Metaspace)
存储位置 JVM 堆内存 本地内存(Native Memory)
大小限制 -XX:MaxPermSize 固定上限 默认无上限,可设 -XX:MaxMetaspaceSize
垃圾回收 依赖 Full GC 独立回收,条件更宽松
OOM 错误 OutOfMemoryError: PermGen space OutOfMemoryError: Metaspace

【困难】Java 中的 Young GC、Old GC、Full GC 和 Mixed GC 的区别是什么?

Young GC

Young GC 又称为 YGC 或 Minor GC,即年轻代垃圾回收

  • 目标:高效回收短期对象,减少全局停顿时间,避免频繁扫描老年代。
  • 作用范围:仅回收 年轻代(Eden + Survivor 区,即 S0/S1)。
  • 触发条件:当年轻代内存(尤其是 Eden 区)被填满时触发。
    • Eden 区空间不足:新对象优先分配在 Eden 区,Eden 满时触发 YGC。
    • Eden + Survivor 区空间不足:若 Eden + Survivor 区无法容纳存活对象,触发 YGC,部分对象可能直接晋升老年代。
    • Full GC 前置操作:如 Parallel Scavenge,默认在 Full GC 前先执行 YGC(可通过 -XX:+ScavengeBeforeFullGC 控制)。
  • 关键机制
    • 对象分配:新对象优先分配在 Eden 区。
    • 对象晋升
      • 年龄阈值:-XX:MaxTenuringThreshold(默认 15)。
      • 提前晋升:若 Survivor 区空间不足,存活对象直接进入老年代。
    • 复制算法:存活对象在 Eden 和 Survivor 区间拷贝,清空原区域。
  • 特点
    • 回收速度快(通常毫秒级),但会触发 STW。
    • 高效回收:针对短期对象,减少老年代扫描。
  • 要点
    • Survivor 区溢出:存活对象过多时,直接晋升老年代,可能引发老年代积压。
    • 与 Full GC 的关系:Minor GC 前会检查老年代剩余空间,若不足可能触发 Full GC(取决于 GC 策略)。
  • 参数
    • -XX:MaxTenuringThreshold=15:晋升老年代的年龄阈值。
    • -XX:SurvivorRatio=8:Eden 区与单个 Survivor 区的比例(默认 8:1:1)。

Old GC

Old GC (Major GC 或 OGC) ,老年代垃圾回收

  • 作用范围:只针对老年代。
  • 触发条件:当老年代空间不足时触发,通常是当从年轻代晋升到老年代的对象过多,或者老年代的存活对象数量达到一定阈值时。
  • 执行方式:只回收老年代的对象,年轻代不受影响。
  • 特点:执行时间比 Young GC 长,因为老年代中的对象存活时间更长,且数量较多。

Full GC

Full GC,全堆垃圾回收

  • 作用范围:对整个堆内存(包括年轻代和老年代)进行回收。
  • 触发条件:当老年代空间不足且无法通过老年代垃圾回收释放足够空间,或其他情况导致系统内存压力较大时触发(如 System.gc () 调用)。
  • 执行方式:回收所有代(年轻代、老年代)中的垃圾,并且可能会伴随着元空间的回收。
  • 特点:回收时间最长,会触发整个 JVM 的停顿(Stop - The - World),对性能有较大影响,通常不希望频繁发生。

Full GC 是对整个 Java 堆(年轻代 + 老年代)方法区(元空间/Metaspace) 进行垃圾回收,部分收集器还会回收直接内存(如 ZGC)。

Full GC 特点

  • Stop-The-World(STW) 时间较长(秒级),对性能影响显著。
  • 回收算法因 GC 类型而异(如 Serial Old 使用标记-整理,CMS 使用并发标记-清除)。

Full GC 触发条件

触发条件 具体原因 关联参数/备注
老年代空间不足 老年代无法通过垃圾回收释放足够空间,无法容纳新晋升的对象 -Xmx(老年代最大值)、-XX:CMSInitiatingOccupancyFraction(CMS 触发阈值)
永久代/元空间不足 Java 7 及之前:永久代(PermGen)耗尽
Java 8+:元空间(Metaspace)超过阈值
-XX:MetaspaceSize-XX:MaxMetaspaceSize(Java 8+)
显式调用 System.gc() 代码调用 System.gc() 或通过 jmap -dump 等工具触发(不保证立即执行) -XX:+DisableExplicitGC(禁用显式 GC)
空间分配担保失败 年轻代晋升时,老年代剩余空间不足(Promotion Failed -XX:HandlePromotionFailure(JDK 6u24 后默认启用)
晋升老年代失败 大对象或长期存活对象直接进入老年代,但老年代空间不足 -XX:PretenureSizeThreshold(大对象直接晋升阈值)
平均晋升大小预测失败 Young GC 前,统计发现历史平均晋升大小 > 老年代当前剩余空间 依赖 JVM 自适应策略(如 -XX:+UseAdaptiveSizePolicy

减少 Full GC 的优化策略

优化方向 具体措施 关键参数示例
调整堆内存 增大堆大小,避免老年代频繁耗尽 -Xms4g -Xmx4g(初始和最大堆一致,避免动态扩容)
增大年轻代比例 减少对象过早晋升到老年代 -XX:NewRatio=2(老年代:新生代=2:1)、-Xmn2g(直接设置年轻代大小)
调整元空间大小 避免元空间动态扩展触发 Full GC -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
避免大对象直接晋升 减少大对象分配或调整晋升阈值 -XX:PretenureSizeThreshold=1m(>1MB 对象直接进老年代)
选择低延迟 GC 算法 如 G1 或 ZGC,减少 Full GC 频率 -XX:+UseG1GC-XX:+UseZGC
监控与调优 通过日志分析 Full GC 原因(如 -XX:+PrintGCDetails jstat -gcutiljmap -histo 等工具辅助定位问题。

常见表现与影响

  • 应用卡顿:STW 导致所有业务线程暂停(如接口超时、TPS 骤降)。
  • 频繁 Full GC:可能由内存泄漏、不合理对象分配或 JVM 参数配置不当引起(如 -Xmx 过小)。

优化建议

  • 避免内存泄漏:检查长生命周期对象(如缓存)是否无限制增长。
  • 调整 JVM 参数
    • 增大老年代空间(-Xmx-Xms 设为一致,避免动态扩容触发 GC)。
    • 优化晋升阈值(-XX:MaxTenuringThreshold)。
    • 使用更高效的 GC 器(如 G1/ZGC 替代 CMS)。
  • 禁用显式 GC:添加 -XX:+DisableExplicitGC

关键参数

参数 作用
-XX:+PrintGCDetails 打印 GC 日志,分析 Full GC 原因
-XX:MetaspaceSize=256m 设置元空间初始大小
-XX:CMSInitiatingOccupancyFraction=70 CMS 老年代占用率触发阈值

总结:Full GC 是 JVM 内存回收的最后手段,触发条件复杂,需结合日志和监控定位根本原因,针对性优化堆大小、GC 策略或代码逻辑。

Mixed GC

Mixed GC (仅适用于 G1 GC 的混合垃圾回收)

  • 作用范围:同时回收年轻代和部分老年代区域。
  • 触发条件:当 G1 垃圾回收器发现老年代区域的垃圾过多时触发。
  • 执行方式:混合回收年轻代和部分老年代区域,主要目的是减少老年代中的垃圾积压。
  • 特点:结合了 YGC 的快速回收和 OGC 的深度回收,尽量减少停顿时间,适用于大内存应用。

【困难】Java 的 CMS 垃圾回收流程是怎样的?

CMS 是一种以低延迟为目标的垃圾回收器,主要用于老年代回收,其核心流程分为四个阶段,其中两个阶段会触发 STW(Stop-The-World),其余阶段与用户线程并发执行。

CMS 收集器运行步骤如下:

  1. 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  2. 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
  4. 并发清除:回收在标记阶段被鉴定为不可达的对象。不需要停顿。

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

CMS 的缺陷与应对措施

  • 内存碎片:长期运行后可能触发 Full GC(压缩内存),通过 -XX:CMSFullGCsBeforeCompaction 设置压缩频率。
  • 并发模式失败(Concurrent Mode Failure)
    • 老年代空间不足时,退化为 Serial Old 收集器(STW 时间更长)。
    • 优化:调整 -XX:CMSInitiatingOccupancyFraction(默认 68%,建议 70-80%)。
  • 浮动垃圾:需预留空间(通过 -XX:+UseCMSInitiatingOccupancyOnly 避免动态调整阈值)。

【困难】Java 的 G1 垃圾回收流程是怎样的?

G1 通过分区和增量回收实现低延迟,兼顾吞吐量与内存碎片控制,是 JDK 9 后的默认垃圾回收器。

核心设计思想

  • 分区(Region)模型:将堆划分为多个大小相等的 Region(默认约 2048 个),动态分代(逻辑区分 Eden/Survivor/Old/Humongous 区)。
  • 停顿预测模型:根据用户设定的 -XX:MaxGCPauseMillis(默认 200ms),优先回收垃圾最多(Garbage-First)的 Region
  • 并发标记:减少 STW 时间,但最终标记和拷贝阶段仍需停顿。
  • 混合回收:兼顾年轻代和老年代,避免 Full GC。
  • 适用场景:大堆内存(4GB+)、需平衡吞吐与延迟的应用(如 JDK9+默认 GC)。

G1 的垃圾回收分为两大阶段:

  1. 并发标记阶段(Concurrent Marking,基于 SATB 算法)
  2. 对象拷贝阶段(Evacuation,STW)

G1 并发标记阶段(SATB-Based)

  1. 初始标记(Initial Marking,STW):标记从 GC Roots 直接可达的对象。
    • 短暂 STW(Stop-The-World)。
    • 使用**外部 Bitmap **记录存活对象(而非对象头)。
    • 通常与年轻代回收(Young GC)同步触发。
  2. 并发标记(Concurrent Marking):递归标记所有可达对象。
    • 与用户线程并发执行。
    • 使用** SATB(Snapshot-At-The-Beginning)**算法记录标记过程中的引用变化(通过写屏障维护)。
  3. 最终标记(Final Marking,STW):处理 SATB 队列中的引用变更,完成最终标记。
    • 短暂 STW。
    • 修正并发标记期间漏标的对象。
  4. 清理阶段(Cleanup,STW)
    • 统计每个 Region 的存活对象比例。
    • 回收完全无存活对象的 Region(直接整区回收)。
    • 生成回收集合(CSet)供后续拷贝阶段使用。

对象拷贝阶段(Evacuation,STW)

  • 作用:将回收集合(CSet)中的存活对象拷贝到空闲 Region。
  • 流程
    1. 根据标记结果,选择垃圾比例高的 Region 组成 CSet。
    2. 并行将 CSet 中的存活对象复制到新 Region(类似复制算法)。
    3. 清空原 Region,加入空闲列表。
  • 特点
    • 完全 STW,是 G1 的主要停顿来源。
    • 支持混合回收(Mixed GC):同时回收年轻代和老年代 Region。

G1 Mixed GC(混合回收)

  • 触发条件:老年代占用超过阈值(-XX:InitiatingHeapOccupancyPercent,默认 45%)。
  • 行为
    1. 在年轻代回收时,**额外选择部分老年代 Region **加入 CSet。
    2. 通过-XX:G1MixedGCLiveThresholdPercent控制老年代 Region 的回收阈值(存活对象比例低于该值才回收)。

关键机制

  • Remembered Set(RSet):每个 Region 维护一个 RSet,记录其他 Region 对它的引用,避免全堆扫描。
  • Humongous 区:存放大对象(超过 Region 50%),直接分配在 Old 区,避免反复拷贝。

参数配置

参数 作用
-XX:+UseG1GC 启用 G1 回收器
-XX:MaxGCPauseMillis=200 目标最大停顿时间
-XX:InitiatingHeapOccupancyPercent=45 触发 Mixed GC 的堆占用率阈值
-XX:G1HeapRegionSize=2M 设置 Region 大小(1MB~32MB,需为 2 的幂)

优缺点

  • 优势
    • 可控停顿时间,适合大堆(数十 GB)应用。
    • 内存整理减少碎片(复制算法)。
  • 劣势
    • 内存占用较高(RSet 和并发标记开销)。
    • 复杂场景下可能退化为 Serial Old(如分配失败)。

适用场景

  • 替代 CMS,适用于 JDK 8+ 的中大堆应用(如 6GB~100GB)。
  • 对延迟敏感且需平衡吞吐量的场景(如微服务、实时系统)。

【困难】Java 的 ZGC 垃圾回收流程是怎样的?

【困难】JVM 垃圾回收时产生的 concurrent mode failure 的原因是什么?

Concurrent Mode FailureCMS(Concurrent Mark-Sweep) 垃圾回收器在并发清理阶段失败,被迫触发 Full GC(Serial Old) 的现象,导致长时间 STW(Stop-The-World),影响应用响应速度。

CMS 工作流程

  1. 初始标记(Initial Mark):标记根对象直接关联的对象(短暂停顿)。
  2. 并发标记(Concurrent Mark):与应用线程并发,标记老年代存活对象。
  3. 重新标记(Remark):修正并发标记期间变动的对象(短暂停顿)。
  4. 并发清理(Concurrent Sweep):清除垃圾对象(并发执行)。

优化措施

  • 增加老年代内存:调整 -Xmx-XX:CMSInitiatingOccupancyFraction,降低 CMS 触发频率。
  • 调低 CMS 触发阈值:通过 -XX:CMSInitiatingOccupancyFraction=<N> 提前触发回收(如设为 70%)。
  • 碎片整理:配置 -XX:+UseCMSCompactAtFullCollection,在 Full GC 后整理碎片。
  • 增加年轻代内存:减少对象晋升老年代的频率,降低老年代压力。

典型 CMS 参数配置示例

1
2
3
4
java -XX:+UseConcMarkSweepGC \
-XX:CMSInitiatingOccupancyFraction=70 \
-XX:+UseCMSCompactAtFullCollection \
-Xmx4g -Xms4g YourApplication

MyBatis 面试

【简单】MyBatis 中 #{} 和 ${} 的区别是什么?

MyBatis 中 #{}${} 的区别对比

特性 #{}(预编译占位符) ${}(字符串拼接)
底层原理 使用 PreparedStatement,生成带 ? 的 SQL,预编译防止注入。 直接拼接字符串到 SQL 中,无参数化处理。
SQL 注入风险 ❌ 安全(自动转义特殊字符)。 ✔️ 高风险(需手动过滤参数)。
适用场景 动态条件值(如 WHERE id = #{value})。 动态表名、列名(如 ORDER BY ${column})。
数据类型处理 自动识别 Java 类型,匹配 JDBC 类型(如 DateTIMESTAMP)。 原样替换,可能导致语法错误(如字符串未加引号)。
性能 预编译 SQL 可复用,高效。 每次生成新 SQL,效率较低。
示例 xml SELECT * FROM user WHERE name = #{name} xml SELECT * FROM ${tableName}

关键结论

  • **优先用 #{}**:处理用户输入或条件值,确保安全。
  • **谨慎用 ${}**:仅用于非用户输入的动态 SQL 部分(如动态表名),需手动过滤参数。
  • 常见错误
    • 错误:ORDER BY #{column}(预编译后引号包裹列名,语法错误)。
    • 正确:ORDER BY ${column} LIMIT #{limit}(混合使用)。

底层机制对比

#{} 的执行流程(安全)

1
2
3
4
5
-- 生成的 SQL(预编译)
SELECT * FROM user WHERE id = ?;

-- 参数值通过 PreparedStatement 安全传递
pstmt.setInt(1, 5);

${} 的执行流程(风险)

1
2
-- 生成的 SQL(直接拼接)
SELECT * FROM user WHERE name = 'Alice' OR '1'='1'; -- 注入攻击示例

何时必须使用 ${}

  1. 动态表名/列名

    1
    SELECT * FROM ${tableName} WHERE ${column} = #{value}
  2. SQL 函数或关键字

    1
    ORDER BY ${sortField} ${sortOrder}

安全建议

  • 使用 ${} 时,用 @Param 注解白名单校验:
    1
    List<User> selectByTable(@Param("tableName") String tableName);
    1
    2
    3
    4
    5
    6
    <!-- 手动校验表名合法性 -->
    SELECT * FROM ${tableName}
    WHERE 1=1
    <if test="tableName in {'user', 'order'}">
    AND status = #{status}
    </if>

【简单】MyBatis 如何实现一对一、一对多的关联查询 ?

【简单】使用 MyBatis 的 mapper 接口调用时有哪些要求?

【简单】MyBatis 自带的连接池有了解过吗?

【简单】MyBatis 和 Hibernate 有哪些差异?

对比维度 MyBatis Hibernate
SQL 灵活性 方便优化 SQL,灵活性高 自动生成 SQL,复杂查询需 HQL 或原生 SQL,灵活性较低
学习成本 需熟悉 SQL 和数据库特性,适合有 SQL 经验的团队。 面向对象思维,适合快速上手 ORM 的团队。
开发效率 需手动编写 SQL 和结果映射,适合定制化需求。 自动化 CRUD,快速开发简单应用。
缓存机制 提供一级/二级缓存,需手动管理。 内置多级缓存(查询缓存、集合缓存),自动化程度高。
数据库兼容性 SQL 依赖具体数据库语法,移植性较差 通过方言(Dialect)适配多数据库,移植性好
关联映射 需手动配置 <association>/<collection> 自动管理对象关系(如 @OneToMany),配置简洁。
适用场景 复杂查询、高性能系统(如金融、电商)。 快速开发、对象模型复杂的应用(如管理后台)。

总结选择建议

  • 选择 MyBatis

    • 需要精细控制 SQL,追求极致性能。
    • 项目涉及多表复杂查询或数据库特性优化。
  • 选择 Hibernate

    • 快速开发,业务以简单 CRUD 为主。
    • 团队熟悉 ORM,希望减少 SQL 编写。

混合使用:部分项目用 MyBatis 处理复杂查询,Hibernate 处理简单模块。

【中等】说说 MyBatis 的缓存机制?

【中等】MyBatis 写个 Xml 映射文件,再写个 DAO 接口就能执行,这个原理是什么?

【中等】MyBatis 动态 sql 有什么用?执行原理?有哪些动态 sql?

【中等】MyBatis 是否支持延迟加载?如果支持,它的实现原理是什么?

【中等】简述 MyBatis 的插件运行原理,以及如何编写一个插件?

【中等】JDBC 编程有哪些不足之处,MyBatis 是如何解决的?

【中等】MyBatis 都有哪些 Executor 执行器?它们之间的区别是什么?

【中等】MyBatis 如何实现数据库类型和 Java 类型的转换的?

【中等】MyBatis 有哪些核心组件?

MyBatis 有以下核心组件:

  • **SqlSessionFactoryBuilder**:负责创建 SqlSessionFactory 实例。用完即弃。
  • **SqlSessionFactory**:负责创建 SqlSession 实例。全局单例,配置中心。
  • **SqlSession**:通过方法签名和 Mapper 相互映射。请求级核心,需及时关闭。
  • **Mapper**:映射器是一些由用户创建的、绑定 SQL 语句的接口。轻量级对象,随用随建。

下面是它们之间的关系:

1
2
SqlSessionFactoryBuilder → SqlSessionFactory → SqlSession → Mapper Proxy
(方法级) (应用级) (请求级) (方法级)

img

SqlSessionFactoryBuilder

  • 生命周期方法级(最短)
  • 作用:用于构建 SqlSessionFactory,解析 XML 配置(如 mybatis-config.xml)。
  • 特点
    • 构建完成后即可销毁,无状态,不占用资源。
    • 通常作为局部变量使用。
1
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSessionFactory

  • 生命周期应用级(最长)
  • 作用:创建 SqlSession,全局唯一,线程安全。
  • 特点
    • 通常作为单例存在于整个应用运行期间。
    • 维护数据库连接池和全局配置(如缓存、别名)。
1
2
3
4
5
6
7
8
9
10
// 推荐通过单例管理
public class MyBatisUtil {
private static final SqlSessionFactory factory;
static {
factory = new SqlSessionFactoryBuilder().build(inputStream);
}
public static SqlSessionFactory getFactory() {
return factory;
}
}

SqlSession

  • 生命周期请求/事务级
  • 作用:执行 SQL、获取 Mapper 接口实例、管理事务。
  • 特点
    • 非线程安全,每次请求需创建新实例,用完后必须关闭(避免连接泄漏)。
    • 默认不自动提交事务,需手动 commit()rollback()
1
2
3
4
5
try (SqlSession session = factory.openSession()) {  // 自动关闭
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1);
session.commit(); // 提交事务
}

Mapper

  • 生命周期方法级(与 SqlSession 绑定)
  • 作用:通过动态代理将接口方法调用转换为 SQL 执行。
  • 特点
    • SqlSession 创建,生命周期跟随 SqlSession
    • 无需手动实现,MyBatis 自动生成代理类。
1
2
// 代理对象随 SqlSession 销毁而失效
UserMapper mapper = session.getMapper(UserMapper.class);

【中等】能详细说说 MyBatis 的执行流程吗?

img

【困难】MyBatis 的架构是如何设计的?

MyBatis 的架构设计通过 分层解耦动态代理 实现了 SQL 与 Java 代码的分离,其核心在于:

  • 配置驱动:集中管理 SQL 和映射规则。
  • 组件化:各层职责单一,易于扩展(如插件)。
  • 平衡灵活与易用:既保留 JDBC 的掌控力,又简化了重复操作。

这种设计使其在需要高性能和灵活 SQL 的场景中表现优异,尤其适合中大型复杂业务系统。

MyBatis 的架构设计以 SQL 与 Java 对象的灵活映射 为核心,采用分层模块化设计,平衡了灵活性与易用性。

img

分层架构设计

MyBatis 的架构分为四层,各层职责明确,通过接口解耦:

层级 核心组件 职责
接口层 SqlSessionMapper 接口 提供开发者使用的 API(如 selectOneinsert),屏蔽底层实现细节。
核心处理层 ExecutorStatementHandler 执行 SQL 语句、处理参数绑定和结果映射,实现插件拦截链。
基础支撑层 DataSourceTransaction 管理数据库连接池、事务,提供类型转换(TypeHandler)和缓存支持。
扩展层 Interceptor(插件) 通过动态代理拦截核心组件,实现功能扩展(如分页、性能监控)。

基础支撑层

功能:为上层提供通用能力支持

  • **类型处理器 (TypeHandler)**:处理 Java 类型与 JDBC 类型转换(如 StringVARCHAR)。支持自定义扩展(如枚举类型转换)。
  • 连接管理:集成连接池(如 HikariCP、Druid),管理数据库连接。
  • 事务管理:提供 JDBC 和 Managed 两种事务模式(可集成 Spring 事务)。
  • 缓存管理:一级缓存(SqlSession 级别)、二级缓存(Mapper 级别)。支持 Redis、Ehcache 等第三方缓存集成。

核心处理层

功能:执行 SQL 并处理结果映射

  • **配置解析 (Configuration)**:加载 mybatis-config.xmlMapper.xml,存储所有配置信息(如别名、插件)。
  • **SQL 解析 (SqlSource & BoundSql)**:解析动态 SQL(<if><foreach>),生成可执行的 SQL 字符串和参数映射。
  • 执行器 (Executor)
    • 类型
      • SimpleExecutor:默认执行器,每次执行新开 PreparedStatement
      • ReuseExecutor:复用 Statement 对象。
      • BatchExecutor:批量操作优化。
    • 职责:调用 JDBC 执行 SQL,触发插件拦截链。
  • **结果集处理 (ResultSetHandler)**:将 ResultSet 转换为 Java 对象(根据 ResultMap 或自动映射)。

MySQL 面试之事务和锁篇

MySQL 事务

扩展阅读:

【简单】什么是事务,什么是 ACID?

事务指的是满足 ACID 特性的一组操作。事务内的 SQL 语句,要么全执行成功,要么全执行失败。可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。通俗来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败

ACID 是数据库事务正确执行的四个基本要素。

  • 原子性(Atomicity)
    • 事务被视为不可分割的最小单元,事务中的所有操作要么全部提交成功,要么全部失败回滚。
    • 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
  • 一致性(Consistency)
    • 数据库在事务执行前后都保持一致性状态。
    • 在一致性状态下,所有事务对一个数据的读取结果都是相同的。
  • 隔离性(Isolation)
    • 一个事务所做的修改在最终提交以前,对其它事务是不可见的。
  • 持久性(Durability)
    • 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
    • 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。

一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性。

【中等】事务存在哪些并发一致性问题?

事务中存在的并发一致性问题有:

  • 丢失修改
  • 脏读
  • 不可重复读
  • 幻读

“丢失修改”是指一个事务的更新操作被另外一个事务的更新操作替换

如下图所示,T1 和 T2 两个事务对同一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。

“脏读(dirty read)”是指当前事务可以读取其他事务未提交的数据

如下图所示,T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。

“不可重复读(non-repeatable read)”是指一个事务内多次读取同一数据,过程中,该数据被其他事务所修改,导致当前事务多次读取的数据可能不一致

如下图所示,T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。

“幻读(phantom read)”是指一个事务内多次读取同一范围的数据,过程中,其他事务在该数据范围新增了数据,导致当前事务未发现新增数据

事务 T1 读取某个范围内的记录时,事务 T2 在该范围内插入了新的记录,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。

【中等】长事务可能会导致哪些问题?

长事务可能会导致以下问题:

  • 锁竞争与资源阻塞
    • 长事务长时间持有锁,导致其他事务阻塞,增加系统等待时间,降低并发性能。
    • 业务线程因数据库请求等待而阻塞,可能引发连锁反应(如服务雪崩),造成严重线上事故。
  • 死锁风险增加 - 多个长事务互相等待对方释放锁,容易形成死锁,导致系统无法正常执行。
  • 主从延迟问题 - 主库执行时间长,从库同步及重放耗时增加,导致主从数据长时间不一致。
  • 回滚效率低下 - 长事务执行中途失败时,回滚操作会浪费已执行的资源与时间,影响系统效率。

【困难】事务的二阶段提交是什么?

事务的二阶段提交确保 redo log(重做日志)binlog(二进制日志) 的一致性,防止崩溃恢复时出现数据丢失或不一致。

两阶段流程

  • Prepare 阶段(准备阶段) - InnoDB 写入 redo log,并标记为 prepare 状态(事务预提交,但未最终提交)。
  • Commit 阶段(提交阶段) - MySQL Server 写入 binlog(记录 DML 操作)。binlog 写入成功后,InnoDB 将 redo log 状态改为 commit,完成事务提交。

binlog 和 redo log 的区别

特性 redo log binlog
所属层级 InnoDB 引擎层 MySQL Server 层
日志类型 记录数据页的物理日志 记录 DML/DDL 操作的逻辑日志
存储方式 固定大小,环形写入 追加写入,可无限增长
主要用途 崩溃恢复(保证数据持久性) 主从复制、数据恢复、备份

为什么需要二阶段提交?

无论是单独先写 redo log 或先写 binlog,都可能导致数据不一致:

  1. 先写 redo log,后写 binlog(宕机时 binlog 未写入) - redo log 恢复数据,但 binlog 缺失该事务 → 主从数据不一致
  2. 先写 binlog,后写 redo log(宕机时 redo log 未写入) - binlog 有记录,但 redo log 未提交 → 数据库实际数据丢失,与 binlog 不一致。

为了解决以上问题,所以需要事务二阶段提交(reparecommit),以确保写入两日志的原子性。

二阶段提交如何保证一致性?

MySQL 崩溃恢复时,检查两日志状态:

  • redo log prepare,binlog 未写入 - 直接回滚(两日志均无有效记录)。
  • redo log prepare,binlog 已写入 - 对比两日志数据:
    • 一致:提交事务(redo log commit)。
    • 不一致:回滚事务(保证数据一致)。

【中等】有哪些事务隔离级别,分别解决了什么问题?

为了解决以上提到的并发一致性问题,SQL 标准提出了四种“事务隔离级别”来应对这些问题。事务隔离级别等级越高,越能保证数据的一致性和完整性,但是执行效率也越低。因此,设置数据库的事务隔离级别时需要做一下权衡。

事务隔离级别从低到高分别是:

  • 读未提交(read uncommitted) - 是指,事务中的修改,即使没有提交,对其它事务也是可见的
  • 读已提交(read committed) - 是指,事务提交后,其他事务才能看到它的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
    • 读已提交解决了脏读的问题
    • 读已提交是大多数数据库的默认事务隔离级别,如 Oracle。
  • 可重复读(repeatable read) - 是指:保证在同一个事务中多次读取同样数据的结果是一样的
    • 可重复读解决了不可重复读问题
    • 可重复读是 InnoDB 存储引擎的默认事务隔离级别
  • 串行化(serializable ) - 是指,强制事务串行执行,对于同一行记录,加读写锁,一旦出现锁冲突,必须等前面的事务释放锁。
    • 串行化解决了幻读问题。由于强制事务串行执行,自然避免了所有的并发问题。
    • 串行化策略会在读取的每一行数据上都加锁,这可能导致大量的超时和锁竞争。这对于高并发应用基本上是不可接受的,所以一般不会采用这个级别。

事务隔离级别对并发一致性问题的解决情况:

隔离级别 丢失修改 脏读 不可重复读 幻读
读未提交 ✔️️️
读已提交 ✔️️️ ✔️️️
可重复读 ✔️️️ ✔️️️ ✔️️️
可串行化 ✔️️️ ✔️️️ ✔️️️ ✔️️️

【中等】MySQL 的默认事务隔离级别是什么?为什么?

事务隔离级别等级越高,越能保证数据的一致性和完整性,但是执行效率也越低。因此,设置数据库的事务隔离级别时需要做一下权衡。

MySQL 中的事务功能是在存储引擎层实现的,并非所有存储引擎都支持事务功能。比如 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。

大部分数据库的默认隔离级别是“读已提交”。然而,InnoDB 的默认隔离级别是“可重复读”。这是为了兼容早期 binlog 的 statement 格式问题。如果使用可重复读以下的隔离级别,使用了 statement 格式的 binlog 会产生主从数据不一致的问题。

此外,在 InnoDB 中,可重复读隔离级别虽然不能解决幻读,但是可以很大程度的避免幻读的发生。根据不同的查询方式,分别提出了避免幻读的方案:

  • 针对快照读(普通 select 语句),通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
  • 针对当前读select ... for update 等语句),通过 Next-Key Lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 Next-Key Lock,如果有其他事务在 Next-Key Lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

很大程度的避免幻读,不代表完全解决幻读问题,下面是两个示例:

  • 对于快照读,MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。
  • 对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。

【困难】MySQL 是如何实现事务的?

MySQL 主要是通过 Redo LogUndo LogMVCC 来实现事务。

  • MySQL 利用锁(行锁、间隙锁等等机制),控制数据的并发修改,满足事务的隔离性。
  • Redo Log(重做日志),它会记录事务对数据库的所有修改,当 MySQL 发生宕机或崩溃时,通过重放 redo log 就可以恢复数据,用来满足事务的持久性。
  • Undo Log(回滚日志),它会记录事务的反向操作,简单地说就是保存数据的历史版本,用于事务的回滚,使得事务执行失败之后可以恢复之前的样子。实现原子性和隔离性
  • MVCC(多版本并发控制),满足了非锁定读的需求,提高了并发度,实现了读已提交和可重复读两种隔离级别,实现了事务的隔离性。

事务实现了原子性、隔离性和持久性特性后,本身就达到了一致性的目的。

【困难】什么是 MVCC?

MVCC 是 Multi Version Concurrency Control 的缩写,即“多版本并发控制”。MVCC 的设计目标是提高数据库的并发性,采用非阻塞的方式去处理读/写并发冲突,可以将其看成一种乐观锁。

不仅是 MySQL,包括 Oracle、PostgreSQL 等其他关系型数据库都实现了各自的 MVCC,实现机制没有统一标准。MVCC 是 InnoDB 存储引擎实现事务隔离级别的一种具体方式。其主要用于实现读已提交和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

::: info MVCC 实现原理

:::

MVCC 的实现原理,主要基于隐式字段、UndoLog、ReadView 来实现。

隐式字段

InnoDB 存储引擎中,数据表的每行记录,除了用户显示定义的字段以外,还有几个数据库隐式定义的字段:

  • row_id - 隐藏的自增 ID。如果数据表没有指定主键,InnoDB 会自动基于 row_id 产生一个聚簇索引。
  • trx_id - 最近修改的事务 ID。事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
  • roll_pointer - 回滚指针,指向这条记录的上一个版本。

UndoLog

MVCC 的多版本指的是多个版本的快照,快照存储在 UndoLog 中。该日志通过回滚指针 roll_pointer 把一个数据行的所有快照链接起来,构成一个版本链

ReadView

ReadView 就是事务进行快照读时产生的读视图(快照)

ReadView 有四个重要的字段:

  • m_ids - 指的是在创建 ReadView 时,当前数据库中“活跃事务”的事务 ID 列表。注意:这是一个列表,“活跃事务”指的就是,启动了但还没提交的事务
  • min_trx_id - 指的是在创建 ReadView 时,当前数据库中“活跃事务”中事务 id 最小的事务,也就是 m_ids 的最小值。
  • max_trx_id - 这个并不是 m_ids 的最大值,而是指创建 ReadView 时当前数据库中应该给下一个事务分配的 ID 值,也就是全局事务中最大的事务 ID 值 + 1;
  • creator_trx_id - 指的是创建该 ReadView 的事务的事务 ID。

在创建 ReadView 后,我们可以将记录中的 trx_id 划分为三种情况:

  • 已提交事务
  • 已启动但未提交的事务
  • 未启动的事务

ReadView 如何判断版本链中哪个版本可见?

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:

  • trx_id == creator_trx_id - 表示 trx_id 版本记录由 ReadView 所代表的当前事务产生,当然可以访问。
  • trx_id < min_trx_id - 表示 trx_id 版本记录是在创建 ReadView 之前已提交的事务生成的,当前事务可以访问。
  • trx_id >= max_trx_id - 表示 trx_id 版本记录是在创建 ReadView 之后才启动的事务生成的,当前事务不可以访问。
  • min_trx_id <= trx_id < max_trx_id - 需要判断 trx_id 是否在 m_ids 列表中
    • 如果 trx_idm_ids 列表中,表示生成 trx_id 版本记录的事务依然活跃(未提交事务),当前事务不可以访问。
    • 如果 trx_id 不在 m_ids 列表中,表示生成 trx_id 版本记录的事务已提交,当前事务可以访问。

这种通过“版本链”来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

【困难】MVCC 实现了哪些隔离级别,如何实现的?

对于“读已提交”和“可重复读”隔离级别的事务来说,它们都是通过 MVCC 的 ReadView 机制来实现的,区别仅在于创建 ReadView 的时机不同。ReadView 可以理解为一个数据快照。

MVCC 如何实现可重复读隔离级别

可重复读隔离级别只有在启动事务时才会创建 ReadView,然后整个事务期间都使用这个 ReadView。这样就保证了在事务期间读到的数据都是事务启动前的记录。

举个例子,假设有两个事务依次执行以下操作:

  • 初始,表中 id = 1 的 value 列值为 100。
  • 事务 2 读取数据,value 为 100;
  • 事务 1 将 value 设为 200;
  • 事务 2 读取数据,value 为 100;
  • 事务 1 提交事务;
  • 事务 2 读取数据,value 依旧为 100;

以上操作,如下图所示。T2 事务在事务过程中,是否可以看到 T1 事务的修改,可以根据 ReadView 中描述的规则去判断。

从图中不难看出:

  • 对于 trx_id = 100 的版本记录,比对 T2 事务 ReadView ,trx_id < min_trx_id,因此在 T2 事务中的任意时刻都可见;
  • 对于 trx_id = 101 的版本记录,比对 T2 事务 ReadView ,可以看出 min_trx_id <= trx_id < max_trx_id ,且 trx_idm_ids 中,因此 T2 事务中不可见。

综上所述,在 T2 事务中,自始至终只能看到 trx_id = 100 的版本记录。

MVCC 如何实现读已提交隔离级别

读已提交隔离级别每次读取数据时都会创建一个 ReadView。这意味着,事务期间的多次读取同一条数据,前后读取的数据可能会出现不一致——因为,这期间可能有另外一个事务修改了该记录,并提交了事务。

举个例子,假设有两个事务依次执行以下操作:

  • 初始,表中 id = 1 的 value 列值为 100。
  • 事务 2 读取数据(创建 ReadView),value 为 0;
  • 事务 1 将 value 设为 100;
  • 事务 2 读取数据(创建 ReadView),value 为 0;
  • 事务 1 提交事务;
  • 事务 2 读取数据(创建 ReadView),value 为 100;

以上操作,如下图所示,T2 事务在事务过程中,是否可以看到其他事务的修改,可以根据 ReadView 中描述的规则去判断。

从图中不难看出:

  • 对于 trx_id = 100 的版本记录,比对 T2 事务 ReadView ,trx_id < min_trx_id,因此在 T2 事务中的任意时刻都可见;
  • 对于 trx_id = 101 的版本记录,比对 T2 事务 ReadView ,可以看出第二次查询时(T1 更新未提交),min_trx_id <= trx_id < max_trx_id ,且 trx_idm_ids 中,因此 T2 事务中不可见;而第三次查询时(T1 更新已提交),trx_id < min_trx_id,因此在 T2 事务中可见;

综上所述,在 T2 事务中,当 T1 事务提交前,可读取到的是 trx_id = 100 的版本记录;当 T1 事务提交后,可读取到的是 trx_id = 101 的版本记录。

MVCC + Next-Key Lock 如何解决幻读

MySQL InnoDB 引擎的默认隔离级别虽然是“可重复读”,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:

  • 针对快照读(普通 SELECT 语句),通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
  • 针对当前读SELECT ... FOR UPDATE 等语句),通过 Next-Key Lock(记录锁+间隙锁)方式解决了幻读,因为当执行 SELECT ... FOR UPDATE 语句的时候,会加上 Next-Key Lock,如果有其他事务在 Next-Key Lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好的避免了幻读问题。

【困难】各事务隔离级别是如何实现的?

四种隔离级别具体是如何实现的呢?

以 InnoDB 的事务实现来说明:

  • 对于“读未提交”隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
  • 对于“串行化”隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
  • 对于“读提交”和“可重复读”隔离级别的事务来说,它们都是通过 ReadView 来实现的,区别仅在于创建 ReadView 的时机不同。ReadView 可以理解为一个数据快照。
    • “读提交”隔离级别是在“每个语句执行前”都会重新生成一个 ReadView
    • “可重复读”隔离级别是在“启动事务时”生成一个 ReadView,然后整个事务期间都在用这个 ReadView。

MySQL 锁

【中等】MySQL 中有哪些锁?

为了解决并发一致性问题,MySQL 支持了很多种锁来实现不同程度的隔离性,以保证数据的安全性。

独享锁和共享锁

独享锁(Exclusive):简写为 X 锁,又称为“写锁”、“排它锁”。

  • 特性
    • 用于写入操作
    • 完全排他,同一时间仅允许一个事务持有
  • 加锁
    • SELECT ... FOR UPDATE
    • DML 语句(隐式加锁)

共享锁(Shared):简写为 S 锁,又称为“读锁”。

  • 特性
    • 用于读取操作
    • 允许多事务并发持有,互不阻塞
  • 加锁
    • SELECT ... LOCK IN SHARE MODE
    • SELECT ... FOR SHARE(MySQL 8.0+)

存储引擎实现差异

  • MyISAM:仅支持表级锁
    • LOCK TABLES ... READ → S 锁
    • LOCK TABLES ... WRITE → X 锁
  • InnoDB:支持行级锁和表级锁
    • 默认使用行锁(更细粒度)
    • 特殊场景退化为表锁(如无索引更新)

悲观锁和乐观锁

基于加锁方式分类,MySQL 可以分为悲观锁和乐观锁。

  • 悲观锁 - 假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作
    • 在查询完数据的时候就把事务锁起来,直到提交事务(COMMIT
    • 实现方式:使用数据库中的锁机制
  • 乐观锁 - 假设最好的情况——每次访问数据时,都假设数据不会被其他线程修改,不必加锁。只在更新的时候,判断一下在此期间是否有其他线程更新该数据。
    • 实现方式:更新数据时,先使用版本号机制或 CAS 算法检查数据是否被修改

为什么要引入乐观锁?

乐观锁也是一种通用的锁机制,在很多软件领域,都存在乐观锁机制。

锁,意味着互斥,意味着阻塞。在高并发场景下,锁越多,阻塞越多,势必会拉低并发性能。那么,为了提高并发度,能不能尽量不加锁呢?

乐观锁,顾名思义,就是假设最好的情况——每次访问数据时,都假设数据不会被其他线程修改,不必加锁。虽然不加锁,但不意味着什么都不做,而是在更新的时候,判断一下在此期间是否有其他线程更新该数据。乐观锁最常见的实现方式,是使用版本号机制或 CAS 算法(Compare And Swap)去实现。

【示例】MySQL 乐观锁示例

假设,order 表中有一个字段 status,表示订单状态:status 为 1 代表订单未支付;status 为 2 代表订单已支付。现在,要将 id 为 1 的订单状态置为已支付,则操作如下:

1
2
3
4
5
select status, version from order where id=#{id}

update order
set status=2, version=version+1
where id=#{id} and version=#{version};

乐观锁的优点是:减少锁竞争,提高并发度。

乐观锁的缺点是:

  • 存在 ABA 问题。所谓的 ABA 问题是指在并发编程中,如果一个变量初次读取的时候是 A 值,它的值被改成了 B,然后又其他线程把 B 值改成了 A,而另一个早期线程在对比值时会误以为此值没有发生改变,但其实已经发生变化了
  • 如果乐观锁所检查的数据存在大量锁竞争,会由于不断循环重试,产生大量的 CPU 开销

全局锁

全局锁是 MySQL 中一种锁住整个数据库实例的机制,使数据库处于只读状态,禁止所有数据修改操作(DML、DDL)。

加锁、解锁

加全局锁(Flush Tables With Read Lock, FTWRL)

1
FLUSH TABLES WITH READ LOCK;

阻塞所有 写操作(INSERT/UPDATE/DELETE)DDL(ALTER/DROP),但允许读操作(SELECT)。

释放锁

1
UNLOCK TABLES;

应用场景

  • 数据库备份(确保备份数据一致性)
  • 主从同步初始化
  • 防止数据修改(维护期间禁止写入)

表级锁

  • 表锁锁定整张表,是 MyISAM 引擎的默认锁类型,InnoDB 引擎在特定情况下也会使用。
    • 表共享读锁(Table Read Lock)LOCK TABLE table_name READ
      • 所有会话可以同时读取该表
      • 禁止任何会话写入(INSERT/UPDATE/DELETE)
      • 读锁之间不互斥(多个事务可同时持有读锁)
    • 表独占写锁(Table Write Lock)LOCK TABLE table_name WRITE
      • 仅允许当前会话读写该表
      • 其他会话无法读取或写入(完全排他)
      • 写锁与其他所有锁互斥(包括读锁和其他写锁)
  • 元数据锁(Metadata Lock, MDL):自动加锁,保护表结构,防止在查询或修改时表结构被更改。
    • 增删改查,加读锁
    • 结构变更,加写锁
  • 意向锁(Intention Lock):用于快速判断表中是否有行被锁定,从而优化锁冲突检测效率。
    • 意向共享锁(IS):表示事务准备在表的某些行上加 S 锁(共享锁),如 SELECT ... LOCK IN SHARE MODE
    • 意向独享锁(IX):表示事务准备在表的某些行上加 X 锁(排他锁),如 SELECT ... FOR UPDATE 或 DML 操作。
  • 自增锁(Auto Increment Lock):确保并发插入时自增值的正确分配。
    • 确保自增值连续:防止多个事务同时插入数据时,自增列出现重复或跳跃。
    • 保证唯一性:避免并发插入时多个事务获取相同的自增值,导致主键冲突。
    • 支持事务回滚:如果事务回滚,已分配的自增值不会回退,但后续插入仍会继续递增,确保不重复。

行级锁

  • 记录锁(Record Lock):锁定索引中的单条记录

    • 仅锁住符合条件的行,不影响其他行
    • 如果表无索引,InnoDB 会退化为表锁
      1
      2
      -- 对 id=1 的记录加锁
      SELECT * FROM users WHERE id = 1 FOR UPDATE;
  • 间隙锁(Gap Lock):锁定索引记录之间的间隙,防止其他事务在这个范围内插入数据。

    • 仅存在于可重复读隔离级别,为了解决幻读问题

      1
      2
      -- 锁定 20-30 之间的间隙
      SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
  • 临键锁(Next-Key Lock)记录锁+间隙锁的组合,锁定记录及前面的间隙。

    • InnoDB 默认的行锁方式
    • 同时防止幻读和保证当前读的一致性
      1
      2
      -- 锁定 id>10 的记录及后面的间隙
      SELECT * FROM users WHERE id > 10 FOR UPDATE;
  • 插入意向锁(Insert Intention Lock):在 INSERT 操作前设置,表示准备插入

    • 不阻塞其他插入意向锁
    • 会等待间隙锁释放
      1
      2
      -- 自动获取插入意向锁
      INSERT INTO users VALUES (15, 'John');

【中等】死锁是如何产生的?

死锁是指,多个事务竞争同一资源,并请求锁定对方占用的资源,从而导致恶性循环的现象

死锁产生条件(必须同时满足)

  • 互斥:资源一次只能被一个事务占用(如行锁)。
  • 占有并等待:事务持有资源的同时,等待其他事务释放资源。
  • 不可强占:事务已获得的资源不能被强制抢占,只能主动释放。
  • 循环等待:事务之间形成循环等待。

【困难】如何避免死锁?

死锁的四个必要条件互斥、占有且等待、不可强占、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。由此可知,要想避免死锁,就要从这几个必要条件上去着手:

  • 更新表时,尽量使用主键更新,减少冲突;
  • 避免长事务,尽量将长事务拆解,可以降低与其它事务发生冲突的概率;
  • 设置合理的锁等待超时参数,我们可以通过 innodb_lock_wait_timeout 设置合理的等待超时阈值,特别是在一些高并发的业务中,我们可以尽量将该值设置得小一些,避免大量事务等待,占用系统资源,造成严重的性能开销。
  • 在编程中尽量按照固定的顺序来处理数据库记录,假设有两个更新操作,分别更新两条相同的记录,但更新顺序不一样,有可能导致死锁;
  • 在允许幻读和不可重复读的情况下,尽量使用读已提交事务隔离级别,可以避免 Gap Lock 导致的死锁问题;
  • 还可以使用其它的方式来代替数据库实现幂等性校验。例如,使用 Redis 以及 ZooKeeper 来实现,运行效率比数据库更佳。

【困难】如何解决死锁?

当出现死锁以后,有两种策略:

  • 设置事务等待锁的超时时间。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
  • 开启死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。

在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。但是,若直接把这个时间设置成一个很小的值,比如 1s,也是不可取的。当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。

所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect 的默认值本身就是 on。为了解决死锁问题,不同数据库实现了各自的死锁检测和超时机制。InnoDB 的处理策略是:将持有最少行级排它锁的事务进行回滚。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的:每当事务被锁时,就要查看它所依赖的线程是否被其他事务锁住,如此循环,来判断是否出现了循环等待,也就是死锁。因此,死锁检测可能会耗费大量的 CPU。

参考资料

MySQL 面试之索引篇

综合

【简单】什么是索引?为什么要使用索引?

“索引”是数据库为了提高查找效率的一种数据结构

日常生活中,我们可以通过检索目录,来快速定位书本中的内容。索引和数据表,就好比目录和书,想要高效查询数据表,索引至关重要。在数据量小且负载较低时,不恰当的索引对于性能的影响可能还不明显;但随着数据量逐渐增大,性能则会急剧下降。因此,设置合理的索引是数据库查询性能优化的最有效手段

【简单】索引的优点和缺点是什么?

✅️️️️️️️ 索引的优点:

  • 索引大大减少了服务器需要扫描的数据量,从而加快检索速度。
  • 索引可以帮助服务器避免排序和临时表
  • 索引可以将随机 I/O 变为顺序 I/O
  • 支持行级锁的数据库,如 InnoDB 会在访问行的时候加锁。使用索引可以减少访问的行数,从而减少锁的竞争,提高并发
  • 唯一索引可以确保每一行数据的唯一性,通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统的性能。

❌ 索引的缺点:

  • 创建和维护索引要耗费时间,这会随着数据量的增加而增加。
  • 索引需要占用额外的物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立联合索引那么需要的空间就会更大。
  • 写操作(INSERT/UPDATE/DELETE)时很可能需要更新索引,导致数据库的写操作性能降低

【中等】何时适用索引?何时不适用索引?

  • 索引不是越多越好,不要为所有列都创建索引。要考虑到索引的维护代价、空间占用和查询时回表的代价。索引一定是按需创建的,并且要尽可能确保足够轻量。一旦创建了多字段的联合索引,我们要考虑尽可能利用索引本身完成数据查询,减少回表的成本。
  • 考虑删除未使用的索引
  • 尽量的扩展索引,不要新建索引。索引需要占用额外的存储空间;此外,表更新时,需要同步维护索引。索引越多,意味着维护所付出的成本越高,因此,应尽量扩展已有索引,而不是不假思索的新建索引。

✅️️️️ 什么情况适用索引?

  • 高频查询的字段:频繁作为 WHERE 条件或 JOIN 条件的字段,应考虑设为索引。
  • 频繁用于 ORDER BYGROUP BYDISTINCT 的列。将该列作为索引,可以帮助加快这些操作。

❌ 什么情况不适用索引?

  • 非常小的表:对于非常小的表,大部分情况下简单的全表扫描更高效。
  • 特大型的表:建立和使用索引的代价将随之增长。可以考虑使用分区技术或 Nosql。
  • 高频更新的表:表更新时,需要同步维护索引,有额外的开销,会影响性能。
  • 低频查询的字段:很少作为 WHERE 条件或 JOIN 条件的字段,建立索引而带来的空间开销和维护成本可能超过查询性能提升所带来的收益。
  • 高度重复的字段:若索引字段的重复度高,意味着选择性低(如性别字段只有男和女),索引的效果不明显,且会增加额外的存储空间。
  • 长文本的字段:如 TEXT、BLOB 或非常长的 VARCHAR 类型,字段常包含大量数据。
    • 数据量大时,无法用内存排序,只能利用磁盘文件排序,速度很慢。
    • 数据页默认 16KB,存储数据有限,超出范围,需要扫描多次 I/O。
    • 这种类型的数据如果有查询需求,应考虑使用 ES 来进行全文检索。

【中等】哪些情况下,索引会失效?

导致索引失效的情况有:

  • 违反最左前缀原则
    • ❌跳过了最左列(如索引 (a,b,c),但查询 WHERE b=1
    • ❌中间列被跳过(如 WHERE a=1 AND c=3c 无法使用索引)
  • 对索引使用函数或计算
    • WHERE YEAR(date_column) = 2023
    • WHERE column * 2 = 10
    • WHERE SUBSTRING(name, 1, 3) = 'Tom'
  • 数据类型不匹配(隐式类型转换)
    • WHERE string_column = 123(字符串列用数字比较)
    • WHERE int_column = '123'(数字列用字符串比较)
  • 使用 OR 连接非索引列
    • WHERE a=1 OR b=2(如果 b 无索引,全表扫描)
    • WHERE a=1 OR a=2a 有索引,可以优化)
  • 使用范围查询(>、<、BETWEEN、LIKE)后的列失效
    • WHERE a=1 AND b=2a, b 都能用索引)
    • WHERE a>1 AND b=2a 是范围查询,b 无法用索引)
    • WHERE name LIKE '%abc'(前导通配符 % 导致索引失效)
  • 使用 !=<>NOT INIS NULLIS NOT NULL
    • WHERE status != 'active'
    • WHERE age NOT IN (18, 20)
    • WHERE phone IS NULL

【简单】索引有哪些分类?

MySQL 索引可以从以下四个维度来分类:

  • 按【数据结构】分类:B+tree 索引、Hash 索引、Full-text 索引
  • 按【物理存储】分类:聚簇索引、二级索引(辅助索引)
  • 按【字段特性】分类:主键索引、普通索引、前缀索引
  • 按【字段个数】分类:单列索引、联合索引(复合索引、组合索引)

【简单】= 和 in 的顺序对于命中索引是否有影响?

不需要考虑 =IN 等的顺序,MySQL 会自动优化这些条件的顺序,以匹配尽可能多的索引列。

【示例】如有索引 (a, b, c, d),查询条件 c > 3 and b = 2 and a = 1 and d < 4a = 1 and c > 3 and b = 2 and d < 4 等顺序都是可以的,MySQL 会自动优化为 a = 1 and b = 2 and c > 3 and d < 4,依次命中 a、b、c、d。

索引数据结构

【简单】索引有哪些常见数据结构?

在 MySQL 中,索引是在存储引擎层而不是服务器层实现的,所以,并没有统一的索引标准。不同存储引擎的索引的数据结构也不相同。下面是 MySQL 常用存储引擎对一些主要索引数据结构的支持:

索引数据结构/存储引擎 InnoDB 引擎 MyISAM 引擎 Memory 引擎
B+ 树索引 ✅️️️️️️️ ✅️️️️️️️ ✅️️️️️️️
Hash 索引 ✅️️️️️️️
Full Text 索引 ✅️️️️️️️ ✅️️️️️️️

MySQL 索引的常见数据结构:

  • 哈希索引
    • 因为索引数据结构紧凑,所以查询速度非常快
    • 只支持等值比较查询 - 包括 =IN()<=>不支持任何范围查询,如 WHERE price > 100
    • 无法用于排序 - 因为哈希索引数据不是按照索引值顺序存储的。
    • 不支持部分索引匹配查找 - 因为哈希索引时使用索引列的全部内容来进行哈希计算的。如,在数据列 (A,B) 上建立哈希索引,如果查询只有数据列 A,无法使用该索引。
    • 不能用索引中的值来避免读取行 - 因为哈希索引只包含哈希值和行指针,不存储字段,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响不大。
    • 哈希索引有可能出现哈希冲突
      • 出现哈希冲突时,必须遍历链表中所有的行指针,逐行比较,直到找到符合条件的行。
      • 如果哈希冲突多的话,维护索引的代价会很高。
  • B 树索引
    • 适用于全键值查找键值范围查找键前缀查找,其中键前缀查找只适用于最左前缀查找。
    • 所有的关键字(可以理解为数据)都存储在叶子节点,非叶子节点并不存储真正的数据,所有记录节点都是按键值大小顺序存放在同一层叶子节点上。
    • 所有的叶子节点由指针连接。

【中等】为什么 InnoDB 采用 B+ 树索引?

二叉搜索树的特点是:每个节点的左儿子小于父节点,父节点又小于右儿子。其查询时间复杂度是 $$O(log(N))$$。

当然为了维持 $$O(log(N))$$ 的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更新的时间复杂度也是 $$O(log(N))$$。

随着数据库中数据的增加,索引本身大小随之增加,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘 I/O 消耗,相对于内存存取,I/O 存取的消耗要高几个数量级。可以想象一下一棵几百万节点的二叉树的深度是多少?如果将这么大深度的一颗二叉树放磁盘上,每读取一个节点,需要一次磁盘的 I/O 读取,整个查找的耗时显然是不能够接受的。那么如何减少查找过程中的 I/O 存取次数?

一种行之有效的解决方法是减少树的深度,将二叉树变为 N 叉树(多路搜索树),而 B+ 树就是一种多路搜索树

B+ 树索引适用于全键值查找键值范围查找键前缀查找,其中键前缀查找只适用于最左前缀查找。

理解 B+Tree 时,只需要理解其最重要的两个特征即可:

  • 首先,所有的关键字(可以理解为数据)都存储在叶子节点,非叶子节点并不存储真正的数据,所有记录节点都是按键值大小顺序存放在同一层叶子节点上。
  • 其次,所有的叶子节点由指针连接。如下图为简化了的 B+Tree。

B+ 树 vs B 树

  • B+ 树只在叶子节点存储数据,而 B 树的非叶子节点也要存储数据,所以 B+ 树的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。
  • 另外,B+ 树叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。

B+ 树 vs 二叉树

  • 对于有 N 个叶子节点的 B+ 树,其搜索复杂度为 O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。
  • 在实际的应用当中, d 值是大于 100 的,这样就保证了,即使数据达到千万级别时,B+ 树的高度依然维持在 13 层左右,也就是说一次数据查询操作只需要做 13 次的磁盘 I/O 操作就能查询到目标数据。
  • 而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+ 树高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。

一言以蔽之,使用 B+ 树,而不是二叉树,是为了减少树的高度,也就是为了减少磁盘 I/O 次数。

B+ 树索引和 Hash 索引的差异

  • B+ 树索引支持范围查询;Hash 索引不支持。
  • B+ 树索引支持联合索引的最左匹配原则;Hash 索引不支持。
  • B+ 树索引支持排序;Hash 索引不支持。
  • B+ 树索引支持模糊查询;Hash 索引不支持。
  • Hash 索引的等值查询比 B+ 树索引效率高。

综上,Hash 索引的应用场景很苛刻,不适用于绝大多数场景。

【中等】B+ 树索引能存多少数据?

在 InnoDB 存储引擎中,B+ 树默认数据页大小为 16KB

所有 B+ 树都是从高度为 1 的树开始,然后根据数据的插入,慢慢增加树的高度。随着插入 B+ 树索引的记录变多,1 个页(16K)无法存放这么多数据,所以会发生 B+ 树的分裂,B+ 树的高度变为 2。

非叶子节点可存储的记录数

根节点和中间节点存放的是索引键对,由(索引键、指针)组成。

索引键就是排序的列,而指针是指向下一层的地址,在 MySQL 的 InnoDB 存储引擎中占用 6 个字节。若主键是 BIGINT 类型,占 8 个字节。也即是说,这样的一个键值对需要 8+6 = 14 字节。

1
非叶子节点可存储的记录数 = 页大小(16K) / 键值对大小(14) ≈ 1100

叶子节点可存储的记录数

为了方便计算,假设数据记录的平均大小为 1000 字节(实际一般小于这个值),则

1
叶子节点可存储的记录数 = 页大小(16K) / 记录平均大小(1000) ≈ 16

由此可知,树高度为 2 的 B+ 树索引,有一个根节点,约 1100 个叶子节点。因此,最多能存放的记录数为:

1
二层 B+ 树记录数  171100 * 16 =  17,600

如何推算不同高度的 B+ 树可存储的记录数

综上所述,数据记录的平均大小为 1000 字节,主键为 BIGINT 的表,可以按如下推算其可存储的记录数:

  • 高度为 2 的 B+树索引最多能存放约 1100 * 16 = 17,600 条记录(约 1.76 万),查询时只需 2 次 I/O。
  • 高度为 3 的 B+树索引最多能存放约 1100 * 1100 * 16 = 19,360,000 条记录(约 2 千万),查询时只需 3 次 I/O。
  • 高度为 4 的 B+树索引最多能存放约 1100 * 1100 * 1100 * 16 = 21,296,000,000 条记录(约 200 多亿),查询时只需 4 次 I/O。

优化 B+ 树索引的插入性能:

  • 顺序插入(如自增 ID 或时间列)的维护代价小,性能较好。
  • 无序插入(如用户昵称)会导致页分裂、旋转等开销较大的操作,影响性能。
  • 主键设计应尽量使用顺序值(如自增 ID),以保证高并发场景下的性能。

聚簇索引和非聚簇索引

【中等】聚簇索引和非聚簇索引有什么区别?

根据叶子节点的内容,索引类型分为主键索引和非主键索引。

主键索引又被称为聚簇索引(clustered index),其叶子节点存的是整行数据

  • 聚簇表示数据行和相邻的键值紧凑地存储在一起,因为数据紧凑,所以访问快。
  • 因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引
  • InnoDB 的聚簇索引实际是在同一个结构中保存了 B 树的索引和数据行。

非主键索引又被称为二级索引(secondary index),其叶子节点存的是主键的值。数据存储在一个位置,索引存储在另一个位置,索引中包含指向数据存储位置的指针。

  • 如果语句是 select * from T where ID=500,即聚簇索引查询方式,则只需要搜索主键索引树;
  • 如果语句是 select * from T where k=5,即非聚簇索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表

也就是说,基于非聚簇索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。

显然,主键长度越小,非聚簇索引的叶子节点就越小,非聚簇索引占用的空间也就越小。

【简单】什么是覆盖索引?

覆盖索引是指:二级索引上的信息满足查询所需的所有字段,不需要回表查询聚簇索引上的数据

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段

字段特性索引

【简单】AUTO_INCREMENT 列达到最大值时会发生什么?

配置主键

在 MySQL 中,如果表定义的自增 ID 到达上限后,再申请下一个 ID,得到的值不变!因此会导致重复值的错误。

没有配置主键

如果 InnoDB 表中没有配置主键,InnoDB 会自动创建一个不可见的、长度为 6 个字节的 row_id 作为默认主键。

InnoDB 在全局维护一个 dict_sys.row_id 值。每次插入一行数据时,都会获取当前的 dict_sys.row_id 值,并将其加 1。row_id 的范围是 02^48 - 1。当 row_id 达到上限后,会从 0 开始重新循环。

如果插入的新数据的 row_id 在表中已存在,老数据会被新数据覆盖,且不会产生任何报错。可以通过 gdb 动态修改 dict_sys.row_id 来验证这一行为。

【简单】普通键和唯一键,应该怎么选择?

唯一索引的主要作用是保证数据的唯一性,而普通索引则更灵活。在业务代码保证不会写入重复数据的情况下,普通索引和唯一索引在查询性能上几乎没有差别。

普通索引 在更新操作中性能更优,尤其是在写多读少的场景下,能够利用 change buffer 减少磁盘 I/O。

唯一索引 适用于需要保证数据唯一性的场景,但在更新操作中性能较差,因为它无法使用 change buffer。

在业务允许的情况下,优先选择普通索引,因为它可以利用 change buffer 来提升更新性能。如果业务要求必须保证数据的唯一性,则必须使用唯一索引。

查询过程的性能差异:对于查询操作,普通索引和唯一索引的性能差异微乎其微。唯一索引在找到第一个满足条件的记录后会停止检索,而普通索引需要继续查找下一个记录,但由于数据页的读取方式,这种差异可以忽略不计。

更新过程的性能差异:更新操作中,普通索引可以利用 change buffer 来优化性能,而唯一索引则不能使用 change buffer。

  • change buffer 是一种将更新操作缓存在内存中的机制,减少了对磁盘的随机读取,从而提升了更新操作的性能。
  • 唯一索引在更新时需要检查唯一性约束,必须将数据页读入内存,增加了磁盘 I/O 的开销。

change buffer 的应用

  • change buffer 的数据是持久化的,即使机器掉电重启,change buffer 中的数据也不会丢失,因为它会被写入磁盘。
  • change buffer 适用于写多读少的场景,如账单类、日志类系统,因为这些场景下数据页在写入后不会立即被访问,change buffer 可以显著减少磁盘 I/O。
  • 对于写后立即查询的场景,change buffer 的效果不明显,甚至可能增加维护成本。

change buffer vs. redo log

  • redo log 主要减少随机写磁盘的 I/O 消耗,将随机写转换为顺序写。
  • change buffer 主要减少随机读磁盘的 I/O 消耗,通过缓存更新操作来减少磁盘读取。

【中等】为什么不推荐使用外键?

逻辑外键是一种在应用程序层面上管理和维护数据完整性的方法,而不是通过数据库本身的外键约束。主要是利用应用程序代码来保证引用的完整性。

逻辑外键的优缺点:

优点

  • 灵活性高:应用程序层面控制,可以更灵活地实现复杂的业务逻辑。
  • 性能优化:避免了数据库层面的约束检查,可以在某些情况下提高性能(详细看扩展知识)。
  • 跨数据库兼容性:逻辑外键在不同类型的数据之间更容易迁移。

缺点

  • 代码复杂性增加:需要在应用程序代码中手动实现和维护引用完整性,增加了代码的复杂性和错误的可能性。
  • 一致性风险:如果应用程序代码未正确实现引用完整性检查,可能导致数据不一致。
  • 维护成本高:逻辑外键需要开发人员持续关注和维护,增加了维护成本。

物理外键的优缺点:

优点

  • 自动维护引用完整性:数据库会自动检查和维护外键约束,确保数据的一致性。
  • 减少应用层复杂性:开发人员不需要手动管理引用完整性,减少了代码的复杂性和错误的可能性。
  • 数据完整性保障:数据库层面的约束能够更有效地防止非法数据的插入或更新。

缺点

  • 性能开销:外键约束会增加插入、更新和删除操作的开销,特别是在处理大量数据时。
  • 迁移和复制的复杂性:在进行数据库迁移或复制时,外键约束可能会增加复杂性,需要小心处理。
  • 灵活性较低:物理外键在某些复杂业务逻辑下可能不够灵活,需要更多的手动控制。

【中等】什么是前缀索引?

“前缀索引”是指索引开始的部分字符。对于 BLOB/TEXT 这种文本类型的列,必须使用前缀索引,因为数据库往往不允许索引这些列的完整长度。

前缀索引的优点是可以大大节约索引空间,从而提高索引效率

前缀索引的缺点是会降低索引的区分度。此外,**order by 无法使用前缀索引,无法把前缀索引用作覆盖索引**。

字段个数索引

【中等】什么是索引最左匹配原则?

使用联合索引时,查询条件必须从索引的最左列开始匹配。

其底层原理是,InnoDB 的索引采用 B+ 树数据结构,按字段顺序存储。其存储结构,决定了其查询时,必须遵循从左到右的顺序。

具体来说,最左匹配原则有以下几个要点:

为方便直观的阐述,这里假设有一张表 t,设置了联合索引 (a, b, c)

  • 必须包含最左列:如果查询条件中不包含最左列,则联合索引失效。即查询条件必须包含 a 才能使用索引。
    • WHERE a=1(能用索引)。
    • WHERE b=2(不能使用索引,因为跳过了 a)。
  • 连续匹配:不能跳过中间列,否则后面的列无法索引。
    • WHERE a=1 AND b=2(能使用 a, b 两列索引)。
    • WHERE a=1 AND c=3(只能用到 ac 无法索引,因为跳过了 b)。值得一提的是,MySQL 5.6 支持了索引下推(InnoDB 和 MyISAM 支持),允许这种情况下,将匹配 a 字段的数据推送到引擎层,由引擎层在这些数据中过滤出匹配 c 字段的数据,以此提升查询效率。
  • 遇到范围查询,右侧失效
    • 遇到 ><、前缀 LIKE(%xx) 会停止匹配
    • 注意:若遇到 >=、<=、BETWEEN、后缀 LIKE(xx%)这种范围查询,不会停止匹配,因为这些查询包含一个等值判断,可以直接定位到某个数据,向后扫描

最左前缀匹配命中示例

假设有索引 (name, age, city)

1
2
3
4
5
WHERE name='Alice' AND age=25 AND city='Beijing'  -- 完整使用索引
WHERE name='Alice' AND age=25 -- 使用 name 和 age
WHERE name='Alice' -- 仅使用 name
WHERE age=25 AND city='Beijing' -- 跳过了 name,无法使用索引
WHERE name='Alice' AND city='Beijing' -- 只能用到 name,跳过了 age,但可以应用索引下推

综上,优化 SQL 时,应尽量让查询条件符合最左前缀匹配原则,以提高查询效率。

【中等】什么是索引下推?

索引下推是一种减少回表查询、提高查询效率的技术。索引下推主要应用于联合索引。

它允许 MySQL 在使用索引查找数据时,将部分查询条件下推到存储引擎层进行过滤,从而减少需要从表中读取的数据行,减少 IO 操作。

索引下推注意点:

  • MySQL 5.6 及以后版本支持索引下推,适用于 InnoDB 和 MyISAM 存储引擎。
  • 综上,包含子查询,索引下推可能不会生效。
  • 使用函数或表达式时,索引下推不会生效。
  • 使用聚簇索引(主键)查询时,索引下推不会生效,因为它主要针对非聚簇索引。

扩展阅读:https://zhuanlan.zhihu.com/p/121084592

参考资料

MySQL CRUD

::: info 概述

CRUD 由英文单词 Create, Read, Update, Delete 的首字母组成,即增删改查

本文通过介绍基本的 MySQL CRUD 方法,向读者呈现如何访问 MySQL 数据。

扩展阅读:SQL 语法必知必会

:::

阅读全文 »

MySQL 数据类型

::: info 概述

数据类型在 MySQL 中扮演着至关重要的角色,它定义了表中每个字段可以存储的数据种类和格式。

MySQL 支持多种类型,大致可以分为三类:数值、时间和字符串类型。

:::

阅读全文 »