10亿订单咋分库分表,正好前两天在公司楼下喝咖啡,隔壁组的小王就在那边吐槽,说他们搞双十一订单库,差点没把他头发薅光...哎呀,反正这种量你不用分,MySQL一张表撑死也顶不住嘛,对吧,你看你10亿订单,你每行1KB,这就10TB了,MySQL官方不是说的嘛,单表5000万、1亿,顶天了,再大你物理文件都要炸了。
就说前天晚上快十点了,还没下班,我们Leader过来拍桌子:“兄弟们,电商新系统订单要爆发了,单表撑不住了,你们搞下分库分表,线上别出事!”你说急不急?本来想着简单,分表分库,谁不会写个hash?但真动手,才发现坑一堆。
最传统的就是,按用户ID、订单ID啥的hash一把,比如mod 128,就像:
long orderId = ...;
int tableIndex = (int) (orderId % 128); // 128张表
String tableName = "order_" + tableIndex;
你看,这代码搁这看简单,其实生产上一堆事。你要考虑老数据迁移,业务怎么兼容,还有分库的情况,分库不就得搞多数据源,配置写一堆,SpringBoot一配置错,测试环境直接404。
我们那会儿用的ShardingSphere,老哥你用过没?其实它能帮你做路由、分表规则啥的,但也有坑。你查个订单得先查一堆表,效率不高。比如用户想查他的所有订单,如果分表字段不是用户ID,你基本就要全表扫描。
水平拆分和垂直拆分
唉,这个其实要分两种,大家别搞混了。像10亿订单这种,纯体量大,通常都是水平拆分,比如每张表1千万,10亿也就100张表。垂直拆分是字段太多,比如主表放关键信息,扩展表丢点不常用的,你这大表都拆小表,但数量级还没变。
// 水平分表
public String getTableName(long orderId) {
return "order_" + (orderId % 128);
}
有些公司还喜欢按时间分,比如一月一表,查历史订单就直接查历史表,线上查询也快。我们之前线上挂过,就是有个定时任务没切表,结果数据全进了一张表,查一次卡半天。
再讲点业务痛点
有一次,产品那边临时要查某用户一年所有订单,Leader让我用脚本跑一遍。你要想啊,分了128张表,还分了8个库,你怎么查?只能用for循环遍历所有表,然后union all回来。
String sql = "";
for (int i = 0; i < 128; i++) {
sql += "SELECT * FROM order_" + i + " WHERE user_id = ? UNION ALL ";
}
sql = sql.substring(0, sql.length() - 10); // 去掉最后的"UNION ALL"
你们猜咋样,MySQL直接报警,连接池都撑爆了,还把缓存打满,晚上加班加了俩小时,后来优化成分批查询,才算稳住。
分布式ID别搞错
还有个坑,很多人orderId用自增,分库分表就挂了,ID重复。得用雪花算法、UUID啥的。我们生产用美团Leaf,ID是全局唯一的,hash分表也就不怕重复。
// 雪花ID
public class SnowflakeIdWorker {
// ... 省略实现
public synchronized long nextId() { ... }
}
分布式事务真别碰
最后再吐槽一句,什么分布式事务,能不碰就不碰。我们有一次非要搞个全局事务,结果性能掉一半,生产直接顶不住。一般大厂都是保证最终一致性,发MQ异步补偿。
反正就那几句话吧,分库分表没有银弹。你量真有那么大,先考虑查多还是写多,读多就得多考虑查询性能,写多考虑分片规则和扩展性。别忘了监控、限流都要加好,不然线上爆了Leader第一个骂你。