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

项目跑起来之前的那些事

小组放了十天假,闲在家里也怪无聊,正好可以读读学长写的项目代码,让我瞧一瞧学长的风姿(~ ̄▽ ̄)~

思来想去,暂时还是读 河南师范大学附属中学图书馆 这个项目 ,更贴近我此刻的水准,让我舒服点。

目录

总:

一、初始化配置

1、调用入口:

2、配置初始化函数详解(核心逻辑示例)

3、配置文件结构

4、配置结构体定义

5、全局配置储存

二、数据库初始化

1、调用入口

2、配置检查

3、构建数据库连接字符串

4、创建数据库引擎

5、配置数据库日志

6、配置数据库时区

7、配置数据库连接池

8、表结构的同步

9、组合以上函数

三、服务层依赖注入

1、服务容器:依赖管理的"中央枢纽"

2、服务供应商接口(定契约)

3、服务供应商实现(持有服务实例)

4、服务实例化(工厂模式)

5、全局注册:将服务注入容器

四、控制器层依赖注入

五、路由初始化

1、初始化路由

2、顶层路由容器(统一管理路由)

3、模块路由组

4、router.Init() 核心实现

5、路由组初始化(例)

六、定时任务的启动

1、启动入口

2、管理器结构

3、初始化管理器

4、定时任务设置

5、核心同步方法

6、获取什么样的信息?

7、定期清扫

8、优雅停止


: 为防止他人公司的权益被侵犯,本篇博客将会以自己学习总结的知识为基准,并借助伪代码进行通用的逻辑展示。

如果排除Dockerfile,这就是本项目的基本骨架:

📁 api/                    # 主要代码目录
├── 📁 controller/         # 控制器层(处理HTTP请求)
├── 📁 service/           # 业务逻辑层
├── 📁 model/             # 数据模型层
├── 📁 router/            # 路由配置
├── 📁 middleware/        # 中间件
├── 📁 db/               # 数据库连接
├── 📁 conf/             # 配置文件
└── main.go              # 程序入口

在这里,我们可以看到,以上是前后端分离后的标准的MVC架构

处理请求的途径,大致如下:

HTTP请求 → Router → Middleware → Controller → Service → Model → Database↓Response ← JSON序列化 ← 业务处理

任何事物,都会有一个先后顺序,先者会为后者铺路,两者相辅相成。

而我对本系统是如何启动的,非常感兴趣。

所以本篇博客,主要聚焦在,本系统开启后,代码层面那些发挥了作用,为什么会发挥作用。

总:

程序入口 main()↓
1. 配置初始化 conf.InitConfig()↓
2. 数据库初始化 db.InitDB()↓
3. 服务层依赖注入 service.GroupApp↓
4. 控制器层依赖注入 controller.ApiGroupApp↓
5. 资源初始化 initBaseResource()↓
6. 路由初始化 router.Init()↓
7. HTTP服务器启动 httpServer.Start()↓
8. 定时任务启动 cron.NewOperationLogSyncManager()↓
9. 监控服务启动 /metrics↓
10. 业务定时任务 Graduate/RemindReturn/CancelBorrow↓
11. 信号监听 优雅关闭

如下为代码逻辑:

func main() {// 初始化配置与数据连接config.Init()db.InitDB()// 注册服务与控制器service.Init()controller.Init(service)// 初始化基础资源initResources()// 启动主服务router := router.Setup()server := httpServer.Start(router)// 启动后台任务go startBackgroundTask()// 启动监控服务startMonitorServer()// 注册其他定时任务cron.RegisterTasks()// 优雅退出处理waitForExit()server.Shutdown()log.Info("服务已停止")
}// 初始化基础资源
func initResources() {if err := service.InitResources(); err != nil {log.Error("资源初始化失败: ", err)}
}// 启动后台任务(示例)
func startBackgroundTask() {if err := task.Start(); err != nil {log.Error("后台任务启动失败: ", err)} else {log.Info("后台任务启动成功")}
}// 启动监控服务(示例)
func startMonitorServer() {http.Handle("/metrics", monitor.Handler())if err := http.ListenAndServe(monitor.Addr(), nil); err != nil {log.Error("监控服务启动失败: ", err)}
}// 等待退出信号
func waitForExit() {sig := make(chan os.Signal)signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)<-sig
}

一、初始化配置

1、调用入口:

func main() {    // 传入配置文件存放目录,启动配置初始化(通用路径命名)conf.InitConfig("config-dir")  // 后续业务逻辑(如数据库初始化、服务启动等)// ...
}

2、配置初始化函数详解(核心逻辑示例)

// InitConfig 通用配置初始化函数:加载配置文件、绑定环境变量、初始化日志等
func InitConfig(configDir string) {    // 1. 配置Viper:指定配置文件的路径、类型与名称viper.AddConfigPath(configDir)        // 添加配置文件搜索目录viper.SetConfigType("yml")            // 支持YAML格式(通用配置格式)viper.SetConfigName("app-config")     // 配置文件名(不含扩展名,避免项目专属命名)// 2. 读取配置文件:失败则终止应用(基础容错逻辑)if err := viper.ReadInConfig(); err != nil {// 注意,读取成功后,会存入内存log.Fatalf("配置文件读取失败: %v", err)  // 通用日志函数,替换项目专属日志名}// 3. 绑定环境变量:适配容器化/多环境部署(通用变量绑定逻辑)_ = viper.BindEnv("server.port", "APP_PORT")       // 服务端口绑定环境变量_ = viper.BindEnv("server.monitorPort", "MONITOR_PORT")  // 监控端口绑定// 更多通用配置项的环境变量绑定(如数据库地址、超时时间等)// ...// 4. 初始化日志级别:从配置动态读取(通用日志级别适配)switch viper.GetString("log.level") {case "debug":log.SetLevel(log.DebugLevel)case "info":log.SetLevel(log.InfoLevel)case "warn":log.SetLevel(log.WarnLevel)default:log.SetLevel(log.InfoLevel)  // 默认级别兜底}// 5. 启用配置热更新:支持开发/调试场景,无需重启应用viper.WatchConfig()// 6. 调试日志:打印加载的配置项(开发环境用,生产可关闭)log.Infoln("--------- 加载的配置列表 --------")for _, key := range viper.AllKeys() {log.Infoln(key, ":", viper.Get(key))}// 7. 初始化全局配置对象:将Viper配置映射到结构体(通用类型安全处理)global.AppConfig = NewConfig() 
}// 这我一般放在model中,刚好在model下方
func NewConfig() *Config {...为结构体绑定具体内容 _api := &Api{Host:        viper.GetString("api.host"),....}_log := &Log{Level:            viper.GetString("log.level"),...}...return &Config{Log:      _log,Api:      _api,...}
}

3、配置文件结构

咱们这里按照 “服务基础配置(端口、地址)、日志配置、第三方依赖配置(数据库、缓存)” 分层,示例格式(YAML):

# 通用配置文件示例(无业务属性)
server:port: 8080monitorPort: 9090
log:level: infooutput: console
db:host: ${DB_HOST}  # 引用环境变量,避免硬编码port: ${DB_PORT:3306}

4、配置结构体定义

与配置文件分层对应,确保类型安全

// Config 通用配置结构体(无业务专属字段)
type Config struct {Server ServerConfig `mapstructure:"server"`Log    LogConfig    `mapstructure:"log"`DB     DBConfig     `mapstructure:"db"`
}type ServerConfig struct {Port        int    `mapstructure:"port"`MonitorPort int    `mapstructure:"monitorPort"`
}// 其他子结构体(LogConfig、DBConfig)按通用字段定义
// ...

5、全局配置储存

通过通用包(如global)管理全局配置,提供安全访问接口,避免直接暴露全局变量。

二、数据库初始化

1、调用入口

func main() {    // ...db.InitDB()// ...
}

2、配置检查

// InitDB 通用PostgreSQL数据库初始化入口
func InitDB() {// 检查全局配置是否已加载(通用配置校验逻辑)if global.AppConfig == nil {log.Fatal("数据库初始化失败:请先初始化应用配置")}// 若数据库配置项缺失,同样终止流程(基础参数校验)if global.AppConfig.DB == nil {log.Fatal("数据库初始化失败:配置中缺少数据库相关参数")}
}

3、构建数据库连接字符串

// 步骤2:拼接PostgreSQL通用连接字符串(DSN)
func buildDSN() string {// 从全局配置中读取通用数据库参数dbCfg := global.AppConfig.DB// 通用DSN格式:适配PostgreSQL标准连接协议dsn := fmt.Sprintf("%s://%s:%s@%s:%d/%s?sslmode=disable",dbCfg.Driver,   // 数据库驱动(固定为postgres)dbCfg.Username, // 数据库用户名(配置注入)dbCfg.Password, // 数据库密码(配置/环境变量注入)dbCfg.Host,     // 数据库主机地址dbCfg.Port,     // 数据库端口dbCfg.Name      // 数据库名)return dsn
}

4、创建数据库引擎

// 步骤3:初始化XORM引擎
func createDBEngine(dsn string) (*xorm.Engine, error) {// 调用XORM创建引擎,传入驱动与DSNengine, err := xorm.NewEngine(global.AppConfig.DB.Driver, dsn)if err != nil {// 日志仅提示错误,不暴露完整DSNlog.Fatalf("数据库引擎创建失败:%v", err)return nil, err}log.Info("数据库引擎创建成功")return engine, nil
}

5、配置数据库日志

// 步骤4:设置数据库操作日志(通用日志级别适配)
func configDBLog(engine *xorm.Engine) {// 启用SQL语句打印(开发环境调试用,生产可配置关闭)engine.ShowSQL(true)// 从全局配置读取日志级别,映射到XORM日志级别logLevel := global.AppConfig.Log.Levelswitch logLevel {case "debug":engine.Logger().SetLevel(log.LOG_DEBUG) // 调试级:打印所有SQL与详情case "info":engine.Logger().SetLevel(log.LOG_INFO)  // 信息级:打印关键操作case "warn":engine.Logger().SetLevel(log.LOG_WARNING) // 警告级:仅打印警告与错误case "error":engine.Logger().SetLevel(log.LOG_ERR)    // 错误级:仅打印错误default:engine.Logger().SetLevel(log.LOG_INFO)   // 默认:信息级}log.Infof("数据库日志级别已设置为:%s", logLevel)
}

6、配置数据库时区

// 步骤5:同步数据库时区与应用时区(通用时区处理)
func configDBTimezone(engine *xorm.Engine) {// 1. 查询数据库当前时区(通用SQL,无业务关联)results, err := engine.Query("show timezone")if err != nil {log.Errorf("查询数据库时区失败:%v", err)return}// 提取数据库时区(简化处理,聚焦逻辑而非具体字段解析)dbTimezone := string(results[0]["TimeZone"])log.Infof("数据库当前时区:%s", dbTimezone)// 2. 加载时区并设置到引擎if loc, err := time.LoadLocation(dbTimezone); err != nil {log.Errorf("加载时区失败:%v,将使用默认时区", err)} else {engine.DatabaseTZ = loc // 设置数据库时区}// 3. 设置应用与数据库的时区对齐(通用本地时区配置)engine.TZLocation = time.Local // 或从全局配置读取本地时区
}

在这里,我详细的说一下,DatabaseTZ与TZLocation的区别与作用。
他们两者的存在,一个是为了解决数据库存储的时间,另一个是为了优化用户读取的时间。

// 数据库时区:UTC-5(数据库服务器的实际时区)
engine.DatabaseTZ = loc  // 从 "show timezone" 查询得到

存储时 :应用程序时间 → 数据库时区 → 存储到数据库

// 应用程序时区:UTC+8(用户期望看到的时区)
engine.TZLocation = g.Loc  // 从配置文件读取

读取时 :数据库时间 → 应用程序时区 → 显示给用户

实际应用场景:

- 数据库服务器在美国(UTC-5)
- 应用程序部署在中国(UTC+8)
- 用户在中国使用系统1.存储时 :应用程序时间 → 数据库时区 → 存储到数据库
2.读取时 :数据库时间 → 应用程序时区 → 显示给用户
这样确保了:- 数据库中的时间数据是一致的
- 用户看到的时间是符合本地习惯的
- 不同时区的用户访问同一系统时,看到的时间都是正确的本地时间

7、配置数据库连接池

// 步骤6:配置数据库连接池(通用性能参数)
func configDBPool(engine *xorm.Engine) {dbCfg := global.AppConfig.DB// 最大打开连接数:控制同时与数据库建立的连接数engine.SetMaxOpenConns(dbCfg.MaxOpenConns)// 最大空闲连接数:空闲时保留的连接数,避免频繁创建连接engine.SetMaxIdleConns(dbCfg.MaxIdleConns)// 连接最大生存时间:避免长期空闲连接失效connLifetime := time.Duration(dbCfg.ConnMaxLifetimeSec) * time.Secondengine.SetConnMaxLifetime(connLifetime)log.Info("数据库连接池参数配置完成")
}

在这里我详细的说一下,最大连接数、最大空闲数、最大生存时间。

最大连接数:

1、MaxOpenConns(最大打开连接数)

- 高并发场景 :假设你的图书管理系统同时有 200 个用户在借书
- 没有限制时 :可能会创建 200 个数据库连接,数据库服务器压力巨大
- 设置为 100 后 :最多只能有 100 个连接,第 101-200 个请求需要等待
- 实际效果 :保护数据库不被过多连接压垮

// 配置最大空闲连接数为 10

2、engine.SetMaxIdleConns(10)

- 业务高峰期 :上午 9-11 点,有 50 个活跃连接处理借还书业务
- 业务低谷期 :下午 2-4 点,只有 5 个用户在使用系统
- 没有空闲连接 :每次查询都要重新建立连接(耗时 10-50ms)
- 保留 10 个空闲连接 :下次查询直接使用现有连接(耗时 1-2ms)

// 配置连接最大生存时间为 1 小时

3、connLifetime := time.Duration(3600) * time.Second

      engine.SetConnMaxLifetime(connLifetime)

- 问题场景 :数据库服务器配置了 8 小时自动断开空闲连接
- 应用程序 :保持连接 10 小时不释放
- 结果 :连接已被数据库断开,但应用程序不知道,使用时报错
- 设置 1 小时后 :应用程序主动在 1 小时后关闭连接,避免使用失效连接

8、表结构的同步

// 步骤7:(可选)表结构同步(通用逻辑,无业务关联)
// 注:生产环境建议通过迁移工具(如go-migrate)管理表结构,而非运行时同步
func syncDBTable(engine *xorm.Engine) {// 示例:同步通用业务表err := engine.Sync2(new(common.BaseTable),  // 通用基础表(如含ID、创建时间的父表)new(common.DataTable)   // 通用数据表(示例))if err != nil {log.Errorf("表结构同步失败:%v", err)return}log.Info("表结构同步完成")
}

9、组合以上函数

// 整合所有步骤:完整初始化流程
func InitDB() {// 步骤1:检查配置if global.AppConfig == nil || global.AppConfig.DB == nil {log.Fatal("数据库初始化失败:配置未就绪")}// 步骤2:构建DSNdsn := buildDSN()// 步骤3:创建引擎engine, err := createDBEngine(dsn)if err != nil {return}// 步骤4:配置日志configDBLog(engine)// 步骤5:配置时区configDBTimezone(engine)// 步骤6:配置连接池configDBPool(engine)// 步骤7:(可选)表结构同步(根据场景启用)// syncDBTable(engine)// 步骤8:保存引擎到全局变量(通用全局存储,无业务属性)global.DBEngine = enginelog.Info("数据库初始化完成,引擎已就绪")
}

三、服务层依赖注入

依赖注入(Dependency Injection,DI)是解耦代码、提升可测试性与可维护性的核心设计模式
直接看不懂也没关系,多看几次就会了

1、服务容器:依赖管理的"中央枢纽"

service/container.go

// ServiceContainer 通用服务容器结构体
type ServiceContainer struct {// BaseServiceSupplier:持有所有基础服务的供应商BaseServiceSupplier service.BaseSupplier
}// GlobalContainer 全局服务容器实例:提供全应用服务访问入口
var GlobalContainer = new(ServiceContainer)

2、服务供应商接口(定契约)

通过接口规范服务的获取方式(Getter 模式),确保访问服务的一致性。同时隔离服务定义与实现

文件:service/supplier.go

// BaseSupplier 通用服务供应商接口(剔除业务专属服务名)
type BaseSupplier interface {// 通用业务服务:替换具体业务服务GetUserService() *UserServiceGetOrderService() *OrderServiceGetLogService() *LogServiceGetCacheService() *CacheServiceGetStatService() *StatService// 可扩展:根据通用场景增加其他基础服务...
}

啥是Getter模式呢?

Getter方法的核心价值:封装的字段统一小写,仅通过方法对外暴露访问能力(正如下方所示:

3、服务供应商实现(持有服务实例)

// baseSupplier 通用服务供应商的具体实现
// 所有服务字段为「小写非导出」,仅通过Getter方法暴露
type baseSupplier struct {// 字段小写(非导出),外部无法直接访问/修改userService   *UserServiceorderService  *OrderServicelogService    *LogServicecacheService  *CacheServicestatService   *StatService
}
// 通过大写Getter方法(符合Go接口规范)对外暴露服务
// GetUserService 实现BaseSupplier接口的Getter方法,返回用户服务实例
func (s *baseSupplier) GetUserService() *UserService {    ...return s.userService
}// GetOrderService 实现BaseSupplier接口的Getter方法,返回订单服务实例
func (s *baseSupplier) GetOrderService() *OrderService {...return s.orderService
}
...

4、服务实例化(工厂模式)

通过SetUp函数(工厂模式)统一初始化所有服务,集中管理服务的创建逻辑,便于后续拓展

// SetUp 通用服务初始化函数:创建并返回服务供应商实例
func SetUp() BaseSupplier {// 1. 用构造函数初始化服务(非导出字段只能在当前包内赋值)userService := NewUserService()orderService := NewOrderService()logService := NewLogService()cacheService := NewCacheService()statService := NewStatService()// 2. 给baseSupplier的非导出字段赋值(当前包内可访问)return &baseSupplier{userService:   userService,orderService:  orderService,logService:    logService,cacheService:  cacheService,statService:   statService,}
}
// 当然这些还可以在精妙些

5、全局注册:将服务注入容器

在注册阶段,将初始化好的服务供应商注入全局容器。

如下:

func main() {...// 1. 初始化服务供应商baseSupplier := service.SetUp()// 2. 将供应商注入全局服务容器service.GlobalContainer = &service.ServiceContainer{BaseServiceSupplier: baseSupplier,}// 后续:启动服务器、初始化其他组件......}

注册阶段:在main函数中注册所有服务
解析阶段:在需要时通过 GlobalContainer 获取所有服务
生命周期管理:所有服务都是单例(“准单例”),由容器管理

在我看来,单例模式,有两个要点。
1、整个程序中,只存在唯一实例(本项目据中,通过get...方法获取的服务实例,都是已存在全局变量中的,指向同一个内存)。
2、提供全局访问点,来获取实例。

四、控制器层依赖注入

服务器通过 “接收服务容器” 的方式获取所需服务,避免了直接创建服务实例,降低了耦合。

// ControllerSupplier 通用控制器服务供应商(无业务属性)
type ControllerSupplier struct {UserApi  *UserApi  // 控制器实例OrderApi *OrderApi // 控制器实例
}// SetUp 控制器层服务注入:从全局容器获取服务并绑定到控制器
func SetUp(container *service.ServiceContainer) *ControllerSupplier {return &ControllerSupplier{// 为UserApi注入UserService依赖UserApi: &UserApi{UserService: container.BaseServiceSupplier.GetUserService(),},// 为OrderApi注入OrderService依赖OrderApi: &OrderApi{OrderService: container.BaseServiceSupplier.GetOrderService(),},}
}

通过这种方式,可以很轻松的实现DI依赖注入。
控制器不在主动依赖new,而是通过容器来被动接收依赖。
因此就更容易维护,mock也更方便。

五、路由初始化

1、初始化路由

在最初router.Init()会创建Gin引擎实例,并完成路由配置,最终传递给HTTP服务器启动

func main() {....   // 1. 初始化路由:获取配置完成的Gin引擎ginEngine := router.Init()// 2. 基于路由引擎创建HTTP服务器并启动httpServer := server.GetServerInstance(ginEngine)httpServer.Start()....
}

2、顶层路由容器(统一管理路由)

文件:router/router.go

// Routers 顶层路由容器:聚合所有功能模块的路由组
type Routers struct {HealthRouterGroup  health.RouterGroup  // 健康检查模块路由组BusinessRouterGroup business.RouterGroup // 核心业务模块路由组
}// GlobalRouters 全局路由容器实例:提供模块路由访问入口
var GlobalRouters = new(Routers)

3、模块路由组

健康检查:

// RouterGroup 健康检查模块路由组:仅包含健康检查相关路由逻辑
type RouterGroup struct {BaseHealthRouter // 基础健康检查路由(如存活检测、就绪检测)
}

核心业务:

// RouterGroup 核心业务模块路由组:聚合通用业务路由
type RouterGroup struct {UserRouter    // 用户相关路由(通用模块)OrderRouter   // 订单相关路由(通用模块)ResourceRouter// 资源相关路由(通用模块)LogRouter     // 日志相关路由(通用模块)
}

4、router.Init() 核心实现

router.Init() 是路由初始化的核心函数,负责如下四个责任:
1、创建Gin引擎
2、注册中间件
3、划分路由
4、绑定接口

(伪代码:)

// Init 通用路由初始化函数:返回配置完成的Gin引擎
func Init() *gin.Engine {// 3.1 步骤1:创建Gin引擎 + 注册基础中间件// - 新建Gin引擎(默认模式,可根据环境切换debug/release)ginEngine := gin.New()// 注册通用中间件:跨域、日志、异常恢复ginEngine.Use(middleware.Cors(), // 跨域中间件(适配前后端分离场景)// 日志中间件:跳过健康检查路径,避免日志冗余gin.LoggerWithConfig(gin.LoggerConfig{SkipPaths: []string{"/health/live", "/health/ready"},}),gin.Recovery(), // 异常恢复中间件:防止panic导致服务崩溃)// 3.2 步骤2:注册性能分析工具(通用调试能力)pprof.Register(ginEngine) // 注册pprof,支持/debug/pprof路径分析性能// 3.3 步骤3:划分路由分组(按访问权限隔离)// (1)公共路由组:无需认证,如健康检查、公开注册接口publicGroup := ginEngine.Group(""){// 绑定健康检查路由(来自健康检查模块)healthRouter := GlobalRouters.HealthRouterGrouphealthRouter.InitBaseHealthRouter(publicGroup)// 绑定公共业务路由(如用户注册)businessRouter := GlobalRouters.BusinessRouterGroupbusinessRouter.InitPublicUserRouter(publicGroup)}// (2)认证路由组:需登录/权限校验,包含核心业务接口authGroup := ginEngine.Group("")// 注册认证中间件:拦截未登录请求,跳过健康检查路径authGroup.Use(middleware.AuthWithConfig(middleware.AuthConfig{SkipPaths: []string{"/health/live", "/health/ready"},})){// 绑定核心业务路由(来自业务模块)businessRouter := GlobalRouters.BusinessRouterGroupbusinessRouter.InitUserRouter(authGroup)    // 用户管理路由businessRouter.InitOrderRouter(authGroup)   // 订单管理路由businessRouter.InitResourceRouter(authGroup)// 资源管理路由businessRouter.InitLogRouter(authGroup)     // 日志查询路由}// 3.4 步骤4:注册文档与静态资源路由// (1)API文档路由:集成Swagger,便于接口调试authGroup.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))// (2)静态资源路由:提供文件访问能力(如用户上传的图片)ginEngine.Static("/static/files", "./static/files")// 3.5 步骤5:配置通用参数验证器// 为Gin绑定自定义参数验证规则(如ID格式校验、时间格式校验)if validatorEngine, ok := binding.Validator.Engine().(*validator.Validate); ok {// 注册“ID格式验证”规则_ = validatorEngine.RegisterValidation("common_id_check", validator.CommonIDValidator)// 注册“时间格式验证”规则_ = validatorEngine.RegisterValidation("time_format_check", validator.TimeFormatValidator)}return ginEngine
}

5、路由组初始化(例)

每个模块的路由会通过独立的InitXxxRouter方法绑定到对应的路由组 "实现路由逻辑与容器解耦"

// UserRouter 用户模块路由:封装用户相关接口注册逻辑
type UserRouter struct{}// InitUserRouter 将用户接口绑定到认证路由组
func (ur *UserRouter) InitUserRouter(routerGroup *gin.RouterGroup) {// 1. 创建用户模块路由子分组(路径前缀:/user)userSubGroup := routerGroup.Group("user")// 2. 获取用户控制器实例(通过依赖注入容器)userApi := controller.GlobalApiContainer.BusinessApiGroup.GetUserApi()// 3. 绑定具体接口(HTTP方法+路径+控制器处理函数)userSubGroup.POST("/add", userApi.CreateUser)    // 创建用户userSubGroup.GET("/info", userApi.GetUserInfo)   // 获取用户信息userSubGroup.PUT("/update", userApi.UpdateUser)  // 更新用户信息userSubGroup.DELETE("/delete", userApi.DeleteUser)// 删除用户
}

六、定时任务的启动

在了解这个包之前,必须要了解github.com/robfig/cron/v3,这个包的用途。
拥有一定基础,才能更好的进行学习。

1、这里的定时任务是为了解决什么问题?

想象一下,你的系统需要定期做这样一件事:1、从 A 数据库(如分析型数据库)获取原始数据
2、经过筛选和转换后,存储到 B 数据库(如业务型数据库)
3、定期清理 B 数据库中过期的数据这个定时任务就像一个智能搬运工,按照设定的时间规律自动完成这些工作,无需人工干预。咱们这里可以假设:
这个搬运工:每5分钟去 A 数据库 检查一次,把重要的用户操作"搬运"到 B 数据库 中,方便后续查询和分析。

代码结构:

main.go (启动入口)
└── cron/data_sync_manager.go (定时任务管理器)└── service/sync_service.go (业务逻辑实现)

1、启动入口

// 初始化并启动数据同步定时任务
dataSyncManager := cron.NewDataSyncManager()
go func() {if err := dataSyncManager.Start(); err != nil {log.WithError(err).Error("定时任务启动失败")} else {log.Info("定时任务启动成功")}
}()

通过go,合理运用go语言的特性,高并发

2、管理器结构

type DataSyncManager struct {cronEngine          *cron.Cron       // 定时器引擎(闹钟)syncService         *SyncService     // 同步服务(实际干活的)lastSyncTime        time.Time        // 上次同步时间(工作记录)consecutiveFailures int              // 连续失败次数(错误跟踪)mutex               sync.RWMutex     // 读写锁(保护共享数据)
}

具体作用:

cronEngine:就像闹钟,到点提醒该工作了
syncService:就像具体干活的工人
lastSyncTime:就像工作日记,记录上次工作时间
mutex:就像日记本的锁,防止多人同时修改

3、初始化管理器

func NewDataSyncManager() *DataSyncManager {// 1. 确保目标数据库表存在if err := ensureTargetTableExists(); err != nil {log.WithError(err).Error("确保目标数据表存在失败")}return &DataSyncManager{// 2. 创建带日志的定时器cronEngine: cron.New(cron.WithLogger(cron.VerbosePrintfLogger(log.StandardLogger()))),// 3. 初始化同步服务syncService: NewSyncService(),// 4. 初始同步时间设为5分钟前lastSyncTime: time.Now().Add(-5 * time.Minute),}
}

4、定时任务设置

func (m *DataSyncManager) Start() error {// 1. 每5分钟执行一次数据同步_, err := m.cronEngine.AddFunc("*/5 * * * *", func() {m.syncDataWithRetry()})if err != nil {return err}// 2. 每月1号凌晨2点执行数据清理_, err = m.cronEngine.AddFunc("0 2 1 * *", func() {m.cleanExpiredData()})if err != nil {return err}m.cronEngine.Start() // 启动定时器return nil
}

初次接触是需要掌握这个的:

时间表达式入门:

  • */5 * * * *:每 5 分钟(* 表示任意值,/ 表示间隔)
  • 0 2 1 * *:每月 1 号凌晨 2 点(分 时 日 月 周)

常见表达式示例:

  • 0 * * * *:每小时整点
  • 0 0 * * *:每天凌晨
  • 0 0 * * 0:每周日凌晨

5、核心同步方法

func (m *DataSyncManager) syncDataWithRetry() {// 安全保护:捕获可能的程序异常defer func() {if r := recover(); r != nil {log.WithField("error", r).Error("同步任务发生异常")}}()// 读取上次同步时间(读操作加读锁)m.mutex.RLock()lastSync := m.lastSyncTimem.mutex.RUnlock()// 执行同步err := m.syncService.SyncData(lastSync)if err != nil {// 同步失败:更新失败计数(写操作加写锁)m.mutex.Lock()m.consecutiveFailures++m.mutex.Unlock()log.WithError(err).Error("数据同步失败")return}// 同步成功:更新状态m.mutex.Lock()m.consecutiveFailures = 0  // 重置失败计数// 时间向前推1分钟,避免遗漏边缘数据m.lastSyncTime = time.Now().Add(-1 * time.Minute)m.mutex.Unlock()
}

注意,一般同步时间,都需要向前推进1分钟,这是为了防止因网络延迟,而导致的时间差。
这样,此后每一次,都会刚好向前推进1分钟。

6、获取什么样的信息?

func (s *SyncService) querySourceData(startTime, endTime time.Time) ([]SourceData, error) {query := `     SELECT id, content, create_time, status, user_id, operation_typeFROM source_table WHERE create_time >= ? AND create_time < ?AND status = 200                -- 只同步成功的数据AND user_id != 'anonymous'      -- 排除匿名用户AND operation_type != 'view'    -- 排除仅查看的操作ORDER BY create_time ASCLIMIT 10000                        -- 限制单次处理数量`// 执行查询并返回结果...
}

查询优化技巧:

  • 时间范围过滤:只查需要的时间段
  • 状态过滤:只同步有效数据
  • 数量限制:防止一次性处理过多数据导致内存问题

7、定期清扫

func (m *DataSyncManager) cleanExpiredData() {// 安全保护defer func() {if r := recover(); r != nil {log.WithField("error", r).Error("数据清理任务发生异常")}}()log.Info("开始执行数据清理任务")startTime := time.Now()// 清理一年前的数据if err := m.syncService.CleanExpiredData(1); err != nil {log.WithError(err).Error("数据清理任务失败")return}log.WithField("耗时", time.Since(startTime)).Info("数据清理任务完成")
}// 服务层实现
func (s *SyncService) CleanExpiredData(expireYears int) error {expireTime := time.Now().AddDate(-expireYears, 0, 0)// 执行删除操作affectedRows, err := global.DB.Where("create_time < ?", expireTime).Delete(&BizDataTable{})log.Info("清理了", affectedRows, "条过期数据")return err
}

节省空间、提高性能、并且符合数据保留政策

8、优雅停止

func (m *DataSyncManager) Stop() {if m.cronEngine == nil {return}// 停止定时器并等待当前任务完成ctx := m.cronEngine.Stop()select {case <-ctx.Done():log.Info("定时任务已安全停止")case <-time.After(10 * time.Second):log.Warn("定时任务停止超时")}
}

就像下班前要把手头的工作做完再走,避免工作到一半导致数据混乱。

本篇博客,就先到这里,后续会继续出博客进行补充( •̀ ω •́ )✧


在最后我想特别的感谢,智超学长提供的学习机会,与凯龙学长的耐心指导 ^0^


http://www.xdnf.cn/news/1407979.html

相关文章:

  • shell的原理和Linux的指令效果演示
  • Rust 登堂 之 枚举和整数(八)
  • K8s学习笔记(一)——
  • 试试 Xget 加速 GitHub 克隆仓库
  • React前端开发_Day12_极客园移动端项目
  • Windows中如何将Docker安装在E盘并将Docker的镜像和容器存储在E盘的安装目录下
  • IDM(Internet Download Managerv 6.38)破除解版下载!IDM 下载器永久免费版!提升下载速度达5倍!安装及使用
  • Google 的 Agent2Agent 协议 (A2A):带示例的指南
  • Java试题-选择题(26)
  • Swin Transformer基本原理与传统Transformer对比图解
  • Lua基础知识精炼
  • vim-plugin AI插件
  • 运筹说 第141期 | 启发式算法:用简单规则、破解复杂问题
  • 网络端口与服务对应表 - 白帽子安全参考指南
  • C#基础(③CMD进程)
  • LLM记账智能体-MCP服务-实现步骤与效果展示
  • @Value注解的底层原理(一)
  • (一) aws上微服务
  • C++ 快速复习指南(上半部分)
  • 我开发了一个自动还原源码的小工具
  • AI辅助编程日记和chat历史开源Series 1:VSCode + GitHub Copilot 自动下载及安装软件
  • 《打破 “慢“ 的黑箱:前端请求全链路耗时统计方案》
  • Vue3 响应式基础
  • 前端学习——JavaScript基础
  • 创维LB2004_安装软件教程
  • 37. 解数独
  • GaRe:面向非约束户外照片集的可重光照 3D 高斯溅射技术简要解析
  • Android开发-活动页面
  • C# .Net8 WinFormsApp使用日志Serilog组件
  • c++ Effective c++ 条款5