去提问»
提问背景 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 数据库查询嵌套死锁问题

发布时间:2026-08-19 22:41 👁️ 阅读:2

你的代码出现整站卡死、无限转圈的现象,是非常经典的 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. 改进效果

  1. 杜绝死锁:外层循环的 tagRows 连接在第二步就已经主动关闭,进入第三步循环时,全局只持有一个数据库连接,绝不会发生互相等待导致的死锁。
  2. 整站顺畅:连接池不再阻塞,高并发访问下系统依然稳定高速地返回数据。