postgres 事务隔离级别

事务隔离

MVCC的实现方法有两种:
1.写新数据时,把旧数据移到一个单独的地方,如回滚段中,其他人读数据时,从回滚段中把旧的数据读出来;
2.写数据时,旧数据不删除,而是把新数据插入。
PostgreSQL数据库使用第二种方法,而Oracle数据库和MySQL中的innodb引擎使用的是第一种方法。
与racle数据库和MySQL中的innodb引擎相比较,PostgreSQL的MVCC实现方式的优缺点如下。
优点:
1.事务回滚可以立即完成,无论事务进行了多少操作;
2.数据可以进行很多更新,不必像Oracle和MySQL的Innodb引擎那样需要经常保证回滚段不会被用完,也不会像oracle数据库那样经常遇到“ORA-1555”错误的困扰;
缺点:
1.旧版本数据需要清理。PostgreSQL清理旧版本的命令成为Vacuum;
2.旧版本的数据会导致查询更慢一些,因为旧版本的数据存在于数据文件中,查询时需要扫描更多的数据块。
(本段转自《PostgreSQL修炼之道》)

各个级别不希望发生的现象是

  • 脏读(dirty reads)
    一个事务读取了另一个未提交的并行事务写的数据。
时间 事务A 事务B
T1 开始事务
T2 开始事务
T3 查询账户余额1000
T4 去除500元,余额500
T5 查询余额为500(脏读)
  • 不可重复读(non-repeatable reads)
    一个事务重新读取前面读取过的数据, 发现该数据已经被另一个已提交的事务修改过。
时间 事务A 事务B
T1 开始事务
T2 查询余额为1000 开始事务
T3 查询账户余额1000
T4 去除500元,余额500
T5 提交事务
T6 查询余额为500
T7 提交事务
  • 幻读(phantom read)
    当前事务中重复执行相同的查询,返回的记录数因另一个事物插入或删除而得到不同的结果
时间 事务A 事务B
T1 开始事务
T2 select count(*) from Foos where flag1=1 //(10条) 开始事务
T3 update Foos set flag2=2 where flag1=1 //(10条)
T4 insert into Foos (..,flag1,...) values (.., 1 ,..)
T5 提交事务
T6 select count(*) from Foos where flag1=1 //(11条)
T7 update Foos set flag2=2 where flag1=1 //(更新11条)
T8 提交事务

会看到新插入的那条数据会被更新

标准SQL事务隔离级别

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 Allowed, but not in PG
可串行化 不可能 不可能 不可能

事务隔离级别

在PostgreSQL里,你可以请求四种可能的事务隔离级别中的任意一种。
但是在内部, 实际上只有三种独立的隔离级别,分别对应读已提交可重复读可串行化
如果你选择了读未提交的级别, 实际上你用的是读已提交,
在Postgre的重复读下,幻读是不可能的, 所以实际的隔离级别可能比你选择的更严格。

  • 读未提交 Read Uncommitted
    另一个事务中只要更新的记录(不需要等到提交), 当前事务就会读取到更新的数据 (脏读)

  • 读已提交 Read Committed
    读已提交是PostgreSQL里的缺省隔离级别。
    当一个事务运行在这个隔离级别时, SELECT查询(没有FOR UPDATE/SHARE子句)只能看到其它事务已提交的数据。
    实际上,SELECT 查询看到一个在查询开始运行的瞬间该数据库的一个快照。 不过,SELECT看得见其自身所在事务中之前的更新的执行结果,即使它们尚未提交。
    请注意, 在同一个事务里两个相邻的SELECT命令可能看到不同的快照,因为其它事务会坑你在两个SELECT执行期间提交。
    不会出现可脏读,但是不可重复读

  • 可重复读 Repeatable Read
    即使数据被其他事物修改, 当前事务也不会读取到新的数据
    重复读事务中的查询看到的是事务开始时的快照, 而不是该事务内部当前查询开始时的快照,这样, 同一个事务内部后面的SELECT命令总是看到同样的数据,
    也就是说,它们看不到 它们自身事务开始之后提交的其他事务所做出的改变。
    不会出现可脏读, 可重复读, 可以幻读

  • 可串行化 Serializable
    可串行化级别提供最严格的事务隔离。这个级别为所有已提交事务模拟串行的事务执行, 就好像事务将被一个接着一个那样串行(而不是并行)的执行。
    不过,正如可重复读隔离级别一样, 使用这个级别的应用必须准备在串行化失败的时候重新启动事务。
    事实上,该隔离级别和可重复读希望的完全一样, 它只是监视这些条件,以所有事务的可能的序列不一致的(一次一个)的方式执行并行的可串行化事务执行的行为。
    这种监测不引入任何阻止可重复读出现的行为,但有一些开销的监测,检测条件这可能会导致串行化异常 将触发串行化失败。

读已提交(Read Committed Isolation Level)

不可重复读

ActiveRecord::Base.isolation_level(:read_committed) do
   Foo.transaction do
        print Foo.first.bar # 1
        sleep(10) # 在此期间, 其它事务更新了Foo#bar
        print Foo.first.bar # 2
    end
end

可重复读(Repeatable Read Isolation Level)

可重复读

ActiveRecord::Base.isolation_level(:repeatable_read) do
   Foo.transaction do
        print Foo.first.bar # 1
        sleep(10) # 在此期间, 其它事务更新了Foo#bar
        print Foo.first.bar # 1
    end
end

该级别的应用必须准备好重试事务,因为可能会发生串行化失败。
下面这种情况事务T2会发生串行化失败

# 事务T1
ActiveRecord::Base.isolation_level(:repeatable_read) do
   Foo.transaction do
      print Foo.where(id: 1).update_all(bar: 11)
      print Foo.find(1).bar
      sleep 5
    end
end

# 事务T2
ActiveRecord::Base.isolation_level(:repeatable_read) do
   Foo.transaction do
      print Foo.where(id: 1).update_all(bar: 12)
      print Foo.find(1).bar
    end
end

下面这种情况事务T2不会发生串行化失败

# 事务T1
ActiveRecord::Base.isolation_level(:repeatable_read) do
   Foo.transaction do
      print Foo.where(id: 1).update_all(bar: 11)
      print Foo.find(1).bar
      sleep 5
    end
end

# 事务T2
ActiveRecord::Base.isolation_level(:repeatable_read) do
   Foo.transaction do
      sleep 6
      print Foo.where(id: 1).update_all(bar: 12)
      print Foo.find(1).bar
    end
end

在Postgre的重复读下,幻读是不可能的

但是测试的时候发现这种状况

# 事务T1
ActiveRecord::Base.isolation_level(:repeatable_read) do
   Foo.transaction do
       Foo.create!(bar: 2)
       sleep 5
    end
end

# 事务T2
ActiveRecord::Base.isolation_level(:repeatable_read) do
   Foo.transaction do
      print Foo.where(bar: 2).count # 1
      sleep 10
      print Foo.where(bar: 2).count # 2
      Foo.where(bar: 2).update_all(bar: 1) # 2
    end
end

可串行化(Serializable Isolation Level)

可重复读下不会发生的串行化失败在可串行化会失败
下面这种情况事务T2会发生串行化失败

# 事务T1
ActiveRecord::Base.isolation_level(:serializable) do
   Foo.transaction do
      print Foo.where(id: 1).update_all(bar: 11)
      print Foo.find(1).bar
      sleep 5
    end
end

# 事务T2
ActiveRecord::Base.isolation_level(:serializable) do
   Foo.transaction do
      sleep 6
      print Foo.where(id: 1).update_all(bar: 12)
      print Foo.find(1).bar
    end
end

多个事务并发时可能遇到的问题

  • Lost Update 更新丢失

    • 第一类更新丢失,回滚覆盖:撤消一个事务时,在该事务内的写操作要回滚,把其它已提交的事务写入的数据覆盖了。
    • 第二类更新丢失,提交覆盖:提交一个事务时,写操作依赖于事务内读到的数据,读发生在其他事务提交前,写发生在其他事务提交后,把其他已提交的事务写入的数据覆盖了。这是不可重复读的特例。
  • Non-Repeatable Read 不可重复读:一个事务中两次读同一行数据,可是这两次读到的数据不一样。

  • Phantom Read 幻读:一个事务中两次查询,但第二次查询比第一次查询多了或少了几行或几列数据。

回滚覆盖

时间 事务A 事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000
T4 取出100,余额改为900
T5 读余额为1000
T6 汇入100,余额改为1100
T7 提交事务,余额定为1100
T8 撤销事务,余额改回1000
T9 最终余额1000,更新丢失

这种更新丢失在pg的隔离级别下是不会发生的

提交覆盖

时间 事务A 事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000
T4 读余额为10000
T5 取出100,余额改为900
T6 提交事务,余额定为900
T7 汇入100,余额改为1100
T8 提交事务,余额定为1100
T8 最终余额1100,更新丢失

不做并发控制的前提下, 读已提交隔离级别下很容易发生更新丢失的问题,
可重复读, 可串行化 可以避免更新丢失的问题
比如下面这段代码

# 事务T1
Foo.transaction do
  Foo = Foo.find(1)
  Foo.bar = Foo.bar + 10
  sleep 5
  Foo.save
end

# 事务T2
Foo.transaction do
  Foo = Foo.find(1)
  Foo.bar = Foo.bar + 5
  Foo.save
end

读已提交隔离级别下可以通过锁来防止更新丢失

  • 拿掉代码中的临时变量
# 事务T1
Foo.transaction do
  Foo = Foo.find(1)
  Foo.increment!(:bar, 10)
  sleep 5
  Foo.increment!(:bar, 10)
end

# 事务T2
Foo.transaction do
  Foo = Foo.find(1)
  Foo.increment!(:bar, 10)
end
# 事务T1
Foo = Foo.find(1)
Foo.with_lock do
    Foo.bar = Foo.bar + 10
    Foo.save(validate: false)
    sleep 10
    Foo.bar = Foo.bar + 10
    Foo.save(validate: false)
end 

# 事务T1
Foo = Foo.find(1)
Foo.with_lock do
  Foo.bar = Foo.bar + 10
  Foo.save(validate: false)
end 

tip

  • combine query
# bad
unless rerord.approved?
    # balabala # 多个thread可能同时到达这里
    rerord.update(approved: true)
end # 并发下会导致一些问题

# better
update_count = Rerord.where(id: id, approved: false).update_all(approved: true)
# 根据上面的理论 并发下 不会导致某个record会被重复更新
if update_count == 1
    # balabala
end

参考: https://www.postgresql.org/docs/9.5/static/transaction-iso.html