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

news2024/11/15 4:46:05

项目背景

项目中需要用到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 := "dev:dreame@2020@tcp(10.10.37.108:13306)/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.coloradmin.cn/o/2159534.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Docker与Kubernetes学习

基本概述 Docker 是一个流行的容器化平台,允许开发人员在容器中创建、部署和运行应用程序。 Docker 提供了一组工具和 API,使开发人员能够构建和管理容器化应用程序,包括 Docker Engine、Docker Hub 和 Docker Compose。 Kubernetes 是一个…

MySQL如何实现并发控制?(上)

前言 最开始学习数据库的时候都会被问到一个问题:“数据库系统相比与文件系统最大的优势是什么?”。具体的优势有很多,其中一个很重要的部分是:数据库系统能够进行更好的并发访问控制。 那么,数据库系统到底是怎么进…

yolov5-7转onnx并推理(包括缩放图推理与原始图片推理)

一、yolov5转onnx 先安装onnx, onnxruntime-gpu, ( pip install 就可以) 1. 静态模型: python export.py --weights yolov5s.pt --include onnx2.动态模型: python export.py --weights yolov5s.pt --include onnx --dynamic3.这里谈谈静态与动态的…

在虚幻引擎中实时显示帧率

引擎自带了显示帧率的功能 但是只能在编辑器中显示 , 在游戏发布后就没有了 , 所以我们要自己做一个 创建一个控件蓝图 创建画布和文本 , 修改文本 文本绑定函数 , 点击创建绑定 添加一个名为 FPS 的变量 格式化文本 用大括号把变量包起来 {FPS Int} FPS 然后转到事件图表…

【html】基础(一)

本专栏内容为:前端专栏 记录学习前端,分为若干个子专栏,html js css vue等 💓博主csdn个人主页:小小unicorn ⏩专栏分类:js专栏 🚚代码仓库:小小unicorn的代码仓库🚚 &am…

怎么录制游戏视频?精选5款游戏录屏软件

对于热爱游戏的你来说,记录游戏中的精彩瞬间并分享给朋友或粉丝,无疑是一种享受。然而,在众多录屏软件中,如何选择最适合你的那一款?今天,我们就为大家精选了五款游戏录屏软件,需要的朋友快来选…

【LeetCode:1014. 最佳观光组合 + 思维题】

🚀 算法题 🚀 🌲 算法刷题专栏 | 面试必备算法 | 面试高频算法 🍀 🌲 越难的东西,越要努力坚持,因为它具有很高的价值,算法就是这样✨ 🌲 作者简介:硕风和炜,…

EfficientFormer实战:使用EfficientFormerV2实现图像分类任务(一)

摘要 EfficientFormerV2是一种通过重新思考ViT设计选择和引入细粒度联合搜索策略而开发出的新型移动视觉骨干网络。它结合了卷积和变换器的优势,通过一系列高效的设计改进和搜索方法,实现了在移动设备上既轻又快且保持高性能的目标。这一成果为在资源受…

【Python报错已解决】NameError: name ‘variable‘ is not defined

🎬 鸽芷咕:个人主页 🔥 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活! 专栏介绍 在软件开发和日常使用中,BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

把任务管理器里面的vmware usb arbitrition停了,虚拟机一直识别不到手机设备了

在设备管理器--服务 里面找到VMware usb arbitrition服务,点击“启用”就好了。 参考大佬的文章: 吐血经验!!!解决虚拟机连不上USB!最全!_为什么vmware虚拟机不能连接上usb设备-CSDN博客

C语言 | Leetcode C语言题解之第432题全O(1)的数据结构

题目: 题解: //哈希队列 //哈希检查是否存在 //双编标维护次数顺序 #define MAXSIZE 769/* 选取一个质数即可 */ typedef struct hashNode {char* string; //字符串int count; //次数struct doubleListNode* dList;str…

箭头与数字识别系统源码分享

箭头与数字识别检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer V…

JavaScript 安装库npm报错

今天在编写JavaScript代码时,缺少了包express。 const express require(express); const app express();app.get(/, (req, res) > {res.send(Hello, world!); });app.listen(3000, () > {console.log(Server is running on port 3000); });npm install exp…

【C++指南】C++中nullptr的深入解析

💓 博客主页:倔强的石头的CSDN主页 📝Gitee主页:倔强的石头的gitee主页 ⏩ 文章专栏:《C指南》 期待您的关注 目录 引言 一、nullptr的引入背景 二、nullptr的特点 1.类型安全 2.明确的空指针表示 3.函数重载支…

从决策树到GBDT、随机森林

何为决策树 决策树(Decision Tree),它是一种以树形数据结构来展示决策规则和分类结果的模型,作为一种归纳学习算法,其重点是将看似无序、杂乱的已知数据,通过某种技术手段将它们转化成可以预测未知数据的树…

3D 模型GLTF、GLB格式文件介绍使用

一、介绍 GLTF(GL Transmission Format)和 GLB(GL Binary)是用于在 Web 和各种应用程序中传输和加载 3D 场景和模型的开放标准格式。它们由 Khronos Group 开发,旨在提供一种高效、可扩展且易于使用的 3D 内容格式。以…

Java项目中Linux跑起来

要让一个web项目跑起来首先需要tomat和jdk的包(Linux版本) 之后可以使用Xftp工具将包传到linux中 可以新建一个java包专门放这些文件 之后将其这些包解压出来 tar -xvf jdk-8u141-linux-x64.tar.gz //换自己的包名使用命令配置自己的环境变量 vim /et…

计算机专业选题推荐-基于python的协同过滤酒店推荐系统

精彩专栏推荐订阅:在下方主页👇🏻👇🏻👇🏻👇🏻 💖🔥作者主页:计算机毕设木哥🔥 💖 文章目录 一、协同过滤酒店…

【C++ 11多线程加速计算实操教程】

【C 11多线程加速计算实操教程】 1. 了解线程的基本概念2. 创建线程2.1 启动线程的基本示例:2.2 运行结果 3. 线程加速计算3.1 演示如何使用多个线程计算数组的和:3.2 运行结果3.3 结果分析3.4 拓展学习 4. 互斥量(Mutex)4.1 演示…

Qt中多语言的操作(以QtCreator为例)

1、首先,我们在代码中与文本相关的且需要支持多语言的地方,用tr来包含多语言key(多语言key是我们自己定义的),如下 //举例 QPushButton* btnnew QPushButton(this); btn->move(20,20); btn->resize(100,50); //…