Go 后端面试复盘:语言、并发与服务实践 · 19. database/sql、事务与连接池至20. Redis 高频知识
前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。
19. database/sql、事务与连接池
19.1 sql.DB 是单个数据库连接吗?
不是。sql.DB 是并发安全的数据库句柄和连接池管理器。它会按需建立、复用和回收底层连接,应长期复用,而不是每次请求 Open/Close。
19.2 sql.Open 会立即连接数据库吗?
通常不会,它主要初始化句柄。需要验证连接时调用 PingContext:
db, err := sql.Open(driver, dsn)
if err != nil {
return err
}
if err := db.PingContext(ctx); err != nil {
db.Close()
return err
}19.3 连接池有哪些重要参数?
SetMaxOpenConns:最大打开连接数。SetMaxIdleConns:最大空闲连接数。SetConnMaxLifetime:连接最大生命周期。SetConnMaxIdleTime:连接最大空闲时间。
调优要结合:
- 数据库可承受连接数。
- 服务副本数量。
- 查询延迟。
- 峰值并发。
- 数据库或代理的连接回收策略。
最大连接数不是越大越好。过多连接会增加数据库上下文切换、锁竞争和内存。
19.4 为什么要关闭 Rows?
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
// Scan
}
if err := rows.Err(); err != nil {
return err
}不关闭可能长期占用连接。循环结束后还必须检查 rows.Err(),因为迭代过程可能发生网络或扫描错误。
19.5 QueryRowContext 的错误在哪里返回?
错误通常延迟到 Scan:
err := db.QueryRowContext(ctx, q, id).Scan(&name)
if errors.Is(err, sql.ErrNoRows) {
// not found
}19.6 事务的 ACID 是什么?
- Atomicity:原子性,全部成功或全部回滚。
- Consistency:一致性,事务前后满足约束。
- Isolation:隔离性,并发事务之间的可见性规则。
- Durability:持久性,提交后的结果在规定故障模型下持久保存。
19.7 常见事务隔离级别是什么?
| 隔离级别 | 可能防止/允许的问题 |
|---|---|
| Read Uncommitted | 可能脏读、不可重复读、幻读 |
| Read Committed | 防脏读,仍可能不可重复读、幻读 |
| Repeatable Read | 同一事务重复读通常一致;具体幻读行为看数据库实现 |
| Serializable | 最强隔离,吞吐和冲突成本更高 |
实际行为必须结合 MySQL InnoDB、PostgreSQL 等具体实现,不能只背 SQL 标准。
19.8 Go 中事务怎么写?
func transfer(ctx context.Context, db *sql.DB) (err error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
if _, err = tx.ExecContext(ctx, "..."); err != nil {
return err
}
if _, err = tx.ExecContext(ctx, "..."); err != nil {
return err
}
err = tx.Commit()
return err
}注意:
- 事务内所有操作必须使用
tx,不能误用db。 - Commit 也可能失败,必须检查。
- 事务尽量短,不要在事务内做远程调用。大事务持锁久、undo/日志多、冲突高,失败重试成本也高;缩短事务时仍要保持原子性边界。
- 超时和重试要区分错误类型。
- Rollback 在 Commit 成功后返回已完成错误通常可忽略。
19.9 如何防止 SQL 注入?
使用参数化查询:
db.QueryContext(ctx, "SELECT * FROM users WHERE id = ?", id)占位符随数据库驱动不同。
参数化只能替代值,不能直接替代表名、列名、排序方向。动态标识符必须使用白名单映射,不能拼接用户输入。
19.10 什么是 N+1 查询?
先查一批主记录,再为每条记录单独查关联数据,导致 1+N 次数据库往返。
解决:
- JOIN。
- 批量
IN查询。 - DataLoader/批量加载。
- 合理预加载。
- 缓存。
选择时考虑返回数据膨胀、索引和业务一致性。
19.11 如何实现幂等写入?
常见方案:
- 唯一业务键 + 唯一索引。
- 幂等请求表。
INSERT ... ON CONFLICT/ON DUPLICATE KEY。- 状态机条件更新。
- 去重 token。
应用层先查询再插入不能独立保证并发安全,最终应由数据库唯一约束兜底。
19.12 SQL 优化如何验证?
先保证结果等价,再看执行计划和同条件测量。IN、EXISTS、子查询和 JOIN 没有通用的速度排名;优化器、索引、数据分布和返回行数都会改变结果。
- 只返回需要的字段,减少传输与不必要读取。
id IN (2, 3)可表达两个离散值;改成范围必须确认不会额外包含不需要的值。OR改成UNION会带来去重,UNION ALL又可能产生重复行,不能当作机械替换。LIKE '%li%'与LIKE 'li%'的匹配语义不同,不能为了可能利用索引改变需求。- 用实际数据库版本的
EXPLAIN分析访问路径,结合扫描行数、耗时和真实数据量复测。
完整 SQL、索引、事务隔离和日志机制见 MySQL 专题。Go 调用层仍需检查查询与迭代错误,并保持事务边界。
20. Redis 高频知识
20.1 Redis 为什么快?
- 数据主要在内存。
- 核心命令处理模型避免大量共享数据加锁。
- 高效数据结构。
- 网络事件循环和 I/O 多路复用。
- 协议相对简单。
“单线程”不能简单理解为 Redis 整个进程只有一个线程;不同版本和功能可能使用后台线程或 I/O 线程。关键是核心命令执行具有串行化特征。
20.2 Redis 常见数据结构有哪些?
- String:缓存、计数器、分布式锁值。
- Hash:对象字段。
- List:简单队列。
- Set:去重集合、共同关系。
- Sorted Set:排行榜、延迟任务索引。
- Stream:消息流。
- Bitmap:签到、状态位。
- HyperLogLog:近似基数统计。
- GEO:地理位置。
20.3 什么是缓存穿透、击穿和雪崩?
缓存穿透:
- 查询根本不存在的数据,每次都打到数据库。
- 方案:缓存空值、布隆过滤器、参数校验。
缓存击穿:
- 某个热点 key 过期,大量请求同时打到数据库。
- 方案:互斥重建、逻辑过期、热点永不过期加主动更新、请求合并。
缓存雪崩:
- 大量 key 同时过期或 Redis 整体不可用。
- 方案:过期时间加随机抖动、多级缓存、高可用、限流降级。
20.4 Cache Aside 模式是什么?
读:
- 查缓存。
- 未命中查数据库。
- 回填缓存。
写:
- 更新数据库。
- 删除缓存。
通常选择删除而不是直接更新缓存,减少并发覆盖和无效更新。但仍存在短暂不一致,需要根据业务采用延迟双删、消息通知、版本号、订阅 binlog 等策略。
20.5 Redis 分布式锁需要注意什么?
基本要点:
SET key value NX PX ttl原子加锁。- value 必须是请求唯一 token。
- 解锁用 Lua 脚本“比较 token 后删除”,避免删掉他人的锁。
- TTL 防止持有者崩溃后永久死锁。
- 任务可能超过 TTL,需要续期或业务 fencing token。
分布式锁不能自动保证业务绝对安全:
- GC 停顿、网络分区、进程暂停可能导致锁过期后旧持有者继续执行。
- 对强一致资源,最好使用数据库约束、事务、租约加 fencing token 或一致性系统。
20.6 Redis Pipeline 和事务有什么区别?
- Pipeline:批量发送命令,减少网络往返,不提供整体原子性。
- MULTI/EXEC:命令按顺序执行,中间不会插入其他客户端命令,但不等同于传统数据库事务,也没有自动回滚。
- Lua:脚本内命令整体原子执行,但长脚本会阻塞其他命令。
20.7 Redis 持久化方式有哪些?
- RDB:周期性快照,文件紧凑、恢复快,但可能丢失最近一段数据。
- AOF:记录写命令,数据丢失窗口通常更小,文件更大,需要重写。
- 混合持久化:结合快照和增量日志的特点。
持久化配置要结合数据可丢失程度、恢复时间目标和磁盘性能。
20.8 热 key 和大 key 有什么危害?
热 key:
- 单节点 CPU/带宽集中。
- 可能击穿下游。
大 key:
- 单次操作阻塞时间长。
- 网络传输大。
- 删除和迁移成本高。
- 集群槽迁移受影响。
治理:
- 拆分 key。
- 本地缓存。
- 读副本或多副本方案。
- 限制集合大小。
- 渐进删除。
- 监控命令延迟、流量和 key 规模。
继续阅读
- 排错
- 排错
- 排错