练习题
通过练习题巩固本章所学知识。
练习 1:数据库选型分析
你正在设计一个电商平台的数据库,需求如下:
- 用户表:约 100 万用户
- 订单表:每天新增 10 万订单,历史订单需要长期保存
- 商品浏览日志:每天约 500 万条记录,只需保留最近 30 天
- 需要支持复杂的订单查询(多表 JOIN、分组统计)
问题:你会选择什么数据库方案?说明理由。
参考答案 (2 个标签)
数据库选型 系统设计
推荐方案:混合数据库方案
┌─────────────────────────────────────┐
│ 应用层 │
└──────┬────────────────────────┬─────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ MySQL │ │ MongoDB │
│ (主数据库) │ │ (日志存储) │
│ │ │ │
│ - 用户表 │ │ - 浏览日志 │
│ - 商品表 │ │ (30 天自动过期)│
│ - 订单表 │ │ │
│ - 库存表 │ │ │
└─────────────┘ └──────────────┘
│
▼
┌─────────────┐
│ Redis │
│ (缓存层) │
│ │
│ - 热点商品 │
│ - 会话缓存 │
│ - 库存计数 │
└─────────────┘理由分析:
MySQL:
- 订单需要事务保证(ACID)
- 复杂的 JOIN 查询需求
- 用户、商品数据结构化程度高
MongoDB:
- 浏览日志写多读少
- 数据结构灵活
- 可设置 TTL 自动过期
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}问题:
- 上面的代码缺少了什么?如果步骤 2 执行后、步骤 3 执行前系统崩溃了,会发生什么?
- 什么是 ACID?请解释每个字母的含义。
- 如何修改代码,使其符合 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)
);常见的查询场景:
- 根据用户 ID 查询该用户的所有订单
- 查询某个时间段内的订单
- 统计每个用户的订单总数
- 根据邮箱查找用户
问题:你应该创建哪些索引?说明理由。
参考答案 (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+)。
问题:
- 如何设计 Redis 缓存方案?
- 如何处理缓存更新(用户修改个人信息)?
- 缓存过期时间如何设置?
参考答案 (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 user2. 缓存更新策略:
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 分钟 | 不常变化,可接受短暂不一致 |
| 用户 Session | 7 天 | 与登录状态一致 |
| 热点数据 | 5 分钟 | 减少缓存击穿风险 |
| 统计数据 | 1 小时 | 允许一定延迟 |
练习 5:数据归档设计
你的 API 日志表已经有 5000 万条记录,并且每天新增 10 万条。查询开始变慢,备份时间越来越长。
问题:
- 设计一个数据归档方案
- 如何选择归档周期?
- 归档后如何查询历史数据?
参考答案 (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'])本节挑战
🎯 动手实践:
- 在本地安装 MySQL 和 Redis
- 创建本文档中描述的数据库表结构
- 实现一个简单的缓存层
- 使用 benchmark 工具测试性能提升
💡 思考题:
- 如果 Redis 宕机了,系统会怎样?如何应对?
- 如何保证数据库和缓存的数据一致性?
- 什么时候应该考虑分库分表?
