前言
postgres在数据量达到500万左右单表,查询速度就会明显变慢。当数据具备明显分区界限,并且其数据总量大于500万时,那么这样的数据,最好分表存放。
分表存放的数据具备以下特点:
- 不同区的数据,无查询依赖。只存在单区实时查询。
- 单区数据量有上限,控制在百万级。
- 无法预估未来会出现几个区,也就是无法预估上限。
本次方案中的分表实现,是在go中通过代码实现分区的,原因有:
- 数据库属于运维职能,业务分表属于业务开发职能。分表由开发做,可操作性更丰富,和运维组交付存在多余的提单成本。
- 分表使用通用的orm框架分表,这样的框架会同时支持mysql,oracal等常见关系型数据库,也就是同一份分表逻辑,能适配到不同的数据库里,合理应对跨库,移库等操作,省去分表策略在跨库的方案调用和调整。
实现
package db
import (
"fmt"
"github.com/fwhezfwhez/cmap"
"github.com/fwhezfwhez/errorx"
"github.com/garyburd/redigo/redis"
"github.com/jinzhu/gorm"
"strings"
"time"
)
var existTable = cmap.NewMapV2(nil, 2, 15*time.Second)
type table interface {
TableName() string // 分表后的表名
SourceTableName() string // 母表表名
}
var tmpl = `create table ${table_name} (like ${source_table_name} including all);`
func MustHaveTable(
engine *gorm.DB, // 提供数据查表是否创建,以及创表句柄
t table, // 提供表的源表名,和分表名
f func() redis.Conn, // 提供获取conn的方法,用于分布式环境创建唯一)
) (bool, error) {
tablename := t.TableName()
created, exist := existTable.Get(tablename)
if exist && created == true {
return true, nil
}
if engine.HasTable(t) {
existTable.SetEx(tablename, true, 60)
return true, nil
}
// 15秒内的并发下,只会有一条,走进创建语句
conn := f()
defer conn.Close()
if !once(conn, fmt.Sprintf("%s:auto_create_%s:%s", config.Node.AppName, config.Node.Mode, t.TableName()), 15) {
return true, nil
}
sql := strings.ReplaceAll(tmpl, "${table_name}", t.TableName())
sql = strings.ReplaceAll(sql, "${source_table_name}", t.SourceTableName())
var seqName = fmt.Sprintf("%s_id_seq", tablename)
// do create sequence
if e := engine.Exec(strings.ReplaceAll("create sequence ${seq_name} start with 1 increment by 1 no minvalue no maxvalue cache 1", "${seq_name}", seqName)).Error; e != nil {
return false, errorx.Wrap(e)
}
// do create table
if e := engine.Exec(sql).Error; e != nil {
return false, errorx.Wrap(e)
}
if e := engine.Exec(strings.ReplaceAll(strings.ReplaceAll("alter table ${table_name} alter column id set default nextval('${seq_name}')", "${table_name}", tablename), "${seq_name}", seqName)).Error; e != nil {
return false, errorx.Wrap(e)
}
existTable.SetEx(t.TableName(), true, 60)
return true, nil
}
func once(conn redis.Conn, key string, seconds int) bool {
rs, e := redis.String(conn.Do("set", key, "done", "ex", seconds, "nx"))
if e == redis.ErrNil {
return false
}
if rs == "OK" {
return true
}
return false
}
- 注意,代码中省略了config.Appname和config.Mode 表示应用名(xxxsrv)和模式(pro,dev,local)
使用
以下用例,表示了UserData数据,按照game_id分表
type UserData struct {
Id int `gorm:"column:id;default:" json:"id" form:"id"`
GameId int `gorm:"column:game_id;default:" json:"game_id" form:"game_id"`
CreatedAt time.Time `gorm:"column:created_at;default:" json:"created_at" form:"created_at"`
Cards json.RawMessage `gorm:"column:cards;default:" json:"cards" form:"cards"`
}
func (o CardHeap) TableName() string {
return fmt.Sprintf("user_data_%d", o.GameId)
}
func (o CardHeap) SourceTableName() string {
return "user_data"
}
func main() {
var ud = UserData{
GameId: 9,
}
_, e := db.MustHaveTable(engine, chg, func() redis.Conn {
return redistool.RedisPool.Get()
})
if e != nil {
return 0, errorx.Wrap(e)
}
}