基于gorm.io/sharding分表中间件使用案例

项目背景

项目中需要用到mysql的分表场景,调研了一些常用的分库分表中间件,比如,mycat,小米的Gaea,这两个中间件太重了,学习成本较大,另外mycat不是go写的。我们需要一个轻量级的go版本的分表中间件。所以,把目光放在了如下这个开源组件上。go-gorm/sharding: High performance table sharding plugin for Gorm. (github.com)icon-default.png?t=O83Ahttps://github.com/go-gorm/sharding

案例

自定义分表函数以及主键生成自定义函数。该案例仅用于测试,分表的逻辑通常不会基于时间进行分表。测试中使用的主键生成方法也仅仅是用于测试,实际项目中并不推荐使用,该方式对于高并发场景并不友好,实际场景中使用预先生成的方式或是其他的分布式id生成器更好。

事实上,这个库有一些默认的主键id生成方式。

const (
	// Use Snowflake primary key generator
	PKSnowflake = iota
	// Use PostgreSQL sequence primary key generator
	PKPGSequence
	// Use MySQL sequence primary key generator
	PKMySQLSequence
	// Use custom primary key generator
	PKCustom
)

但是,在这个自定义分表逻辑场景中,使用默认的主键id生成方式,出现了bug。

 比如,使用KMySQLSequence这种主键生成方式,第二次执行插入sql操作就报了Error 1062 (23000): Duplicate entry '2' for key 'orders_2025.PRIMARY'这个错误。我大概看了一下导致错误的原因在于如下这个方法,这个方法的意思是主键id获取数据库中最后一次插入操作所生成的自增ID值,这就导致,第二次执行插入sql操作时,主键取的是已经存在的,导致报了上面的错误。

初步分析下来是这个原因,目前尚未验证这个错误原因。

func (s *Sharding) genMySQLSequenceKey(tableName string, index int64) int64 {
	var id int64
	err := s.DB.Exec("UPDATE `" + mySQLSeqName(tableName) + "` SET id = LAST_INSERT_ID(id + 1)").Error
	if err != nil {
		panic(err)
	}
	err = s.DB.Raw("SELECT LAST_INSERT_ID()").Scan(&id).Error
	if err != nil {
		panic(err)
	}

	return id
}

 使用PKSnowflake这种主键生成方式,导致panic。

 初步分析下来的原因是这个index源码中默认设定的最大值是1024,但是,在我们这个场景中,分表的后缀超过了1024,来到了2024,2025等。

同样,该错误原因尚未深入分析,可能不是这个原因,尚待进一步分析。

func (s *Sharding) genSnowflakeKey(index int64) int64 {
	return s.snowflakeNodes[index].Generate().Int64()
}

测试案例如下: 

package test

import (
	"fmt"
	"testing"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/sharding"
)

var globalDB *gorm.DB

type Order struct {
	ID        int64  `gorm:"primaryKey"`
	OrderId   string `gorm:"sharding:order_id"` // 指明 OrderId 是分片键
	UserID    int64
	ProductID int64
	OrderDate time.Time
}

// 自定义 ShardingAlgorithm
func customShardingAlgorithm(value any) (suffix string, err error) {
	if orderId, ok := value.(string); ok {
		// 截取字符串,截取前8位,获取年份
		orderId = orderId[0:8]
		orderDate, err := time.Parse("20060102", orderId)
		if err != nil {
			return "", fmt.Errorf("invalid order_date")
		}
		year := orderDate.Year()
		return fmt.Sprintf("_%d", year), nil
	}
	return "", fmt.Errorf("invalid order_date")
}

// customePrimaryKeyGeneratorFn 自定义主键生成函数
func customePrimaryKeyGeneratorFn(tableIdx int64) int64 {
	var id int64
	seqTableName := "gorm_sharding_orders_id_seq" // 序列表名
	db := globalDB
	// 使用事务来确保主键生成的原子性
	tx := db.Begin()
	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
		}
	}()

	// 锁定序列表以确保并发安全(可选,取决于你的 MySQL 配置和并发级别)
	// 注意:在某些 MySQL 版本和配置中,使用 LOCK TABLES 可能不是最佳选择
	// 这里仅作为示例,实际应用中可能需要更精细的并发控制策略
	tx.Exec("LOCK TABLES " + seqTableName + " WRITE")

	// 查询当前的最大 ID
	tx.Raw("SELECT id FROM " + seqTableName + " ORDER BY id DESC LIMIT 1").Scan(&id)

	// 更新序列表(这里直接递增 1,实际应用中可能需要更复杂的逻辑)
	newID := id + 1
	tx.Exec("INSERT INTO "+seqTableName+" (id) VALUES (?)", newID) // 这里假设序列表允许插入任意 ID,实际应用中可能需要其他机制来确保 ID 的唯一性和连续性

	// 释放锁定
	tx.Exec("UNLOCK TABLES")

	// 提交事务
	if err := tx.Commit().Error; err != nil {
		panic(err) // 实际应用中应该使用更优雅的错误处理机制
	}

	return newID
}

// Test_Gorm_Sharding 用于测试 Gorm Sharding 插件
func Test_Gorm_Sharding(t *testing.T) {
	// 连接到 MySQL 数据库
	dsn := "***:******@tcp(ip:port)/sharding_db2?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN: dsn,
	}), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}
	globalDB = db

	// 配置 Gorm Sharding 中间件,使用自定义的分片算法
	middleware := sharding.Register(sharding.Config{
		ShardingKey:           "order_id",
		ShardingAlgorithm:     customShardingAlgorithm, // 使用自定义的分片算法
		PrimaryKeyGenerator:   sharding.PKCustom,
		PrimaryKeyGeneratorFn: customePrimaryKeyGeneratorFn,
	}, "orders") // 逻辑表名为 "orders"
	db.Use(middleware)

	// 创建 Order 表
	err = db.Exec(`CREATE TABLE IF NOT EXISTS orders_2024 (
		id BIGINT PRIMARY KEY,
		order_id VARCHAR(50),
		user_id INT,
		product_id INT,
		order_date DATETIME
	)`).Error
	if err != nil {
		panic("failed to create table")
	}
	err = db.Exec(`CREATE TABLE IF NOT EXISTS orders_2025 (
		id BIGINT PRIMARY KEY,
		order_id VARCHAR(50),
		user_id INT,
		product_id INT,
		order_date DATETIME
	)`).Error
	if err != nil {
		panic("failed to create table")
	}

	// 示例:插入订单数据
	order := Order{
		OrderId:   "20240101ORDER0001",
		UserID:    1,
		ProductID: 100,
		OrderDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
	}
	err = db.Create(&order).Error
	if err != nil {
		fmt.Println("Error creating order:", err)
	}
	order2 := Order{
		OrderId:   "20250101ORDER0001",
		UserID:    1,
		ProductID: 100,
		OrderDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
	}
	err = db.Create(&order2).Error
	if err != nil {
		fmt.Println("Error creating order:", err)
	}

	// 查询示例
	var orders []Order
	err = db.Model(&Order{}).Where("order_id=?", "20240101ORDER0001").Find(&orders).Error
	if err != nil {
		fmt.Println("Error querying orders:", err)
	}
	fmt.Printf("Selected orders: %#v\n", orders)

	// 更新示例
	err = db.Model(&Order{}).Where("order_id=? and id=?", "20240101ORDER0001", int64(14)).Update("product_id", 102).Error
	if err != nil {
		fmt.Println("Error updating order:", err)
	}

	// 再次查询示例,验证更新是否生效
	err = db.Model(&Order{}).Where("order_id=?", "20240101ORDER0001").Find(&orders).Error
	if err != nil {
		fmt.Println("Error querying orders:", err)
	}
	fmt.Printf("Selected orders: %#v\n", orders)

	// 删除示例
	err = db.Model(&Order{}).Where("order_id=? and id =?", "20240101ORDER0001", int64(16)).Delete(&Order{}).Error
	if err != nil {
		fmt.Println("Error deleting order:", err)
	}

	// 再次查询示例,验证删除是否生效
	err = db.Model(&Order{}).Where("order_id=?", "20240101ORDER0001").Find(&orders).Error
	if err != nil {
		fmt.Println("Error querying orders:", err)
	}
	fmt.Printf("Selected orders: %#v\n", orders)
}

遗留问题 

1.上文中提到了两种默认主键生成方式报bug问题需要进一步定位以确定根因。

2.该组件的增删改查仅支持查询条件中包含分表主键的场景,对于,不包含主键需要全表扫描的场景并不支持,当然,全表扫描最优解并不是通过mysql的能力解决,最好是借助其他的方案,比如,通过es的方案。但是,对于,我们目前的场景中依然存在上述需求,所以,尚需考虑该场景如何解决。

3.IN查询也不支持。

本质上只支持,根据分表键路由到对应的表进行单表查询。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/883826.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android使用OpenCV 4.5.0实现扑克牌识别(源码分享)

一、显示效果展示 二、OpenCV 4.5.0 OpenCV 4.5.0是OpenCV(Open Source Computer Vision Library,开源计算机视觉库)的一个重要更新版本,该版本在多个方面进行了优化和新增了多项功能。 三、ONNX模型 ONNX(Open Neu…

HCIP——HCIA回顾

第一章.HCIA复习 引入场景 其实IA我们主要学习的无非就是数据包在网络中传递的基本过程,我们设计一个场景,通过回顾web页面的请求过程,将IA学过的基本网络工作原理来串一遍。 (在本例中,ISP网络为学校提供了DNS服务,…

学习C++的第七天!

1.虚函数是在基类中用 virtual 关键字声明的函数,可以在派生类中被重写。纯虚函数是在虚函数的基础上,在基类中被初始化为 0 的函数,含有纯虚函数的类是抽象类,不能被实例化。 2.如果基类的析构函数不是虚函数,当通过…

今年双十一不被割韭菜!要买就要高品质好物~总结五款好物推荐!

双十一购物盛宴如约而至,面对琳琅满目的商品是否感到选择有些迷茫?别担心,专为选择困难症的朋友们准备了一份精选好物清单,旨在丰富您的数字生活体验。无需犹豫,这份指南将助您轻松锁定心仪之选,把握时机&a…

ER-Nerf 数字人训练视频的准备

数字人训练视频的准备 分辨率帧率时长背景处理推荐工具举个例子 分辨率 布局Value横屏1920x1080竖屏1080x1920 帧率 25fps 时长 大于1分钟(60秒)的视频 解释: 1、数字人采用循环的播放模式,时长越长会降低出现前后拼接的现象…

票据直联“票通全球”,全球司库热点服务之三——在线开票

在当今全球化的经济浪潮中,企业的财务管理面临着日益复杂的挑战。如何高效、安全地进行资金结算和票据管控,成为企业发展的关键问题。而全球司库票据直联服务的出现,为企业提供了一种创新的解决方案,尤其是在线开票方面&#xff0…

语言模型发展史

四个阶段 第一阶段:基于规则和统计的语言模型 由人工设计特征并使用统计方法对固定长度的文本窗口序列进行建模分析,这种建模方式也被称为N-gram语言模型。 优点: 1)采用极大似然估计, 参数易训练 2)完全包含了前n-…

Python和C++及MATLAB距离相关性生物医学样本统计量算法及数据科学

🎯要点 统计观测值之间距离计算代谢组学和脂质组学分析相关距离矩阵计算卡方检验偏差校正快速计算距离协方差算法大规模生物系统分析距离矩阵相关性测试石油勘探统计学关系 Python距离矩阵 在数学、计算机科学,尤其是图论中,距离矩阵是一…

mysql 内存被打满记录

一:早上收到报警:提示:您的云数据库RDS的1个实例因存储空间满将被锁定,请关注实例的存储空间使用情况,可通过存储扩容或空间清理解除锁定。后续查看错误日志如下:磁盘没有空间了 没有多余的空间写binlog和…

随记——机器学习

前言 本来有个500块钱的单子,用机器学习做一个不知道什么鸟的识别,正好有数据集,跑个小项目,过一下机器学习图像识别的流程,用很短的时间记录下来..... 一、数据预处理 将数据集分为训练集和测试集,直接…

onload_tcpdump命令抓包报错Onload stack [7,] already has tcpdump process

最近碰到Onload 不支持同时运行多个 tcpdump 进程的报错,实际上使用了ps查询当时系统中并没有tcpdump相关进程存在。需要重启服务器本机使用onload加速的相关进程后才能使用onload_tcpdump正常抓包,很奇怪,之前确实没遇到这样的问题&#xff…

李宏毅机器学习2023-HW10-Adversarial Attack

文章目录 TaskBaselineFGSM (Fast Gradient Sign Method (FGSM)I-FGSM(Iterative Fast Gradient Sign Method)MI-FGSM(Momentum Iterative Fast Gradient Sign Method)M-DI2-FGSM(Diverse Input Momentum Iterative Fast Gradient Sign Method) Reportfgsm attackJepg Compress…

【ADC】ΔΣ ADC 中数字滤波器的延迟以及 SAR ADC 与 ΔΣ ADC 的差异对比总结

本文学习于TI 高精度实验室课程,深入探讨 delta-sigma 转换器中使用的数字滤波器。具体来说,本文将重点介绍数字滤波器如何引入延迟,因为这是 SAR 和 delta-sigma ADC 之间的显著差异。 文章目录 一、低延迟数字滤波器二、高延迟数字滤波器三…

探索EasyCVR视频融合平台:在视频编解码与转码领域的灵活性优势

随着视频监控技术的飞速发展,各类应用场景对视频数据的处理需求日益复杂多样。从公共安全到智慧城市,再到工业监控,高效、灵活的视频处理能力成为衡量视频融合平台性能的重要标准。在众多解决方案中,EasyCVR视频融合平台凭借其在视…

大规模预训练语言模型的参数高效微调

人工智能咨询培训老师叶梓 转载标明出处 大规模预训练语言模型(PLMs)在特定下游任务上的微调和存储成本极高,这限制了它们在实际应用中的可行性。为了解决这一问题,来自清华大学和北京人工智能研究院的研究团队探索了一种优化模型…

基础漏洞——SSRF

目录 一.原理 二.引起ssrf的函数 三.这些函数具体作用 (1)File_get_content() (2)Fsockopen() (3)Curl_exec() 四.常见的业务场景(可能出现的漏洞的地方,漏洞挖掘&#xff09…

展锐平台的手机camera 系统isptool 架构

展锐平台的isptool 主要用于支持展锐各代芯片isp的各效果模块快速tuning和参数生成打包。 具体需要: 一、工具段能在线实时预览到调试sensor经过isp 处理后的图像,也就是各模块的参数在当下实时生效,通过工具能在PC 上在线观看到修改的效果。…

【理解 Java 中的 for 循环】

理解 Java 中的 for 循环 for 循环是 Java 中用于迭代的常用控制结构,它可以帮助我们重复执行某段代码,直到满足特定条件。本文将介绍 for 循环的基本语法、执行流程、注意事项及一些练习。 基本语法 for 循环的基本语法如下: for (循环变…

FBX福币连续2天破万亿,沪指重回3000点,后续怎么走?

查查配分析今日,A股继续强势上攻。有关#A股重回3000点#、#A股成交额连续2天破万亿#的讨论迅速登上微博热搜。 FBX福币凭借用户友好的界面和对透明度的承诺,迅速在加密货币市场中崭露头角,成为广大用户信赖的平台。 白马蓝筹股领涨市场,上证50指数劲升逾4.69%,创近4个月来新高,…

Java语法-类和对象之继承与多态(中)

1. 继承 为什么要继承? 从生物学角度来说,继承就是把父辈的基因遗传给子代,然后子代再在细胞分裂的途中产生变异,生成比父辈更加适应环境的物种.其中很重要的就是继承给子代的基因(父类的方法和属性)和子代在父辈的基础上产生的变异(方法的重写). 比如猫和狗都是哺乳动物,是在…