
PHP数据库水平拆分实战:从单表千万到分库分表的平滑演进
大家好,我是33blog的技术作者。最近在项目中遇到了一个棘手的问题——用户表数据量突破了千万级别,查询性能急剧下降。经过一番调研和实战,我们最终通过水平拆分解决了这个瓶颈。今天就来分享这段踩坑经历,希望能帮助遇到同样问题的朋友。
为什么需要水平拆分?
记得项目初期,我们的用户表设计很简单,所有数据都放在一张表里。但随着业务增长,这张表变得异常臃肿。最明显的问题是:
- 简单查询都要好几秒才能返回
- 数据库服务器CPU经常跑满
- 备份和恢复时间越来越长
这时候垂直拆分已经解决不了问题,水平拆分成了必然选择。
拆分策略的选择
我们对比了几种常见的拆分方案:
- 按用户ID取模:简单均匀,但扩容麻烦
- 按时间范围:适合时序数据,但我们用户分布不均匀
- 按地域:业务相关性不强
最终选择了用户ID取模的方式,虽然扩容复杂,但数据分布最均匀。这里有个经验:如果预计数据量会持续快速增长,建议一开始就考虑好扩容方案。
具体实现步骤
1. 设计分表规则
我们决定分成8张表,规则很简单:user_id % 8。对应的表名就是user_0到user_7。
// 获取分表后缀的函数
function getTableSuffix($user_id) {
return $user_id % 8;
}
// 构建真实表名
function getRealTableName($user_id) {
return 'user_' . getTableSuffix($user_id);
}
2. 数据迁移方案
这是最头疼的部分!我们采用了双写方案:先在新旧表同时写入,等数据同步完成后再切流。关键是要确保数据一致性:
// 双写示例
function createUser($user_data) {
$user_id = $user_data['user_id'];
$table_suffix = getTableSuffix($user_id);
// 写入新表
$new_table = 'user_' . $table_suffix;
DB::table($new_table)->insert($user_data);
// 同时写入旧表(迁移期间)
if (config('database.migration_mode')) {
DB::table('users')->insert($user_data);
}
}
3. 查询路由封装
为了对业务代码透明,我们封装了查询路由层:
class UserRepository {
public function find($user_id) {
$table_name = getRealTableName($user_id);
return DB::table($table_name)->where('user_id', $user_id)->first();
}
public function batchFind($user_ids) {
$results = [];
// 按分表分组查询
$grouped_ids = [];
foreach ($user_ids as $user_id) {
$table_suffix = getTableSuffix($user_id);
$grouped_ids[$table_suffix][] = $user_id;
}
foreach ($grouped_ids as $suffix => $ids) {
$table_name = 'user_' . $suffix;
$users = DB::table($table_name)->whereIn('user_id', $ids)->get();
$results = array_merge($results, $users->toArray());
}
return $results;
}
}
踩坑记录与解决方案
坑1:跨分片查询问题
有一次产品需要查询最近注册的100个用户,这个需求在分表后变得很复杂。我们的解决方案是:
- 建立全局索引表记录最新用户ID
- 对非分片键查询建立查询条件映射
坑2:事务一致性
跨分片的事务是个大难题。我们最终放弃了分布式事务,改用最终一致性:
// 使用消息队列保证最终一致性
function updateUserProfile($user_id, $profile_data) {
// 先更新主表
$table_name = getRealTableName($user_id);
DB::table($table_name)->where('user_id', $user_id)->update($profile_data);
// 发送消息同步到其他相关表
Queue::push(new SyncUserProfile($user_id, $profile_data));
}
性能提升效果
拆分后的效果立竿见影:
- 单表查询从2-3秒降到50ms以内
- 数据库负载从90%+降到30%左右
- 备份时间从4小时缩短到30分钟
总结与建议
水平拆分是个系统工程,需要从业务、技术、运维多个角度考虑。我的建议是:
- 不要过早拆分,单表千万级别其实还能优化
- 拆分前做好充分测试,特别是数据迁移方案
- 考虑使用成熟的分库分表中间件,减少造轮子
- 一定要有回滚方案,拆分过程中随时可能出问题
这次拆分让我们团队对分布式系统有了更深的理解。虽然过程很痛苦,但看到系统性能大幅提升的那一刻,所有的努力都值得了。希望我的经验对你有帮助!
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
源码库 » PHP数据库水平拆分实战
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
源码库 » PHP数据库水平拆分实战
