跳转至

第3章:数据库架构设计

数据库架构设计

3.1 数据库架构概述

数据库架构的重要性

数据库是后端系统的核心,数据库架构的设计直接影响系统的性能、可用性和可扩展性。

数据库架构的类型

  1. 单机数据库:单台数据库服务器
  2. 主从复制:一主多从,读写分离
  3. 分库分表:水平拆分和垂直拆分
  4. 分布式数据库:多节点协同工作

3.2 关系型数据库设计

3.2.1 数据库设计原则

范式化

第一范式(1NF): - 每个字段都是不可分割的

第二范式(2NF): - 满足1NF - 非主键字段完全依赖于主键

第三范式(3NF): - 满足2NF - 非主键字段不传递依赖于主键

SQL
-- 违反3NF的设计
CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    user_name VARCHAR(100),  -- 传递依赖
    product_id INT,
    product_name VARCHAR(100),  -- 传递依赖
    amount DECIMAL(10, 2)
);

-- 符合3NF的设计
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    product_id INT,
    amount DECIMAL(10, 2),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

反范式化

在某些场景下,为了性能考虑,可以适当违反范式化。

适用场景: - 读多写少 - 需要快速查询 - 数据更新不频繁

SQL
-- 反范式化示例:在订单表中冗余用户名
CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    user_name VARCHAR(100),  -- 冗余字段
    product_id INT,
    product_name VARCHAR(100),  -- 冗余字段
    amount DECIMAL(10, 2)
);

-- 优点:查询订单时不需要JOIN
-- 缺点:更新用户信息时需要同步更新订单表

3.2.2 索引设计

索引类型

主键索引: - 自动创建 - 唯一索引 - 聚簇索引

唯一索引: - 保证字段唯一性 - 可以有多个

普通索引: - 最常用的索引 - 可以有多个

全文索引: - 用于文本搜索 - MySQL 5.6+支持

组合索引: - 多个字段的组合 - 遵循最左前缀原则

SQL
-- 创建索引
CREATE INDEX idx_user_email ON users(email);
CREATE UNIQUE INDEX idx_user_username ON users(username);
CREATE INDEX idx_order_user_product ON orders(user_id, product_id);

-- 全文索引
CREATE FULLTEXT INDEX idx_article_content ON articles(content);

索引设计原则

  1. 选择合适的字段
  2. 经常用于WHERE的字段
  3. 经常用于JOIN的字段
  4. 经常用于ORDER BY的字段
  5. 经常用于GROUP BY的字段

  6. 避免过度索引

  7. 索引占用空间
  8. 影响写入性能
  9. 维护成本高

  10. 使用组合索引

  11. 遵循最左前缀原则
  12. 考虑字段选择性
  13. 考虑查询模式

  14. 定期维护索引

  15. 分析索引使用情况
  16. 删除无用索引
  17. 重建碎片化索引
SQL
-- 分析索引使用情况
SELECT
    table_name,
    index_name,
    cardinality,
    seq_in_index
FROM information_schema.statistics
WHERE table_schema = 'your_database';

-- 删除无用索引
DROP INDEX idx_unused ON users;  -- INDEX索引加速查询

3.2.3 查询优化

慢查询分析

SQL
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

-- 查看慢查询
SHOW VARIABLES LIKE 'slow_query_log%';

EXPLAIN分析

SQL
-- 使用EXPLAIN分析查询
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';  -- EXPLAIN查看查询执行计划

-- 输出解释
-- id: 查询序号
-- select_type: 查询类型
-- table: 表名
-- type: 访问类型(ALL, index, range, ref, eq_ref, const, system)
-- possible_keys: 可能使用的索引
-- key: 实际使用的索引
-- key_len: 索引长度
-- ref: 索引比较的列
-- rows: 预计扫描的行数
-- Extra: 额外信息

查询优化技巧

  1. **避免SELECT ***

    SQL
    -- 不推荐
    SELECT * FROM users WHERE id = 1;
    
    -- 推荐
    SELECT id, name, email FROM users WHERE id = 1;
    

  2. 使用索引

    SQL
    -- 不推荐(无法使用索引)
    SELECT * FROM users WHERE YEAR(created_at) = 2024;
    
    -- 推荐
    SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
    

  3. 避免子查询

    SQL
    -- 不推荐
    SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);  -- 子查询:嵌套在另一个查询中
    
    -- 推荐
    SELECT u.* FROM users u
    INNER JOIN orders o ON u.id = o.user_id  -- JOIN连接多个表
    WHERE o.amount > 1000;
    

  4. 使用LIMIT

    SQL
    -- 不推荐
    SELECT * FROM orders ORDER BY created_at DESC;
    
    -- 推荐
    SELECT * FROM orders ORDER BY created_at DESC LIMIT 100;
    

3.3 NoSQL数据库

3.3.1 NoSQL类型

文档型数据库(MongoDB)

特点: - 灵活的数据模型 - 水平扩展 - 高性能

适用场景: - 内容管理 - 日志存储 - 实时分析

Python
from pymongo import MongoClient

client = MongoClient('mongodb://localhost:27017/')
db = client['mydb']
collection = db['users']

# 插入文档
user = {
    'name': 'John',
    'email': 'john@example.com',
    'age': 30,
    'address': {
        'city': 'Beijing',
        'country': 'China'
    }
}
collection.insert_one(user)

# 查询文档
result = collection.find_one({'name': 'John'})

键值型数据库(Redis)

特点: - 极高的性能 - 丰富的数据结构 - 支持持久化

适用场景: - 缓存 - 会话存储 - 排行榜

Python
import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 字符串
r.set('key', 'value')
value = r.get('key')

# 哈希
r.hset('user:1', 'name', 'John')
r.hset('user:1', 'email', 'john@example.com')
name = r.hget('user:1', 'name')

# 列表
r.lpush('tasks', 'task1')
r.lpush('tasks', 'task2')
task = r.rpop('tasks')

# 集合
r.sadd('tags', 'python')
r.sadd('tags', 'redis')
tags = r.smembers('tags')

# 有序集合
r.zadd('leaderboard', {'user1': 100, 'user2': 200})
top_users = r.zrange('leaderboard', 0, 1, desc=True)

列式数据库(Cassandra)

特点: - 高写入性能 - 线性可扩展 - 无单点故障

适用场景: - 时间序列数据 - 日志存储 - 大数据分析

Python
import uuid
from cassandra.cluster import Cluster

cluster = Cluster(['127.0.0.1'])
session = cluster.connect('mykeyspace')

# 创建表
session.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id UUID PRIMARY KEY,
        name TEXT,
        email TEXT
    )
""")

# 插入数据
user_id = uuid.uuid4()
session.execute(
    "INSERT INTO users (id, name, email) VALUES (%s, %s, %s)",
    (user_id, 'John', 'john@example.com')
)

# 按主键查询(Cassandra 要求按分区键查询)
rows = session.execute("SELECT * FROM users WHERE id = %s", (user_id,))
for row in rows:
    print(row.name, row.email)

# 如需按非主键字段查询,需创建二级索引
# CREATE INDEX IF NOT EXISTS idx_users_name ON users(name);
# rows = session.execute("SELECT * FROM users WHERE name = 'John'")

图数据库(Neo4j)

特点: - 专门处理关系数据 - 高效的图遍历 - 灵活的数据模型

适用场景: - 社交网络 - 推荐系统 - 知识图谱

Python
from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687",
                              auth=("neo4j", "password"))

def create_person(driver, name):
    with driver.session() as session:
        session.run("CREATE (p:Person {name: $name})", name=name)

def create_friendship(driver, person1, person2):
    with driver.session() as session:
        session.run("""
            MATCH (p1:Person {name: $person1})
            MATCH (p2:Person {name: $person2})
            CREATE (p1)-[:FRIEND]->(p2)
        """, person1=person1, person2=person2)

# 创建节点
create_person(driver, "Alice")
create_person(driver, "Bob")

# 创建关系
create_friendship(driver, "Alice", "Bob")

3.3.2 NoSQL选型

需求 推荐数据库
缓存 Redis
文档存储 MongoDB
时序数据 InfluxDB、Cassandra
图数据 Neo4j
搜索 Elasticsearch
大数据分析 HBase、Cassandra

3.4 读写分离

3.4.1 读写分离原理

读写分离是指将读操作和写操作分离到不同的数据库实例上。

优点: - 提高读性能 - 提高系统可用性 - 降低主库压力

缺点: - 数据同步延迟 - 架构复杂度增加 - 需要处理一致性问题

3.4.2 读写分离实现

MySQL主从复制

主库配置(my.cnf)

INI
[mysqld]
server-id=1
log-bin=mysql-bin
binlog-format=ROW

从库配置(my.cnf)

INI
[mysqld]
server-id=2
relay-log=mysql-relay-bin
read-only=1

在从库上执行

SQL
CHANGE MASTER TO
    MASTER_HOST='master_host',
    MASTER_USER='repl_user',
    MASTER_PASSWORD='password',
    MASTER_LOG_FILE='mysql-bin.000001',
    MASTER_LOG_POS=0;

START SLAVE;

应用层读写分离

Python
import random

class DatabaseRouter:
    def __init__(self, master, slaves):
        self.master = master
        self.slaves = slaves

    def get_connection(self, operation):
        if operation == 'read':
            # 随机选择一个从库
            return random.choice(self.slaves)
        else:
            # 写操作使用主库
            return self.master

    def execute(self, operation, sql, params=None):
        conn = self.get_connection(operation)
        cursor = conn.cursor()
        cursor.execute(sql, params or ())
        if operation == 'read':
            return cursor.fetchall()
        else:
            conn.commit()
            return cursor.rowcount

# 使用示例
master = MySQLConnection('master_host')
slaves = [
    MySQLConnection('slave1_host'),
    MySQLConnection('slave2_host'),
]

router = DatabaseRouter(master, slaves)

# 读操作
results = router.execute('read', 'SELECT * FROM users')

# 写操作
router.execute('write', 'INSERT INTO users (name) VALUES (%s)', ('John',))

3.5 分库分表

3.5.1 分库分表类型

垂直分库

按业务模块拆分数据库。

示例: - user_db:用户相关表 - order_db:订单相关表 - product_db:商品相关表

优点: - 业务边界清晰 - 降低单库压力 - 便于团队协作

缺点: - 跨库JOIN困难 - 分布式事务复杂

SQL
-- 垂直分库前
CREATE TABLE users (id INT, name VARCHAR(100));
CREATE TABLE orders (id INT, user_id INT, amount DECIMAL);
CREATE TABLE products (id INT, name VARCHAR(100));

-- 垂直分库后
-- user_db
CREATE TABLE users (id INT, name VARCHAR(100));

-- order_db
CREATE TABLE orders (id INT, user_id INT, amount DECIMAL);

-- product_db
CREATE TABLE products (id INT, name VARCHAR(100));

水平分表

按数据量拆分表。

分片策略: - 范围分片:按ID范围 - Hash分片:按ID的Hash值 - 一致性Hash:使用一致性Hash算法

Python
# Hash分片示例
def get_shard(user_id, num_shards):
    return user_id % num_shards

def get_table_name(user_id, num_shards):
    shard_id = get_shard(user_id, num_shards)
    return f"users_{shard_id}"

# 使用示例
user_id = 12345
num_shards = 4
table_name = get_table_name(user_id, num_shards)
# table_name = "users_1"

# 查询(表名来自内部逻辑计算,非用户输入,因此可安全拼接)
# ⚠️ 注意:若table_name可能来自用户输入,必须使用白名单验证
sql = f"SELECT * FROM {table_name} WHERE id = %s"

3.5.2 分库分表中间件

ShardingSphere

YAML
# ShardingSphere配置
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/demo_ds_0
    username: root
    password: root           # ⚠️ 仅为示例,生产环境应使用环境变量或密钥管理服务
  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/demo_ds_1
    username: root
    password: root           # ⚠️ 仅为示例,生产环境应使用环境变量或密钥管理服务

rules:
  - !SHARDING
    tables:
      t_order:
        actualDataNodes: ds_${0..1}.t_order_${0..1}
        tableStrategy:
          standard:
            shardingColumn: order_id
            shardingAlgorithmName: t_order_inline
        databaseStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: db_inline
    shardingAlgorithms:
      t_order_inline:
        type: INLINE
        props:
          algorithm-expression: t_order_${order_id % 2}
      db_inline:
        type: INLINE
        props:
          algorithm-expression: ds_${user_id % 2}

MyCAT

XML
<!-- MyCAT配置 -->
<mycat:schema xmlns:mycat="http://io.mycat/">
    <schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100">
        <table name="user" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />
    </schema>

    <dataNode name="dn1" dataHost="localhost1" database="db1" />
    <dataNode name="dn2" dataHost="localhost1" database="db2" />
    <dataNode name="dn3" dataHost="localhost1" database="db3" />

    <dataHost name="localhost1" maxCon="1000" minCon="10" balance="0">
        <writeHost host="hostM1" url="localhost:3306" user="root" password="123456">
        </writeHost>
    </dataHost>
</mycat:schema>

3.6 实战练习

练习1:设计一个电商系统的数据库

设计一个电商系统的数据库,包括: 1. 用户表 2. 商品表 3. 订单表 4. 订单详情表 5. 购物车表

要求: - 合理设计表结构 - 创建必要的索引 - 考虑分库分表方案

练习2:优化慢查询

分析以下慢查询,给出优化方案:

SQL
SELECT * FROM orders
WHERE user_id = 123
AND created_at >= '2024-01-01'
AND status = 'completed'
ORDER BY created_at DESC;

练习3:设计分库分表方案

为一个千万级用户的社交系统设计分库分表方案: 1. 确定分片键 2. 选择分片策略 3. 设计分片算法

3.7 面试准备

常见面试题

  1. 什么是数据库范式?
  2. 如何设计数据库索引?
  3. 什么是读写分离?如何实现?
  4. 什么是分库分表?有哪些策略?
  5. MySQL和MongoDB的区别?

项目经验准备

准备一个数据库优化项目: - 优化前的问题 - 优化方案 - 优化效果

3.8 总结

本章介绍了数据库架构设计,包括关系型数据库设计、NoSQL数据库、读写分离和分库分表。数据库架构是后端系统的核心,需要根据业务需求合理设计。

关键要点

  1. 数据库设计需要平衡范式化和性能
  2. 索引是提升查询性能的关键
  3. NoSQL适合特定场景,不能完全替代关系型数据库
  4. 读写分离可以提高读性能
  5. 分库分表是应对大数据量的有效方案

下一步

下一章将深入学习缓存架构设计,包括缓存策略、缓存一致性等内容。