面试题答案
一键面试1. 锁粒度控制
- 细化锁范围:
- 分析代码逻辑,确定哪些数据需要保护,将大的锁保护范围拆分成多个小的锁。例如,原本一个锁保护整个数据结构,可根据不同操作类型或数据子集,使用多个锁分别保护。比如在一个电商系统中,库存和订单数据原本由同一把锁保护,可将库存操作和订单操作分别用不同锁,降低锁竞争。
- 对于经常变动的数据和不常变动的数据,分开加锁。如系统配置信息不常变,业务数据常变,对它们使用不同锁,减少业务操作时锁等待。
- 避免不必要的锁:
- 检查代码中是否存在对只读数据也加锁的情况,对于只读操作,无需加锁。例如在一个新闻展示系统中,新闻内容一经发布很少修改,读取新闻时无需加锁。
- 对于一些非共享资源的操作,无需使用锁。如每个goroutine内部的局部变量操作,不必加锁。
2. 锁类型选择
- 读写锁(sync.RWMutex):
- 当系统中读操作远多于写操作时,使用读写锁能显著提升性能。读操作时,多个goroutine可同时获取读锁进行读取,只有写操作才需要独占写锁。比如在一个文章阅读量统计系统中,大量用户读取文章阅读量(读操作),而只有在用户阅读行为发生时才更新阅读量(写操作),此时适合使用读写锁。
- 注意在写操作前,要先获取写锁,防止读操作正在进行时写操作执行导致数据不一致。写操作完成后,及时释放写锁。
- 信号量(sync.Semaphore):
- 可以控制同时访问共享资源的goroutine数量。比如系统连接数据库的最大连接数有限,使用信号量来限制同时访问数据库的goroutine数量,避免过多连接导致数据库性能下降。
- 在需要限制资源使用数量的场景下,信号量比Mutex锁更灵活,可根据实际需求动态调整允许的并发数。
3. goroutine调度优化
- 合理分配goroutine数量:
- 根据系统资源(如CPU核心数、内存大小等)和业务负载,合理设置goroutine数量。如果goroutine数量过多,会导致上下文切换频繁,消耗系统资源。例如在一个计算密集型任务中,goroutine数量可设置为与CPU核心数相近。
- 使用工作池(worker pool)模式,预先创建一定数量的goroutine,将任务分配到这些goroutine中执行,避免频繁创建和销毁goroutine带来的开销。
- 优先级调度:
- 根据业务需求,为不同类型的任务设置优先级。例如在一个实时通信系统中,消息推送任务优先级高于系统配置更新任务。通过优先级调度,优先执行重要任务,减少重要任务的等待时间,提升系统整体响应性能。
- 可以使用第三方库如
go - scheduler
来实现更灵活的优先级调度。
4. 死锁检测与预防
- 死锁检测工具:
- 使用Go内置的
runtime/debug
包中的Stack
函数,在程序运行时捕获死锁。当程序出现死锁时,通过调用Stack
函数获取当前goroutine的堆栈信息,分析死锁原因。 - 借助
go tool trace
工具,它可以分析程序的运行轨迹,包括goroutine的创建、调度、同步等信息,帮助定位死锁发生的位置和原因。
- 使用Go内置的
- 死锁预防策略:
- 按照固定顺序获取锁,避免循环依赖。例如,系统中有锁A、锁B、锁C,所有goroutine都按照A -> B -> C的顺序获取锁,可防止死锁。
- 使用
context
包来控制goroutine的生命周期和取消操作。在获取锁超时的情况下,及时取消goroutine的操作,避免无限等待导致死锁。例如,使用context.WithTimeout
函数设置获取锁的超时时间。