在 Postgres 中基于 MVCC(多版本并发控制)技术 ,使用一种称为“版本链”的技术来维护对数据库中行的并发访问, 来实现事务隔离级别的。

MVCC-多版本并发控制

在 Postgres 中,每当一行被修改时,Postgres 就会创建一个新版本的该行,并将其添加到版本链的末尾。版本链包含了所有当前和以前的行版本,这样可以避免并发读写时的数据竞争和数据不一致问题。

MVCC避免了传统的数据库系统的锁定方法,将锁争夺最小化来允许多用户环境中的合理性能。

常见并发问题

  1. 脏读: 一个事务读取了另一个并行未提交事务写入的数据
  2. 不可重复读 一个事务重新读取之前读取过的数据,发现该数据已经被另一个事务(在初始读之后提交)修改。
  3. 幻读 一个事务重新执行一个返回符合一个搜索条件的行集合的查询, 发现满足条件的行集合因为另一个最近提交的事务而发生了改变。
  4. 序列化异常 成功提交一组事务的结果与这些事务所有可能的串行执行结果都不一致。

事务隔离级(解决方案)

事务隔离级别指的是多个并发事务之间的可见性和可操作性,SQL标准定义了四种隔离级别,分别是:

  1. Read Uncommitted(读未提交) 这是最低的事务隔离级别,它允许一个事务读取其他事务未提交的数据。这可能会导致脏读、不可重复读和幻读问题。
  2. Read Committed(读提交) 在该隔离级别下,事务只能看到已经提交的数据。它解决了脏读问题,但仍然存在不可重复读和幻读问题。
  3. Repeatable Read(可重复读) 在该隔离级别下,事务在执行期间看到的所有数据都是一致的。它解决了不可重复读问题,但仍然存在幻读问题。
  4. Serializable(可串行化) 在该隔离级别下,事务看到的所有数据都是一致的,并且完全避免了脏读、不可重复读和幻读问题。这是最高的事务隔离级别,但也是最慢的,因为它会导致大量的锁竞争。

在PostgreSQL中,你可以请求四种标准事务隔离级别中的任意一种,但是内部只实现了三种不同的隔离级别,即 PostgreSQL 的读未提交模式的行为和读已提交相同。

golang lib/pq设置隔离等级

在golang中,使用lib/pq包,通过执行“SET TRANSACTION ISOLATION LEVEL …”来设置隔离等级。以下测试脏读的例子:

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/lib/pq"
	"time"
)

func main() {
	// 连接数据库
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		panic(err)
	}
	defer db.Close()

	// 设置隔离级别为读未提交
	_, err = db.Exec("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
	//_, err = db.Exec("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
	if err != nil {
		panic(err)
	}
	// 开始第一个事务,修改用户 "Bob" 的年龄
	tx1, err := db.Begin()
	if err != nil {
		panic(err)
	}
	go func() {
		_, err := tx1.Exec("UPDATE users SET age = 6 WHERE name = 'Bob'")
		if err != nil {
			panic(err)
		}
		rows, err := tx1.Query("SELECT age FROM users WHERE name = 'Bob'")
		if err != nil {
			panic(err)
		}
		var age int
		if rows.Next() {
			err = rows.Scan(&age)
			if err != nil {
				panic(err)
			}
		}
		fmt.Printf("tx1 Bob's age is %d\n", age)
	}()

	// 暂停 1 秒钟,以便让第二个事务读取到未提交的数据
	time.Sleep(time.Second)
	// 开始第二个事务,读取用户 "Bob" 的年龄
	tx2, err := db.Begin()
	if err != nil {
		panic(err)
	}
	rows, err := tx2.Query("SELECT age FROM users WHERE name = 'Bob'")
	if err != nil {
		panic(err)
	}
	var age int
	if rows.Next() {
		err = rows.Scan(&age)
		if err != nil {
			panic(err)
		}
	}
	fmt.Printf("tx2 Bob's age is %d\n", age)
	// 提交第一个事务
	err = tx1.Commit()
	if err != nil {
		panic(err)
	}
	// 提交第二个事务
	err = tx2.Commit()
	if err != nil {
		panic(err)
	}
}

执行输出如下:

tx1 Bob's age is 6
tx2 Bob's age is 5

上例中,分别开启两个隔离级别为“读未提交”事务tx1、tx2,其中tx1对一行数据(Bob)的字段(Age)进行了修改,接着两个事务分别读取,即使在tx1提交之前,tx2也不会读取到未提交的数据,有效避免脏读等并发问题。

小结

PostgreSQL 的事务隔离级别和 MVCC 技术为开发人员提供了强大的并发性能和数据一致性保证。同时MVCC并发控制模型,降低了锁竞争和死锁的风险,允许并发事务访问相同的数据而不会相互干扰。对查询(读)数据的锁请求与写数据的锁请求不冲突,所以读不会阻塞写,而写也从不阻塞读。但是,在选择事务隔离级别时,需要考虑不同事务隔离级别的性能以及应用程序的需求和性能特征,并根据实际情况进行权衡,充分利用 MVCC 技术的优点。

参考