一文讲清数据库的分库分表

server/2024/12/15 8:49:22/

想必大家在面试的时候都被问到过数据库的分库分表应该怎么做

分库分表指的是是将大型数据库分割成多个小型数据库或表格的技术,旨在通过分散数据来提升性能、增加可扩展性和简化管理。随着数据量的增长,传统的单体数据库可能会遭遇性能瓶颈,而分库分表能有效解决这些问题,支持系统线性扩展,确保高效的数据处理和响应速度,同时降低运维复杂度和成本。

今天我就分享一下我对此的一些见解。(如有错误,欢迎指正)

一、选择合适的数据库驱动和ORM框架(如果使用)

  1. 数据库驱动
    • Golang支持多种数据库驱动,如database/sql包提供了与数据库交互的标准接口。对于MySQL,常用的驱动是github.com/go - sql - driver/mysql。确保在项目中正确导入和初始化驱动,例如:
      import ("database/sql"_ "github.com/go - sql - driver/mysql")func main() {db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database_name")if err!= nil {// 处理错误}defer db.Close()}
  1. ORM框架(可选)
    • 如果项目使用ORM框架,如GORM,它可以简化数据库操作,包括分库分表的实现。GORM提供了方便的API来定义模型和执行数据库操作。导入GORM和相关的数据库驱动(以MySQL为例):
      import ("gorm.io/driver/mysql""gorm.io/gorm")func main() {dsn := "user:password@tcp(127.0.0.1:3306)/database_name?charset=utf8mb4&parseTime=True&loc=Local"db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err!= nil {// 处理错误}}

二、确定分库分表策略

  1. 水平分表策略
    1. 按范围划分
      • 例如,对于抽奖记录,按照时间范围进行分表。可以每月创建一张新表,表名可以采用lottery_records_202401(表示2024年1月的抽奖记录)这样的格式。在代码中,需要根据抽奖时间来确定操作哪一张表。
    2. 按哈希划分
      • 对于用户表,按照用户ID进行哈希取模分表。假设要将用户数据分散到10张表中,可以计算user_id % 10,根据结果将用户数据存储到user_0user_1等对应的表中。在查询用户数据时,同样先计算哈希值,然后确定要查询的表。
  2. 垂直分库策略
    1. 按照业务模块划分数据库。例如,将用户信息存储在一个数据库user_db)中,抽奖规则存储在另一个数据库lottery_rule_db)中,抽奖结果存储在第三个数据库lottery_result_db)等。在代码中,需要根据操作的业务模块来选择不同的数据库连接。

三、实现分库分表逻辑

  1. 基于SQL操作实现(不使用ORM)
    1. 水平分表操作示例(按哈希划分用户表)
      • 在查询用户数据时:
        func QueryUser(db *sql.DB, userID int) (*User, error) {tableName := fmt.Sprintf("user_%d", userID%10)querySQL := fmt.Sprintf("SELECT * FROM %s WHERE user_id =? ", tableName)row := db.QueryRow(querySQL, userID)user := &User{}err := row.Scan(&user.ID, &user.Name, &user.Age)if err!= nil {return nil, err}return user, nil}
  - 在插入用户数据时:
        func InsertUser(db *sql.DB, user *User) error {tableName := fmt.Sprintf("user_%d", user.ID%10)insertSQL := fmt.Sprintf("INSERT INTO %s (user_id, name, age) VALUES (?,?,?)", tableName)stmt, err := db.Prepare(insertSQL)if err!= nil {return err}defer stmt.Close()_, err = stmt.Exec(user.ID, user.Name, user.Age)return err}
  1. 垂直分库操作示例(选择不同数据库连接)
    • 假设已经有两个数据库连接userDBlotteryRuleDB
        func QueryUserInfo(userDB *sql.DB, userID int) (*UserInfo, error) {querySQL := "SELECT * FROM user_info WHERE user_id =?"row := userDB.QueryRow(querySQL, userID)userInfo := &UserInfo{}err := row.Scan(&userInfo.ID, &userInfo.Email, &userInfo.Address)if err!= nil {return nil, err}return userInfo, nil}func QueryLotteryRule(lotteryRuleDB *sql.DB, ruleID int) (*LotteryRule, error) {querySQL := "SELECT * FROM lottery_rule WHERE rule_id =?"row := lotteryRuleDB.QueryRow(querySQL, ruleID)lotteryRule := &LotteryRule{}err := row.Scan(&lotteryRule.ID, &lotteryRule.Probability, &lotteryRule.PrizeType)if err!= nil {return nil, err}return lotteryRule, nil}
  1. 基于ORM框架(如GORM)实现
    1. 水平分表操作示例(按哈希划分用户表)
      • 可以通过自定义GORM插件来实现分表逻辑。首先定义插件结构体:
        type ShardingPlugin struct{}```- 实现GORM的Plugin接口方法,在`Name`方法中返回插件名称,在`Initialize`方法中实现分表逻辑:- ```Gofunc (p ShardingPlugin) Name() string {return "ShardingPlugin"}func (p ShardingPlugin) Initialize(db *gorm.DB) error {// 根据用户ID计算表名db.Callback().Query().Before("gorm:query").Register("sharding:query", func(db *gorm.DB) {userID, ok := db.Statement.Vars["user_id"].(int)if ok {tableName := fmt.Sprintf("user_%d", userID%10)db.Statement.Table(tableName)}})db.Callback().Create().Before("gorm:create").Register("sharding:create", func(db *gorm.DB) {userID, ok := db.Statement.Vars["user_id"].(int)if ok {tableName := fmt.Sprintf("user_%d", userID%10)db.Statement.Table(tableName)}})return nil}
  • 在初始化GORM时注册这个插件:
        func main() {dsn := "user:password@tcp(127.0.0.1:3306)/database_name?charset=utf8mb4&parseTime=True&loc=Local"db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Plugins: []gorm.Plugin{ShardingPlugin{}},})if err!= nil {// 处理错误}}
  1. 垂直分库操作示例(选择不同数据库连接)
    • 在GORM中,可以通过定义不同的数据库连接实例来操作不同的数据库。假设已经定义了userDBlotteryRuleDB两个GORM数据库实例:
        func QueryUserInfo(userDB *gorm.DB, userID int) (*UserInfo, error) {userInfo := &UserInfo{}err := userDB.Where("user_id =?", userID).First(userInfo).Errorif err!= nil {return nil, err}return userInfo, nil}func QueryLotteryRule(lotteryRuleDB *gorm.DB, ruleID int) (*LotteryRule, error) {lotteryRule := &LotteryRule{}err := lotteryRuleDB.Where("rule_id =?", ruleID).First(lotteryRule).Errorif err!= nil {return nil, err}return lotteryRule, nil}

四、数据迁移和同步

  1. 初始数据迁移
  • 当实施分库分表策略时,需要将原有数据迁移到新的数据库结构中。如果是水平分表,可以编写数据迁移脚本,按照分表策略将数据从旧表复制到新表。例如,对于按时间范围分表的抽奖记录:
      func MigrateLotteryRecords() error {oldDB, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/old_database_name")if err!= nil {return err}defer oldDB.Close()newDB, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/new_database_name")if err!= nil {return err}defer newDB.Close()rows, err := oldDB.Query("SELECT * FROM old_lottery_records")if err!= nil {return err}defer rows.Close()for rows.Next() {record := &LotteryRecord{}err := rows.Scan(&record.ID, &record.UserID, &record.LotteryDate)if err!= nil {return err}// 根据抽奖日期确定新表名newTableName := fmt.Sprintf("lottery_records_%d", record.LotteryDate.Year()*100 + int(record.LotteryDate.Month()))insertSQL := fmt.Sprintf("INSERT INTO %s (id, user_id, lottery_date) VALUES (?,?,?)", newTableName)stmt, err := newDB.Prepare(insertSQL)if err!= nil {return err}defer stmt.Close()_, err = stmt.Exec(record.ID, record.UserID, record.LotteryDate)if err!= nil {return err}}return nil}
  1. 数据同步机制
  • 在分库分表后,可能需要建立数据同步机制,以确保数据的一致性。例如,在分布式系统中,当一个服务更新了用户表的数据,可能需要通过消息队列(如Kafka)将更新事件发送到其他相关服务,其他服务收到消息后对相应的分表进行更新操作。以下是一个简单的示例,使用Kafka进行数据同步:
      import ("github.com/Shopify/sarama")func UpdateUserAndSync(userDB *sql.DB, kafkaProducer sarama.SyncProducer, user *User) error {// 更新用户数据err := UpdateUser(userDB, user)if err!= nil {return err}// 发送数据更新消息到Kafkamessage := &sarama.ProducerMessage{Topic: "user_update_topic",Value: sarama.StringEncoder(fmt.Sprintf("user_id:%d", user.ID)),}_, _, err = kafkaProducer.SendMessage(message)return err}func KafkaConsumerLoop(kafkaConsumer sarama.Consumer, userDB *sql.DB) {consumer, err := kafkaConsumer.ConsumePartition("user_update_topic", 0, sarama.OffsetNewest)if err!= nil {// 处理错误}defer consumer.Close()for message := range consumer.Messages() {// 解析消息,获取用户IDuserIDStr := string(message.Value)userID, err := strconv.Atoi(userIDStr[len("user_id:"):])if err!= nil {// 处理错误}// 根据用户ID更新其他分表中的用户数据user, err := QueryUser(userDB, userID)if err!= nil {// 处理错误}// 更新其他分表...}}

五、性能测试和优化

  1. 性能测试
  • 在实施分库分表后,需要对系统进行性能测试,以验证是否达到了预期的性能提升效果。可以使用性能测试工具,如go - bench来测试数据库操作的性能。例如,测试查询用户数据的性能:
      func BenchmarkQueryUser(b *testing.B) {db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database_name")if err!= nil {b.Fatal(err)}defer db.Close()for i := 0; i < b.N; i++ {userID := iQueryUser(db, userID)}}
  1. 优化调整
    • 根据性能测试结果,对分库分表策略和代码进行优化调整。例如,如果发现某些查询操作仍然较慢,可以考虑优化索引策略、调整分片规则或者增加缓存机制等。如果是使用ORM框架,还可以优化ORM的配置,如调整GORM的PreloadJoins策略来减少不必要的数据库查询。

结语

今天就分享到这里,如果你对上面的内容有疑问或者你有更好的思路,欢迎在评论区留言!

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以私信我。


http://www.ppmy.cn/server/150313.html

相关文章

画图,matlab,

clear;close all;clc;tic;dirOutput dir(*.dat); % 罗列所有后缀-1.dat的文件列表&#xff0c;罗列BDDATA的数据 filenames string({dirOutput.name}); % 提取文件名%% 丢包统计 FILENAMES [""]; LOSS_YTJ [ ]; LOSS_RAD [ ]; LOSS_ETH [ ]…

如何有效地规避空格的输入?

我发现你不管是使用C语言的gets函数还是使用c的getline函数都不能躲避空格&#xff0c;只能躲避回车&#xff0c;那么当我想规避空格的时候&#xff0c;我应该使用什么捏&#xff1f; 天选符号---->>>> "%s" <<<<------- 如果你只是来找一…

使用idea创建一个JAVA WEB项目

文章目录 1. javaweb项目简介2. 创建2.1 idea新建项目2.2 选择&#xff0c;命名2.3 打开2.4 选择tomcat运行2.5 结果 3. 总结 1. javaweb项目简介 JavaWeb项目是一种基于Java技术的Web应用程序&#xff0c;主要用于开发动态网页和Web服务。这种项目能够构建在Java技术栈之上&a…

航空航天总线协议分析ARINC429

ARINC429是商用飞机和运输机运用最广泛的总线之一&#xff0c;ARINC是美国航空无线电公司(Aeronautical Radio INC.)的缩写&#xff0c;ARINC429总线协议是美国航空电子工程委员会于1977年7月提出发表并获批准使用&#xff0c;它的规范全称是数字式信息传输系统(Digital Inform…

排队论、负载均衡和任务调度关系

目录 排队论、负载均衡和任务调度关系 一、排队论 二、负载均衡 三、任务调度 四、总结 排队论、负载均衡和任务调度关系 排队论为负载均衡和任务调度提供了数学理论和方法支持 排队论、负载均衡和任务调度是三个相关但不同的概念。以下是对这三个概念的详细解释和它们之…

软考高级 架构师 第六章 计算机系统其他基础知识

第六章其他计算机系统知识 1.计算机语言 计算机语言主要由一套指令构成&#xff0c;这种指令一般包含三大部分&#xff1a;表达式、流程控制和集合。 表达式&#xff1a;变量、常量、字面量和运算符 流程控制&#xff1a;分支、循环、函数和异常 集合&#xff1a;字符串、数组…

华为FreeBuds Pro 4丢了如何找回?(附查找功能使用方法)

华为FreeBuds Pro 4查找到底怎么用&#xff1f;华为FreeBuds Pro 4有星闪精确查找和离线查找&#xff0c;离线查找功能涵盖播放铃声、导航定位、星闪精确查找、上线通知、丢失模式、遗落提醒等。星闪精确查找是离线查找的子功能&#xff0c;当前仅华为FreeBuds Pro 4充电盒支持…

day10性能测试(2)——Jmeter安装环境+线程组+Jmeter参数化

【没有所谓的运气&#x1f36c;&#xff0c;只有绝对的努力✊】 目录 1、LoadRunner vs Jmeter 1.1 LoadRunner 1.2 Jmeter 1.3 对比小结 2、Jmeter 环境安装 2.1 安装jdk 2.2 安装Jmeter 2.3 小结 3、Jmeter 文件目录结构 4、Jmeter默认配置修改 5、Jmeter元件、组…