Go 分库分表技术方案对比及实践

3,275 阅读5分钟

分库分表技术方案

分库分表是一种常用的数据库分片技术,可以将一个大型数据库分成多个较小的数据库,以提高数据库的性能和可扩展性。在分库分表中,为了保证数据的一致性,需要将相同的数据放在同一台服务器或同一个数据库中。分库分表可以实现分布式环境下的负载均衡和高可用性,同时也可以避免因服务器或数据库的故障导致的数据丢失和访问异常。

在 Go 语言中实现分库分表(sharding)的方法有很多种,这里列举了一些常见的策略:

  1. 一致性哈希

一致性哈希算法可以根据数据的哈希值将数据映射到不同的服务器或数据库上,从而实现数据的分布式存储和访问。

具体来说,一致性哈希算法将整个哈希值空间抽象成一个环形结构,将服务器或数据库的节点映射到环上。对于每个数据项,先计算出它的哈希值,然后沿着环的顺时针方向查找,找到的第一个节点就是这个数据项所属的服务器或数据库。如果环上没有节点,则需要添加一个新节点,并将数据项映射到新节点上。

  1. 取模哈希(Modulus Hashing):

取模哈希是一种简单且常见的分库分表策略。其核心思想是根据某个字段(如用户 ID)计算哈希值,然后对数据库或表的数量取模。这种方式实现简单,但缺点是当数据库或表的数量发生变化时,需要重新计算哈希值,可能导致数据迁移。

func getDBIndexByMod(userId int64, numDBs int64) int64 {
    return userId % numDBs
}

func getTableIndexByMod(userId int64, numTables int64) int64 {
    return userId % numTables
}
  1. 范围分片(Range-based Sharding)

范围分片是根据某个字段的范围(如用户 ID 范围)来划分数据。例如,ID 在 1-1000 的用户数据存储在数据库 1 的表 1,ID 在 1001-2000 的用户数据存储在数据库 1 的表 2。这种方式的优点是可以根据具体业务需求灵活划分数据,但缺点是数据可能分布不均匀,而采用自增ID划分会更好确保分布均匀。

  1. 目录表(Lookup Table):

目录表是在数据库中维护一个额外的表,用于记录每个数据行所属的数据库和表。当需要查询数据时,首先查询目录表以确定数据所在的数据库和表,然后再执行实际的查询。这种方式可以灵活地支持各种分库分表策略,但可能导致额外的查询开销。

  1. 基于代理的分库分表:

基于代理的分库分表是在应用程序和数据库之间加入一个代理层,负责根据某种策略将请求路由到相应的数据库和表。这种方式可以将分库分表逻辑与应用程序解耦,但可能导致额外的网络延迟。常见的基于代理的分库分表中间件有 MyCAT、Vitess 等。

各个方法的优缺点对比如下:

算法名称优点缺点
基于取模的分表算法实现简单,易于理解和部署数据倾斜严重,不适合高并发、高负载的场景
基于范围的分表算法可以将数据按照指定的范围分配到不同的表中,适合按照时间或者 ID 等范围进行分表数据分布不均衡,可能会导致某些表过于庞大或者过于空闲
基于哈希的分表算法可以将数据均匀地分配到不同的表中,避免数据倾斜的问题当需要增加或删除表时,需要重新计算哈希值,会对系统造成一定的压力
一致性哈希算法可以动态地添加或删除节点,适应系统的动态变化数据倾斜的情况下,仍然存在节点过度集中的问题,需要通过虚拟节点来解决
动态分区算法可以根据数据访问的热点自动调整数据的分布,使得热点数据均匀地分布在不同的表中实现复杂,需要一定的计算资源和管理成本

一致性哈希技术实践

基于一致性哈希的分库分表的代码示例如下

  1. 安装第三方库

使用 github.com/stathat/consistent 库作为一致性哈希的实现。首先安装它:

go get github.com/stathat/consistent
  1. 创建了连接池

创建一个 consistent 实例以及数据库连接池:

package main

import (
        "database/sql""fmt"
        _ "github.com/go-sql-driver/mysql""github.com/stathat/consistent"
)

const (
        dbUser     = "user"
        dbPassword = "password"
        dbHost     = "localhost"
        dbPort     = "3306"
)

var (
        consistentHash *consistent.Consistent
        dbPools        map[string]*sql.DB
)

func init() {
        // 初始化一致性哈希实例
        consistentHash = consistent.New()

        // 初始化数据库连接池
        dbPools = make(map[string]*sql.DB)
}

func addDBPool(dbName string) {
        // 添加数据库实例到一致性哈希
        consistentHash.Add(dbName)

        // 创建数据库连接池并添加到 dbPools
        dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPassword, dbHost, dbPort, dbName)
        db, err := sql.Open("mysql", dsn)
        if err != nil {
                panic(err)
        }

        dbPools[dbName] = db
}
  1. 获取数据库连接

创建一个函数来根据键(比如用户 ID)获取对应的数据库连接:

func getDBConnectionByKey(key string) (*sql.DB, error) {
        dbName, err := consistentHash.Get(key)
        if err != nil {
                return nil, err
        }

        db, ok := dbPools[dbName]
        if !ok {
                return nil, fmt.Errorf("no db connection found for dbName: %s", dbName)
        }

        return db, nil
}
  1. 执行 SQL 语句

使用 getDBConnectionByKey 函数来执行 SQL 语句:

func getUserById(userId string) (*User, error) {
        db, err := getDBConnectionByKey(userId)
        if err != nil {
                return nil, err
        }

        user := &User{}
        err = db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", userId).Scan(&user.Id, &user.Name, &user.Email)
        if err != nil {
                return nil, err
        }

        return user, nil
}