数据存储
难度等级:⭐⭐⭐ 前置知识:编程语言基础 后续衔接:微服务架构、缓存策略
学习路径
- 入门阶段:掌握 SQL 基础,理解关系模型
- 进阶阶段:深入索引优化、事务隔离、分库分表
- 精通阶段:能够进行数据库架构选型和性能调优
一、关系型数据库
1.1 SQL Server
SQL Server 是微软推出的企业级关系型数据库管理系统,采用 T-SQL(Transact-SQL)作为其扩展的 SQL 方言。T-SQL 在标准 SQL 的基础上增加了编程语言的特性,包括变量、流程控制、异常处理等。
存储过程与函数:存储过程是预编译的 SQL 代码块,存储在数据库服务器端,可以被多次调用。其优势在于减少网络传输、提高执行效率、增强安全性。
-- 创建存储过程示例
CREATE PROCEDURE usp_GetUserOrders
@UserId INT,
@StartDate DATE = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT u.UserName, o.OrderId, o.OrderDate, o.TotalAmount
FROM Users u
INNER JOIN Orders o ON u.UserId = o.UserId
WHERE u.UserId = @UserId
AND (@StartDate IS NULL OR o.OrderDate >= @StartDate)
ORDER BY o.OrderDate DESC;
END
执行计划分析:SQL Server 提供了图形化的执行计划查看工具。通过分析执行计划,可以识别慢查询的瓶颈所在,如全表扫描、索引查找、哈希连接等操作。重点关注 Cost 占比高的操作符,以及是否存在 Missing Index 提示。
性能调优要点:
- 合理设计索引,避免过度索引导致写入性能下降
- 使用参数化查询防止 SQL 注入并提升计划复用率
- 定期更新统计信息,确保优化器做出正确的执行计划选择
- 监控锁等待和死锁,使用
sp_who2或 Extended Events 进行诊断
1.2 MySQL
MySQL 是全球最流行的开源关系型数据库,以其轻量、高效、易用著称。InnoDB 是 MySQL 的默认存储引擎,支持事务、行级锁和外键约束。
InnoDB 引擎架构:InnoDB 的核心组件包括 Buffer Pool(缓冲池)、Redo Log(重做日志)、Undo Log(回滚日志)和 Doublewrite Buffer(双写缓冲)。Buffer Pool 用于缓存数据页和索引页,是 InnoDB 性能优化的关键参数,通常建议设置为物理内存的 50%-80%。
索引结构:MySQL 主要使用 B+ 树作为索引结构。B+ 树的所有数据都存储在叶子节点,叶子节点之间通过双向链表连接,这使得范围查询和排序操作非常高效。
-- 查看表的索引信息
SHOW INDEX FROM users;
-- 创建复合索引
CREATE INDEX idx_user_status_created ON users(status, created_at);
-- 使用 EXPLAIN 分析查询执行计划
EXPLAIN SELECT * FROM users WHERE status = 'active' AND created_at > '2024-01-01';
主从复制原理:MySQL 主从复制基于 binlog(二进制日志)。主库将数据变更写入 binlog,从库的 I/O 线程读取 binlog 并写入 relay log,SQL 线程重放 relay log 中的事件实现数据同步。复制模式包括异步复制、半同步复制和组复制(MGR)。
查询优化技巧:
- 避免使用
SELECT *,只查询需要的列 - 合理使用覆盖索引,避免回表查询
- 对于分页查询,使用延迟关联或游标分页替代
LIMIT offset, count - 使用
STRAIGHT_JOIN强制连接顺序,当优化器选择不佳时
1.3 PostgreSQL
PostgreSQL 被誉为”世界上最先进的开源关系型数据库”,以其强大的扩展能力和对 SQL 标准的严格遵循而闻名。
MVCC 机制:PostgreSQL 采用多版本并发控制(MVCC)来实现高并发。每次事务修改数据时,不会直接覆盖旧数据,而是创建新版本。每个元组包含 xmin(创建事务 ID)和 xmax(删除事务 ID)系统列,通过对比事务 ID 来判断元组对当前事务是否可见。这种机制使得读写操作不会互相阻塞。
JSONB 数据类型:PostgreSQL 原生支持 JSON 和 JSONB 两种 JSON 类型。JSONB 是二进制格式,支持索引(GIN 索引),查询性能优于 JSON。这使得 PostgreSQL 在某些场景下可以替代 MongoDB 等文档数据库。
-- 创建包含 JSONB 列的表
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
attributes JSONB
);
-- 创建 GIN 索引支持 JSONB 查询
CREATE INDEX idx_products_attributes ON products USING GIN (attributes);
-- JSONB 查询示例
SELECT * FROM products
WHERE attributes @> '{"category": "electronics"}';
-- JSONB 更新示例
UPDATE products
SET attributes = jsonb_set(attributes, '{price}', '99.99')
WHERE id = 1;
窗口函数:PostgreSQL 支持完整的窗口函数,可以在不改变结果集行数的情况下进行聚合计算。
-- 计算每个部门内按薪资排名的员工
SELECT
name, department, salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank,
AVG(salary) OVER (PARTITION BY department) as dept_avg
FROM employees;
扩展生态:PostgreSQL 拥有丰富的扩展,如 PostGIS(地理空间数据)、pg_partman(分区管理)、TimescaleDB(时序数据)、pg_stat_statements(性能分析)等。
1.4 索引原理
B+ 树结构:B+ 树是一种自平衡的多路搜索树,是关系型数据库最常用的索引结构。其特点包括:
- 所有数据都存储在叶子节点,非叶子节点仅存储键值和指针
- 叶子节点通过双向链表连接,支持高效的范围查询
- 树的高度通常为 2-4 层,意味着大多数查询只需 2-4 次磁盘 I/O
- 每个节点可以存储多个键值(通常 1000+),减少了树的高度
聚簇索引与非聚簇索引:
- 聚簇索引(Clustered Index):数据行的物理存储顺序与索引顺序一致。InnoDB 的主键就是聚簇索引,叶子节点直接存储完整数据行。
- 非聚簇索引(Secondary Index):叶子节点存储的是主键值,需要通过主键回表查询获取完整数据。
覆盖索引:当查询所需的所有列都包含在索引中时,无需回表查询,直接从索引中获取数据,这称为覆盖索引(Covering Index)。
-- 覆盖索引示例
-- 索引: idx_user_status_email (status, email)
SELECT email FROM users WHERE status = 'active'; -- 覆盖索引,无需回表
SELECT email, name FROM users WHERE status = 'active'; -- 需要回表查询 name
索引失效场景:
- 对索引列使用函数或表达式:
WHERE YEAR(created_at) = 2024 - 隐式类型转换:字符串字段不加引号
WHERE phone = 13800000000 - 模糊查询左通配符:
WHERE name LIKE '%john' - OR 条件中部分列无索引:
WHERE status = 'active' OR age > 25 - 联合索引不满足最左前缀原则
- 数据量过小或区分度过低,优化器选择全表扫描
1.5 事务与锁
ACID 特性:
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚。通过 Undo Log 实现。
- 一致性(Consistency):事务执行前后,数据库从一个一致性状态转换到另一个一致性状态。
- 隔离性(Isolation):多个事务并发执行时,彼此之间互不干扰。通过锁和 MVCC 实现。
- 持久性(Durability):事务一旦提交,对数据的修改是永久性的。通过 Redo Log 实现。
事务隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能影响 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 最低 |
| READ COMMITTED | 不可能 | 可能 | 可能 | 较低 |
| REPEATABLE READ | 不可能 | 不可能 | 可能(InnoDB 通过 Next-Key Lock 解决) | 中等 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 最高 |
-- 设置事务隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 显式事务控制
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT;
锁类型:
- 共享锁(S 锁):读锁,多个事务可以同时持有
- 排他锁(X 锁):写锁,同一时间只能有一个事务持有
- 意向锁:表级锁,表明事务稍后需要在行上加什么类型的锁
- 记录锁(Record Lock):锁定单个索引记录
- 间隙锁(Gap Lock):锁定索引记录之间的间隙
- 临键锁(Next-Key Lock):记录锁 + 间隙锁,解决幻读问题
死锁处理:
- 死锁发生的四个必要条件:互斥、占有并等待、不可剥夺、循环等待
- InnoDB 采用 wait-for graph 检测死锁,自动回滚代价较小的事务
- 预防策略:按固定顺序访问资源、一次性获取所有锁、设置锁等待超时
-- 查看死锁信息
SHOW ENGINE INNODB STATUS;
-- 设置锁等待超时(避免长时间阻塞)
SET innodb_lock_wait_timeout = 10;
1.6 分库分表
垂直拆分:
- 垂直分库:按业务模块拆分,如用户库、订单库、商品库。遵循单一职责原则,降低库之间的耦合。
- 垂直分表:将大表按列拆分,常用列和不常用列分离。例如用户表拆分为基础信息表和扩展信息表。
水平拆分:
- 水平分表:同一库内将数据按规则分散到多个结构相同的表中
- 水平分库:将数据分散到多个物理库中,通常配合分表使用
分片策略:
- 范围分片:按 ID 范围或时间范围分片,扩展性差(热点问题)
- 哈希分片:
shard_id = hash(key) % shard_count,数据分布均匀 - 一致性哈希:解决哈希分片扩容时数据迁移量大的问题
- 地理分片:按用户所在地区分片,满足数据本地化合规要求
-- 哈希分片示例(用户 ID 取模)
-- 用户表分为 4 个分片
shard_id = user_id % 4
-- 查询路由:user_id = 1024 → shard 0
SELECT * FROM users_0 WHERE user_id = 1024;
ShardingSphere:Apache ShardingSphere 是一套开源的分布式数据库解决方案,提供数据分片、读写分离、分布式事务等能力。支持 Java 和 Proxy 两种接入方式。
数据迁移方案:
- 双写迁移:新旧库同时写入,逐步切读,风险最低
- 停机迁移:业务暂停期间完成数据迁移,简单但影响可用性
- 增量同步:基于 binlog 实时同步,配合全量迁移使用
分库分表带来的问题:
- 跨库 JOIN 需要通过应用层组装或冗余字段解决
- 分布式事务需要引入 XA、Seata 等方案
- 全局唯一 ID 需要雪花算法、号段模式等方案
- 分页查询性能下降,需要二次查询或游标分页
二、NoSQL 数据库
2.1 Redis
Redis(Remote Dictionary Server)是一个开源的内存数据结构存储,可用作数据库、缓存和消息中间件。其数据模型完全基于内存,因此读写性能极高,单实例可达 10 万+ QPS。
核心数据结构:
| 数据结构 | 底层实现 | 典型场景 | 时间复杂度 |
|---|---|---|---|
| String | 简单动态字符串(SDS) | 缓存、计数器、分布式锁 | O(1) |
| Hash | 压缩列表/哈希表 | 对象存储、购物车 | O(1) |
| List | 快速列表(quicklist) | 消息队列、时间线 | O(1) 两端操作 |
| Set | 整数集合/哈希表 | 标签、共同好友 | O(1) |
| ZSet | 跳表(skiplist)+ 哈希表 | 排行榜、延迟队列 | O(log N) |
# String 操作
SET user:1001:name "张三"
GET user:1001:name
INCR page_views
EXPIRE user:1001:name 3600
# Hash 操作
HSET user:1001 name "张三" age 25 email "zhangsan@example.com"
HGETALL user:1001
HINCRBY user:1001 age 1
# List 操作(消息队列)
LPUSH task_queue "task_1" "task_2" "task_3"
BRPOP task_queue 0 # 阻塞弹出
# ZSet 操作(排行榜)
ZADD leaderboard 100 "player_1" 85 "player_2" 92 "player_3"
ZREVRANGE leaderboard 0 9 WITHSCORES # Top 10
持久化机制:
- RDB(Redis Database):定时快照,通过
SAVE或BGSAVE触发。优点是文件紧凑、恢复速度快;缺点是可能丢失最后一次快照后的数据。 - AOF(Append Only File):记录每次写操作,通过
appendfsync控制刷盘策略(always/everysec/no)。优点是数据安全性高;缺点是文件较大、恢复速度慢。 - 混合持久化(Redis 4.0+):结合 RDB 和 AOF 的优点,AOF 文件前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
# 持久化配置示例
CONFIG SET save "900 1 300 10 60 10000"
CONFIG SET appendonly yes
CONFIG SET appendfsync everysec
CONFIG SET auto-aof-rewrite-percentage 100
CONFIG SET auto-aof-rewrite-min-size 64mb
集群模式:
- Redis Cluster:官方推荐的集群方案,采用去中心化架构,16384 个哈希槽分布在多个节点上。支持自动分片和故障转移,至少需要 6 个节点(3 主 3 从)。
- Sentinel(哨兵):提供高可用方案,监控主节点状态并自动进行故障转移,但不支持数据分片。
- Codis:豌豆荚开源的代理方案,对客户端透明,但已逐渐被 Redis Cluster 取代。
分布式锁实现:
# 基于 SETNX 的简单分布式锁
SET lock:resource_1 unique_value NX EX 30
# 释放锁(使用 Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
# Redisson 实现(Java)
RLock lock = redisson.getLock("lock:resource_1");
lock.lock(30, TimeUnit.SECONDS); // 自动续期
try {
// 业务逻辑
} finally {
lock.unlock();
}
2.2 MongoDB
MongoDB 是一个基于分布式文件存储的文档数据库,使用 BSON(Binary JSON)格式存储数据。其灵活的模式(Schema-less)使其非常适合快速迭代的开发场景。
文档模型:MongoDB 的数据组织形式是文档(Document),类似于 JSON 对象。文档可以嵌套,支持数组和子文档,这使得数据模型更贴近面向对象的设计。
// 插入文档
db.users.insertOne({
name: "张三",
age: 25,
email: "zhangsan@example.com",
addresses: [
{ type: "home", city: "北京", detail: "朝阳区..." },
{ type: "work", city: "北京", detail: "海淀区..." }
],
metadata: { created_at: new Date(), status: "active" }
});
// 嵌套查询
db.users.find({ "addresses.city": "北京" });
// 数组元素匹配
db.users.find({ addresses: { $elemMatch: { type: "home", city: "北京" } } });
聚合管道:MongoDB 的聚合框架通过管道(Pipeline)模式处理数据,每个阶段对文档进行转换或过滤。
// 聚合管道示例
db.orders.aggregate([
{ $match: { status: "completed", order_date: { $gte: ISODate("2024-01-01") } } },
{ $group: {
_id: "$user_id",
total_orders: { $sum: 1 },
total_amount: { $sum: "$amount" },
avg_amount: { $avg: "$amount" }
}},
{ $sort: { total_amount: -1 } },
{ $limit: 10 },
{ $lookup: {
from: "users",
localField: "_id",
foreignField: "_id",
as: "user_info"
}}
]);
索引类型:
- 单字段索引、复合索引
- 多键索引(数组字段)
- 文本索引(全文搜索)
- 地理空间索引(位置查询)
- TTL 索引(自动过期删除)
// 创建复合索引
db.users.createIndex({ status: 1, created_at: -1 });
// 创建 TTL 索引(30 天后自动删除)
db.sessions.createIndex({ last_accessed: 1 }, { expireAfterSeconds: 2592000 });
// 查看查询执行计划
db.users.find({ status: "active" }).explain("executionStats");
副本集(Replica Set):MongoDB 的高可用方案,由一个主节点(Primary)、多个从节点(Secondary)和一个可选的仲裁节点(Arbiter)组成。主节点处理所有写操作,从节点通过 oplog 复制数据。当主节点故障时,自动选举新的主节点。
2.3 Elasticsearch
Elasticsearch 是一个分布式、RESTful 风格的搜索和分析引擎,基于 Apache Lucene 构建。其核心优势在于全文检索和实时数据分析。
倒排索引:Elasticsearch 使用倒排索引实现快速全文搜索。倒排索引将文档中的每个词映射到包含该词的文档列表。
词条 → 文档 ID 列表
"数据库" → [1, 3, 7, 12]
"索引" → [2, 3, 5, 7, 11]
"优化" → [1, 5, 8, 12]
当搜索 “数据库 优化” 时,取两个列表的交集即可快速定位相关文档 [1, 12]。
核心概念:
- Index(索引):相当于关系数据库中的表
- Document(文档):一条数据记录,JSON 格式
- Type(类型):7.0 版本后已废弃,一个 Index 只有一种 Type
- Shard(分片):索引的水平拆分单元,支持分布式存储
- Replica(副本):分片的备份,提供高可用和读扩展
DSL 查询:
{
"query": {
"bool": {
"must": [
{ "match": { "title": "数据库优化" } }
],
"filter": [
{ "range": { "publish_date": { "gte": "2024-01-01" } } },
{ "term": { "status": "published" } }
],
"should": [
{ "match_phrase": { "content": "B+树" } }
],
"minimum_should_match": 1
}
},
"highlight": {
"fields": { "title": {}, "content": {} }
},
"sort": [
{ "publish_date": "desc" },
{ "_score": "desc" }
],
"from": 0,
"size": 20
}
分片机制:创建索引时可以指定主分片数量(创建后不可修改)和副本分片数量(可动态调整)。每个分片是一个独立的 Lucene 索引,查询时协调节点将请求路由到相关分片并合并结果。
聚合分析:
{
"size": 0,
"aggs": {
"by_category": {
"terms": { "field": "category.keyword", "size": 10 },
"aggs": {
"avg_price": { "avg": { "field": "price" } },
"monthly_sales": {
"date_histogram": {
"field": "sale_date",
"calendar_interval": "month"
}
}
}
}
}
}
三、ORM 框架对比
3.1 EF Core(.NET)
Entity Framework Core 是微软官方推出的轻量级、跨平台 ORM 框架,是 EF 的现代化重写版本。
Code First 模式:通过 C# 类定义数据库表结构,配合 Migration 工具自动创建和更新数据库。
// 实体定义
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
public ICollection<Order> Orders { get; set; }
}
// DbContext 配置
public class AppDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasIndex(u => u.Email)
.IsUnique();
modelBuilder.Entity<User>()
.HasMany(u => u.Orders)
.WithOne(o => o.User)
.HasForeignKey(o => o.UserId);
}
}
// LINQ 查询
var activeUsers = await context.Users
.Where(u => u.Status == "active")
.Include(u => u.Orders)
.OrderByDescending(u => u.CreatedAt)
.Take(20)
.ToListAsync();
性能优化:
- 使用
AsNoTracking()避免变更跟踪开销(只读场景) - 避免 N+1 查询,使用
Include预加载关联数据 - 合理使用
Select投影只查询需要的字段 - 批量操作使用第三方库如 EFCore.BulkExtensions
3.2 Spring Data JPA(Java)
Spring Data JPA 是基于 Hibernate 的抽象层,通过 Repository 接口大幅简化了数据访问层的代码。
Repository 抽象:
// 实体定义
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
private String name;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
// Repository 接口
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
// 方法名派生查询
Optional<User> findByEmail(String email);
List<User> findByNameContainingAndStatus(String name, String status);
// JPQL 查询
@Query("SELECT u FROM User u WHERE u.status = :status ORDER BY u.createdAt DESC")
Page<User> findActiveUsers(@Param("status") String status, Pageable pageable);
// 原生 SQL 查询
@Query(value = "SELECT * FROM users WHERE created_at > ?1", nativeQuery = true)
List<User> findUsersCreatedAfter(Date date);
}
// Specification 动态查询
public class UserSpecifications {
public static Specification<User> hasStatus(String status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
public static Specification<User> createdAfter(Date date) {
return (root, query, cb) -> cb.greaterThan(root.get("createdAt"), date);
}
}
// 使用
Specification<User> spec = UserSpecifications.hasStatus("active")
.and(UserSpecifications.createdAfter(startDate));
List<User> users = userRepository.findAll(spec);
审计功能:通过 @EntityListeners(AuditingEntityListener.class) 和 @EnableJpaAuditing 自动维护创建时间、修改时间、创建人等字段。
3.3 MyBatis(Java)
MyBatis 是一个半自动化的 ORM 框架,将 SQL 与 Java 代码解耦,适合需要精细控制 SQL 的场景。
XML 映射:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="UserResultMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="email" column="user_email"/>
<collection property="orders" ofType="Order" columnPrefix="o_">
<id property="id" column="id"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="findActiveUsers" resultMap="UserResultMap">
SELECT u.user_id, u.user_name, u.user_email,
o.id AS o_id, o.amount AS o_amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE u.status = #{status}
ORDER BY u.created_at DESC
</select>
<!-- 动态 SQL -->
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND user_name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
</where>
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO users (name, email, status) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.email}, #{user.status})
</foreach>
</insert>
</mapper>
插件机制:通过拦截器(Interceptor)可以实现分页、审计、性能监控等功能。PageHelper 是最常用的分页插件。
代码生成器:MyBatis Generator 可以根据数据库表自动生成实体类、Mapper 接口和 XML 文件,减少样板代码编写。
3.4 SQLAlchemy(Python)
SQLAlchemy 是 Python 生态中最流行的 ORM 框架,提供了从高层 ORM 到低层 SQL 表达式的完整抽象。
Declarative 模式:
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship, declarative_base
from datetime import datetime
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(50), nullable=False)
email = Column(String(100), unique=True, nullable=False)
status = Column(String(20), default='active')
created_at = Column(DateTime, default=datetime.utcnow)
orders = relationship('Order', back_populates='user')
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
amount = Column(Integer)
user = relationship('User', back_populates='orders')
# Session 使用
from sqlalchemy.orm import Session
with Session(engine) as session:
# 查询
active_users = session.query(User)\
.filter(User.status == 'active')\
.order_by(User.created_at.desc())\
.limit(20)\
.all()
# 关联查询(预加载避免 N+1)
from sqlalchemy.orm import joinedload
users_with_orders = session.query(User)\
.options(joinedload(User.orders))\
.filter(User.status == 'active')\
.all()
# 新增
new_user = User(name='张三', email='zhangsan@example.com')
session.add(new_user)
session.commit()
关系映射:支持一对一、一对多、多对多关系映射,通过 relationship() 和 back_populates 实现双向关联。
3.5 Dapper(.NET)
Dapper 是一个轻量级的”微 ORM”,以高性能著称,通常比原生 ADO.NET 稍慢,但远快于 EF Core。
// 基本查询
using var connection = new SqlConnection(connectionString);
var users = await connection.QueryAsync<User>(
"SELECT * FROM Users WHERE Status = @Status ORDER BY CreatedAt DESC",
new { Status = "active" });
// 多映射(一对多)
var userDictionary = new Dictionary<int, User>();
var usersWithOrders = await connection.QueryAsync<User, Order, User>(
@"SELECT u.*, o.Id as OrderId, o.Amount, o.UserId
FROM Users u
LEFT JOIN Orders o ON u.Id = o.UserId
WHERE u.Status = @Status",
(user, order) => {
if (!userDictionary.TryGetValue(user.Id, out var userEntry)) {
userEntry = user;
userEntry.Orders = new List<Order>();
userDictionary.Add(userEntry.Id, userEntry);
}
if (order != null) userEntry.Orders.Add(order);
return userEntry;
},
new { Status = "active" },
splitOn: "OrderId");
// 批量操作
await connection.ExecuteAsync(
"INSERT INTO Users (Name, Email) VALUES (@Name, @Email)",
users.Select(u => new { u.Name, u.Email }));
扩展方法:通过 Dapper.Contrib 可以获取 Insert、Update、Delete、Get 等扩展方法,进一步简化操作。
3.6 选型指南
| 维度 | EF Core | Spring Data JPA | MyBatis | SQLAlchemy | Dapper |
|---|---|---|---|---|---|
| 学习曲线 | 中等 | 中等 | 较低 | 中等 | 低 |
| SQL 控制力 | 中等 | 中等 | 高 | 中等 | 极高 |
| 开发效率 | 高 | 高 | 中等 | 高 | 中等 |
| 性能 | 中等 | 中等 | 高 | 中等 | 极高 |
| 适合场景 | 快速开发 | 企业级应用 | 复杂 SQL | Python 项目 | 高性能场景 |
选型建议:
- CRUD 为主、业务逻辑简单:选择全自动 ORM(EF Core / Spring Data JPA)
- 复杂查询、存储过程多:选择半自动 ORM(MyBatis)或微 ORM(Dapper)
- 高性能要求:Dapper 或原生 SQL,配合代码生成器减少样板代码
- 混合方案:读写分离,读操作使用 ORM 提升开发效率,写操作使用原生 SQL 保证性能
四、缓存策略
4.1 缓存模式
Cache Aside(旁路缓存):最常用的缓存模式。读操作先查缓存,未命中则查数据库并写入缓存;写操作先更新数据库,再删除缓存。
读流程:
1. 查询缓存 → 命中则返回
2. 未命中 → 查询数据库
3. 将结果写入缓存
4. 返回数据
写流程:
1. 更新数据库
2. 删除缓存(而非更新,避免并发问题)
为什么写操作要删除缓存而不是更新缓存?原因有三:一是更新缓存可能涉及复杂的计算(如多表关联);二是并发场景下,先更新缓存再更新数据库,可能导致其他线程读取到旧缓存数据;三是删除操作比更新操作更简单、更高效。
Read Through(读穿透):应用程序只与缓存交互,缓存负责从数据库加载数据。缓存对应用程序透明,适合缓存中间件场景。
Write Through(写穿透):写操作同时写入缓存和数据库,保证缓存与数据库的强一致性。缺点是写入性能下降,因为需要等待数据库写入完成。
Write Behind(写回):写操作只写入缓存,缓存异步将数据刷入数据库。性能最高但存在数据丢失风险,适合对一致性要求不高的场景,如计数器、浏览量统计等。
4.2 缓存问题
缓存穿透:大量请求查询的数据在缓存和数据库中都不存在,导致所有请求都打到数据库。
解决方案:
- 布隆过滤器:在缓存层之前增加布隆过滤器,快速判断 key 是否可能存在。布隆过滤器使用多个哈希函数将元素映射到位数组中,查询时如果任一位置为 0 则元素一定不存在。
- 缓存空值:即使数据库查询结果为空,也将空值写入缓存,设置较短的过期时间(如 5 分钟)。
// 缓存空值示例
public User getUserById(Long id) {
String key = "user:" + id;
User user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user == EMPTY_USER ? null : user; // 空值标记
}
user = userMapper.selectById(id);
if (user == null) {
redisTemplate.opsForValue().set(key, EMPTY_USER, 5, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
return user;
}
缓存击穿:某个热点 key 过期瞬间,大量并发请求同时打到数据库。
解决方案:
- 互斥锁:使用分布式锁保证只有一个线程回源数据库,其他线程等待
- 逻辑过期:缓存中不设置物理过期时间,而是在值中保存过期时间戳,由后台异步线程更新
// 互斥锁解决缓存击穿
public User getUserWithLock(Long id) {
String key = "user:" + id;
String lockKey = "lock:user:" + id;
User user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 双重检查
user = redisTemplate.opsForValue().get(key);
if (user != null) return user;
// 回源数据库
user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,短暂等待后重试
try { Thread.sleep(50); } catch (InterruptedException e) {}
return getUserWithLock(id);
}
return user;
}
缓存雪崩:大量 key 同时过期或 Redis 宕机,导致数据库瞬间承受巨大压力。
解决方案:
- 过期时间加随机值:在基础过期时间上增加随机偏移(如 1-5 分钟),避免集中过期
- 集群高可用:使用 Redis Cluster 或 Sentinel 保证可用性
- 服务降级与熔断:使用 Hystrix 或 Sentinel 进行熔断降级,保护数据库
// 过期时间加随机值
int baseExpire = 1800; // 30 分钟
int randomExpire = baseExpire + new Random().nextInt(300); // 随机 +0~5 分钟
redisTemplate.opsForValue().set(key, value, randomExpire, TimeUnit.SECONDS);
4.3 分布式缓存
Redis Cluster:官方推荐的分布式方案,采用去中心化架构。数据通过哈希槽(16384 个)分布在多个主节点上,每个主节点可以有从节点提供高可用。客户端直接连接对应节点,无需代理。
# Redis Cluster 哈希槽分配
key → CRC16(key) % 16384 → 槽号 → 对应节点
# 示例:三个主节点
Node A: 槽 0-5460
Node B: 槽 5461-10922
Node C: 槽 10923-16383
Codis:豌豆荚开源的 Redis 代理方案,通过 Proxy 层实现数据分片,对客户端透明。支持平滑扩容缩容,但增加了代理层的性能开销。目前已逐渐被 Redis Cluster 取代。
一致性 Hash:解决传统哈希分片扩容时数据迁移量大的问题。一致性 Hash 将哈希空间组织成一个环,节点和数据都映射到环上,数据归属于顺时针方向最近的节点。当节点增减时,只影响相邻节点的数据。
一致性 Hash 环示意:
节点A(100)
/ \
数据(50) 数据(150)
| |
数据(80) 节点B(200)
\ /
节点C(300)
节点B 下线 → 原属于 B 的数据迁移到 C
节点D 加入 → 从相邻节点分出一部分数据给 D
缓存与数据库一致性策略:
- 延迟双删:先删除缓存,再更新数据库,最后再删除一次缓存,解决并发读写导致的不一致
- 订阅 binlog:通过 Canal 等工具订阅 MySQL binlog,异步更新缓存,保证最终一致性
- 设置缓存过期时间:即使缓存更新失败,也会在过期后自动刷新
五、学习资源推荐
书籍推荐
| 书名 | 作者 | 适合阶段 | 核心内容 |
|---|---|---|---|
| 《SQL 必知必会》 | Ben Forta | 入门 | SQL 基础语法、查询、聚合、连接 |
| 《高性能 MySQL》 | Baron Schwartz 等 | 进阶 | 索引优化、查询优化、复制与高可用 |
| 《MySQL 技术内幕:InnoDB 存储引擎》 | 姜承尧 | 进阶 | InnoDB 架构、锁、事务、调优 |
| 《Redis 设计与实现》 | 黄健宏 | 进阶 | Redis 数据结构、持久化、集群原理 |
| 《数据密集型应用系统设计》(DDIA) | Martin Kleppmann | 精通 | 分布式系统、存储引擎、一致性 |
| 《PostgreSQL 指南》 | Gilles Darold 等 | 进阶 | PostgreSQL 内部机制、优化、扩展 |
在线课程
- 极客时间《MySQL 实战 45 讲》:林晓斌(丁奇)主讲,从 MySQL 架构、索引、事务、锁等角度深入讲解
- 极客时间《Redis 核心原理与实践》:覆盖 Redis 数据结构、持久化、集群等核心原理
- Coursera “Database Management Essentials”:科罗拉多大学出品,系统学习数据库基础
- 极客时间《后端存储实战课》:涵盖 MySQL、Redis、MongoDB、Elasticsearch 等主流存储
官方文档
- MySQL 官方文档:权威参考,重点关注 InnoDB 和 Optimization 章节
- PostgreSQL 官方文档:质量极高的文档,推荐阅读 MVCC 和 Indexes 章节
- Redis 官方文档:命令参考、集群指南、持久化机制
- Elasticsearch 官方文档:DSL 查询、聚合分析、集群管理
- MongoDB 官方文档:聚合管道、索引、副本集
实践建议
- 动手搭建:在本地或云服务器搭建 MySQL 主从、Redis Cluster、MongoDB 副本集
- 性能压测:使用 sysbench 对 MySQL 进行压测,使用 redis-benchmark 测试 Redis 性能
- 执行计划分析:对日常开发中的慢查询使用 EXPLAIN 分析,理解索引使用情况
- 参与开源:关注 ShardingSphere、Canal 等开源项目,学习分布式数据库架构设计
- 生产排查:学习使用 pt-query-digest、slowlog、performance_schema 等工具进行生产问题排查