三、数据库架构演进
3.1 单库单表 → 读写分离 → 分库分表
[第一阶段] 单库单表
┌─────────────┐
│ MySQL主库 │
│ (垂直拆分) │
└─────────────┘
[第二阶段] 读写分离
┌─────────────┐ ┌─────────────┐
│ MySQL主库 │ ──→ │ MySQL从库1 │
│ (写入) │ └─────────────┘
└─────────────┘ ┌─────────────┐
└─────────→│ MySQL从库2 │
└─────────────┘
[第三阶段] 分库分表
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ DB0(orders)│ │ DB1(orders)│ │ DB2(orders)│
│ 表0~表3 │ │ 表4~表7 │ │ 表8~表11 │
└─────────────┘ └─────────────┘ └─────────────┘
3.2 分库分表实战(ShardingSphere)
分片键选择原则
均匀性:数据分布均匀,避免热点
查询友好:80%以上的查询都带分片键
不可变:分片键一旦确定不能修改
水平分表示例:订单表按user_id分片
// ShardingSphere配置(application.yml)
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://localhost:3306/db0
ds1:
url: jdbc:mysql://localhost:3306/db1
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders_$->{0..3} # 2库 × 4表 = 8张表
table-strategy:
inline:
sharding-column: user_id
algorithm-expression: orders_$->{user_id % 4}
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds$->{user_id % 2}
key-generator:
column: id
type: SNOWFLAKE # 雪花算法生成全局唯一ID
分页跨分片查询优化
// 问题:跨分片分页(需要从所有分片取数据归并)
// 例如:ORDER BY create_time LIMIT 10 OFFSET 100
// 优化方案1:禁止跨分片分页,强制带分片键查询
@GetMapping("/orders")
public Page<Order> getOrders(@RequestParam Long userId, @RequestParam int page) {
// 查询必须带userId,可以直接路由到特定分片
return orderService.findByUserId(userId, page);
}
// 优化方案2:使用Elasticsearch作为二级索引
// 主键查询走MySQL分库分表,复杂查询走ES
全局ID生成器:雪花算法(Snowflake)
public class SnowflakeIdGenerator {
// 64位ID结构:
// 1位符号位(0) + 41位时间戳(毫秒) + 10位机器ID + 12位序列号
private final long twepoch = 1288834974657L; // 起始时间戳(2020-01-01)
private final long workerIdBits = 5L; // 机器ID位数
private final long datacenterIdBits = 5L; // 数据中心ID位数
private final long sequenceBits = 12L; // 序列号位数
private final long maxWorkerId = ~(-1L << workerIdBits); // 31
private final long maxDatacenterId = ~(-1L << datacenterIdBits); // 31
private final long sequenceMask = ~(-1L << sequenceBits); // 4095
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("workerId范围: 0~31");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
// 时钟回拨处理
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 回拨在5ms内,等待
waitUntil(lastTimestamp);
timestamp = timeGen();
} else {
throw new RuntimeException("时钟回拨超过5ms,拒绝生成ID");
}
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒序列号用完,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << (workerIdBits + datacenterIdBits + sequenceBits))
| (datacenterId << (workerIdBits + sequenceBits))
| (workerId << sequenceBits)
| sequence;
}
private void waitUntil(long targetTimestamp) {
while (timeGen() < targetTimestamp) {
Thread.yield();
}
}
}
四、读写分离与主从复制
4.1 MySQL主从复制原理
主库(Writer) 从库(Reader)
│ │
│ 1. 写操作写入binlog │
▼ │
┌─────────┐ │
│ binlog │ │
└────┬────┘ │
│ 2. dump线程发送binlog │ 3. I/O线程接收并写入relay_log
└──────────────────────────────→┌──────────┐
│ relay_log│
└─────┬────┘
│ 4. SQL线程执行relay_log
▼
┌─────────┐
│ 从库 │
└─────────┘
4.2 Sharding-JDBC读写分离配置
spring:
shardingsphere:
masterslave:
name: ms
master-data-source-name: master
slave-data-source-names: slave1,slave2
load-balance-algorithm-type: ROUND_ROBIN # 轮询,可选RANDOM
props:
sql-show: true # 显示SQL路由信息
4.3 主从延迟解决方案
延迟监控
-- 主库上执行
SHOW MASTER STATUS;
-- 记录File和Position
-- 从库上执行
SHOW SLAVE STATUS\G
-- 重点看:
-- Seconds_Behind_Master: 延迟秒数(从库落后主库的时间)
-- Master_Log_File / Read_Master_Log_Pos: 从库IO线程读到的位置
-- Relay_Master_Log_File / Exec_Master_Log_Pos: 从库SQL线程执行的位置
延迟应对策略
// 策略1:强制主库读(写后立即读的场景)
@Hint(name = "master", value = "true")
public Order getLatestOrder(Long userId) {
// 使用Hint强制路由到主库
return orderMapper.selectByUserId(userId);
}
// 策略2:业务容忍,降级处理
@Retryable(value = {StaleDataException.class}, maxAttempts = 3)
public Order getOrderWithRetry(Long orderId) {
Order order = orderMapper.selectFromSlave(orderId);
if (order == null && isRecentlyWritten(orderId)) {
// 最近写入的数据从库未同步,稍后重试
throw new StaleDataException("数据同步中");
}
return order;
}
// 策略3:缓存最新数据(写操作后更新缓存)
@CachePut(value = "orders", key = "#order.id")
public Order createOrder(Order order) {
orderMapper.insert(order);
return order;
}