postgresql 事务
插图由 玛丽亚·莱塔(MariaLetta)/ free-gophers-pack组成 ,Renee French 创作的地鼠 。
事务是一系列数据库操作,分为一个单元。 所有操作都可以提交或回滚。 最简单的例子是余额转移。
如果两个帐户之间有余额,那么在Alice和Bob之间进行转帐时,我们需要从Alice余额中减去并在一个操作中增加Bob余额。 该操作SQL代码如下所示:
BEGIN ;
UPDATE users SET balance = balance - 10 WHERE name = 'Alice' ;
UPDATE users SET balance = balance + 10 WHERE name = 'Bob' ;
COMMIT ;
我不会深入探讨Postgres如何在内部处理所有这些问题,而只关注Go示例。
Go有两个主要的not-orm库与Postgres pg / lib和jackc / pgx一起使用 。 Pgx是更好的选择,我将在示例中使用它。 尽管pg / lib支持database/sql
,但是它不再被维护并且有一些问题,例如发生错误时出现panic
,而不是返回错误。
让我们来看一些更复杂的示例:
假设我们有一个用户表,每个用户都有名称,余额和group_id。 种子表有5个用户,每个用户的余额为100,分为3组。
CREATE TABLE users
(
id serial ,
name text ,
balance integer ,
group_id integer ,
PRIMARY KEY ( id )
);
INSERT INTO users ( name , balance, group_id )
VALUES ( 'Bob' , 100 , 1 ),
( 'Alice' , 100 , 1 ),
( 'Eve' , 100 , 2 ),
( 'Mallory' , 100 , 2 ),
( 'Trent' , 100 , 3 );
我们需要在一个ACID事务中将数据读取到程序中,对其进行处理,然后进行更新。 如果其他人尝试同时更新同一数据,则事务的行为将取决于其隔离级别而有所不同。
隔离度
它理论上有4个隔离级别,Postgres仅支持其中3个。 和4现象,不同的隔离级别应防止。
Read uncommitted
, Read committed
,可Repeatable read
和可Serializable
。
Read uncommitted
等于Read committed
,是Postgres中的默认隔离级别。
隔离级别的目标是防止不良现象:脏读,不可重复读,幻像读和序列化异常。
脏读
基本上,它是读取不同事务中未提交的更改。 所有事务均受Postgres保护,以防脏读,无法读取尚未提交的更改。
隔离的默认级别Read Committed
,等于Postgres中的Read uncommitted
。
在不同数据库中Read uncommitted
读取允许脏读取。
首先,我们需要准备到同一数据库的两个单独的连接,以便同时发送与它们两个的事务:
ctx = context.Background()
conn1, err := pgx.Connect(ctx, connString)if err != nil {
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n" , err)
os.Exit( 1 )
}
defer conn1.Close(ctx)
conn2, err := pgx.Connect(ctx, connString)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n" , err)
os.Exit( 1 )
}
defer conn2.Close(ctx)
尝试脏读:
- 将Bob余额更改为256 Inside主交易。
- 从交易中读取Bob余额。
- 通过第二个连接读取Bob平衡。
- 提交交易。
如果可能进行脏读,则步骤2和3的读取结果将相同。 但是由于更改是在事务内部进行的,因此在提交之前在外部是不可用的。
tx, err := conn1.Begin(ctx)if err != nil {
panic (err)
}
tx.Exec(ctx, "SET TRANSACTION ISOLATION LEVEL " +isolationLevel)
_, err = tx.Exec(ctx, "UPDATE users SET balance = 256 WHERE name='Bob'" )
if err != nil {
fmt.Printf( "Failed to update Bob balance in tx: %v\n" , err)
}
var balance int
row := tx.QueryRow(ctx, "SELECT balance FROM users WHERE name='Bob'" )
row.Scan(&balance)
fmt.Printf( "Bob balance from main transaction after update: %d\n" , balance)
row = conn2.QueryRow(ctx, "SELECT balance FROM users WHERE name='Bob'" )
row.Scan(&balance)
fmt.Printf( "Bob balance from concurrent transaction: %d\n" , balance)
if err := tx.Commit(ctx); err != nil {
fmt.Printf( "Failed to commit: %v\n" , err)
}
对于两个隔离级别,结果将是相同的:
Dirty read
Isolation level - READ UNCOMMITTED
Bob balance from main transaction after update: 256
Bob balance from concurrent transaction: 100
Final table state:
1 | Bob | 256 | 1
2 | Alice | 100 | 1
3 | Eve | 100 | 2
4 | Mallory | 100 | 2
5 | Trent | 100 | 3
Isolation level - READ COMMITTED
Bob balance from main transaction after update: 256
Bob balance from concurrent transaction: 100
Final table state:
1 | Bob | 256 | 1
2 | Alice | 100 | 1
3 | Eve | 100 | 2
4 | Mallory | 100 | 2
5 | Trent | 100 | 3
不可重复读
事务从行中读取一些值,并且这些值可以在事务结束之前通过并发转换进行更改。 为防止这种情况,可Repeatable read
隔离级别在提交事务之前再次读取所有这些值,如果值与初始值不同,则取消事务。 否则,如果忽略数据更改,则是不可重复的读取情况。
使用已Read committed
Repeatable read
,可Repeatable read
隔离级别进行测试:
- 从交易中读取Bob余额。
- 通过第二个连接将Bob余额更改为1000。
- 从交易中将Bob余额更改为110。
- 提交交易。
Read committed
事务只会忽略并发更改并将其覆盖。 使用Repeatable read
Postgres在步骤3中检测到并发更改并停止事务。
tx, err := conn1.Begin(ctx)if err != nil {
panic (err)
}
tx.Exec(ctx, "SET TRANSACTION ISOLATION LEVEL " +isolationLevel)
row := tx.QueryRow(ctx, "SELECT balance FROM users WHERE name='Bob'" )
var balance int
row.Scan(&balance)
fmt.Printf( "Bob balance at the beginning of transaction: %d\n" , balance)
fmt.Printf( "Updating Bob balance to 1000 from connection 2\n" )
_, err = conn2.Exec(ctx, "UPDATE users SET balance = 1000 WHERE name='Bob'" )
if err != nil {
fmt.Printf( "Failed to update Bob balance from conn2 %e" , err)
}
_, err = tx.Exec(ctx, "UPDATE users SET balance = $1 WHERE name='Bob'" , balance+ 10 )
if err != nil {
fmt.Printf( "Failed to update Bob balance in tx: %v\n" , err)
}
if err := tx.Commit(ctx); err != nil {
fmt.Printf( "Failed to commit: %v\n" , err)
}
结果不同,在第二种情况下,交易失败:
Nonrepeatable read
Isolation level - READ COMMITTED
Bob balance at the beginning of transaction: 100
Updating Bob balance to 1000 from connection 2
Final table state:
1 | Bob | 110 | 1
2 | Alice | 100 | 1
3 | Eve | 100 | 2
4 | Mallory | 100 | 2
5 | Trent | 100 | 3
Isolation level - REPEATABLE READ
Bob balance at the beginning of transaction: 100
Updating Bob balance to 1000 from connection 2
Failed to update Bob balance in tx: ERROR: could not serialize access due to concurrent update (SQLSTATE 40001)
Failed to commit: commit unexpectedly resulted in rollback
Final table state:
1 | Bob | 1000 | 1
2 | Alice | 100 | 1
3 | Eve | 100 | 2
4 | Mallory | 100 | 2
5 | Trent | 100 | 3
幻影阅读
虚拟读取类似于不可重复读取 ,但是它涉及在事务中选择的一组行 。 如果进行外部更改,则一组行也会更改,这是幻像读取的情况。 Repeatable read
级别可防止它在Postgres中使用。
使用已Read committed
Repeatable read
,可Repeatable read
隔离级别进行测试:
- 从事务中读取group_id = 2的用户。
- 通过第二个连接将Bob移到组2。
- 再次从事务中读取group_id = 2的用户。
- 更新所选用户余额+15。
- 提交交易。
Read committed
为Read committed
事务将在步骤1和3上读取不同的行。使用Repeatable read
Postgres将保存从事务开始的数据,而1和3的读取将返回同一行集,与并发更改隔离。
tx, err := conn1.Begin(ctx)if err != nil {
panic (err)
}
tx.Exec(ctx, "SET TRANSACTION ISOLATION LEVEL " +isolationLevel)
var users []User
var user User
rows, _ := tx.Query(ctx, "SELECT name, balance FROM users WHERE group_id = 2" )
for rows.Next() {
var user User
rows.Scan(&user.Name, &user.Balance)
users = append (users, user)
}
fmt.Printf( "Users in group 2 at the beginning of transaction:\n%v\n" , users)
fmt.Printf( "Cuncurrent transaction moves Bob to group 2\n" )
conn2.Exec(ctx, "UPDATE users SET group_id = 2 WHERE name='Bob'" )
users = []User{}
rows, _ = tx.Query(ctx, "SELECT name, balance FROM users WHERE group_id = 2" )
for rows.Next() {
rows.Scan(&user.Name, &user.Balance)
users = append (users, user)
}
fmt.Printf( "Users in group 2 after cuncurrent transaction:\n%v\n" , users)
fmt.Printf( "Update selected users balances by +15\n" )
for _, user := range users {
_, err = tx.Exec(ctx, "UPDATE users SET balance = $1 WHERE name=$2" , user.Balance+ 15 , user.Name)
if err != nil {
fmt.Printf( "Failed to update in tx: %v\n" , err)
}
}
if err := tx.Commit(ctx); err != nil {
fmt.Printf( "Failed to commit: %v\n" , err)
}
根据第二选择的结果,受升级影响的不同用户的结果是不同的:
Phantom read
Isolation level - READ COMMITTED
Users in group 2 at the beginning of transaction:
[{Eve 100} {Mallory 100}]
Cuncurrent transaction moves Bob to group 2
Users in group 2 after cuncurrent transaction:
[{Eve 100} {Mallory 100} {Bob 100}]
Update selected users balances by +15
Final table state:
1 | Bob | 115 | 2
2 | Alice | 100 | 1
3 | Eve | 115 | 2
4 | Mallory | 115 | 2
5 | Trent | 100 | 3
Isolation level - REPEATABLE READ
Users in group 2 at the beginning of transaction:
[{Eve 100} {Mallory 100}]
Cuncurrent transaction moves Bob to group 2
Users in group 2 after cuncurrent transaction:
[{Eve 100} {Mallory 100}]
Update selected users balances by +15
Final table state:
1 | Bob | 100 | 2
2 | Alice | 100 | 1
3 | Eve | 115 | 2
4 | Mallory | 115 | 2
5 | Trent | 100 | 3
序列化异常
假设我们有几个正在进行的并发事务,它们都使用表进行一些读取和写入。 如果最终表状态将取决于运行和提交这些事务的顺序,则为Serialization anomaly 。
在这种情况下,成绩可能会受到比赛条件的影响。 隔离级别可Serializable
帮助可防止此类问题。 我不得不说,即使在这种序列化级别下,一些罕见的情况仍然可能导致这种现象。
使用可Repeatable read
和可Serializable
隔离级别进行测试:
- 使用第二个连接启动第二个事务。
- 将第二个事务隔离级别设置为与主事务相同。
- 从事务1中读取group_id = 2的用户余额总和。
- 将Bob与事务2移到组2。
- 从事务1中读取group_id = 2的用户。
- 通过1次操作以+ sum更新所选用户余额。
- 提交主要交易。
- 提交第二笔交易。
具有“可Repeatable read
和“可Repeatable read
事务将被提交而没有错误。 使用Serializable
隔离级别,将不会提交第二个事务。 这两个事务使用相同的数据工作,并且提交的顺序将影响结果,这可能导致不可预测的结果。 Postgres将阻止提交第二笔交易,以防止这种不确定性。
tx, err := conn1.Begin(ctx)if err != nil {
panic (err)
}
tx.Exec(ctx, "SET TRANSACTION ISOLATION LEVEL " +isolationLevel)
tx2, err := conn2.Begin(ctx)
if err != nil {
panic (err)
}
tx2.Exec(ctx, "SET TRANSACTION ISOLATION LEVEL " +isolationLevel)
var sum int
row := tx.QueryRow(ctx, "SELECT SUM(balance) FROM users WHERE group_id = 2" )
row.Scan(&sum)
tx2.Exec(ctx, "UPDATE users SET group_id = 2 WHERE name='Bob'" )
if err != nil {
fmt.Printf( "Error in tx2: %v\n" , err)
}
rows, _ := tx.Query(ctx, "SELECT name, balance FROM users WHERE group_id = 2" )
type User struct {
Name string
Balance int
}
var users []User
for rows.Next() {
var user User
rows.Scan(&user.Name, &user.Balance)
users = append (users, user)
}
for _, user := range users {
_, err = tx.Exec(ctx, "UPDATE users SET balance = $1 WHERE name=$2" , user.Balance+sum, user.Name)
if err != nil {
fmt.Printf( "Failed to update in tx: %v\n" , err)
}
}
if err := tx.Commit(ctx); err != nil {
fmt.Printf( "Failed to commit tx: %v\n" , err)
}
if err := tx2.Commit(ctx); err != nil {
fmt.Printf( "Failed to commit tx2: %v\n" , err)
}
在第二种情况下,事务失败并显示“由于事务之间的读/写依赖性而无法序列化访问”错误:
Serialization anomaly
Isolation level - REPEATABLE READ
Final table state:
1 | Bob | 100 | 2
2 | Alice | 100 | 1
3 | Eve | 300 | 2
4 | Mallory | 300 | 2
5 | Trent | 100 | 3
Isolation level - SERIALIZABLE
Failed to commit tx2: ERROR: could not serialize access due to read/write dependencies among transactions (SQLSTATE 40001)
Final table state:
1 | Bob | 100 | 1
2 | Alice | 100 | 1
3 | Eve | 300 | 2
4 | Mallory | 300 | 2
5 | Trent | 100 | 3
结论
当您具有多个连接并可以同时访问Postgres数据库时,请仔细选择隔离级别。 较高的隔离级别可提供安全性,但会降低性能。 另外,您应该检查该事务是否已成功提交,并在必要时重复进行。
本文中的所有示例:
https://github.com/kochetkov-av/go-postgresql-transaction-isolation
翻译自: https://hackernoon.com/postgresql-transaction-isolation-levels-with-go-examples-lt5g3yh5
postgresql 事务