小组放了十天假,闲在家里也怪无聊,正好可以读读学长写的项目代码,让我瞧一瞧学长的风姿(~ ̄▽ ̄)~
思来想去,暂时还是读 河南师范大学附属中学图书馆 这个项目 ,更贴近我此刻的水准,让我舒服点。
目录
注: 为防止他人公司的权益被侵犯,本篇博客将会以自己学习总结的知识为基准,并借助伪代码进行通用的逻辑展示。
如果排除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^