目录
事务(Transaction)
事务是数据库操作中的一个逻辑单元,由一系列的数据库操作组成。这一系列操作要么全部执行并且提交,要么全部回滚,确保数据的一致性和完整性。事务具有以下四个主要特性,通常被称为ACID特性:
- 原子性(Atomicity):事务作为一个整体,所有的操作要么全部执行,要么全部不执行,不会出现部分执行的情况。
- 一致性(Consistency):在事务开始和结束时,数据库的状态都是一致的,符合预定的约束条件。
- 隔离性(Isolation):一个事务的执行不影响另一个事务的执行,除非另一个事务等待第一个事务完成。
- 持久性(Durability):一旦事务提交,所做的修改就会被永久保存在数据库中,不会因系统故障或崩溃而丢失。
在Go语言中,使用database/sql
包进行事务的基本步骤如下:
- 开始事务:使用
db.Begin()
方法启动一个事务,返回一个事务对象tx
。 - 执行SQL操作:使用事务对象
tx
执行多个SQL操作,如tx.Exec()
、tx.Query()
等。 - 提交或回滚事务:如果所有SQL操作都成功执行,使用
tx.Commit()
提交事务;如果出现错误,使用tx.Rollback()
回滚事务。
事务的使用场景
- 转账操作:转账操作涉及从一个账户扣款和另一个账户加款,这两个操作必须同时成功,否则数据会不一致。
- 订单处理:在提交订单时,可能需要减少库存并增加订单记录,这些操作需要保证同时成功。
- 批量操作:在批量插入、更新或删除数据时,确保所有操作要么全部执行,要么全部回滚。
注意事项
- 尽量减少事务的粒度:长时间的事务会占用数据库资源,可能导致其他操作被阻塞。尽量确保事务中的操作只涉及必要的数据库访问。
- 避免在事务中进行长时间的操作:如果在事务中进行文件IO、网络请求等长时间操作,可能会导致数据库连接被长时间占用,影响系统性能。
- 正确处理事务的提交和回滚:确保在所有可能的错误情况下,事务能够正确回滚,并释放相关资源。
数据库连接方法
dsn := "username:password@tcp(127.0.0.1:3306)/dbname"
方法名 | 描述 | 示例 |
---|---|---|
sql.Open() |
打开数据库连接 | db, err := sql.Open("mysql", dsn) |
db.Ping() |
测试数据库连接是否有效 | err = db.Ping() |
db.Close() |
关闭数据库连接 | defer db.Close() |
事务方法
方法名 | 描述 | 示例 |
---|---|---|
db.Begin() |
开始一个事务 | tx, err := db.Begin() |
tx.Rollback() |
回滚事务 | tx.Rollback() |
tx.Commit() |
提交事务 | err = tx.Commit() |
查询和执行方法
方法名 | 描述 | 示例 |
---|---|---|
tx.Exec() |
执行不返回结果的SQL语句,用于CREATE、INSERT、UPDATE、DELETE等操作 | tx.Exec("create table ...") |
tx.Query() |
执行返回多行结果的SQL查询 | rows, err := tx.Query("select ...") |
tx.QueryRow() |
执行返回单行结果的SQL查询 | tx.QueryRow("select ...") |
stmt.Exec() |
使用预处理语句执行SQL语句 | stmt.Exec("f", "g") |
预处理语句
方法名 | 描述 | 示例 |
---|---|---|
tx.Prepare() |
创建预处理语句 | stmt, err := tx.Prepare(...) |
stmt.Close() |
关闭预处理语句 | defer stmt.Close() |
查询结果处理
方法名 | 描述 | 示例 |
---|---|---|
rows.Next() |
逐行迭代查询结果 | rows.Next() |
rows.Scan() |
将当前行的列值赋值给变量 | rows.Scan(&s1, &s2) |
rows.Err() |
检查查询和迭代过程中的错误 | rows.Err() |
rows.Close() |
关闭结果集,释放相关资源 | defer rows.Close() |
预处理语句(Prepared Statements)
预处理语句是指在数据库中提前编译和优化的SQL语句模板,可以在之后多次重复使用。预处理语句的主要优点如下:
- 提高效率:数据库可以提前编译和优化预处理语句,减少每次执行SQL时的解析时间,特别是在需要多次执行相同SQL语句时。
- 防止SQL注入:通过参数化的SQL语句,用户输入的数据不会直接嵌入到SQL语句中,降低了SQL注入的风险。
- 减少网络开销:在需要多次执行相同的SQL语句时,客户端只需要发送参数,不需要每次都发送完整的SQL语句,减少网络通信的数据量。
在Go语言中,使用预处理语句的基本步骤如下:
- 准备预处理语句:使用
tx.Prepare()
方法创建一个预处理语句对象stmt
。 - 执行预处理语句:使用
stmt.Exec()
方法执行预处理语句,传递参数。 - 关闭预处理语句:执行完毕后,使用
stmt.Close()
方法释放相关资源。
以下是一个使用预处理语句的示例:
stmt, err := tx.Prepare("INSERT INTO table1 (column1, column2) VALUES(?, ?)")
if err != nil {
fmt.Printf("准备预处理语句失败:%v\n", err)
return
}
defer stmt.Close()
// 第一次执行
_, err = stmt.Exec("f", "g")
if err != nil {
tx.Rollback()
fmt.Printf("执行预处理语句第一次失败:%v\n", err)
return
}
// 第二次执行
_, err = stmt.Exec("h", "i")
if err != nil {
tx.Rollback()
fmt.Printf("执行预处理语句第二次失败:%v\n", err)
return
}
在这个示例中,预处理语句一次创建,多次执行,提升了效率,并降低了SQL注入的风险。
预处理语句的使用场景
- 批量插入:在需要插入大量数据时,使用预处理语句可以显著提高效率。
- 频繁执行相同SQL语句:在需要多次执行相同的SQL语句时,使用预处理语句可以减少数据库的解析开销,提高执行速度。
- 防止SQL注入:在处理用户输入的数据时,使用预处理语句可以有效防止SQL注入攻击。
注意事项
- 及时关闭预处理语句:使用完预处理语句后,记得及时关闭,释放数据库资源,避免资源泄漏。
- 正确处理参数:确保传递给预处理语句的参数类型和数量与预处理语句中的占位符相匹配。
- 避免过度使用:虽然预处理语句有诸多优势,但在不需要多次执行同一SQL语句的情况下,创建预处理语句可能会带来额外的开销,影响性能。
总的示例代码:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
const (
dsn = "username:password@tcp(localhost:3306)/test"
)
func main() {
db, err := sql.Open("mysql", dsn)
if err != nil {
fmt.Printf("打开数据库连接失败:%v\n", err)
return
}
defer db.Close()
if err := db.Ping(); err != nil {
fmt.Printf("数据库连接不可用:%v\n", err)
return
}
transactionExample(db)
}
func transactionExample(db *sql.DB) {
tx, err := db.Begin()
if err != nil {
fmt.Printf("开始事务失败:%v\n", err)
return
}
defer func() {
if err != nil {
fmt.Println("事务回滚中...")
rollbackErr := tx.Rollback()
if rollbackErr != nil && rollbackErr != sql.ErrTxDone {
fmt.Printf("事务回滚失败:%v\n", rollbackErr)
}
}
}()
// 创建表
fmt.Println("创建表...")
err = createTable(tx)
if err != nil {
return
}
// 插入数据
fmt.Println("插入数据到table1...")
err = insertData(tx)
if err != nil {
return
}
// 更新数据
fmt.Println("更新table1的值...")
err = updateData(tx)
if err != nil {
return
}
// 删除数据
fmt.Println("删除数据...")
err = deleteData(tx)
if err != nil {
return
}
// 查询多行数据
fmt.Println("查询多行数据...")
err = queryMultiRows(tx)
if err != nil {
return
}
// 查询单行数据
fmt.Println("查询单行数据...")
err = querySingleRow(tx)
if err != nil {
return
}
// 预处理语句插入数据
fmt.Println("使用预处理语句插入数据...")
err = insertWithPrepare(tx)
if err != nil {
return
}
// 提交事务
fmt.Println("提交事务...")
err = tx.Commit()
if err != nil {
fmt.Printf("提交事务失败:%v\n", err)
return
}
fmt.Println("事务处理成功。")
}
func createTable(tx *sql.Tx) error {
_, err := tx.Exec("create table if not exists table1 (column1 nchar(10), column2 nchar(10))")
return err
}
func insertData(tx *sql.Tx) error {
_, err := tx.Exec("insert into table1 (column1, column2) values ('a','b'), ('c','d'), ('e','f')")
return err
}
func updateData(tx *sql.Tx) error {
_, err := tx.Exec("update table1 set column1 = 'c' where column1 = 'a'")
return err
}
func deleteData(tx *sql.Tx) error {
_, err := tx.Exec("delete from table1 where column1 = 'b'")
return err
}
func queryMultiRows(tx *sql.Tx) error {
rows, err := tx.Query("select * from table1")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var s1, s2 string
err := rows.Scan(&s1, &s2)
if err != nil {
return fmt.Errorf("扫描失败:%v", err)
}
fmt.Printf("table1数据:%s, %s\n", s1, s2)
}
if err := rows.Err(); err != nil {
return fmt.Errorf("遍历table1失败:%v", err)
}
return nil
}
func querySingleRow(tx *sql.Tx) error {
var c string
err := tx.QueryRow("select column1 from table1 where column1 = 'e'").Scan(&c)
if err != nil {
if err == sql.ErrNoRows {
fmt.Println("没有找到匹配的行。")
} else {
return fmt.Errorf("查询单行数据失败:%v", err)
}
return nil
}
fmt.Printf("单行数据:%s\n", c)
return nil
}
func insertWithPrepare(tx *sql.Tx) error {
stmt, err := tx.Prepare("insert into table1 (column1, column2) values(?, ?)")
if err != nil {
return fmt.Errorf("准备预处理语句失败:%v", err)
}
defer stmt.Close()
_, err = stmt.Exec("f", "g")
if err != nil {
return fmt.Errorf("执行预处理语句失败:%v", err)
}
return nil
}