小组放了十天假,闲在家里也怪无聊,正好可以读读学长写的项目代码,让我瞧一瞧学长的风姿(~ ̄▽ ̄)~
思来想去,暂时还是读 河南师范大学附属中学图书馆 这个项目 ,更贴近我此刻的水准,让我舒服点。
目录
注: 为防止他人公司的权益被侵犯,本篇博客将会以自己学习总结的知识为基准,并借助伪代码进行通用的逻辑展示。
如果排除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: 8080 monitorPort: 9090 log: level: info output: 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创建引擎,传入驱动与DSN engine, err := xorm.NewEngine(global.AppConfig.DB.Driver, dsn) if err != nil { // 日志仅提示错误,不暴露完整DSN log.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.Level switch 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.Second engine.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:构建DSN dsn := 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 = engine log.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() *UserService GetOrderService() *OrderService GetLogService() *LogService GetCacheService() *CacheService GetStatService() *StatService // 可扩展:根据通用场景增加其他基础服务 ... }
啥是Getter模式呢?
Getter方法的核心价值:封装的字段统一小写,仅通过方法对外暴露访问能力(正如下方所示:
3、服务供应商实现(持有服务实例)
// baseSupplier 通用服务供应商的具体实现 // 所有服务字段为「小写非导出」,仅通过Getter方法暴露 type baseSupplier struct { // 字段小写(非导出),外部无法直接访问/修改 userService *UserService orderService *OrderService logService *LogService cacheService *CacheService statService *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.HealthRouterGroup healthRouter.InitBaseHealthRouter(publicGroup) // 绑定公共业务路由(如用户注册) businessRouter := GlobalRouters.BusinessRouterGroup businessRouter.InitPublicUserRouter(publicGroup) } // (2)认证路由组:需登录/权限校验,包含核心业务接口 authGroup := ginEngine.Group("") // 注册认证中间件:拦截未登录请求,跳过健康检查路径 authGroup.Use(middleware.AuthWithConfig(middleware.AuthConfig{ SkipPaths: []string{"/health/live", "/health/ready"}, })) { // 绑定核心业务路由(来自业务模块) businessRouter := GlobalRouters.BusinessRouterGroup businessRouter.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.lastSyncTime m.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_type FROM source_table WHERE create_time >= ? AND create_time < ? AND status = 200 -- 只同步成功的数据 AND user_id != 'anonymous' -- 排除匿名用户 AND operation_type != 'view' -- 排除仅查看的操作 ORDER BY create_time ASC LIMIT 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^