0f68a64829
Deploy Examination / deploy (push) Successful in 10s
Subtopics: - distributed-microservice: 45 questions (分布式微服务架构) - message-queue: 45 questions (消息队列) - k8s-observability: 45 questions (K8s与可观测性) - go-java-concurrency: 45 questions (Go/Java并发模型) - database-advanced: 35 questions (数据库进阶) - ai-engineering: 35 questions (AI工程实践) Question types: single_choice, true_false, fill_blank, short_answer, code_reading
309 lines
32 KiB
JSON
309 lines
32 KiB
JSON
{
|
||
"topic": "database-advanced",
|
||
"type": "code_reading",
|
||
"schema_version": "1.0.0",
|
||
"generated": "2026-09-09T00:00:00+08:00",
|
||
"questions": [
|
||
{
|
||
"id": "cr-001",
|
||
"type": "code_reading",
|
||
"difficulty": 4,
|
||
"tags": [
|
||
"mysql",
|
||
"bplus-tree",
|
||
"composite-index",
|
||
"covering-index",
|
||
"index"
|
||
],
|
||
"question": "分析以下建表语句和查询,判断索引使用情况",
|
||
"code": "CREATE TABLE orders (\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\n user_id INT NOT NULL,\n product_id INT NOT NULL,\n status TINYINT DEFAULT 0,\n amount DECIMAL(10,2),\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n INDEX idx_user_product_status (user_id, product_id, status)\n) ENGINE=InnoDB;\n\n-- 查询 Q1\nSELECT user_id, product_id, status FROM orders\nWHERE user_id = 100 AND product_id = 500;\n\n-- 查询 Q2\nSELECT user_id, product_id, status, amount FROM orders\nWHERE product_id = 500 AND user_id = 100;\n\n-- 查询 Q3\nSELECT user_id, product_id, status FROM orders\nWHERE user_id = 100 AND status = 1;",
|
||
"language": "sql",
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "single_choice",
|
||
"question": "查询 Q1 中,MySQL 优化器能否将 WHERE 条件顺序自动调整以命中联合索引 (user_id, product_id, status) 的最左前缀?",
|
||
"options": {
|
||
"A": "不能,WHERE 子句顺序必须与索引列顺序完全一致",
|
||
"B": "能,优化器会根据 WHERE 条件自动匹配索引列,不受书写顺序影响",
|
||
"C": "不能,MySQL 只会按索引从左到右扫描,不处理 WHERE 顺序",
|
||
"D": "取决于 SQL 模式 setting"
|
||
},
|
||
"answer": "B",
|
||
"explanation": "MySQL 优化器会分析 WHERE 子句中的等值条件,自动将 user_id=100 和 product_id=500 与联合索引的前两列匹配,不受 SQL 中书写顺序的影响。Q1 的两个等值条件恰好连续覆盖索引前两列 (user_id, product_id),因此可以完全命中索引前缀,type 为 ref。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "short_answer",
|
||
"question": "Q2 与 Q1 的查询条件相同(只是书写顺序不同),且 Q2 多查了 amount 列。请分析:(a) Q2 的索引扫描类型是什么?(b) Q1 是覆盖索引查询吗?Q2 呢?",
|
||
"answer": "(a) Q2 的扫描类型是 ref,与 Q1 相同。(b) Q1 是覆盖索引查询——其 SELECT 的列 user_id、product_id、status 全部包含在 idx_user_product_status 中,无需回表。Q2 不是覆盖索引查询——amount 不在索引中,需要通过聚簇索引回表取值。",
|
||
"keywords": [
|
||
"ref",
|
||
"覆盖索引",
|
||
"回表",
|
||
"Extra: Using index"
|
||
],
|
||
"scoring_rubric": "Q2 扫描类型正确得1分,Q1 覆盖索引判断正确得1分,Q2 不是覆盖索引并解释回表得1分",
|
||
"explanation": "虽然 WHERE 子句中 product_id 写在 user_id 前面,但 MySQL 优化器会自动调整匹配顺序。Q1 的 SELECT 列 (user_id, product_id, status) 全在联合索引中,EXPLAIN 的 Extra 会显示 'Using index'(覆盖索引)。Q2 多了 amount 列,不在索引中,必须回表到聚簇索引获取数据,Extra 显示 'Using index condition' 或无 Using index。"
|
||
},
|
||
{
|
||
"index": 3,
|
||
"type": "short_answer",
|
||
"question": "查询 Q3 的 WHERE 条件跳过了联合索引的第二列 product_id(只有 user_id=100 和 status=1),MySQL 能否同时利用索引中的 user_id 和 status 列?请分析其索引使用情况。",
|
||
"answer": "MySQL 只能利用索引的第一列 user_id 进行范围扫描(type=ref),将 user_id=100 的所有行取出来后,在 server 层对 status=1 做过滤。索引的第三列 status 无法直接用于索引查找,因为跳过了第二列 product_id,违反了最左前缀原则。",
|
||
"keywords": [
|
||
"最左前缀",
|
||
"索引截断",
|
||
"server 层过滤"
|
||
],
|
||
"scoring_rubric": "提到只利用 user_id 列得1分,提到最左前缀原则得1分,提到需要 server 层过滤得1分",
|
||
"explanation": "联合索引 (user_id, product_id, status) 要求从最左列开始连续使用。Q3 的条件 user_id=100 AND status=1 中间跳过了 product_id,MySQL 只能使用索引的 user_id 部分进行查找,EXPLAIN 中 key_len 只反映 user_id 的长度(4 字节 INT)。剩余的 status=1 条件在 server 层通过 'Using where' 过滤,而非在存储引擎层通过索引过滤。如果 status 选择性很高,可以考虑创建 idx_user_status(user_id, status) 辅助索引。"
|
||
}
|
||
],
|
||
"explanation": "本题考察 B+树联合索引的三个核心概念:(1) 最左前缀原则——索引列必须从左到右连续使用,但 WHERE 条件的书写顺序不影响优化器匹配;(2) 覆盖索引——当查询列全部包含在索引中时,可避免回表,性能最优;(3) 索引列跳跃——跳过中间列会导致后续列无法参与索引查找,只能退化为 server 层过滤。InnoDB 的 B+树二级索引叶子节点存储的是主键值,非覆盖索引查询必须通过主键回表到聚簇索引获取完整行数据。",
|
||
"source": null,
|
||
"related": []
|
||
},
|
||
{
|
||
"id": "cr-002",
|
||
"type": "code_reading",
|
||
"difficulty": 3,
|
||
"tags": [
|
||
"mysql",
|
||
"explain",
|
||
"index"
|
||
],
|
||
"question": "分析以下 EXPLAIN 执行计划输出,判断查询性能",
|
||
"code": "EXPLAIN SELECT o.id, o.amount, u.name\nFROM orders o\nJOIN users u ON o.user_id = u.id\nWHERE o.status = 1\n AND o.created_at >= '2026-01-01'\n AND u.city = 'Beijing';\n\n-- 输出结果(简化):\n+----+------+-------+------------------+---------+------+----------+-------+\n| id | type | table | possible_keys | key | rows | filtered | Extra |\n+----+------+-------+------------------+---------+------+----------+-------+\n| 1 | ALL | o | NULL | NULL | 500K | 10.00 | Using where |\n| 1 | ref | u | PRIMARY | PRIMARY | 1 | 25.00 | Using where |\n+----+------+-------+------------------+---------+------+----------+-------+",
|
||
"language": "sql",
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "single_choice",
|
||
"options": {
|
||
"A": "驱动表是 u,被驱动表是 o,因为 u 的 rows=1 更小",
|
||
"B": "驱动表是 o,被驱动表是 u,优化器选择了 o 作为驱动表",
|
||
"C": "无法确定驱动表,取决于 MySQL 版本",
|
||
"D": "两张表同时扫描,不存在驱动/被驱动关系"
|
||
},
|
||
"question": "根据 EXPLAIN 输出,哪张表是驱动表(第一张被访问的表)?",
|
||
"answer": "B",
|
||
"explanation": "EXPLAIN 输出中 id=1 相同的情况下,输出顺序即为表的访问顺序。orders 表(别名 o)排在第一行,是驱动表。虽然 users 表的 rows=1 更小,但优化器认为先全表扫描 orders 表再对每行用主键查 users 表的总体代价更低(这是没有索引支持 o 表过滤条件时的无奈选择)。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "single_choice",
|
||
"options": {
|
||
"A": "orders 表缺少 status 和 created_at 的联合索引,应创建 INDEX(status, created_at)",
|
||
"B": "users 表缺少 city 索引,应创建 INDEX(city)",
|
||
"C": "应使用 STRAIGHT_JOIN 强制 u 表作为驱动表",
|
||
"D": "增大 innodb_buffer_pool_size 即可解决全表扫描问题"
|
||
},
|
||
"question": "orders 表 type=ALL 导致全表扫描 50 万行,最有效的优化措施是什么?",
|
||
"answer": "A",
|
||
"explanation": "orders 表 type=ALL 且 possible_keys=NULL,说明没有可用索引。status 和 created_at 都是 WHERE 条件列,创建联合索引 (status, created_at) 可以让优化器利用索引定位 status=1 且 created_at >= '2026-01-01' 的行,避免全表扫描。即使 users 表缺少 city 索引(确实也缺),但 orders 表扫描 50 万行是当前最大的性能瓶颈。"
|
||
},
|
||
{
|
||
"index": 3,
|
||
"type": "short_answer",
|
||
"question": "users 表的 EXPLAIN 显示 filtered=25.00,这意味着什么?优化 users 表查询的最佳方案是什么?",
|
||
"answer": "filtered=25.00 表示通过索引(PRIMARY)找到的行中,只有 25% 满足 u.city='Beijing' 的 WHERE 条件。即每次对 orders 表的一行用主键查找 users 表时,返回的行还要在 server 层过滤掉 75%。最佳方案是创建 INDEX(city) 或 INDEX(city, name),使 city='Beijing' 能直接在索引层面完成过滤,减少回表。",
|
||
"keywords": [
|
||
"filtered",
|
||
"server 层过滤",
|
||
"city 索引"
|
||
],
|
||
"scoring_rubric": "正确解释 filtered 含义得1分,给出创建 city 索引建议得1分",
|
||
"explanation": "EXPLAIN 中的 filtered 列表示存储引擎返回的行中,经过 WHERE 条件过滤后保留的百分比。users 表通过主键 ref 查找返回 1 行,其中约 25% 满足 city='Beijing'。虽然单次过滤代价不高(因为 rows=1),但如果 orders 表有大量行,累积的 server 层过滤开销依然可观。创建 (city) 或 (city, name) 索引后,type 可从 ref 优化为更精准的 ref,filtered 接近 100%。"
|
||
}
|
||
],
|
||
"explanation": "本题考察 EXPLAIN 执行计划的核心字段分析:(1) type——ALL 表示全表扫描,ref 表示索引查找;(2) rows——估算扫描行数,直接影响 I/O 开销;(3) filtered——存储引擎返回行中满足 WHERE 条件的百分比;(4) possible_keys 与 key——可用索引和实际选择的索引;(5) Extra 中的 Using where 表示需要在 server 层额外过滤。优化思路优先级:先消除 ALL 全表扫描,再关注 filtered 过低的问题。",
|
||
"source": null,
|
||
"related": []
|
||
},
|
||
{
|
||
"id": "cr-003",
|
||
"type": "code_reading",
|
||
"difficulty": 5,
|
||
"tags": [
|
||
"mysql",
|
||
"slow-query",
|
||
"explain",
|
||
"index",
|
||
"bplus-tree"
|
||
],
|
||
"question": "分析以下慢查询日志中的 SQL 及其执行计划,找出性能问题并给出优化方案",
|
||
"code": "-- 慢查询日志 (long_query_time = 1)\n# Query_time: 12.345 Lock_time: 0.001 Rows_sent: 15 Rows_examined: 3200000\nSELECT p.id, p.title, p.price, c.name AS category,\n COUNT(DISTINCT r.user_id) AS review_count\nFROM products p\nLEFT JOIN product_categories pc ON p.id = pc.product_id\nLEFT JOIN categories c ON pc.category_id = c.id\nLEFT JOIN reviews r ON r.product_id = p.id\nWHERE p.status = 'active'\n AND p.price BETWEEN 50 AND 200\n AND (c.name = 'Electronics' OR c.name = 'Books')\nGROUP BY p.id, p.title, p.price, c.name\nHAVING review_count >= 5\nORDER BY review_count DESC\nLIMIT 20;\n\n-- EXPLAIN 结果:\n+----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------+\n| id | select_type | table | type | possible_keys | key | key_len | rows | filtered | Extra |\n+----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------+\n| 1 | SIMPLE | p | ALL | NULL | NULL | NULL | 800K | 1.25 | Using where; Using temporary; Using filesort|\n| 1 | SIMPLE | pc | ALL | NULL | NULL | NULL | 1.2M | 10.00 | Using join buffer (block nested loop) |\n| 1 | SIMPLE | c | eq_ref| PRIMARY | PRIMARY| 4 | 1 | 20.00 | Using where |\n| 1 | SIMPLE | r | ALL | NULL | NULL | NULL | 200K | 1.00 | Using join buffer (block nested loop) |\n+----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------+",
|
||
"language": "sql",
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "short_answer",
|
||
"question": "该查询扫描了约 320 万行但只返回 15 行,列出至少 3 个导致性能低下的原因。",
|
||
"answer": "1) products 表 type=ALL 全表扫描 80 万行,缺少 status 和 price 的索引,BETWEEN 条件无法走索引。2) product_categories 表 type=ALL 全表扫描 120 万行,缺少 product_id 外键索引,导致嵌套循环使用 Block Nested Loop 而非索引关联。3) reviews 表同样全表扫描 20 万行,缺少 product_id 索引。4) GROUP BY 产生 Using temporary 和 Using filesort,对大量中间结果进行排序和临时表操作,开销巨大。",
|
||
"keywords": [
|
||
"全表扫描",
|
||
"缺少索引",
|
||
"Block Nested Loop",
|
||
"Using temporary",
|
||
"Using filesort"
|
||
],
|
||
"scoring_rubric": "每个有效原因得1分,最多3分",
|
||
"explanation": "EXPLAIN 中三张表(p、pc、r)type=ALL 且 possible_keys=NULL,说明完全缺少索引。products 表有 80 万行,对每行都要在 product_categories 中扫描 120 万行的笛卡尔积(Block Nested Loop),再对每条匹配在 reviews 中扫描 20 万行,总扫描量呈乘积关系增长,导致 Rows_examined 高达 320 万。加上 GROUP BY + COUNT(DISTINCT) 的排序和去重开销,以及 HAVING 后置过滤,实际有效工作量远超扫描行数。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "short_answer",
|
||
"question": "请给出具体的索引创建方案和 SQL 改写建议,使查询执行时间从 12 秒降到 1 秒以内。",
|
||
"answer": "索引方案:\n1) CREATE INDEX idx_products_status_price ON products(status, price); -- 覆盖 status 等值 + price 范围\n2) CREATE INDEX idx_pc_product_category ON product_categories(product_id, category_id); -- 覆盖 JOIN + category 过滤\n3) CREATE INDEX idx_reviews_product ON reviews(product_id, user_id); -- 覆盖 JOIN + COUNT(DISTINCT user_id)\n\nSQL 改写建议:\n- 将 WHERE 中 c.name IN ('Electronics','Books') 提前到 JOIN products 前作为子查询过滤,先通过 categories 表筛选 category_id,再 JOIN product_categories,减少扫描量。\n- 考虑将 LEFT JOIN 改为 INNER JOIN(如果业务允许),因为 WHERE 条件中引用了 c.name,LEFT JOIN 实际已等效于 INNER JOIN。",
|
||
"keywords": [
|
||
"联合索引",
|
||
"覆盖索引",
|
||
"子查询下推",
|
||
"INNER JOIN"
|
||
],
|
||
"scoring_rubric": "创建 idx_products_status_price 得1分,创建 idx_pc_product_category 得1分,创建 idx_reviews_product 得1分,SQL 改写建议得1分,最多4分",
|
||
"explanation": "索引设计遵循最左前缀原则:idx_products_status_price 让 status='active' 等值查找后在索引层内做 price BETWEEN 50 AND 200 的范围扫描,避免全表扫描。idx_pc_product_category 是覆盖索引,JOIN 和 category_id 过滤都在索引层完成。idx_reviews_product 覆盖 reviews 表的 JOIN 条件和 COUNT(DISTINCT user_id)。SQL 改写的核心思路是先缩小驱动表的扫描范围——通过子查询先筛选出 Electronics 和 Books 的 category_id,再反查 product_categories,将 120 万行扫描大幅缩减。"
|
||
},
|
||
{
|
||
"index": 3,
|
||
"type": "single_choice",
|
||
"options": {
|
||
"A": "增大 sort_buffer_size 到 64MB,避免 filesort 写磁盘",
|
||
"B": "在 products 表上创建 INDEX(status, price, id) 使 GROUP BY p.id 可以利用索引排序",
|
||
"C": "将 COUNT(DISTINCT r.user_id) 改为 COUNT(r.user_id) 并在应用层去重",
|
||
"D": "禁用 GROUP BY 的 ONLY_FULL_GROUP_BY 模式"
|
||
},
|
||
"question": "对于该查询的 GROUP BY + ORDER BY review_count DESC + LIMIT 20,以下哪种优化思路最合理?",
|
||
"answer": "B",
|
||
"explanation": "当前查询需要对大量中间结果做 GROUP BY 并统计 review_count,再排序取 Top 20。如果先通过索引大幅减少参与 JOIN 的行数(从 80 万降到几千),Using temporary 和 Using filesort 的代价会显著降低。创建 INDEX(status, price, id) 不是为了让 GROUP BY p.id 利用索引排序(因为有 JOIN 和聚合函数,MySQL 不会这么做),而是为了让 products 表的扫描从 ALL 变为 range,极大减少参与后续 JOIN 的行数。选项 A 的 sort_buffer_size 只影响排序内存,不改变扫描量;选项 C 改变了语义;选项 D 是危险操作且不解决根本问题。"
|
||
}
|
||
],
|
||
"explanation": "本题是一个典型的慢查询优化场景。核心问题链:products 表缺少索引导致 80 万行全表扫描 → 每行在 product_categories 中做 Block Nested Loop 扫描 120 万行 → 每行在 reviews 中再扫描 20 万行 → 巨大的中间结果集做 GROUP BY + COUNT(DISTINCT) + ORDER BY。优化策略分三层:(1) 创建精准索引消除全表扫描;(2) SQL 改写缩小驱动表数据量;(3) 考虑业务层面的折中(如限制查询范围、异步统计等)。在实际生产中,还需配合慢查询监控、pt-query-digest 分析和执行计划对比来验证优化效果。",
|
||
"source": null,
|
||
"related": []
|
||
},
|
||
{
|
||
"id": "cr-004",
|
||
"type": "code_reading",
|
||
"difficulty": 5,
|
||
"tags": [
|
||
"mysql",
|
||
"lock",
|
||
"deadlock"
|
||
],
|
||
"question": "分析以下并发事务场景,判断是否会发生死锁以及死锁产生的原因",
|
||
"code": "-- 表结构\nCREATE TABLE accounts (\n id INT PRIMARY KEY,\n balance DECIMAL(10,2) NOT NULL,\n INDEX idx_balance (balance)\n) ENGINE=InnoDB;\n\n-- 初始数据: id=1, balance=1000; id=2, balance=2000\n\n-- ========== 事务时间线 ==========\n-- T1 (2026-09-09 10:00:01.000)\nBEGIN;\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\n-- T1 获取 id=1 行的 X 锁\n\n-- T2 (2026-09-09 10:00:01.050)\nBEGIN;\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\n-- T2 获取 id=2 行的 X 锁\n\n-- T1 (2026-09-09 10:00:01.100)\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\n-- T1 尝试获取 id=2 行的 X 锁 → 阻塞(被 T2 持有)\n\n-- T2 (2026-09-09 10:00:01.150)\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\n-- T2 尝试获取 id=1 行的 X 锁 → 检测到死锁!\n\n-- ===== 另一个场景 (场景 B) =====\n-- T3 (事务)\nBEGIN;\nUPDATE accounts SET balance = balance - 50 WHERE balance = 1000;\n-- 使用二级索引 idx_balance 查找 balance=1000 的行\n\n-- T4 (事务)\nBEGIN;\nUPDATE accounts SET balance = balance - 50 WHERE balance = 2000;\n-- 使用二级索引 idx_balance 查找 balance=2000 的行\n\n-- T3\nUPDATE accounts SET balance = balance - 50 WHERE id = 1;\n-- T3 尝试获取 id=1 的 X 锁\n\n-- T4\nUPDATE accounts SET balance = balance - 50 WHERE id = 2;\n-- T4 尝试获取 id=2 的 X 锁",
|
||
"language": "sql",
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "single_choice",
|
||
"options": {
|
||
"A": "场景 A 会死锁,场景 B 不会死锁",
|
||
"B": "场景 A 会死锁,场景 B 也会死锁",
|
||
"C": "场景 A 不会死锁,场景 B 会死锁",
|
||
"D": "两个场景都不会死锁"
|
||
},
|
||
"question": "场景 A 和场景 B 分别会发生死锁吗?",
|
||
"answer": "B",
|
||
"explanation": "场景 A 是经典的交叉锁死锁:T1 持有 id=1 的 X 锁、请求 id=2 的 X 锁;T2 持有 id=2 的 X 锁、请求 id=1 的 X 锁,形成循环等待。场景 B 看似不会死锁(T3 用 balance=1000 更新 id=1,T4 用 balance=2000 更新 id=2),但 InnoDB 的二级索引加锁机制使得:T3 在 idx_balance 上获取 balance=1000 的记录锁后,还需要对聚簇索引的 id=1 加 X 锁;T4 同理。由于两个事务获取二级索引锁的顺序和聚簇索引锁的顺序可能不一致(取决于索引记录的物理顺序),实际上存在死锁风险。如果 idx_balance 上两条记录的顺序与 id 的顺序不同(如 balance=2000 对应 id=2 排在 balance=1000 对应 id=1 前面),就会产生交叉等待。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "short_answer",
|
||
"question": "在场景 A 中,InnoDB 的死锁检测机制是如何工作的?死锁发生后 MySQL 会如何处理?",
|
||
"answer": "InnoDB 使用等待图(wait-for graph)算法检测死锁:维护事务等待关系的有向图,当图中出现环路时即判定死锁。检测时机是每次加锁请求发生阻塞时(T2 执行 UPDATE WHERE id=1 时触发检测)。死锁发生后,InnoDB 选择回滚代价最小的事务(通常是 undo log 量最少的事务)进行自动回滚,并返回错误 ERROR 1213 (40001): Deadlock found。被回滚的事务可以由应用层捕获异常后重试。innodb_deadlock_detect 参数可控制是否开启检测(默认 ON),innodb_lock_wait_timeout 控制锁等待超时。",
|
||
"keywords": [
|
||
"等待图",
|
||
"死锁检测",
|
||
"回滚",
|
||
"innodb_deadlock_detect",
|
||
"锁等待超时"
|
||
],
|
||
"scoring_rubric": "提到等待图/环路检测得1分,提到选择回滚代价最小的事务得1分,提到应用层重试得1分",
|
||
"explanation": "InnoDB 的死锁检测基于等待图算法,每当一个事务请求锁被阻塞时,会检查当前等待关系图是否形成环路。场景 A 中:T1→T2(T1 等待 T2 释放 id=2 的锁),T2→T1(T2 等待 T1 释放 id=1 的锁),形成环路。InnoDB 立即检测到并回滚 T2(假设 T2 的 undo log 更少)。在高并发场景下,频繁的死锁检测会消耗 CPU,可以通过 innodb_deadlock_detect=OFF 配合锁等待超时来降低开销,但这需要应用层处理超时重试逻辑。"
|
||
},
|
||
{
|
||
"index": 3,
|
||
"type": "short_answer",
|
||
"question": "请给出至少 3 种避免场景 A 中死锁的设计策略。",
|
||
"answer": "1) 固定加锁顺序:所有事务按 id 从小到大的顺序访问记录(先锁 id=1 再锁 id=2),消除循环等待条件。2) 使用 SELECT ... FOR UPDATE 显式加锁并统一在事务开始时一次性锁定所有需要的行,减少锁持有时间。3) 将大事务拆分为小事务,缩短锁持有时间窗口。4) 使用乐观锁(version 字段)替代悲观锁,通过 CAS 更新避免长时间持锁。5) 在业务层面确保转账操作的原子性——可以用单一 SQL UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100 来避免两行分两次更新。",
|
||
"keywords": [
|
||
"固定加锁顺序",
|
||
"缩短事务",
|
||
"乐观锁",
|
||
"一次性锁定"
|
||
],
|
||
"scoring_rubric": "每种有效策略得1分,最多3分",
|
||
"explanation": "死锁发生的四个必要条件是:互斥、持有并等待、不可剥夺、循环等待。打破任何一个条件即可避免死锁。固定加锁顺序打破循环等待条件——所有事务按 id 排序后依次获取锁,最多只能形成单向等待链。缩短事务持有时间降低死锁概率(虽然不完全消除)。乐观锁通过应用层版本检查避免数据库层面的锁竞争。对于转账场景,最根本的方案是将两行更新合并为应用层逻辑控制的单条 SQL,避免跨行事务。"
|
||
}
|
||
],
|
||
"explanation": "本题深入考察 InnoDB 锁机制和死锁分析。场景 A 是教科书级的交叉锁死锁,涉及行级 X 锁的互斥和循环等待。场景 B 揭示了一个更隐蔽的死锁来源:InnoDB 的二级索引加锁规则——UPDATE WHERE idx_col = val 会在二级索引上加记录锁,同时需要在聚簇索引(主键)上加 X 锁,两级索引的加锁顺序不一致可能导致死锁。实际生产中,还需要通过 SHOW ENGINE INNODB STATUS 查看 LATEST DETECTED DEADLOCK 区块来确认死锁详情,并结合 general_log 或 binlog 还原事务执行顺序。预防策略的核心思想是:统一加锁顺序、减少锁持有时间、降低锁粒度。",
|
||
"source": null,
|
||
"related": []
|
||
},
|
||
{
|
||
"id": "cr-005",
|
||
"type": "code_reading",
|
||
"difficulty": 4,
|
||
"tags": [
|
||
"redis",
|
||
"cluster",
|
||
"slot",
|
||
"hybrid-persistence",
|
||
"bigkey"
|
||
],
|
||
"question": "分析以下 Redis 集群配置和运维命令,判断集群状态和恢复方案",
|
||
"code": "# ===== Redis 集群节点信息 =====\n$ redis-cli -c -h 10.0.0.1 -p 7001 cluster nodes\n3a1b2c... 10.0.0.1:7001@17001 myself,master - 0 1694236800 1 connected 0-5460\n7d4e5f... 10.0.0.2:7002@17002 master - 0 1694236800 2 connected 5461-10922\nb8c9d0... 10.0.0.3:7003@17003 master - 0 1694236800 3 connected 10923-16383\ne1f2a3... 10.0.0.4:7004@17004 slave 3a1b2c... 0 1694236800 4 connected\na5b6c7... 10.0.0.5:7005@17005 slave 7d4e5f... 0 1694236800 5 connected\n\n# ===== 节点 7002 (10.0.0.2) 宕机后的集群状态 =====\n$ redis-cli -c -h 10.0.0.1 -p 7001 cluster info\ncluster_state:fail\ncluster_slots_assigned:16384\ncluster_slots_ok:10923\ncluster_slots_pfail:0\ncluster_slots_fail:5462\ncluster_known_nodes:6\ncluster_size:3\n\n# ===== 混合持久化配置 (redis.conf) =====\nappendonly yes\nappendfilename \"appendonly.aof\"\naof-use-rdb-preamble yes\naof-loading-trickle-size 4096\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# ===== 恢复操作 =====\n# 1. 尝试恢复 7002 节点\n$ cp /var/lib/redis/7002/dump.rdb /var/lib/redis/7002/appendonlydir/appendonly.aof.1.rdb\n$ redis-server /etc/redis/7002.conf\n\n# 2. 查看恢复后的日志\n$ tail -20 /var/log/redis/7002.log\n[12345] 09 Sep 2026 10:05:00.123 * Done loading RDB preamble from AOF file\n[12345] 09 Sep 2026 10:05:00.234 * Starting BGSAVE for AOF\n[12345] 09 Sep 2026 10:05:00.235 * Background AOF rewrite started by pid 12346\n[12345] 09 Sep 2026 10:05:01.456 * Background AOF rewrite finished successfully",
|
||
"language": "redis",
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "single_choice",
|
||
"options": {
|
||
"A": "slots 5461-10922 失效,且 7005 (slave) 会自动提升为 master 接管这些 slot",
|
||
"B": "slots 5461-10922 失效,但需要手动执行 CLUSTER FAILOVER 才能故障转移",
|
||
"C": "slots 5461-10922 失效,需要手动分配 slot 给其他 master 节点",
|
||
"D": "集群状态为 fail 只是告警,不影响正常的 slot 路由"
|
||
},
|
||
"question": "节点 7002 (master, slots 5461-10922) 宕机后,集群状态 cluster_slots_fail=5462。以下分析正确的是?",
|
||
"answer": "A",
|
||
"explanation": "Redis Cluster 在 master 节点宕机后,其 slave 节点(7005)会在 cluster-node-timeout(默认 15 秒)后被标记为 PFAIL,随后被集群大多数 master 节点投票确认为 FAIL,自动提升为新的 master 并接管原 master 的所有 slot。从 cluster_nodes 输出看,7005 是 7002 的 slave,在正常配置下会自动故障转移。如果 cluster_slots_fail 持续显示 5462 而非恢复为 0,可能是因为 7005 也已下线或 cluster-node-timeout 还未到期。当 cluster_state=fail 时,整个集群对外拒绝服务(所有读写请求返回 CLUSTERDOWN 错误),直到所有 slot 都被正常 master 覆盖。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "short_answer",
|
||
"question": "配置 aof-use-rdb-preamble yes 意味着什么?它如何影响 Redis 的 AOF 文件格式和恢复流程?",
|
||
"answer": "aof-use-rdb-preamble yes 表示开启混合持久化(Redis 4.0+),AOF 文件的格式变为:前半部分是 RDB 格式的全量数据快照,后半部分是 AOF 格式的增量写命令。恢复时的流程为:(1) 先以 RDB 格式快速加载前半部分的全量数据(速度快,因为是二进制格式);(2) 再逐条重放后半部分的 AOF 增量命令(保证数据完整性)。日志中 'Done loading RDB preamble from AOF file' 正是第一步完成的标志。这种混合格式兼顾了 RDB 快速恢复和 AOF 数据完整的优点——纯 AOF 恢复需要逐条重放所有命令(慢),纯 RDB 会丢失最后一次快照后的数据。",
|
||
"keywords": [
|
||
"混合持久化",
|
||
"RDB preamble",
|
||
"AOF",
|
||
"恢复流程"
|
||
],
|
||
"scoring_rubric": "正确解释混合持久化格式得1分,正确描述恢复流程(先RDB后AOF)得1分,对比纯RDB/AOF的优劣得1分",
|
||
"explanation": "混合持久化是 Redis 4.0 引入的优化。传统模式下:RDB 快照恢复快但会丢失快照间隔内的数据;AOF 数据完整但恢复慢(需要逐条执行所有写命令)。混合持久化将两者结合:触发 BGSAVE 时,生成的 AOF 文件以 RDB 头部 + AOF 尾部的方式组织。从配置看,save 900 1 等规则触发 RDB 快照,而 appendonly yes 同时记录所有写命令。aof-use-rdb-preamble yes 确保 AOF 文件以 RDB 格式开头。恢复时 Redis 检测到 RDB preamble 后先快速加载(毫秒到秒级),再重放 AOF 增量(通常很短),兼顾速度和完整性。"
|
||
},
|
||
{
|
||
"index": 3,
|
||
"type": "short_answer",
|
||
"question": "从 cluster_nodes 输出看,该集群有 3 master + 3 slave,slot 分配为 0-5460、5461-10922、10923-16383。如果需要扩展到 6 master 节点(每个 master 约 2730 个 slot),请说明 rehash 操作的大致流程。",
|
||
"answer": "Redis Cluster 扩容 rehash 流程:\n1) 新增 3 个 master 节点(7006/7007/7008)加入集群:CLUSTER MEET <ip> <port>\n2) 为每个新节点分配 slot:从现有 3 个 master 各迁移约 1820 个 slot 给 3 个新节点,例如 CLUSTER ADDSLOTS 或使用 redis-cli --cluster reshard 工具\n3) 迁移过程中,源节点将对应 slot 的数据通过 MIGRATE 命令逐 key 迁移到目标节点\n4) 迁移期间:源节点收到不属于自己的 slot 的请求时,返回 ASK 重定向;目标节点收到未完成迁移的 key 时,返回 MOVED 重定向\n5) 所有 slot 迁移完成后,集群状态恢复正常,新节点开始接收对应 slot 的读写请求\n\n注意:生产环境建议使用 redis-cli --cluster reshard <host:port> 交互式完成,它会自动处理 slot 状态标记和数据迁移。",
|
||
"keywords": [
|
||
"rehash",
|
||
"slot 迁移",
|
||
"MIGRATE",
|
||
"reshard",
|
||
"ASK/MOVED"
|
||
],
|
||
"scoring_rubric": "描述 CLUSTER MEET 加入节点得1分,描述 slot 分配和迁移过程得1分,提到 ASK/MOVED 重定向机制得1分",
|
||
"explanation": "Redis Cluster 的 slot 迁移是在线扩容的核心机制。每个 slot 在迁移前需要将状态从 NODE 设为 MIGRATING(源节点)和 IMPORTING(目标节点)。迁移过程中,源节点对正在迁移的 slot 的请求:如果 key 存在则正常处理,不存在则返回 ASK 重定向到目标节点。目标节点对 IMPORTING 状态的 slot:只有收到 ASKING 命令后的请求才会处理,否则返回 MOVED。这种设计确保了迁移期间集群的可用性——已迁移的 key 在新节点处理,未迁移的 key 在原节点处理,不会丢失数据。redis-cli --cluster reshard 工具封装了上述所有步骤。"
|
||
}
|
||
],
|
||
"explanation": "本题综合考察 Redis Cluster 的运维能力和混合持久化机制。从 cluster_nodes 输出可以分析出集群拓扑:3 master 各负责约 1/3 的 slot(共 16384 个),每个 master 有一个 slave 用于高可用。故障转移流程涉及 PFAIL → FAIL 状态转换和 slave 提升。混合持久化(aof-use-rdb-preamble)是 Redis 4.0+ 的重要特性,将 RDB 的快速加载和 AOF 的数据完整性结合。扩容 rehash 是 Redis Cluster 运维中最复杂的操作之一,涉及 ASK/MOVED 重定向、MIGRATE 命令和 slot 状态管理。实际运维中还需关注 bigkey 问题——如果迁移的 slot 中包含大 key(如百万元素的 Hash/Set),MIGRATE 命令会长时间阻塞,建议先通过 redis-cli --bigkeys 识别并拆分大 key。",
|
||
"source": null,
|
||
"related": []
|
||
}
|
||
]
|
||
} |