当前位置:网站首页>Go 语言使用 MySQL 的常见故障分析和应对方法
Go 语言使用 MySQL 的常见故障分析和应对方法
2022-06-23 11:35:00 【InfoQ】

1、
资源未及时释放

// QueryContext 查询一些结果
// query:select * from test limit 10
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
type Rows struct{
Close( ) error
ColumnTypes( ) ( [ ]*ColumnType, error)
Columns( ) ( [ ]string, error)
Err( ) error
Next( ) bool
NextResultSet( ) bool
Scan(dest ...any) error
}
1.1 实验1-不调用 Rows.Close()
select * from user;
+----+-------+---------------------+----------+--------+
| id | email | register_time | password | status |
+----+-------+---------------------+----------+--------+
| 2 | dw | 2011-11-11 11:01:00 | d | 0 |
+----+-------+---------------------+----------+--------+
1 row in set (0.03 sec)
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"sync"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test")
if err != nil {
panic(err)
}
db.SetMaxOpenConns(1)
// 启动一个单独的协程,用于输出 DB 的状态信息
go func() {
tk := time.NewTicker(3 * time.Second)
defer tk.Stop()
for range tk.C {
bf, _ := json.Marshal(db.Stats())
fmt.Println("db.Stats=", string(bf))
}
}()
// 启动 10 个协程,同时查询数据
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
queryOne(id, db)
}(i)
}
wg.Wait()
fmt.Println("finish")
}
func queryOne(id int, db *sql.DB) {
start := time.Now()
rows, err := db.QueryContext(context.Background(), "select * from user limit 1")
if err != nil {
panic(err)
}
// defer rows.Close()
// 没有从 Rows 里读取结果,也没有调用 rows.Close
fmt.Println("id=", id, "hasNext=", rows.Next(), "cost=", time.Since(start))
}
id= 0 hasNext= true cost= 9.607371ms
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
{
"MaxOpenConnections": 1, // 最大打开连接数,和代码设置的一致,是 1
"OpenConnections": 1, // 已打开的连接数
"InUse": 1, // 正在使用的连接数
"Idle": 0, // 空闲连接数
"WaitCount": 9, // 等待连接数
"WaitDuration": 0, // 等待总耗时(在等待退出时才计数)
"MaxIdleClosed": 0, // 超过最大 idle 数所关闭的连接总数
"MaxIdleTimeClosed": 0, // 超过追到 idle 时间所关闭的连接总数
"MaxLifetimeClosed": 0 // 超过最大生命周期所关闭的连接总数
}
1.2 实验2-调用 Rows.Close()
func queryOne(id int, db *sql.DB) {
start := time.Now()
rows, err := db.QueryContext(context.
Background(), "select * from user limit 1")
if err != nil {
panic(err)
}
defer rows.Close() // 打开了此处的注释,Close 方法会释放资源
fmt.Println("id=", id, "hasNext=", rows.Next(), "cost=", time.Since(start))
}
# go run main.go
id= 9 hasNext= true cost= 4.082448ms
id= 3 hasNext= true cost= 5.670052ms
id= 8 hasNext= true cost= 5.745443ms
id= 5 hasNext= true cost= 6.238615ms
id= 6 hasNext= true cost= 6.520818ms
id= 7 hasNext= true cost= 6.697782ms
id= 4 hasNext= true cost= 6.953454ms
id= 1 hasNext= true cost= 7.1079ms
id= 0 hasNext= true cost= 7.3036ms
id= 2 hasNext= true cost= 7.464726ms
finish
1.3 实验3- 使用带超时的 Context
func queryOne(id int, db *sql.DB) {
start := time.Now()
ctx,cancel:=context.WithTimeout(context.Background(),time.Second) // 关键
defer cancel() // 关键。若将此行替换为 _=cancel,又是另外一种结果了
rows, err := db.QueryContext(ctx , "select * fro m user limit 1")
if err != nil {
// panic (err)
fmt.Println("BeginTx failed:",err)
return
}
// defer rows.Close () // 打开了此处的注 释,Close 方法会释放资源
fmt.Println("id=" , id, "hasNext=", rows.Next(), "cost=", time.Since (start))
}
id= 9 hasNext= true cost= 1.483715ms
id= 3 hasNext= true cost= 175.675µs
id= 6 hasNext= true cost= 1.277596ms
id= 1 hasNext= true cost= 174.307µs
id= 7 hasNext= true cost= 108.061µs
id= 4 hasNext= true cost= 115.072µs
id= 2 hasNext= true cost= 104.046µs
id= 0 hasNext= true cost= 96.833µs
id= 8 hasNext= true cost= 123.758µs
id= 5 hasNext= true cost= 92.791µs
finish
d= 9 hasNext= true cost= 2.581813ms
BeginTx failed: context deadline exceeded
BeginTx failed: context deadline exceeded
BeginTx failed: context deadline exceeded
BeginTx failed: context deadline exceeded
BeginTx failed: context deadline exceeded
BeginTx failed: context deadline exceeded
BeginTx failed: context deadline exceeded
BeginTx failed: context deadline exceeded
BeginTx failed: context deadline exceeded
1.4 解决方案
- 我们应该使用QueryContext 这类支持传入 context 的函数,并且传入带超时控制的 context,并且在逻辑执行完成后,应使用 defer 方法将 context 取消。
- 对于返回一个流类型的结果,使用完成后一定需要调用 Close 方法以释放资源。
- 所有 *sql.DB、*sql.Tx、*sql.Stmt 的返回 *Conn、*Stmt、*Rows 这几种类型的都需要 Close:
type DB/Tx/Stmt struct{
Conn(ctx context.Context) (*Conn, error)
Prepare(query string) (*Stmt, error)
PrepareContext(ctx context.Context, query string) (*Stmt, error)
Query(query string, args ...any) (*Rows, error)
QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
}
type user struct {
ID int64 `ddb:"id"`
Status uint8 `ddb:"status"`
}
func readUsers(ctx context.Context)([]*user,error)
rows, err := cli.QueryContext(ctx, "select * from user where status=1 limit 5")
if err != nil {
return nil,err
}
var userList []*user
err=mysql.ReadRowsClose(rows, &userList)
return userList,err
}
b := &SimpleBuilder{
SQL: "SELECT id,name from user where id=1",
}
type user struct{
Name string `ddb:"name"`
ID int `ddb:"id"`
}
var us []*user
err = mysql.QueryWithBuilderScan(ctx, client, b, &us)
2、事务不完整
func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)
type Tx
func (tx *Tx) Commit() error // 提交事务
func (tx *Tx) Rollback ( ) error // 回滚事务
func (tx *Tx) Exec(query string, args ...any) (Result, error)
func (tx *Tx) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
func (tx *Tx) Prepare(query string) (*Stmt, error)
func (tx *Tx) PrepareContext(ctx context.Context, query string) (*Stmt, error)
func (tx *Tx) Query(query string, args ...any) (*Rows, error)
func (tx *Tx) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
func (tx *Tx) QueryRow(query string, args ...any) *Row
func (tx *Tx) QueryRowContext(ctx context.Context, query string, args ...any) *Row
func (tx *Tx) Stmt(stmt *Stmt) *Stmt
func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt
2.1 和 PHP 的区别
<?php
/* 开始一个事务,关闭自动提交 */
$dbh->beginTransaction();
/* 在全有或全无的基础上插入多行记录(要么全部插入,要么全部不插入) */
$sql = 'INSERT INTO fruit(name, colour, calories) VALUES (?, ?, ?)';
$sth = $dbh->prepare($sql);
foreach ($fruits as $fruit) {
$sth->execute(array(
$fruit->name,
$fruit->colour,
$fruit->calories,
));
}
/* 提交更改 */
$dbh->commit();
// 此代码来自 https://www.php.net/manual/zh/pdo.commit.php
import (
"context"
"database/sql"
"log"
)
var (
ctx context.Context
db *sql.DB
)
func main() {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
log.Fatal(err)
}
id := 37
// 使用 Tx 执行 Update 语句,而不是继续使用 db.Exec
_, execErr := tx.Exec(`UPDATE users SET status = ? WHERE id = ?`, "paid", id)
if execErr != nil {
_ = tx.Rollback()
log.Fatal(execErr)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
}
// 此代码来自于:https://pkg.go.dev/database/[email protected]#example-DB.BeginTx
2.2 实验
func queryOne(id int, db *sql.DB) {
tx,err:=db.BeginTx(context.Background(),nil)
if err!=nil{
panic(err)
}
// defer tx.Rollback()
start := time.Now()
rows, err := tx.QueryContext(context.Background(), "select * from user limit 1")
if err != nil {
panic(err)
}
defer rows.Close()
// 事务没有回滚、提交
fmt.Println("id=", id, "hasNext=", rows.Next(), "cost=", time.Since(start))
}
id= 9 hasNext= true cost= 11.670369ms
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
db.Stats= {"MaxOpenConnections":1,"OpenConnections":1,"InUse":1,"Idle":0,"WaitCount":9,"WaitDuration":0,"MaxIdleClosed":0,"MaxIdleTimeClosed":0,"MaxLifetimeClosed":0}
2.3 解决方案
// 业务逻辑函数的定义,在此函数内实现事务内的增删改查
// 返回 error==nil 则 tx.Commit(),否则 tx.Rollback()
type doFunc func(ctx context.Context, qe QueryExecuto r) error
func BeginTx(ctx context.Context, cli CanBeginTx, opts *sql.TxOptions, do doFunc) error
var cli mysql.Client
updateUserNameByID := func(ctx context.Context, id uint64, name string) error {
// 使用 BeginTx 方法,能更省心的处理事务
err := mysql.BeginTx(ctx, cli, nil, func(ctx context.Context, qe mysq.QueryExecutor) error {
// 其他的数据库更新逻辑略
b1 := &mysql.SimpleBuilder{}
b1.Append("select name from user where uid=?", id)
var oldName string
if err := mysql.QueryRowWithBuilderScan(ctx, qe, b1, &oldName); err != nil {
return err
}
if oldName == "诸葛亮" || oldName == name {
// 返回 err,mysql.BeginTx 方法将会回滚事务
return fmt.Errorf("不需要更新,事务整体回滚")
}
b2 := &mysql.SimpleBuilder{}
b2.Append("update user set name=? where id=?", name, id)
_, err := mysql.ExecWithBuilder(ctx, qe, b2)
if err != nil {
return err
}
// 返回 nil,mysql.BeginTx 方法将会提交事务
return nil
})
return err
}
3、其他原因
3.1 不支持预处理
Name = "demo"
# 其他配置项略
[MySQL]
Username = "example"
# 其他参数略
DSNParams ="charset=utf8&timeout=90s&collation=utf8mb4_unicode_ci&parseTime=true&interpolateParams=true"
4、如何排查
{
"MaxOpenConnections" : 1 , // 最大打开连接数,和代码设置的一致,是 1
"OpenConnections" : 1 , // 已打开的连接数
"InUse" : 1 , // 正在使用的连接数
"Idle" : 0 , // 空闲连接数
"WaitCount" : 9 , // 等待连接数
"WaitDuration" : 0 , // 等待总耗时(在等待退出时才计数)
"MaxIdleClosed" : 0 , // 超过最大 idle 数所关闭的连接总数
"MaxIdleTimeClosed" : 0 , // 超过追到 idle 时间所关闭的连接总数
"MaxLifetimeClosed" : 0 // 超过最大生命周期所关闭的连接总数
}
4.1 集成 GDP 应用面板

4.2 集成监控
client_connpool{servicer="demo_mysql",stats="ConnType"} 1
client_connpool{servicer="demo_mysql",stats="IPTotal"} 1
client_connpool{servicer="demo_mysql",stats="InUseAvg"} 0
client_connpool{servicer="demo_mysql",stats="InUseMax"} 0
client_connpool{servicer="demo_mysql",stats="InUseTotal"} 0
client_connpool{servicer="demo_mysql",stats="NumOpenAvg"} 0
client_connpool{servicer="demo_mysql",stats="NumOpenCfg"} 100
client_connpool{servicer="demo_mysql",stats="NumOpenMax"} 0
client_connpool{servicer="demo_mysql",stats="NumOpenTotal"} 0
4.3 输出到日志
边栏推荐
- ESP32-C3入门教程 问题篇⑧——blufi_example.c:244: undefined reference to `esp_ble_gap_start_advertising
- 请问连接oracle时,这个version 1.54 是什么的version?
- OpenHarmony应用开发【01】
- Deveco device tool helps openharmony device development
- Groovy之Map操作
- php 手写一个完美的守护进程
- Surprise! Amd acquires Xilinx with USD 35billion!
- 视频数据标注工具与平台(数据标注公司)
- How to use the interval repetition memory system in flowus, notation and other note taking software?
- Over a year, time has changed. Chinese chips have made breakthroughs, but American chips are difficult to sell
猜你喜欢

Esp32-cam, esp8266, WiFi, Bluetooth, MCU, hotspot create embedded DNS server

The simplest DIY pca9685 steering gear control program based on the integration of upper and lower computers of C # and 51 single chip microcomputer

@黑马粉丝,这份「高温补贴」你还没领?

【云原生&微服务八】Ribbon负载均衡策略之WeightedResponseTimeRule源码剖析(响应时间加权)

"Core" has spirit "lizard", ten thousand people are online! Dragon Dragon community walks into Intel meetup wonderful review

电脑坏了,换了台电脑,装node环境的时候出了问题,报错URL not defined

塔米狗 | 投资人类型分析以及企业投资类型分析

Simplest DIY steel patriot machine gun controller based on Bluetooth, 51 MCU and steering gear

Force buckle 1319 Number of connected network operations

KDD 2022 | 基于分层图扩散学习的癫痫波预测
随机推荐
惊!AMD 350亿美元收购赛灵思!
Tensorrt笔记(四)推理分割模型
直播带货app源码搭建中,直播CDN的原理是什么?
有没有碰到过flink1.15.0 和 flink-cdc-mysql 2.2.1不兼容的情况?f
What are the top ten securities companies in China? Is it safe to open a mobile account?
quarkus+saas多租户动态数据源切换实现简单完美
【ML】QuantileRegressor
我在佛山,到哪里开户比较好?手机开户安全么?
单向链表实现--计数
在工作中学习的三个方法
如何使用笔记软件 FlowUs、Notion 进行间隔重复?基于公式模版
【云驻共创】无码时代,软件开发如何走向每个人?
php 手写一个完美的守护进程
Voice data annotation tools and platforms
视频数据标注工具与平台(数据标注公司)
[cloud based co creation] overview of the IOT of Huawei cloud HCIA IOT v2.5 training series
Where to find capacitance parameters!?
今天14:00 | 12位一作华人学者开启 ICLR 2022
华为云如何实现实时音视频全球低时延网络架构
How to implement a distributed lock with redis