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开始事务
T2select count(*) from Foos where flag1=1 //(10条)开始事务
T3update Foos set flag2=2 where flag1=1 //(10条)
T4insert into Foos (..,flag1,…) values (.., 1 ,..)
T5提交事务
T6select count(*) from Foos where flag1=1 //(11条)
T7update 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

    原文作者:耿返
    原文地址: https://www.jianshu.com/p/969f63bda3f7
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞