公司动态

Golang项目中Redis与MySQL的高效结合实践

📅 2026/7/22 10:41:34
Golang项目中Redis与MySQL的高效结合实践
1. 为什么要在Golang项目中使用Redis和MySQL在构建现代Web应用时数据存储通常需要同时考虑持久化和缓存两个层面。MySQL作为成熟的关系型数据库擅长处理结构化数据的持久化存储和复杂查询而Redis作为内存数据库则能提供亚毫秒级的读写性能特别适合用作缓存层。我最近用Golang重构了一个电商平台的库存服务实测在引入Redis缓存后商品详情的查询响应时间从平均120ms降到了8ms。这种性能提升在流量高峰期尤为明显数据库负载降低了约65%。下面分享具体实现方案和踩过的坑。2. 环境准备与依赖安装2.1 开发环境配置建议使用Go 1.20版本其对database/sql和context的支持更完善。安装必要的驱动go get github.com/go-sql-driver/mysql go get github.com/redis/go-redis/v9MySQL推荐5.7版本Redis建议6.2版本。生产环境务必配置持久化# redis.conf关键配置 maxmemory 2gb maxmemory-policy allkeys-lru appendonly yes2.2 数据库连接池配置Golang的标准库database/sql自带连接池但需要合理设置参数db, err : sql.Open(mysql, user:passtcp(127.0.0.1:3306)/dbname?parseTimetrue) db.SetMaxOpenConns(25) db.SetMaxIdleConns(10) db.SetConnMaxLifetime(5 * time.Minute)Redis客户端同样需要调优rdb : redis.NewClient(redis.Options{ Addr: localhost:6379, Password: , DB: 0, PoolSize: 100, })3. 缓存策略设计与实现3.1 缓存穿透防护方案当查询不存在的数据时会导致请求直接打到数据库。我们采用布隆过滤器空值缓存的双重防护func GetProduct(id string) (Product, error) { // 先检查布隆过滤器 if !bloomFilter.Test(id) { return Product{}, ErrNotFound } // 尝试从Redis获取 var p Product err : rdb.Get(ctx, product:id).Scan(p) if err redis.Nil { // 查数据库 p, err fetchFromDB(id) if errors.Is(err, sql.ErrNoRows) { // 缓存空值5分钟 rdb.Set(ctx, product:id, , 5*time.Minute) return Product{}, ErrNotFound } // 正常缓存 rdb.Set(ctx, product:id, p, 30*time.Minute) } return p, nil }3.2 缓存雪崩应对措施大量缓存同时失效会导致数据库瞬时压力激增。我们的解决方案设置随机过期时间基础30分钟±5分钟随机采用多级缓存架构实现热点数据永不过期后台更新expire : 30*time.Minute time.Duration(rand.Intn(600))*time.Second rdb.Set(ctx, key, value, expire)4. 事务与数据一致性4.1 双写一致性保障更新数据时需要同时操作MySQL和Redis我们采用先更新数据库再删除缓存的策略func UpdateProduct(p Product) error { // 开启事务 tx, err : db.BeginTx(ctx, nil) if err ! nil { return err } // 更新数据库 if _, err : tx.Exec(UPDATE products SET ... WHERE id?, p.ID); err ! nil { tx.Rollback() return err } // 提交事务 if err : tx.Commit(); err ! nil { return err } // 删除缓存 if err : rdb.Del(ctx, product:p.ID).Err(); err ! nil { log.Printf(缓存删除失败: %v, err) } return nil }4.2 分布式锁实现在秒杀场景下需要使用分布式锁防止超卖func AcquireLock(key string, ttl time.Duration) (bool, string) { token : uuid.New().String() ok, err : rdb.SetNX(ctx, lock:key, token, ttl).Result() if err ! nil { return false, } return ok, token } func ReleaseLock(key, token string) { script : if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end rdb.Eval(ctx, script, []string{lock:key}, token) }5. 性能优化实战技巧5.1 批量查询优化对于商品列表页这种需要查询多条数据的场景使用Redis的Pipeline批量操作func BatchGetProducts(ids []string) (map[string]Product, error) { pipe : rdb.Pipeline() cmds : make(map[string]*redis.StringCmd) for _, id : range ids { cmds[id] pipe.Get(ctx, product:id) } if _, err : pipe.Exec(ctx); err ! nil err ! redis.Nil { return nil, err } results : make(map[string]Product) for id, cmd : range cmds { var p Product if err : cmd.Scan(p); err nil { results[id] p } } return results, nil }5.2 连接池监控指标通过Prometheus监控关键指标// MySQL指标 db.SetConnMaxIdleTime(30 * time.Minute) db.Stats() // 返回连接池状态 // Redis指标 rdb.PoolStats() // 获取连接池统计信息建议告警阈值连接等待数 10空闲连接数 5查询耗时P99 200ms6. 常见问题排查指南6.1 连接泄漏排查现象应用运行一段时间后出现too many connections错误。排查步骤检查是否每次查询后都正确关闭rowsrows, err : db.Query(...) defer rows.Close() // 必须要有监控db.Stats().InUse变化使用SHOW PROCESSLIST查看MySQL连接状态6.2 缓存命中率低优化方案检查缓存键设计是否合理避免过于动态的键调整缓存过期策略增加本地缓存作为二级缓存type LocalCache struct { sync.RWMutex items map[string]Item ttl time.Duration }6.3 Redis内存暴涨处理方法检查是否有大keyredis-cli --bigkeys设置合理的maxmemory-policy对hash等数据结构进行分片存储7. 生产环境部署建议7.1 MySQL配置调优关键参数调整[mysqld] innodb_buffer_pool_size 4G # 总内存的50-70% innodb_log_file_size 256M max_connections 200 wait_timeout 3007.2 Redis高可用方案推荐使用Redis Sentinelrdb : redis.NewFailoverClient(redis.FailoverOptions{ MasterName: mymaster, SentinelAddrs: []string{sentinel1:26379, sentinel2:26379}, })7.3 连接池大小计算公式连接数 (核心数 * 2) 磁盘数例如4核服务器带SSDMySQL连接池 (4 * 2) 1 9 → 设置为10-15Redis连接池可以设置更大些50-100