第3章:数据库架构设计¶
3.1 数据库架构概述¶
数据库架构的重要性¶
数据库是后端系统的核心,数据库架构的设计直接影响系统的性能、可用性和可扩展性。
数据库架构的类型¶
- 单机数据库:单台数据库服务器
- 主从复制:一主多从,读写分离
- 分库分表:水平拆分和垂直拆分
- 分布式数据库:多节点协同工作
3.2 关系型数据库设计¶
3.2.1 数据库设计原则¶
范式化¶
第一范式(1NF): - 每个字段都是不可分割的
第二范式(2NF): - 满足1NF - 非主键字段完全依赖于主键
第三范式(3NF): - 满足2NF - 非主键字段不传递依赖于主键
-- 违反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)
);
反范式化¶
在某些场景下,为了性能考虑,可以适当违反范式化。
适用场景: - 读多写少 - 需要快速查询 - 数据更新不频繁
-- 反范式化示例:在订单表中冗余用户名
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+支持
组合索引: - 多个字段的组合 - 遵循最左前缀原则
-- 创建索引
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);
索引设计原则¶
- 选择合适的字段:
- 经常用于WHERE的字段
- 经常用于JOIN的字段
- 经常用于ORDER BY的字段
-
经常用于GROUP BY的字段
-
避免过度索引:
- 索引占用空间
- 影响写入性能
-
维护成本高
-
使用组合索引:
- 遵循最左前缀原则
- 考虑字段选择性
-
考虑查询模式
-
定期维护索引:
- 分析索引使用情况
- 删除无用索引
- 重建碎片化索引
-- 分析索引使用情况
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 查询优化¶
慢查询分析¶
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- 查看慢查询
SHOW VARIABLES LIKE 'slow_query_log%';
EXPLAIN分析¶
-- 使用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: 额外信息
查询优化技巧¶
-
**避免SELECT ***
-
使用索引
-
避免子查询
-
使用LIMIT
3.3 NoSQL数据库¶
3.3.1 NoSQL类型¶
文档型数据库(MongoDB)¶
特点: - 灵活的数据模型 - 水平扩展 - 高性能
适用场景: - 内容管理 - 日志存储 - 实时分析
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)¶
特点: - 极高的性能 - 丰富的数据结构 - 支持持久化
适用场景: - 缓存 - 会话存储 - 排行榜
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)¶
特点: - 高写入性能 - 线性可扩展 - 无单点故障
适用场景: - 时间序列数据 - 日志存储 - 大数据分析
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)¶
特点: - 专门处理关系数据 - 高效的图遍历 - 灵活的数据模型
适用场景: - 社交网络 - 推荐系统 - 知识图谱
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):
从库配置(my.cnf):
在从库上执行:
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;
应用层读写分离¶
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困难 - 分布式事务复杂
-- 垂直分库前
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算法
# 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¶
# 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¶
<!-- 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:优化慢查询¶
分析以下慢查询,给出优化方案:
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 面试准备¶
常见面试题¶
- 什么是数据库范式?
- 如何设计数据库索引?
- 什么是读写分离?如何实现?
- 什么是分库分表?有哪些策略?
- MySQL和MongoDB的区别?
项目经验准备¶
准备一个数据库优化项目: - 优化前的问题 - 优化方案 - 优化效果
3.8 总结¶
本章介绍了数据库架构设计,包括关系型数据库设计、NoSQL数据库、读写分离和分库分表。数据库架构是后端系统的核心,需要根据业务需求合理设计。
关键要点¶
- 数据库设计需要平衡范式化和性能
- 索引是提升查询性能的关键
- NoSQL适合特定场景,不能完全替代关系型数据库
- 读写分离可以提高读性能
- 分库分表是应对大数据量的有效方案
下一步¶
下一章将深入学习缓存架构设计,包括缓存策略、缓存一致性等内容。