导航菜单

练习题

通过练习题巩固本章所学知识。

练习 1:数据库选型分析

你正在设计一个电商平台的数据库,需求如下:

  • 用户表:约 100 万用户
  • 订单表:每天新增 10 万订单,历史订单需要长期保存
  • 商品浏览日志:每天约 500 万条记录,只需保留最近 30 天
  • 需要支持复杂的订单查询(多表 JOIN、分组统计)

问题:你会选择什么数据库方案?说明理由。

参考答案 (2 个标签)
数据库选型 系统设计

推荐方案:混合数据库方案

┌─────────────────────────────────────┐
│  应用层                              │
└──────┬────────────────────────┬─────┘
       │                        │
       ▼                        ▼
┌─────────────┐         ┌──────────────┐
│  MySQL      │         │  MongoDB     │
│  (主数据库) │         │  (日志存储)  │
│             │         │              │
│  - 用户表   │         │  - 浏览日志  │
│  - 商品表   │         │  (30 天自动过期)│
│  - 订单表   │         │              │
│  - 库存表   │         │              │
└─────────────┘         └──────────────┘


┌─────────────┐
│  Redis      │
│  (缓存层)   │
│             │
│  - 热点商品 │
│  - 会话缓存 │
│  - 库存计数 │
└─────────────┘

理由分析

  1. MySQL

    • 订单需要事务保证(ACID)
    • 复杂的 JOIN 查询需求
    • 用户、商品数据结构化程度高
  2. MongoDB

    • 浏览日志写多读少
    • 数据结构灵活
    • 可设置 TTL 自动过期
  3. Redis

    • 缓存热点商品数据
    • 库存计数器(高并发)
    • 用户会话存储

练习 2:ACID 事务理解

你的 API 平台有一个转账功能,用户 A 给用户 B 转账 100 元:

def transfer(user_a, user_b, amount):
    cursor = db.cursor()

    # 步骤 1:检查 A 的余额
    cursor.execute('SELECT balance FROM users WHERE id = %s', (user_a,))
    balance_a = cursor.fetchone()['balance']

    if balance_a < amount:
        return {'success': False, 'message': '余额不足'}

    # 步骤 2:A 扣钱
    cursor.execute(
        'UPDATE users SET balance = balance - %s WHERE id = %s',
        (amount, user_a)
    )

    # 步骤 3:B 加钱
    cursor.execute(
        'UPDATE users SET balance = balance + %s WHERE id = %s',
        (amount, user_b)
    )

    db.commit()
    return {'success': True}

问题

  1. 上面的代码缺少了什么?如果步骤 2 执行后、步骤 3 执行前系统崩溃了,会发生什么?
  2. 什么是 ACID?请解释每个字母的含义。
  3. 如何修改代码,使其符合 ACID 原则?
参考答案 (2 个标签)
ACID 事务

1. 问题分析

缺少的关键代码:

# 开启事务
db.begin()

# 异常处理
try:
    # ... 转账操作
    db.commit()
except Exception as e:
    db.rollback()  # 回滚
    raise

后果:如果步骤 2 执行后系统崩溃,A 的钱扣了但 B 没收到。数据不一致!

2. ACID 详解

特性含义说明
Atomicity原子性要么全部完成,要么全部失败
Consistency一致性事务前后数据状态都合法
Isolation隔离性并发事务互不干扰
Durability持久性提交后永久保存

3. 符合 ACID 的代码

def transfer(user_a, user_b, amount):
    cursor = db.cursor()
    try:
        db.begin()  # 开启事务

        # 检查余额并加锁
        cursor.execute(
            'SELECT balance FROM users WHERE id = %s FOR UPDATE',
            (user_a,)
        )
        balance_a = cursor.fetchone()['balance']

        if balance_a < amount:
            db.rollback()
            return {'success': False, 'message': '余额不足'}

        # A 扣钱
        cursor.execute(
            'UPDATE users SET balance = balance - %s WHERE id = %s',
            (amount, user_a)
        )

        # B 加钱
        cursor.execute(
            'UPDATE users SET balance = balance + %s WHERE id = %s',
            (amount, user_b)
        )

        db.commit()
        return {'success': True}

    except Exception as e:
        db.rollback()
        return {'success': False, 'message': str(e)}

关键点

ACID 特性代码实现
原子性try/except + rollback()
一致性余额检查 + 约束验证
隔离性FOR UPDATE 行锁
持久性commit() 写入磁盘

练习 3:索引设计

你有一个用户表 users 和订单表 orders

CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  email VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id INT NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  status VARCHAR(50) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

常见的查询场景:

  1. 根据用户 ID 查询该用户的所有订单
  2. 查询某个时间段内的订单
  3. 统计每个用户的订单总数
  4. 根据邮箱查找用户

问题:你应该创建哪些索引?说明理由。

参考答案 (2 个标签)
索引优化 MySQL

推荐索引方案

-- users 表
CREATE INDEX idx_users_email ON users(email);
-- 理由:根据邮箱查找是常见操作

CREATE INDEX idx_users_created ON users(created_at);
-- 理由:可能需要按注册时间筛选用户

-- orders 表
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- 理由:根据用户 ID 查订单(外键查询)

CREATE INDEX idx_orders_created ON orders(created_at);
-- 理由:时间范围查询频繁

CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);
-- 理由:复合索引,优化"查询某用户某时间段的订单"

CREATE INDEX idx_orders_status ON orders(status);
-- 理由:可能需要按订单状态筛选

索引选择原则

  • ✅ 高频查询的 WHERE 条件字段
  • ✅ JOIN 操作的外键字段
  • ✅ 范围查询的字段(时间)
  • ⚠️ 避免过度索引(影响写入性能)
  • ⚠️ 区分度低的字段不适合索引(如性别)

练习 4:缓存设计

你的 API 平台有一个获取用户信息的接口:

def get_user_profile(user_id):
    cursor = db.cursor()
    cursor.execute(
        'SELECT * FROM users WHERE id = %s',
        (user_id,)
    )
    return cursor.fetchone()

随着用户量增长,这个接口成为性能瓶颈(QPS 达到 5000+)。

问题

  1. 如何设计 Redis 缓存方案?
  2. 如何处理缓存更新(用户修改个人信息)?
  3. 缓存过期时间如何设置?
参考答案 (2 个标签)
缓存设计 Redis

1. 缓存方案

import redis
import json

redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)

def get_user_profile(user_id):
    cache_key = f'user:profile:{user_id}'

    # 先查缓存
    cached = redis_client.get(cache_key)
    if cached:
        return json.loads(cached)

    # 缓存未命中,查数据库
    cursor = db.cursor()
    cursor.execute(
        'SELECT * FROM users WHERE id = %s',
        (user_id,)
    )
    user = cursor.fetchone()

    if user:
        # 存入缓存,过期时间 30 分钟
        redis_client.setex(cache_key, 1800, json.dumps(user))

    return user

2. 缓存更新策略

def update_user_profile(user_id, data):
    # 1. 更新数据库
    cursor = db.cursor()
    cursor.execute(
        'UPDATE users SET email=%s, name=%s WHERE id=%s',
        (data['email'], data['name'], user_id)
    )
    db.commit()

    # 2. 删除缓存(下次读取时重新加载)
    cache_key = f'user:profile:{user_id}'
    redis_client.delete(cache_key)

3. 过期时间策略

数据类型过期时间理由
用户基本信息30 分钟不常变化,可接受短暂不一致
用户 Session7 天与登录状态一致
热点数据5 分钟减少缓存击穿风险
统计数据1 小时允许一定延迟

练习 5:数据归档设计

你的 API 日志表已经有 5000 万条记录,并且每天新增 10 万条。查询开始变慢,备份时间越来越长。

问题

  1. 设计一个数据归档方案
  2. 如何选择归档周期?
  3. 归档后如何查询历史数据?
参考答案 (2 个标签)
数据归档 系统设计

1. 归档方案

采用分层归档策略:

┌─────────────────────────────────────┐
│  热数据层(最近 30 天)              │
│  ├─ MySQL 主表                      │
│  └─ 保持索引,支持快速查询          │
├─────────────────────────────────────┤
│  温数据层(30-90 天)               │
│  ├─ MySQL 归档表(按月分区)         │
│  └─ 从库查询,减轻主库压力          │
├─────────────────────────────────────┤
│  冷数据层(90 天以上)              │
│  ├─ CSV 文件 + 对象存储              │
│  └─ 按需加载查询                    │
└─────────────────────────────────────┘

归档脚本

from datetime import datetime, timedelta

def archive_old_logs(days: int = 30):
    cutoff_date = datetime.now() - timedelta(days=days)
    month_str = cutoff_date.strftime('%Y%m')

    with get_db_cursor() as cursor:
        # 创建归档表
        cursor.execute(f'''
            CREATE TABLE IF NOT EXISTS api_logs_{month_str}
            LIKE api_logs
        ''')

        # 移动数据
        cursor.execute(f'''
            INSERT INTO api_logs_{month_str}
            SELECT * FROM api_logs
            WHERE created_at < %s
        ''', (cutoff_date,))

        # 删除已归档的数据
        cursor.execute(
            'DELETE FROM api_logs WHERE created_at < %s',
            (cutoff_date,)
        )

2. 归档周期选择

数据类型归档周期保留时间
API 日志每天30 天热 + 60 天温
用户操作日志每周90 天热 + 270 天温
审计日志每月1 年热 + 2 年冷

3. 历史数据查询

def get_historical_logs(user_id: int, start_date: date, end_date: date):
    """查询历史日志(可能跨越多个归档表)"""
    results = []

    # 查询主表
    with get_db_cursor() as cursor:
        cursor.execute('''
            SELECT * FROM api_logs
            WHERE user_id = %s AND created_at BETWEEN %s AND %s
        ''', (user_id, start_date, end_date))
        results.extend(cursor.fetchall())

    # 查询归档表
    current = start_date
    while current <= end_date:
        month = current.strftime('%Y%m')
        try:
            with get_db_cursor() as cursor:
                cursor.execute(f'''
                    SELECT * FROM api_logs_{month}
                    WHERE user_id = %s AND created_at BETWEEN %s AND %s
                ''', (user_id, start_date, end_date))
                results.extend(cursor.fetchall())
        except:
            pass  # 归档表可能不存在
        # 移动到下一月
        current = (current.replace(day=1) + timedelta(days=32)).replace(day=1)

    return sorted(results, key=lambda x: x['created_at'])

本节挑战

🎯 动手实践

  1. 在本地安装 MySQL 和 Redis
  2. 创建本文档中描述的数据库表结构
  3. 实现一个简单的缓存层
  4. 使用 benchmark 工具测试性能提升

💡 思考题

  • 如果 Redis 宕机了,系统会怎样?如何应对?
  • 如何保证数据库和缓存的数据一致性?
  • 什么时候应该考虑分库分表?

搜索