亿级PV流量下数据库架构如何扛住 淘宝京东双11数据库实战与核心问题分析
说实话,每年双11最让人心跳加速的不是抢购的那一刻,而是后台数据库那根始终绷着没断的弦。今天咱们就聊聊,淘宝和京东这些巨头是怎么把数据库玩出花的,以及背后那些让人拍案叫绝的设计思路。
流量洪峰来了,数据库凭什么不趴窝
先说个直观的数据:淘宝2020年双11峰值QPS(每秒查询量)达到了54.4万,MySQL数据库连接数超过400万,Redis缓存命中率维持在99%以上。这是什么概念?相当于每秒钟有几十万人在同时点击”立即购买”,而数据库扛住了。
数据库扛不住的场景通常是这样的:
普通架构下的崩溃链路:
用户请求 → 应用服务器 → 单台MySQL主库 → 磁盘IO被打满 → 响应超时 → 雪崩
而高可用的架构呢?
亿级流量下的防御体系:
用户请求 → CDN → 网关层 → 多级缓存(本地+分布式)→ MySQL集群 → 分库分表 → 读写分离
↓
Redis集群
↓
消息队列削峰
淘宝的”三板斧”:缓存为王、分库分表、读写分离
缓存:把数据”搬”到离用户最近的地方
淘宝最核心的秘密武器就是缓存。他们的缓存体系分好几层:
第一层:本地缓存(JVM堆内)
// 淘宝早期使用的本地缓存设计思路
public class LocalCache<K, V> {
// 使用Caffeine高性能缓存库
private LoadingCache<K, V> cache = Caffeine.newBuilder()
.maximumSize(10000) // 最多1万条
.expireAfterWrite(30, TimeUnit.SECONDS) // 30秒过期
.build(key -> loadFromRemote(key)); // 远程加载
private V loadFromRemote(K key) {
// 访问Redis或数据库
return redisTemplate.opsForValue().get(key);
}
}
为什么要加本地缓存?因为访问内存的速度是访问网络请求的几万倍。哪怕只是多一层缓存,也能挡掉大量请求。
第二层:分布式缓存(Redis集群)
淘宝自研了Tair(基于Redis增强版),用来替代开源Redis。为什么要自研?因为开源版本在极端场景下不够用:
开源Redis的瓶颈:
1. 单线程模型,连接数一多就排队
2. 持久化(RDB/AOF)在大数据量时耗时过长
3. 集群模式下数据分片的灵活性不足
Tair的改进:
1. 支持多进程模型,提升并发能力
2. 增强型持久化机制,故障恢复更快
3. 支持多种数据模型(String、Hash、SortSet等)
4. 提供缓存自动加载、热点key探测等功能
第三层:浏览器端缓存
这个很多人忽略。淘宝的商品详情页,会利用浏览器的HTTP缓存(ETag、Cache-Control),让用户第二次访问时几乎不用发请求到服务器。
HTTP缓存策略示例:
Cache-Control: public, max-age=60
# 表示这个资源可以缓存60秒,60秒内浏览器直接读本地,不请求服务器
分库分表:把大象切成小份
这是数据库架构师最头疼但也必须做的事情。
为什么需要分库分表?
单台MySQL能扛住多少QPS?通常优化得好的话,5000-10000 QPS已经是极限了。当你的QPS需要几十万的时候,只能把数据拆分到多台机器上。
淘宝的分片策略:
订单库拆分示例:
原始表:orders(单表,数据量爆炸)
↓
分库:order_db_01, order_db_02, ..., order_db_128
↓
分表:每个库内再分128张表
↓
总共:128 × 128 = 16384 张表
分片的key选择很关键。淘宝订单通常用user_id或order_id取模:
// 分库分表示例
public String getTargetDatabase(Long userId) {
int dbIndex = userId % 128; // 128个数据库分片
return "order_db_" + String.format("%03d", dbIndex);
}
public String getTargetTable(Long orderId) {
int tableIndex = orderId % 128; // 128个数据表分片
return "orders_" + String.format("%03d", tableIndex);
}
但这里有个坑——跨分片查询。如果用户要查”我最近100个订单”,按user_id分片没问题。但如果要查”某类商品的销量排名”,这就需要在每个分片上都查一遍,然后汇总。
读写分离:让读请求和写请求各走各的
这是最基础但也最有效的手段。
典型架构:
┌─────────────┐
│ 主库(Master) │ ← 只处理写操作
│ (MySQL) │
└──────┬──────┘
│ 主从复制(异步/半同步)
▼
┌────────────────────────┐
│ 从库集群(Slave) │ ← 处理读操作
│ 1主 + 32从(按需扩展) │
└────────────────────────┘
半同步复制是淘宝双11常用的配置:
-- MySQL半同步复制配置
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = ON;
SET GLOBAL rpl_semi_sync_master_timeout = 1000; -- 1秒超时
-- 从库端
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = ON;
START SLAVE;
为什么要用半同步而不是异步?因为异步复制存在数据丢失风险——主库写了数据但还没传到从库就挂了,那部分数据就丢了。半同步至少保证有一台从库收到了数据,再返回给用户。
京东的”另辟蹊径”:PostgreSQL + 云原生数据库
京东走了一条和淘宝不太一样的路。
从Oracle到MySQL再到PostgreSQL
京东早期用的是Oracle,后来为了成本迁移到MySQL,最近又在某些核心场景尝试PostgreSQL。
为什么京东选择PostgreSQL?
PostgreSQL vs MySQL 对比(京东的视角):
1. JSON支持:PG的JSONB类型性能优于MySQL的JSON
- 京东很多订单数据是半结构化的,用JSONB存储很灵活
2. 扩展能力:PG支持空间数据、全文检索等扩展
- 京东的物流追踪需要空间查询能力
3. 物化视图:PG的物化视图可以大幅简化查询
- 用于实时报表统计
4. 并发控制:PG的MVCC实现更优雅
- 在高并发场景下,锁竞争更少
京东云的数据库服务(JDDB)
京东把内部沉淀的数据库能力开放成了云服务:
京东DB架构特点:
┌─────────────────────────────────┐
│ 智能DBA平台 │
│ · 自动备份恢复 │
│ · 慢查询分析与优化建议 │
│ · 容量预测与扩容预警 │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 分布式数据库中间件 │
│ · 透明分片 │
│ · 分布式事务(XA/2PC) │
│ · 全局序列号生成 │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 多引擎数据层 │
│ · PostgreSQL(核心交易) │
│ · MySQL(一般业务) │
│ · Redis(缓存) │
│ · ClickHouse(数据分析) │
└─────────────────────────────────┘
双11前一个月:数据库架构师最紧张的时刻
不管淘宝还是京东,双11前一个月都是”战备状态”。
压测:模拟真实流量
# 京东内部压测平台的核心逻辑(简化版)
class Double11LoadTester:
def __init__(self):
self.scenarios = [
{"name": "商品浏览", "ratio": 70, "rps": 50000}, # 70%流量是浏览
{"name": "加入购物车", "ratio": 20, "rps": 10000},
{"name": "下单支付", "ratio": 10, "rps": 5000},
]
def generate_traffic(self):
"""生成符合真实分布的流量"""
for scenario in self.scenarios:
# 模拟用户行为路径
yield self._simulate_user_journey(scenario)
def _simulate_user_journey(self, scenario):
"""模拟用户完整行为链"""
# 1. 浏览商品详情
detail_req = self._make_request("GET", f"/item/{random_id()}")
# 2. 查看评价
review_req = self._make_request("GET", f"/reviews/{item_id}")
# 3. 加入购物车
cart_req = self._make_request("POST", "/cart/add",
data={"item_id": item_id, "qty": random(1,5)})
return [detail_req, review_req, cart_req]
压测中发现的问题,比如某个SQL语句慢查询、某张表热点问题,都会在这时候解决。
预热:把热点数据提前加载到缓存
双11开始前几小时,淘宝会做”缓存预热”:
-- 预热核心商品数据到Redis
-- 1. 提前加载TOP 10000热门商品
SELECT item_id, item_name, price, stock
FROM items
WHERE item_id IN (/* 热门商品ID列表 */)
AND status = 'ON_SALE';
-- 2. 写入Redis,设置较长TTL
-- 避免双11当天缓存穿透
降级预案:当扛不住的时候怎么办
这是很多人没想到的——不是所有场景都要扛住。
双11降级策略:
1. 非核心功能下线:
· 关闭商品推荐功能
· 关闭用户评论功能
· 关闭积分兑换功能
2. 静态化降级:
· 商品详情页直接返回静态HTML
· 库存数量暂时不实时更新,显示"库存充足"
3. 排队机制:
· 热门商品开启"排队抢购"
· 用户看到"当前排队第XXX位"
· 实际请求放入消息队列,慢慢处理
那些让人后背发凉的”坑”
坑一:热点Key问题
场景:某款iPhone 15首发,每秒50万请求打向同一行数据
这时候即使分库分表也没用,因为所有请求都打到同一台机器上的同一行数据。
解决方案:
1. 本地缓存 + 分布式缓存双保险
- 每台应用服务器本地缓存热点数据
- Redis只作为数据源,不承担主要读压力
2. 数据分片到多个Redis实例
- 把同一行数据拆成多个field
- 比如:product_1001_cache_1, product_1001_cache_2...
3. 提前预热,延迟更新
- 双11前预热好所有商品库存
- 抢购期间只扣减本地缓存
- 异步批量同步到数据库
坑二:缓存击穿与穿透
# 缓存击穿:热点key过期瞬间,大量请求打到数据库
def get_product_detail(product_id):
# 第一层:本地缓存
cache = local_cache.get(product_id)
if cache:
return cache
# 第二层:分布式缓存
redis_cache = redis.get(product_id)
if redis_cache:
local_cache.set(product_id, redis_cache, ttl=30)
return redis_cache
# 第三层:数据库(击穿发生在这里)
db_data = db.query(f"SELECT * FROM items WHERE id={product_id}")
# 防止穿透:空数据也缓存
if not db_data:
redis.setex(product_id, 60, "") # 空值缓存60秒
return None
redis.setex(product_id, 3600, json.dumps(db_data))
return db_data
坑三:分布式事务的一致性
双11下单涉及:库存扣减、订单创建、优惠券使用、积分扣除等多个系统。
传统方案:Seata分布式事务框架
问题:性能差,双11扛不住
淘宝方案:最终一致性 + 消息队列
// 订单创建流程(最终一致性)
@Transactional
public Order createOrder(OrderRequest request) {
// 1. 创建订单(主库)
Order order = orderMapper.insert(request.toOrder());
// 2. 发送消息(RocketMQ事务消息)
rocketMQTemplate.sendMessageInTransaction(
"order-topic",
buildOrderMessage(order)
);
// 3. 返回"创建成功"给用户
// 注意:不等库存扣减结果
return order;
}
// 消息监听器(异步处理)
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")
public void onMessage(OrderMessage message) {
// 扣减库存
inventoryService.deduct(message.getItemId(), message.getQuantity());
// 使用优惠券
couponService.use(message.getCouponId());
// 扣除积分
pointsService.deduct(message.getUserId(), message.getPoints());
// 更新订单状态
orderService.updateStatus(message.getOrderId(), "PAID");
}
这个方案的核心思想是:先给用户痛快,再慢慢处理。用户看到”下单成功”后,系统后台慢慢扣减库存、更新状态。如果最后发现库存不够,再触发退款流程。
数据库架构设计的核心原则
经过这么多实战,总结几条血泪经验:
原则一:能不进数据库就不进
请求到达顺序:
1. 浏览器HTTP缓存 → 不需要到服务器
2. CDN → 静态资源不走应用服务器
3. 本地缓存(JVM)→ 不进网络
4. Redis缓存 → 不进数据库
5. 数据库 → 最后一步
每一层都能挡住大量请求。假设Redis能挡住99%的请求,那数据库只需要处理1%的流量,压力直接减少99倍。
原则二:异步化是王道
同步流程(不可取):
用户请求 → 查库存 → 查价格 → 查优惠券 → 创建订单 → 扣减库存 → 返回结果
耗时:可能500ms+,且每一步都可能失败
异步流程(推荐):
用户请求 → 创建订单(预下单)→ 返回"排队中" → 异步处理后续
耗时:主流程50ms内返回,后续慢慢处理
原则三:数据分层存储
热数据(最近7天): SSD + InnoDB + 本地缓存
温数据(7-30天): HDD + InnoDB + Redis
冷数据(30天以上): 对象存储(OSS/HDFS)+ 归档
查询优化:
- 热数据:直接查主库
- 温数据:查副本库
- 冷数据:走数据仓库(ClickHouse/Hive)
原则四:做好监控和告警
# Prometheus监控指标配置
metrics:
- mysql_global_status_threads_connected # 当前连接数
- mysql_global_status_threads_running # 活跃线程数
- mysql_global_status_questions # 每秒查询数
- mysql_global_status_slow_queries # 慢查询数
- mysql_innodb_row_lock_time # 行锁等待时间
- mysql_replication_lag # 主从延迟
alerts:
- name: 连接数过高
condition: threads_connected > 10000
action: 自动扩容 + 告警通知
- name: 主从延迟过大
condition: seconds_behind_master > 5
action: 告警 + 暂停部分读流量
未来趋势:云原生数据库会怎么改变游戏规则
淘宝和京东都在大力投入云原生数据库。PostgreSQL的cloud-native版本(比如TimescaleDB、CockroachDB)正在改变架构设计思路。
云原生数据库的优势:
1. 存算分离:存储和计算独立扩展
- 数据存在对象存储(便宜)
- 计算节点按需扩容(弹性)
2. 自动分片:不需要手动配置分库分表
- 数据自动均衡到多个节点
- 扩容缩容对用户透明
3. 全球分布:多地域部署
- 用户访问最近的节点
- 异地多活,容灾能力更强
写在最后
双11数据库架构的本质,是在”成本、性能、可用性”三者之间找平衡。没有完美的架构,只有最适合当前业务的架构。
淘宝和京东的实战经验告诉我们:
- 缓存是第一道防线,能挡多少挡多少
- 分库分表是必选项,单库永远扛不住亿级流量
- 异步化是核心思路,先给用户反馈,再慢慢处理
- 预案比设计更重要,扛不住的时候知道怎么降级
最后说句实在话,数据库架构师最幸福的时刻,不是双11当天的庆功宴,而是第二天早上发现——昨晚的峰值流量,数据库一滴汗都没出。
你遇到过什么数据库相关的坑吗?欢迎在评论区聊聊。