数据库分库分表(一): 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实现水平分表的过程。分表提升了读写性能,但也带来了复杂度的提升,如何使用取决于使用场景。

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

参考

相关推荐
瀚高PG实验室13 小时前
HGDB V9.0.5 .pgpass 免密登录提示 IsEncryptNon-base64 encoding 警告处理
数据库·postgresql·瀚高数据库
KevinAshen6717 小时前
用 Go 修复旧下载工具:从接口 403 到凭证截断的完整复盘
go·github
IvorySQL18 小时前
PostgreSQL 日报|Anonymizer 3.3 修复高危漏洞(10 月 29 日)
数据库·postgresql
geovindu18 小时前
SQL Data Sampling and Approximation Techniques using postgreSQL 18
大数据·数据库·数据仓库·sql·postgresql·数据库开发·数据库架构
小手cool19 小时前
PostgreSQL 基础入门:从安装到核心概念
数据库·postgresql
代码派20 小时前
PostgreSQL 同构迁移上云方案|NineData 数据复制、实时增量与一致性校验
数据库·postgresql·上云·ninedata·迁移·数据库迁移·版本升级
Jack N20 小时前
【Strapi 系列之八】二次开发 + 生产部署,Docker + PostgreSQL + Nginx 一把梭
nginx·docker·postgresql·strapi
瀚高PG实验室21 小时前
瀚高数据库提交数据后,是否需要COMMIT
数据库·postgresql·瀚高数据库
AI开源软件2 天前
SpringBoot信创项目怎么验收?从数据库迁移到交付证据
postgresql·springboot·达梦·opengauss·信创·企业管理系统
l1t2 天前
DeepSeek总结的ALTER FUNCTION SET work_mem:无需全局更改即可修复 PostgreSQL 磁盘溢出
sql·缓存·postgresql