MySQL适合高并发简单写入场景,PostgreSQL更适配复杂查询、强事务及多模数据需求;选型需匹配服务角色、读写特征与运维约束,而非仅看语法。

Go微服务里数据持久化不是“选个驱动连上就行”,而是得根据服务角色、读写特征、事务边界和运维约束来定方案。直接用database/sql裸连 PostgreSQL 或 MySQL 可能跑得通,但上线后大概率踩连接池泄漏、事务不一致、或测试环境无法复现的脏读问题。
MySQL vs PostgreSQL:选型不是看语法,是看事务模型和扩展性
MySQL 在高并发简单写入(如日志、会话)场景下响应快,但它的默认隔离级别是 REPEATABLE READ,在微服务多点更新同一行时容易出现幻读,且 JSON 字段索引能力弱;PostgreSQL 默认 READ COMMITTED 更符合多数业务直觉,支持行级锁、部分索引、物化视图,对复杂查询和事务编排更友好。
- 用 MySQL:适合用户注册、短信验证码等强一致性+低延迟写入,但必须显式加
SELECT ... FOR UPDATE控制并发修改 - 用 PostgreSQL:适合订单、库存、审计日志等需要复杂条件查询、JSONB 存储、或跨表事务的场景,
pgx驱动比lib/pq性能高 20%+,推荐直接用 - 别混用:一个微服务只应绑定一种主库。混合使用会让事务管理、监控指标、备份策略全部失效
连接池配置:不设 SetMaxOpenConns 就等于没配
sql.Open() 只初始化连接池,不建真实连接;真正出问题是在压测或流量突增时,连接数飙升到数据库 max_connections 上限,导致新请求卡在 PING 等待,整个服务雪崩。
-
SetMaxOpenConns(25):必须设,建议值 = 数据库单节点max_connections× 0.3(留余量给其他服务) -
SetMaxIdleConns(25):设成和MaxOpenConns相同,避免空闲连接被频繁销毁重建 -
SetConnMaxLifetime(5 * time.Minute):强制回收长连接,防止数据库端因网络抖动或防火墙超时断连后,Go 客户端仍尝试复用已失效连接 - 漏掉
db.Ping():初始化后不校验连通性,会导致第一个请求失败,错误堆栈还藏在底层,排查成本极高
事务控制:别在 handler 层 start transaction
微服务里事务常被误用成“把所有 DB 操作包进一个 tx”,结果是锁持有时间过长、死锁频发、或回滚范围过大。正确的粒度是按业务用例切分:比如“创建订单 + 扣减库存”必须原子,但“记录操作日志”可异步落库。
- 事务起始点应在 service 层,而非 HTTP handler —— handler 只负责解析参数、调用 service、返回响应
- 用
context.WithValue(ctx, txKey, tx)向下传递事务对象,避免每个 DAO 方法都带*sql.Tx参数 - 避免嵌套事务:PostgreSQL 不支持 savepoint 外的嵌套
BEGIN,MySQL 的START TRANSACTION嵌套实际是 ignore,易造成逻辑错乱 - 写完立刻
tx.Commit()或tx.Rollback(),别依赖 defer —— defer 在 panic 时才触发,正常 return 时不会执行
测试时用 Docker 启真实 DB,而不是 mock
用 github.com/DATA-DOG/go-sqlmock 测试 DAO 层,只能验证 SQL 字符串拼写,完全无法发现外键约束失败、索引缺失导致的慢查询、或事务隔离级别引发的竞态——这些只有真实数据库才能暴露。
- 单元测试启动容器:用
testcontainers-go在测试前拉起临时 PostgreSQL 实例,测试完自动销毁 - SQL 迁移脚本必须纳入测试流程:
goose up或migrate up执行后,再跑 DAO 测试,否则 schema 和代码不同步 - 禁止在测试里硬编码
localhost:5432—— CI 环境数据库地址是动态分配的,要用container.GetPort()获取真实端口 - 测试完清空数据用
TRUNCATE TABLE ... RESTART IDENTITY CASCADE,别用DELETE,否则自增 ID 会越积越大
最常被跳过的环节是连接池监控和事务链路追踪:不采集 db.Stats() 中的 InUse 和 Idle,就等于在生产环境闭着眼开车;不给每个 DB 调用打上 trace ID,一旦出问题,根本分不清是哪个服务、哪个接口、哪条 SQL 拖垮了整个链路。


















