当前位置: 首页 > news >正文

gorm.io/sharding改造:赋能单表,灵活支持多分表策略(下)

背景

 分表组件改造的背景,我在这篇文章《gorm.io/sharding改造:赋能单表,灵活支持多分表策略(上)》中已经做了详细的介绍——这个组件不支持单表多个分表策略,为了突破这个限制做的改造。

在上一篇文章中,我们讨论了注册的改造,注册的改造修改逻辑比较简单,但是,上一篇文章中遗留了一个很重要的议题——在增删改查的实际业务操作中,分表组件究竟如何精准地定位到对应的分表策略,以确保业务逻辑的顺利执行?这篇文章,我们重点讨论这个逻辑。

源码解读

首先,我们需要看一下当我们执行查询,新增,更新或是删除逻辑,其执行流程是什么。比如,这么一个查询。

err := db.Model(&Order{}).Where("user_id = ?", userID).Find(&orders).Error

我们大概梳理一下其执行流程。

  1. 初始化查询
    • 当我们执行查询 err := db.Model(&Order{}).Where("user_id = ?", userID).Find(&orders).Error,首先会通过 db.Model(&Order{}) 初始化一个查询实例,设置相关的模型信息。
  2. 构建查询条件
    • 接着,通过 .Where("user_id = ?", userID) 方法,将查询条件 user_id = ? 以及对应的参数 userID 添加到查询实例中。
  3. 执行查询
    • 调用 .Find(&orders) 方法时,开始执行查询流程。
    • 在 Find 方法中,首先通过 db.getInstance() 获取数据库实例。
    • 然后,检查是否存在查询条件,如果有,则构建 SQL 条件表达式,并将其添加到查询语句中。
    • 设置查询结果的目标对象 dest,即 &orders
    func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {tx = db.getInstance()if len(conds) > 0 {if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {tx.Statement.AddClause(clause.Where{Exprs: exprs})}}tx.Statement.Dest = destreturn tx.callbacks.Query().Execute(tx)
    }
  4. 执行回调和处理
    • 调用 tx.callbacks.Query().Execute(tx) 执行查询回调链。
    • 在 Execute 方法中,会遍历并执行所有注册的查询前和查询后的回调函数。
    func (p *processor) Execute(db *DB) *DB {//省略其他代码逻辑
    ......	
    for _, f := range p.fns {f(db)}//省略其他代码逻辑
    ......	return db
    }
  5. 分片和查询执行
    • 最终,调用 pool.QueryContext 方法,根据上下文、SQL 查询语句和参数执行实际的数据库查询。
    • 在 QueryContext 方法中,会调用 pool.sharding.resolve 方法解析并修改查询语句,以处理数据库分片逻辑。
    • resolve 方法解析 SQL 查询语句,提取表名,并根据表名获取相应的分片配置。
    • 根据分片配置,可能会修改原始查询语句,以适应分片策略。
    func (pool ConnPool) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {var (curTime = time.Now())//该方法根据传入的SQL查询(及其参数)和上下文信息,动态地解析、修改并返回最终的分片 //查询、原始查询、目标表名以及可能出现的错误。_, stQuery, _, err := pool.sharding.resolve(query, args...)if err != nil {return nil, err}// 省略......return rows, err
    }
    func (s *Sharding) resolve(query string, args ...any) (ftQuery, stQuery, tableName string, err error) {ftQuery = querystQuery = queryif len(s.configs) == 0 {return}expr, err := sqlparser.NewParser(strings.NewReader(query)).ParseStatement()if err != nil {return ftQuery, stQuery, tableName, nil}// 省略......tableName = table.Name.Namer, ok := s.configs[tableName]if !ok {return} // 省略......return
    }
    
  6. 返回结果
    • 执行查询后,将结果填充到目标对象 &orders 中,并返回查询结果或错误。

我们重点关注resolve方法,这个方法包含了分表逻辑的处理逻辑:r, ok := s.configs[tableName]获取对应表的分表策略。

通过上述代码的解析,我们现在应该有了解决方案。原来的逻辑获取分表策略是根据表明获取的。那我们只要修改这个逻辑,根据表名+分表键名作为唯一键获取对应的分表策略就能实现我们的目标。

方案

接下来,我们需要思考的是,如何把分表键传进来呢?

我一开始想的是通过解析query获取查询条件中的分表键。但是,当我深入的看了这个逻辑之后,发现这个设想不能实现,因为value, id, keyFind, err = s.nonInsertValue(r.ShardingKey, condition, args...)这个方法中获取查询条件的字段是在这个函数内部实现的,不能保持一个统一的结构,而且改造复杂度比较高。

context在go语言有着广泛的使用场景,所以,我想着通过context的方式把分表键传递进来。有了这个想法,改造起来就很简单了。我们只需要resolve方法增加一个context的传参,并且r, ok := s.configs[tableName]这个获取分表策略,改成用表名+从context中获取的分表键作为键来获取分表策略即可。

如此,我们就实现了根据表名+分表键获取对应分表策略的逻辑,至此,我们的改造任务完成。

案例

我目前也只是简单的测试了两种分表策略的场景,仅仅只覆盖了查询和插入的场景。更复杂的场景还没有测试。诸如并发情况下的场景。

package testimport ("context""fmt""testing""time""gorm.io/driver/mysql""gorm.io/gorm""gorm.io/sharding"
)
var globalDB *gorm.DBtype Order struct {ID        int64  `gorm:"primaryKey"`OrderId   string `gorm:"sharding:order_id"` // 指明 OrderId 是分片键UserID    int64  `gorm:"sharding:user_id"`ProductID int64OrderDate time.TimeOrderYear int
}
// 自定义 ShardingAlgorithm
func customShardingAlgorithm4(value any) (suffix string, err error) {if year, ok := value.(int); ok {return fmt.Sprintf("_%d", year), nil}return "", fmt.Errorf("invalid order_date")
}func customShardingAlgorithmUserId(value any) (suffix string, err error) {if userId, ok := value.(int64); ok {return fmt.Sprintf("_%d", userId%4), nil}return "", fmt.Errorf("invalid user_id")
}// customePrimaryKeyGeneratorFn 自定义主键生成函数
func customePrimaryKeyGeneratorFn(tableIdx int64) int64 {var id int64seqTableName := "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")// 查询当前的最大 IDtx.Raw("SELECT id FROM " + seqTableName + " ORDER BY id DESC LIMIT 1").Scan(&id)// 更新序列表(这里直接递增 1,实际应用中可能需要更复杂的逻辑)newID := id + 1tx.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_Sharding6(t *testing.T) {// 连接到 MySQL 数据库dsn := "dev:xxxx@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 = dbconfig1 := sharding.Config{ShardingKey:       "order_year",ShardingAlgorithm: customShardingAlgorithm4, // 使用自定义的分片算法//PrimaryKeyGenerator: sharding.PKMySQLSequence,PrimaryKeyGenerator:   sharding.PKCustom,PrimaryKeyGeneratorFn: customePrimaryKeyGeneratorFn,}config2 := sharding.Config{ShardingKey:         "user_id",NumberOfShards:      4,ShardingAlgorithm:   customShardingAlgorithmUserId, // 使用自定义的分片算法PrimaryKeyGenerator: sharding.PKSnowflake,          // 使用 Snowflake 算法生成主键}mapConfig := make(map[string]sharding.Config)mapConfig["orders_order_year"] = config1mapConfig["orders_user_id"] = config2// 配置 Gorm Sharding 中间件,使用自定义的分片算法middleware := sharding.RegisterWithKeys(mapConfig) // 逻辑表名为 "orders"db.Use(middleware)// 查询示例var orders []Orderctx, cancel := context.WithCancel(context.Background())defer cancel()ctx = context.WithValue(ctx, "sharding_key", "order_year")db = db.WithContext(ctx)err = db.Model(&Order{}).Where("order_year=? and product_id=?", 2025, 102).Find(&orders).Errorif err != nil {fmt.Println("Error querying orders:", err)}fmt.Printf("sharding key order_year Selected orders: %#v\n", orders)// 查询示例FindByUserID2(db, int64(1))// 示例:插入订单数据InsertOrderByUserId(db)InsertOrderByOrderYear(db)
}func FindByUserID2(db *gorm.DB, userID int64) ([]Order, error) {var orders []Order// 查询示例ctx, cancel := context.WithCancel(context.Background())defer cancel()ctx = context.WithValue(ctx, "sharding_key", "user_id")db = db.WithContext(ctx)err := db.Model(&Order{}).Where("user_id = ?", userID).Find(&orders).Errorif err != nil {fmt.Println("Error querying orders:", err)}fmt.Printf("no sharding key user_id Selected orders: %#v\n", orders)return orders, err
}type OrderByUserId struct {ID        int64  `gorm:"primaryKey"`OrderId   string `gorm:"sharding:order_id"` // 指明 OrderId 是分片键UserID    int64  `gorm:"sharding:user_id"`ProductID int64OrderDate time.Time
}func InsertOrderByUserId(db *gorm.DB) error {ctx, cancel := context.WithCancel(context.Background())defer cancel()ctx = context.WithValue(ctx, "sharding_key", "user_id")db = db.WithContext(ctx)// 示例:插入订单数据order := OrderByUserId{OrderId:   "20240101ORDER0001",UserID:    100,ProductID: 100,OrderDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),}err := db.Table("orders").Create(&order).Errorif err != nil {fmt.Println("Error creating order:", err)}order2 := OrderByUserId{OrderId:   "20250101ORDER0001",UserID:    105,ProductID: 100,OrderDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),}err = db.Table("orders").Create(&order2).Errorif err != nil {fmt.Println("Error creating order:", err)}return err
}func InsertOrderByOrderYear(db *gorm.DB) error {ctx, cancel := context.WithCancel(context.Background())defer cancel()ctx = context.WithValue(ctx, "sharding_key", "order_year")db = db.WithContext(ctx)orderYear := 2024// 示例:插入订单数据order := Order{OrderId:   "20240101ORDER0002",UserID:    1,ProductID: 100,OrderDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),OrderYear: orderYear,}err := db.Create(&order).Errorif err != nil {fmt.Println("Error creating order:", err)}orderYear = 2025order2 := Order{OrderId:   "20250101ORDER0002",UserID:    1,ProductID: 100,OrderDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),OrderYear: orderYear,}err = db.Create(&order2).Errorif err != nil {fmt.Println("Error creating order:", err)}return err
}

总结

通过改造gorm.io/sharding组件,我们实现了根据表名+分表键获取对应分表策略的逻辑。这一改造使得组件能够支持单表多个分表策略,更加灵活和强大。目前,我们已经简单测试了查询和插入场景,更复杂的场景和并发情况还需进一步测试和优化。通过这一改造,我们为业务逻辑的执行提供了更加精准和高效的分表策略定位。

相关文章:

gorm.io/sharding改造:赋能单表,灵活支持多分表策略(下)

背景 分表组件改造的背景,我在这篇文章《gorm.io/sharding改造:赋能单表,灵活支持多分表策略(上)》中已经做了详细的介绍——这个组件不支持单表多个分表策略,为了突破这个限制做的改造。 在上一篇文章中&…...

域渗透AD渗透攻击利用 MS14-068漏洞利用过程 以及域渗透中票据是什么 如何利用

目录 wmi协议远程执行 ptt票据传递使用 命令传递方式 明文口令传递 hash口令传递 票据分类 kerberos认证的简述流程 PTT攻击的过程 MS14-068 漏洞 执行过程 wmi协议远程执行 wmi服务是比smb服务高级一些的,在日志中是找不到痕迹的,但是这个主…...

C++进阶-->继承(inheritance)

1. 继承的概念及定义 1.1 继承的概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要手段,他允许我们在保证原有类的特性基础上还进行扩展,通过继承产生的类叫做派生类(子类),被继承的类叫做基类&a…...

可视化项目 gis 资源复用思路(cesium)

文章目录 可视化项目 gis 资源复用思路底图、模型替换思路具体操作 可视化项目 gis 资源复用思路 背景: A项目的底图、模型 是现在在做的 B项目所需要的,现在要把 B项目的底图之类的替换成 A系统的 底图、模型替换思路 观察可访问系统的 gis 相关网络请…...

SQL实战测试

SQL实战测试 (请写下 SQL 查询语句,不需要展示结果) 表 a DateSalesCustomerRevenue2019/1/1张三A102019/1/5张三A18 1. **用一条 ** SQL 语句写出每个月,每个销售有多少个客户收入多少 输出结果表头为“月”,“销…...

Java 基础教学:基础语法-变量与常量

变量 变量是程序设计中的基本概念,它用于存储信息,这些信息可以在程序执行过程中被读取和修改。 变量的声明 在Java中,声明变量需要指定变量的数据类型以及变量的名称。数据类型定义了变量可以存储的数据种类(例如整数、浮点数…...

vue3使用element-plus手动更改url后is-active和菜单的focus颜色不同步问题

在实习,给了个需求做个新的ui界面,遇到了一个非常烦人的问题 如下,手动修改url时,is-active和focus颜色不同步 虽然可以直接让el-menu-item:focus为白色能解决这个问题,但是我就是想要有颜色哈哈哈,有些执…...

每天五分钟深度学习框架pytorch:从底层实现一元线性回归模型

本文重点 本节课程我们继续搭建一元线性回归模型,不同的是这里我们不使用pytorch框架已经封装好的一些东西,我们做这个目的是为了更加清楚的看到pytorch搭建模型的本质,为了更好的理解,当然实际中我们还是使用pytorch封装好的一些东西,不要重复造轮子。 模型搭建 #定义…...

编辑器加载与AB包加载组合

解释: 这个 ABResMgr 类是一个资源加载管理器,它用于整合 AB包(Asset Bundle)资源加载和 编辑器模式资源加载。通过这个管理器,可以根据开发环境选择资源加载方式,既支持 运行时使用Asset Bundle加载&…...

【c++】vector中的back()函数

nums.back() 是 C 中 std::vector 类的一个成员函数,用于获取数组(向量)中的最后一个元素。以下是一些关于 nums.back() 的详细解释和示例使用: 1. 功能 nums.back() 返回数组 nums 中的最后一个元素。如果数组为空,…...

[分享] SQL在线编辑工具(好用)

在线SQL编写工具(无广告) - 在线SQL编写工具 - Web SQL - SQL在线编辑格式化 - WGCLOUD...

element-ui隐藏表单必填星号

// 必填星号在前显示 去掉 .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label:before { content: !important; margin-right: 0px!important; } // 必填星号在结尾显示 .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__labe…...

自动驾驶系列—激光雷达点云数据在自动驾驶场景中的深度应用

🌟🌟 欢迎来到我的技术小筑,一个专为技术探索者打造的交流空间。在这里,我们不仅分享代码的智慧,还探讨技术的深度与广度。无论您是资深开发者还是技术新手,这里都有一片属于您的天空。让我们在知识的海洋中…...

C#删除dataGridView 选中行

关键在于:从最后一行开始删除。 从前往后删只能删除其中一半,我理解是再remove行的时候dataGridView内部行序列发生了变化,包含在选中行中的特定行会被忽略,从后往前删就可避免这个问题,最后一行的行号影响不到前面的…...

K8S调度不平衡问题分析过程和解决方案

不平衡问题排查 问题描述: 1、业务部署大量pod(据反馈,基本为任务型进程)过程中,k8s node内存使用率表现不均衡,范围从80%到百分之几; 2、单个node内存使用率超过95%,仍未发生pod驱逐,存在node…...

Python中类、继承和方法重写的使用

😀前言 本篇博文将介绍如何定义类、创建类的实例、访问类的成员、使用属性、实现继承及方法重写,希望你能够喜欢 🏠个人主页:晨犀主页 🧑个人简介:大家好,我是晨犀,希望我的文章可以…...

【Neo4j】- 轻松入门图数据库

文章目录 前言-场景一、Neo4j概述二、软件安装部署1.软件下载2.软件部署3.软件使用4.语法学习 总结 前言-场景 这里用大家都了解的关系数据与图数据据库对比着说,更加方便大家理解图数据库的作用 图形数据库和关系数据库均存储信息并表示数据之间的关系。但是,关系…...

LeetCode 206 - 反转链表

解题思路 我们可以使用迭代的方法来实现链表的反转,这里我们先介绍迭代的方法。迭代的思路是:从头节点开始,依次将节点的next指针进行反转,使得当前节点的next指向其前一个节点,然后依次向后移动指针,直至…...

AI生成大片,Movie Gen 可以生成长视频并配上完美的音效,带给观众更好的观看体验。

之前的文章中已经给大家介绍了一些关于长视频生成相关的技术,AI生成大片已经越来越近了。感兴趣的小伙伴可以点击下面链接阅读~ Movie Gen 的工作原理可以简单理解为两个主要部分:一个是生成视频的模型,另一个是生成音频的模型。首先&#x…...

Flink on yarn模式下,JobManager异常退出问题

这个问题排除了很久,其中更换了Flink版本,也更换了Hadoop版本一直无法解决,JobManager跑着跑着就异常退出了。资源管理器上是提示运行结束,运行状态是被Kill掉。 网上搜了一圈,都说内存不足、资源不足,配置…...

面对AI算力需求激增,如何守护数据中心机房安全?

随着人工智能(AI)技术飞速发展,AI算力需求呈现爆发式增长,导致对数据设备电力的需求指数级攀升。这给数据中心带来前所未有的挑战和机遇,从提供稳定的电力供应、优化高密度的部署,到数据安全的隐私保护&…...

Connection --- 连接管理模块

目录 模块设计 模块实现 shared_from_this 模块测试纠错 模块设计 Connection模块是对通信连接也就是通信套接字的整体的管理模块,对连接的所有操作都是通过这个模块提供的接口来完成的。 那么他具体要进行哪些方面的管理呢? 首先每个通信连接都需…...

iconfont图标放置在某个元素的最右边

在网页设计中&#xff0c;如果你想要将iconfont图标放置在某个元素的最右边&#xff0c;你可以通过CSS来实现这个布局。以下是一些基本的CSS代码示例&#xff0c;它们可以帮助你根据不同的布局需求将图标放置在最右边&#xff1a; 内联元素&#xff08;如<span>&#xff…...

Android10 recent键相关总结

目录 初始化流程 点击Recent键流程 RecentsActivity 显示流程 RecentsModel 获取数据管理类 RecentsActivity 布局 已处于Recent界面时 点击recent 空白区域 点击返回键 recent组件配置 Android10 Recent 功能由 System UI&#xff0c;Launcher共同实现。 初始化流程 …...

Ajax:原生ajax、使用FormData的细节问题,数据的载体

人生海海&#xff0c;山山而川&#xff0c;不过尔尔&#xff1b;空空而来&#xff0c;苦苦而过&#xff0c;了了而去 文章目录 原生ajax使用FormData的细节问题数据的载体 原生ajax 执行顺序 创建xhr对象 var xhr new XMLHttpRequest()调用xhr.open(请求方式, url)函数&#…...

【HuggingFace 如何上传数据集 (2) 】国内网络-稳定上传图片、文本等各种格式的数据

【HuggingFace 下载】diffusers 中的特定模型下载&#xff0c;access token 使用方法总结【HuggingFace 下载中断】Git LFS 如何下载指定文件、单个文件夹&#xff1f;【HuggingFace 如何上传数据集】快速上传图片、文本等各种格式的数据 上文的方法因为是 https 协议&#xf…...

GNOME桌面安装dock

Although GNOME Shell integration extension is running, native host connector is not detected. Refer documentation for instructions about installing connector. sudo yum -y install chrome-gnome-shell...

移动app测试有哪些测试类型?安徽软件测试中心分享

科技信息时代&#xff0c;移动app的出现为我们的生活及工作带来了极大的便利。一款app从生产到上线必不可少的就是测试阶段&#xff0c;app测试是保障产品质量和安全的有效手段&#xff0c;那么移动app测试有哪些测试类型呢?安徽软件测试中心又有哪些? 1、功能性测试   需…...

Android 10.0 截屏流程

通常未通过特殊定制的 Android 系统&#xff0c;截屏都是经过同时按住音量下键和电源键来截屏。本篇文章就只讨论使用这些特殊按键来进行截屏。 这里我们就要明白事件是在哪里进行分发拦截的。通过源码的分析&#xff0c;我们发现是在PhoneWindowManager.java 中。 PhoneWindow…...

Axure零基础深入浅出的讲解

在当今的互联网产品设计领域&#xff0c;原型设计已经成为了产品经理、设计师和开发者之间沟通的桥梁。而Axure作为一款功能强大、灵活易用的原型设计工具&#xff0c;正是很多产品经理的得力助手。无论你是产品经理新手&#xff0c;还是资深设计师&#xff0c;Axure都能帮助你…...

网站首页加载特效/写软文是什么意思

安装choco choco 是Windows上类似Mac的HomeBrew的命令行软件安装工具&#xff0c;按Windows键-Q搜索Power Shell&#xff0c;右击选择以管理员身份运行&#xff0c;打开后粘贴以下代码运行 iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex 用choco安装常用软件…...

专门做app的原型网站/seo教程网

好久没有写博客了&#xff0c;今天发表一篇吧&#xff1a;&#xff09;通常的在线进行表字段的增减都会造成表所&#xff0c;如果表较小还能接受&#xff0c;如果过大则这个锁持续的时间会让人比较烦恼&#xff0c;对业务持续性影响较大。Percona 提供了一款关于MySQL管理的工具…...

一个公司如何把网站做好/第三方网站流量统计

先点击"科学舍"&#xff0c;再点击“关注”&#xff0c;这样您就可以免费收到我们的最新内容了&#xff0c;每天都会有更新&#xff0c;完全是免费订阅&#xff0c;请放心关注。本文转自网络&#xff0c;著作权属归原创者所有。如有侵权&#xff0c;请联系我们删除。…...

建设工程合同约定的质量目标/绍兴网站快速排名优化

shirio的功能 Shiro可以非常容易的开发出足够好的应用&#xff0c;其不仅可以用在JavaSE环境&#xff0c;也可以用在JavaEE环境。Shiro可以帮助我们完成&#xff1a;认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛&#xff0c;而且Shiro的API也是非常简…...

如何在网站上做网盘/企业网站优化软件

2022年已经开始了&#xff0c;在新的一年里&#xff0c;又要大干一场了。工欲善其事必先利其器&#xff0c;计划做完之后&#xff0c;总要有能记录待办事项以及任务清单的应用&#xff0c;可以记录自己要做的事情&#xff0c;方便自己及时查看自己有哪些事情没有做&#xff0c;…...

网站空间数据库/百度站长平台电脑版

今年过年各位一定在微信里抢了不少红包。那么当别人是手气王而你只抢到1分钱的时候&#xff0c;你有没有想过&#xff0c;如果你来实现红包的分配算法&#xff0c;会怎么写&#xff1f; 这里我给一个简单的实现方案。 基本思路就是&#xff0c;有多少个红包&#xff0c;就循环多…...