Skip to content

Go 后端面试复盘:语言、并发与服务实践 · 19. database/sql、事务与连接池至20. Redis 高频知识 ​

返回分篇目录

前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。

19. database/sql、事务与连接池 ​

19.1 sql.DB 是单个数据库连接吗? ​

不是。sql.DB 是并发安全的数据库句柄和连接池管理器。它会按需建立、复用和回收底层连接,应长期复用,而不是每次请求 Open/Close。

19.2 sql.Open 会立即连接数据库吗? ​

通常不会,它主要初始化句柄。需要验证连接时调用 PingContext:

go
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? ​

go
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:

go
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 中事务怎么写? ​

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 注入? ​

使用参数化查询:

go
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 模式是什么? ​

读:

  1. 查缓存。
  2. 未命中查数据库。
  3. 回填缓存。

写:

  1. 更新数据库。
  2. 删除缓存。

通常选择删除而不是直接更新缓存,减少并发覆盖和无效更新。但仍存在短暂不一致,需要根据业务采用延迟双删、消息通知、版本号、订阅 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 规模。

继续阅读