提问背景 Question:
访问 baike/sitemap,好像死循环一下,其它页面也都打不开了。这怎么回事
// ////////////////////////////////////////////////////////////////////////////////
// sitemap实现
// ////////////////////////////////////////////////////////////////////////////////
// CategoryGroup 对应 sitemap 模板中的分类节点
type CategoryGroup struct {
CategoryName string
Articles []Article
}
// BaikeSitemapData 对应 sitemap 模板的渲染数据
type BaikeSitemapData struct {
CategoryGroups []CategoryGroup
HasPagination bool
CurrentPage int
TotalPages int
PageNums []int
}
// 3. 百科网站地图/目录树 Handler
// 路由配置:r.GET("/baike/sitemap", ai.Baike_sitemap)
func Baike_sitemap(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
query := r.URL.Query()
pageStr := query.Get("page")
currentPage := 1
if pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
currentPage = p
}
}
// 定义分页配置:每页显示 10 个分类标签
pageSize := 10
offset := (currentPage - 1) * pageSize
// 1. 查询有文章的标签总数(用于分页计算)
var totalTags int
err := fun.DB.QueryRow("SELECT COUNT(1) FROM cms_tag WHERE count > 0").Scan(&totalTags)
if err != nil {
http.Error(w, "计算标签失败: "+err.Error(), http.StatusInternalServerError)
return
}
// 计算总页数
totalPages := (totalTags + pageSize - 1) / pageSize
if totalPages < 1 {
totalPages = 1
}
if currentPage > totalPages {
currentPage = totalPages
offset = (currentPage - 1) * pageSize
}
// 2. 分页获取热门/非空标签列表
tagRows, err := fun.DB.Query("SELECT id, name, count FROM cms_tag WHERE count > 0 ORDER BY count DESC, id DESC LIMIT ? OFFSET ?", pageSize, offset)
if err != nil {
http.Error(w, "查询标签失败: "+err.Error(), http.StatusInternalServerError)
return
}
defer tagRows.Close()
var groups []CategoryGroup
// 3. 循环获取每个标签分类下的高权重公开文章 (每组显示最新 30 篇)
for tagRows.Next() {
var t Tag
if err := tagRows.Scan(&t.ID, &t.Name, &t.Count); err == nil {
var articles []Article
// 查询关联文章列表,必须满足 status = 1 正常状态
articleRows, err := fun.DB.Query(`
SELECT a.id, a.title
FROM cms_article a
INNER JOIN cms_article_tag at ON a.id = at.article_id
WHERE a.status = 1 AND at.tag_id = ?
ORDER BY a.id DESC LIMIT 30`,
t.ID,
)
if err == nil {
for articleRows.Next() {
var a Article
if err := articleRows.Scan(&a.ID, &a.Title); err == nil {
articles = append(articles, a)
}
}
articleRows.Close()
}
// 仅当分类下有文章才展示到目录树中
if len(articles) > 0 {
groups = append(groups, CategoryGroup{
CategoryName: t.Name,
Articles: articles,
})
}
}
}
// 生成分页页码数组
var pageNums []int
for i := 1; i <= totalPages; i++ {
pageNums = append(pageNums, i)
}
data := BaikeSitemapData{
CategoryGroups: groups,
HasPagination: totalPages > 1,
CurrentPage: currentPage,
TotalPages: totalPages,
PageNums: pageNums,
}
// 4. 读取模板文件并渲染
path := filepath.Join(fun.AppPath, "/templates/ai/sitemap.html")
mb, err := fun.TManager.Get(path)
if err != nil {
http.Error(w, "加载地图模板失败: "+err.Error(), http.StatusInternalServerError)
return
}
err = mb.Execute(w, data)
if err != nil {
fun.WriteLog("渲染百科地图页模板报错: %s", err)
}
}
解决 Go 数据库查询嵌套死锁问题
你的代码出现整站卡死、无限转圈的现象,是非常经典的 Go database/sql 连接池死锁(Connection Pool Starvation) 问题。
1. 为什么会死锁、卡死?
在你的核心代码中,发生了嵌套查询:
tagRows, err := fun.DB.Query("SELECT ...") // 1. 这里占用了一个数据库连接
defer tagRows.Close()
for tagRows.Next() {
// ...
// 2. 在循环内部,你又调用了 fun.DB.Query(...) 试图去获取第二个连接!
articleRows, err := fun.DB.Query("...")
}
- 死锁链条:当程序执行外层
tagRows时,它会一直独占持有当前这个数据库连接,直到整个for tagRows.Next()循环彻底结束,连接才会释放。 - 在循环内部,你调用了
fun.DB.Query查询文章。这会向连接池申请一个新的空闲连接。如果你的数据库是 SQLite(通常只允许单个写入/有限读取),或者你的数据库连接池的最大连接数(MaxOpenConns)设得比较小,或者在高并发下连接池满载,内部查询就会一直阻塞等待新连接。 - 恶性循环:内部查询在等新连接,而外层连接因为循环没结束永远无法释放。这就导致了永久死锁。因为连接池被全部占满且不释放,后续所有的 HTTP 请求(包括首页、内容页)只要访问数据库,都会一起挂起。
2. 完美解决方案
核心思路:坚决避免嵌套查询。先一次性把标签数据(Tags)全部读取到内存的 Slice 中,并立刻关闭 tagRows 释放连接。然后再遍历 Slice 去查询每个标签下的文章。
以下是修改后的 Baike_sitemap 完整安全代码:
// 3. 百科网站地图/目录树 Handler
// 路由配置:r.GET("/baike/sitemap", ai.Baike_sitemap)
func Baike_sitemap(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
query := r.URL.Query()
pageStr := query.Get("page")
currentPage := 1
if pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
currentPage = p
}
}
// 定义分页配置:每页显示 10 个分类标签
pageSize := 10
offset := (currentPage - 1) * pageSize
// 1. 查询有文章的标签总数(用于分页计算)
var totalTags int
err := fun.DB.QueryRow("SELECT COUNT(1) FROM cms_tag WHERE count > 0").Scan(&totalTags)
if err != nil {
http.Error(w, "计算标签失败: "+err.Error(), http.StatusInternalServerError)
return
}
// 计算总页数
totalPages := (totalTags + pageSize - 1) / pageSize
if totalPages < 1 {
totalPages = 1
}
if currentPage > totalPages {
currentPage = totalPages
offset = (currentPage - 1) * pageSize
}
// 2. 分页获取热门/非空标签列表
tagRows, err := fun.DB.Query("SELECT id, name, count FROM cms_tag WHERE count > 0 ORDER BY count DESC, id DESC LIMIT ? OFFSET ?", pageSize, offset)
if err != nil {
http.Error(w, "查询标签失败: "+err.Error(), http.StatusInternalServerError)
return
}
// 【关键点 1】定义一个临时 Slice,把数据读入内存中
type TempTag struct {
ID int
Name string
Count int
}
var tempTags []TempTag
for tagRows.Next() {
var t TempTag
if err := tagRows.Scan(&t.ID, &t.Name, &t.Count); err == nil {
tempTags = append(tempTags, t)
}
}
tagRows.Close() // 【关键点 2】读完立即显式关闭,彻底释放该数据库连接!
var groups []CategoryGroup
// 3. 循环获取每个标签分类下的高权重公开文章 (从内存 slice 遍历,此时数据库连接已安全归还)
for _, t := range tempTags {
var articles []Article
// 查询关联文章列表,必须满足 status = 1 正常状态
articleRows, err := fun.DB.Query(`
SELECT a.id, a.title
FROM cms_article a
INNER JOIN cms_article_tag at ON a.id = at.article_id
WHERE a.status = 1 AND at.tag_id = ?
ORDER BY a.id DESC LIMIT 30`,
t.ID,
)
if err == nil {
for articleRows.Next() {
var a Article
if err := articleRows.Scan(&a.ID, &a.Title); err == nil {
articles = append(articles, a)
}
}
articleRows.Close() // 查完立即释放
}
// 仅当分类下有文章才展示到目录树中
if len(articles) > 0 {
groups = append(groups, CategoryGroup{
CategoryName: t.Name,
Articles: articles,
})
}
}
// 生成分页页码数组
var pageNums []int
for i := 1; i <= totalPages; i++ {
pageNums = append(pageNums, i)
}
data := BaikeSitemapData{
CategoryGroups: groups,
HasPagination: totalPages > 1,
CurrentPage: currentPage,
TotalPages: totalPages,
PageNums: pageNums,
}
// 4. 读取模板文件并渲染
path := filepath.Join(fun.AppPath, "/templates/ai/sitemap.html")
mb, err := fun.TManager.Get(path)
if err != nil {
http.Error(w, "加载地图模板失败: "+err.Error(), http.StatusInternalServerError)
return
}
err = mb.Execute(w, data)
if err != nil {
fun.WriteLog("渲染百科地图页模板报错: %s", err)
}
}
3. 改进效果
- 杜绝死锁:外层循环的
tagRows连接在第二步就已经主动关闭,进入第三步循环时,全局只持有一个数据库连接,绝不会发生互相等待导致的死锁。 - 整站顺畅:连接池不再阻塞,高并发访问下系统依然稳定高速地返回数据。