数据库分库分表(一): ent.io实现水平分表

背景

当系统在快速发展中,数据库表的规模越来越庞大,单表行数可能超百万、千万级别时,为了不影响读写的性能,这时候可以考虑分表了。而ent.io是一个基于Go语言的开源ORM,本文将简介ent.io如何实现水平分表。

横向拆分和纵向拆分

那如何做拆分,通常有两种分法,分别是横向拆分(水平拆分)和纵向拆分(垂直拆分)。

  • 纵向拆分: 将一张表某一条记录的多个字段,拆分到多张表中
  • 横向拆分: 把一张表中的不同的记录分别放到不同的表中

ent.io 水平分表

大致步骤

  1. 创建分表规则: 可以选择按照某个字段(如用户ID)进行分表,或者根据时间戳等其他条件进行分表
  2. insert: 根据分表字段写入到不同的分表
  3. 按时间字段动态更新不同的分表
  4. 跨表select: 需要从不同的分表总读取数据聚合返回

接着我们通过一个demo: (按时间字段水平拆分用户表)来详细介绍:

  1. quick demo 快速创建一个User,schema 如下,创建过程参考entgo.io/zh/docs/get... 这里不展开了。
go 复制代码
package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/dialect/entsql"
	"entgo.io/ent/schema"
	"entgo.io/ent/schema/field"
	"fmt"
	"time"
)

// User holds the schema definition for the User entity.
type User struct {
	ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.Int("age").
			Positive(),
		field.String("name").
			Default("unknown"),
		field.Time("date").Default(time.Now()).Optional(),
	}
}

// Edges of the User.
func (User) Edges() []ent.Edge {
	return nil
}

func (User) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entsql.Annotation{Table: fmt.Sprintf("users_%d", time.Now().Unix())},
	}
}
func (User) Hooks() []ent.Hook {
	return []ent.Hook{}
}

func (User) Indexes() []ent.Index {
	return []ent.Index{
	}
}
  1. ent.io 水平分表
go 复制代码
package main

import (
	"context"
	"database/sql"
	entsql "entgo.io/ent/dialect/sql"
	"entgo.io/ent/dialect/sql/schema"
	"fmt"
	"github.com/google/uuid"
	_ "github.com/lib/pq"
	"log"
	"strings"
	"time"
	"tkingo.vip/egs/ent-sharding/ent"
	"tkingo.vip/egs/ent-sharding/ent/user"
)

func main() {
	var (
		ctx       = context.Background()
		startTime = time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
		endTime   = time.Date(2024, time.March, 30, 23, 59, 59, 0, time.UTC)
	)
	dsn := "host=localhost user=postgres password=123456 dbname=test2 port=5432 sslmode=disable TimeZone=Asia/Shanghai"
	client, err := ent.Open("postgres", dsn)
	if err != nil {
		log.Fatalf("failed opening connection to postgres: %v", err)
	}
	defer client.Close()

	db, err := sql.Open("postgres", "postgres://postgres:123456@localhost/test2?sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	db.SetConnMaxLifetime(time.Hour)
	db.SetMaxOpenConns(30)
	defer db.Close()

	// 创建分表
	now := time.Now()
	yearTables := getYearlyTables(now.Year())
	for _, tableName := range yearTables {
		if err := client.Schema.Create(
			context.Background(),
			schema.WithHooks(func(next schema.Creator) schema.Creator {
				return schema.CreateFunc(func(ctx context.Context, tables ...*schema.Table) error {
					var dynamicTables []*schema.Table
					for _, v := range tables {
						nv := v
						nv.Name = tableName
						dynamicTables = append(dynamicTables, nv)
					}
					return next.Create(ctx, dynamicTables...)
				})
			}),
		); err != nil {
			log.Fatalf("failed creating schema resources: %v", err)
		}
	}

	//插入数据: 按不同的月份到更新到不同分表
	var users []ent.User
	februaryTime := time.Date(2024, time.February, 2, 23, 59, 59, 0, time.UTC)
	januaryTime := time.Date(2024, time.January, 2, 23, 59, 59, 0, time.UTC)
	januaryTimeUsers := getMonthlyTable("users", januaryTime)
	februaryUsers := getMonthlyTable("users", februaryTime)
	users = append([]ent.User{}, ent.User{
		Age:  11,
		Name: uuid.NewString(),
		Date: januaryTime,
	})
	err = createBulk(db, januaryTimeUsers, users)
	users = append([]ent.User{}, ent.User{
		Age:  22,
		Name: uuid.NewString(),
		Date: februaryTime,
	})
	err = createBulk(db, februaryUsers, users)

	//更新数据到不同到分表
	_, err = client.User.Update().Where(
		func(s *entsql.Selector) {
			table := entsql.Table(februaryUsers)
			s.Where(entsql.GTE(user.FieldID, 0)).
				From(table)
		}).
		SetAge(33).
		Save(ctx)
	if err != nil {
		log.Fatalf("failed opening connection to postgres: %v", err)
	}
	
	//按起止时间从不同到分表总查询数据
	queryTables := getMonthlyTables(startTime, endTime)
	fmt.Println("queryTables", queryTables)
	s := time.Now()
	for _, queryTable := range queryTables {
		us, err := client.User.Query().Where(func(s *entsql.Selector) {
			s.Where(entsql.LTE(user.FieldDate, time.Now())).
				From(entsql.Table(queryTable)).Select("*")
		}).All(context.Background())
		if err != nil {
			fmt.Println("custom table name failed", err)
		}
		fmt.Println("==================", queryTable, "start")
		fmt.Println(us)
		fmt.Println("==================", queryTable, "end")
	}

	e := time.Now().Sub(s).Seconds()
	fmt.Println("end", e)
}

func createBulk(db *sql.DB, tableName string, users []ent.User) (err error) {
	// 构建批量插入语句的值部分字符串
	values := []string{}
	for _, u := range users {
		dateFormat := u.Date.UTC().Format("2006-01-02 15:04:05-07")
		values = append(values, fmt.Sprintf("(%d, '%s', '%s')", u.Age, u.Name, dateFormat))
	}
	fmt.Println(strings.Join(values, ", "))
	// 构建完整的批量插入语句
	query := fmt.Sprintf("INSERT INTO %s (age,name,date) VALUES %s", tableName, strings.Join(values, ", "))
	fmt.Println(query)
	_, err = db.Exec(query)
	if err != nil {
		fmt.Println("执行插入错误:", err)
		return
	}
	fmt.Println("批量插入成功")
	return nil
}

执行输出

js 复制代码
//OUTPUT
(11, '10b28be1-d1b5-45e3-b18f-4dd1548cc374', '2024-01-02 23:59:59+00')
INSERT INTO users_202401 (age,name,date) VALUES (11, '10b28be1-d1b5-45e3-b18f-4dd1548cc374', '2024-01-02 23:59:59+00')
批量插入成功
(22, 'c68df01a-d31a-4451-a26f-22d565879840', '2024-02-02 23:59:59+00')
INSERT INTO users_202402 (age,name,date) VALUES (22, 'c68df01a-d31a-4451-a26f-22d565879840', '2024-02-02 23:59:59+00')
批量插入成功
queryTables [users_202401 users_202402 users_202403]
================== users_202401 start
[User(id=5, age=11, name=10b28be1-d1b5-45e3-b18f-4dd1548cc374, date=Wed Jan  3 07:59:59 2024)]
================== users_202401 end
================== users_202402 start
[User(id=5, age=33, name=c68df01a-d31a-4451-a26f-22d565879840, date=Sat Feb  3 07:59:59 2024)]
================== users_202402 end
================== users_202403 start
[]
================== users_202403 end
end 0.009452834 

上例中

  1. 使用ent提供的schema.WithHooks,按tablename_年月的格式创建分表: users_202401、users_202402、users_202403 ...
  2. ent暂时没有提供动态表名插入数据,这里作者使用原生替代,并创建2条不同月份的数据插入到不同的分表中(users_202401、users_202402)
  3. 更新数据可以使用.Where(func(s *entsql.Selector) { table := entsql.Table(【动态表名】)的方式动态更新不同分表。
  4. 最终通过输入start、end 2个time.Time类型数据,遍历查询不同分表的数据。可以看到2步骤插入的数据分表插入到了users_202401、users_202402,3步骤的也成功把users_202402表中,id=5的数据的age字段修改为了33.

优缺点

优势:

  1. 提升了读性能:数据被拆分到多个表中,单表行数较少,按时间读取效率更高。
  2. 可扩展性:当数据量的增长时,可以更快添加更多的分表,不影响现有逻辑。

缺点:

  1. 跨表查询更复杂: 比如分页、order-by等:当需要跨多个分表、进行分页查询货排序时。或是需要聚合、代码统计不同的分表。

总结

当系统的数据快速增长,数据库每天以数万数十万的增速,为了不影响读写效率可以考虑分表了。分表通常有横向拆分和纵向拆分,ent.io是一个基于Go语言的开源ORM,文中通过一个按时间字段水平拆分用户表的例子,演示了ent.io实现水平分表的过程。分表提升了读写性能,但也带来了复杂度的提升,如何使用取决于使用场景。

待续,希望对您有所帮助~

参考

相关推荐
一个热爱生活的普通人9 小时前
Go语言中 Mutex 的实现原理
后端·go
孔令飞9 小时前
关于 LLMOPS 的一些粗浅思考
人工智能·云原生·go
小戴同学9 小时前
实时系统降低延时的利器
后端·性能优化·go
cherry523014 小时前
【PostgreSQL】【第4章】PostgreSQL的事务
数据库·postgresql
Golang菜鸟1 天前
golang中的组合多态
后端·go
Serverless社区1 天前
函数计算支持热门 MCP Server 一键部署
go
库海无涯1 天前
如何把数据从SQLite迁移到PostgreSQL
数据库·postgresql·sqlite
cherry52301 天前
【PostgreSQL】【第3章】PostgreSQL的对象操作
数据库·postgresql
Wo3Shi4七1 天前
二叉树数组表示
数据结构·后端·go
网络研究院1 天前
您需要了解的有关 Go、Rust 和 Zig 的信息
开发语言·rust·go·功能·发展·zig