事务隔离
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