您的位置:首页 > 其它

postgres -- 一个问题引发的事务探究(二)

2017-09-04 18:28 288 查看
这一篇来了解下 数据库 的隔离机制

问题研究只针对Postgres 数据库,不针对mysql或者其他。

说道隔离机制,就要先说下数据库事务的4大特性:

⑴ 原子性(Atomicity)

  原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

⑵ 一致性(Consistency)

  一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

  拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

⑶ 隔离性(Isolation)

  隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

  即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

⑷ 持久性(Durability)

  持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

  例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

针对 文章(一)中出现的问题,就是就是隔离性 造成的,所以我们去研究一下 数据库的隔离机制:

隔离机制脏读不可重复读幻读
Read uncommitted允许,但不是在PG中可能可能
Read committed不可能可能可能
Repeatable read不可能不可能允许,但不是在PG中
Serializable不可能不可能不可能
在PostgreSQL中,你可以请求四种标准事务隔离级别中的任意一种。 但是在内部,只实现了三种不同的隔离级别,即:PostgreSQL的读未提交模式的行为类似于读已提交。 这是因为这是把标准的隔离级别映射到PostgreSQL的多版本并发控制架构的唯一合理方法。该表还显示了PostgreSQL的重复读不允许幻读。SQL 标准允许更严格的行为: 四种隔离级别只定义了哪种现像不能发生,没有定义哪种现像一定发生。
可用的隔离级别的行为在下面小节中详细描述。 (本段出自Postgres官方手册中)

查看当前PG使用的隔离级别 :

select current_setting('transaction_isolation');


"read committed"


本地数据库没有设置过,默认级别 好像就是 读已提交。

1.Read uncommitted(读未提交)

一个事务在执行过程中可以看到其他事务没有提交的新插入的记录,而且能看到其他事务没有提交的对已有记录的更新。
脏读(Dirty reads)就会可能发生于此隔离机制下,一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。

但是由上面表格其实可以知道,在PG中是不会出现脏读的,即使你设置成读未提交也没法测试,路过而已。

2.Read committed(读已提交)

这是PG的默认隔离级别。

当一个事务运行使用这个隔离级别时, 一个查询(没有FOR UPDATE/SHARE子句)只能看到查询开始之前(1)已经被提交的数据, 而无法看到未提交的数据或在查询执行期间其它事务提交的数据(2)。实际上,SELECT查询看到的是一个在查询开始运行的瞬间该数据库的一个快照。不过SELECT可以看见在它自身事务中之前执行的更新的效果(3),即使它们还没有被提交。还要注意的是,即使在同一个事务里两个相邻的SELECT命令可能看到不同的数据(4)
因为其它事务可能会在第一个SELECT开始和第二个SELECT开始之间提交。(此为文档原话)。

此段是什么意思,有几处标红的,需要特别注意一下。

1. 表明当前select查找的数据是从 当前 查询开始的时候,注意并不是select所处的事务开始的时候。这边容易出现误解。

2. 表明一旦执行了当前查询,那么在此之后提交的数据不能被读取到。

3.表明select可以读取到自身事务中之前的更新数据。

4.表明select可能会读取到不同的数据,当select2和select1期间其他事务做了提交,更新了数据。这就是所谓的不可重复读。



测试一下读已提交 隔离机制:

表结构:

CREATE TABLE public.student
(
id smallint,
name character(50),
phone character(50)
)
WITH (
OIDS=FALSE
);
ALTER TABLE public.student
OWNER TO postgres;


插入数据:

insert into student(id,name,phone) values (1,'张三','111222');
insert into student(id,name,phone) values (2,'李四','111222');
insert into student(id,name,phone) values (3,'王五','111222333');
insert into student(id,name,phone) values (4,'麻子','111222444');
当前数据显示为4条:
1;"张三                                                ";"111222                                            "
2;"李四                                                ";"111222                                            "
3;"王五                                                ";"111222333                                         "
4;"麻子                                                ";"111222444                                         "


测试用例 1

事务A:

start transaction;
--1 第一次查询,只有4条记录。
select * from student;
--2 第二次查询之前,事务2只是insert了数据,没提交,查询还是只有4条
select * from student;
--3 第三次查询之前,事务2提交了数据,查询数据有5条。
select * from student;


事务B:

start transaction;
insert into student(id,name,phone) values (5,'麻子儿子','111222444');
commit;


从数据查询来看,只能看到查询开始时的一个数据库快照,或者
4000
能看到其他事务已经提交的更新,未提交的更新看不到。

有个说明需要注意下,就是select语句中带有了 for update 语句,顺带测试一下。

测试用例 2

事务A:

start transaction;
select * from student for update;


事务B:

start transaction;
update student set phone = '66666' where id = 1;


1.当执行事务A时,发现可以读出当前数据。但是此时如果执行事务B,就会发现没有信息通知,此处表明了事务B被阻塞了,也就是在 for update期间,是不允许其他事务对满足条件了数据进行任何修改,除非事务A的事务被释放了,此时发现事务B出现了提交按钮,显示此时可以提交,提交事务B,数据被正常修改。

事务B被重新执行时的时间,发现是01.54之后了。

-- 执行查询:
start transaction; update student set phone = '66666' where id = 1;
Query returned successfully: one row affected, 01:54 minutes execution time.
测试用例 3

2.1 那么我们试一试,先执行事务B呢?发现事务B可以正常执行,如下图:

-- 执行查询:
start transaction;
update student set phone = '77777' where id = 1;
Query returned successfully: one row affected, 11 msec execution time.
2.2 但是如果此时执行事务A,发现,没有回复,也即是说明事务A此时被阻塞了,
-- 执行查询:
start transaction; select * from student for update;
2.2 此时commit 事务B:发现事务A被执行了:数据也是事务B执行更新的数据。

-- 执行查询:
COMMIT;
Query returned successfully with no result in 21 msec.
-- 执行查询:
start transaction; select * from student for update;

Total query runtime: 01:12 minutes
检索到 5 行。


那么可以发现在使用for update的情况下,如果已经开启了查询的事务的话,那么在事务没有被释放期间,另外的事务是不能修改数据的。

如果已经在修改数据的事务期间,for update查询的事务也是不能执行的,需要等到修改事务提交之后才能查询出数据。

另外说明下,不可重复读和幻读的强调点不同,不可重复读强调了读取之前读取过的数据,发现已经被其他事务更新过。

幻读强调重新执行执行一个之前的查询,发现数据发生了改变。其他点看起来是一样的。都是数据改变了。

copy一下文档中的描述:

UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE命令在搜索目标行时的行为和SELECT一样: 它们将只找到在命令开始时已经被提交的行。(1)
不过,在被找到时,这样的目标行可能已经被其它并发事务更新(或删除或锁住)。在这种情况下, 即将进行的更新将等待第一个更新事务提交或者回滚(如果它还在进行中)。 如果第一个更新事务回滚,那么它的作用将被忽略并且第二个事务可以继续更新最初发现的行。 如果第一个更新事务提交,若该行被第一个更新者删除(2),则第二个更新事务将忽略该行,否则第二个更新者将试图在该行的已被更新的版本上应用它的操作(2)。该命令的搜索条件(WHERE子句)将被重新计算来看该行被更新的版本是否仍然符合搜索条件。如果符合,则第二个更新者使用该行的已更新版本继续其操作。在SELECT
FOR UPDATE和SELECT FOR SHARE的情况下,这意味着把该行的已更新版本锁住并返回给客户端。  该命令的搜索条件(WHERE子句)将被重新计算来看该行被更新的版本是否仍然符合搜索条件。(3)如果符合,则第二个更新者使用该行的已更新版本继续其操作。在SELECT
FOR UPDATE和SELECT FOR SHARE的情况下,这意味着把该行的已更新版本锁住并返回给客户端。      

1. 注意update和delete操作都是和select一致,查询出的都是命令开始时已经被提交的行。

2.那么如果该行被删除的话,则第二个操作如果是更新操作的话会忽略该行(此处应该是update或者delete都会被hulv),否则则是在该行已被更新的版本上应用它的操作。

3.注意此处也表明where 条件是需要重新判断的,注意点。

测试用例 4:

测试用例4 可以引用第一篇文章的问题 测试实例,不在描述。

测试用例 5:

基础数据如下:

2;"李四                                                ";"111222"

事务A:

start transaction;
delete from student where id = 2;
insert into student(id,name,phone) values (2,'李四','aaa111222');
事务B:

start transaction;
update student set phone = '33333333' where id= 2;


执行事务A时,可见如下:删除成功了,并且新增了一条数据,phone为aaa111222

-- 执行查询:
start transaction; delete from student where id = 2; insert into student(id,name,phone) values (2,'李四','aaa111222');
Query returned successfully: one row affected, 12 msec execution time.
此时执行事务B被阻塞了,直到事务A被提交:但是执行语句中确发现,0行受到影响,可见,在事务A删除的那条语句中,事务B的更新操作直接被忽略了,但是综合测试用例4来看,新增操作不会被忽略,而是基于事务A的数据,新增另外一条。

-- 执行查询:
start transaction; update student set phone = '33333333' where id= 2;
Query returned successfully: 0 rows affected, 10.5 secs execution time.
此时的数据如下:仅仅是事务A的新增操作出来的数据。

2;"李四                                                ";"aaa111222                                         "

3.Repeatable read(可重复读)

以下copy文档描述,基于此分析:

可重复读隔离级别只看到在事务开始之前被提交的数据(1);它从来看不到未提交的数据或者并行事务在本事务执行期间提交的修改(2)(不过,查询能够看见在它的事务中之前执行的更新,即使它们还没有被提交),这是标准特别允许的,标准只描述了每种隔离级别必须提供的最小保护。

这个级别与读已提交不同之处在于,一个可重复读事务中的查询看到
事务中第一个非事务控制语句开始时的一个快照, 而不是事务中当前语句开始时的快照。(3)
因此,在一个单一事务中的后续SELECT 命令看到的是相同的数据,即它们看不到其他事务在本事务启动后提交的修改。
   

基于1,2,3可以发现,可重复读机制 读取的数据是根据事务开始的时间,而不是 当前语句的时间,注意一下。那么也就是在这个事务内,其他事务的修改对它都是不可见的,无论执行多少次select,查询的数据都是此次事务开启时读取的。
测试用例 6:

事务A:

start transaction ISOLATION LEVEL Repeatable read;
delete from student where id = 4;
事务B:

start transaction ISOLATION LEVEL Repeatable read;
update student set phone = 'bbbbbbbbb22222' where id = 4;
执行事务A时如下:
-- 执行查询:
start transaction ISOLATION LEVEL Repeatable read; delete from student where id = 4;
Query returned successfully: one row affected, 10 msec execution time.
此时执行事务B:

ERROR:  could not serialize access due to concurrent update
********** 错误 **********

ERROR: could not serialize access due to concurrent update
SQL 状态: 40001
提交事务A:事务B也能正常提交,只不过在执行期间出错。

测试用例 7:

源数据:

6;"麻子儿子                                              ";"ooooooo                                           "


事务A:

start transaction ISOLATION LEVEL Repeatable read;

select * from student;


事务B:

start transaction ISOLATION LEVEL Repeatable read;

update student set phone = '1111' where id = 6;
执行事务A:但是不启动select语句:此时执行事务B,

-- 执行查询:
start transaction ISOLATION LEVEL Repeatable read;

update student set phone = '111' where id = 6;
Query returned successfully: one row affected, 10 msec execution time.
此时执行事务A中的select语句,发现 并没有改成 111,而还是ooooooo,因此在事务A的事务开启之后,只能读取到当前事务开启时的快照,后续事务的修改对其不可见。

但是 如果使用这个隔离机制就需要 使用这个级别的应用必须准备好由于序列化失败而重试事务。因为一个可重复读事务无法修改或者锁住被其他在可重复读事务开始之后的事务改变的行。当一个应用接收到这个错误消息,它应该中断当前事务并且从开头重试整个事务。在第二次执行中,该事务将见到作为其初始数据库视图一部分的之前提交的改变,这样在使用行的新版本作为新事务更新的起点时就不会有逻辑冲突(文档原话,注意点)

此隔离机制一般不常用,重试事务这个方法也不是这篇博客关注点,暂时记录。

4.Serializable(序列化)

源文档如下copy:

可序列化隔离级别提供了最严格的事务隔离。这个级别为所有已提交事务模拟序列事务执行;就好像事务被按照序列一个接着另一个被执行,而不是并行地被执行。但是,和可重复读级别相似,使用这个级别的应用必须准备好因为序列化失败而重试事务。事实上,这个给力级别完全像可重复读一样地工作,除了它会监视一些条件,这些条件可能导致一个可序列化事务的并发集合的执行产生的行为与这些事务所有可能的序列化(一次一个)执行不一致。这种监控不会引入超出可重复读之外的阻塞,但是监控会产生一些负荷,并且对那些可能导致序列化异常的条件的检测将触发一次序列化失败。
   

序列化 隔离机制 内同一事务的 读和 可重复读是一致的,只能读取从事务开始时的快照,所以不可能出现 重复读的问题。

但是幻读在 可重复读是 没有解决的,可重复读解决的问题主要可以看成是对 同一条数据保证了一致性,但是没有保证对突然出现的行数据的一致性。

序列化解决的就是这个问题。

测试用例 8:

源数据有4条:

事务A:

start transaction ISOLATION LEVEL Serializable;
select * from student;
事务B:

start transaction ISOLATION LEVEL Serializable;
insert into student(id,name,phone) values (7,'麻子孙子','111222444');
执行发现当执行事务A时,可以读取4条数据,接着执行事务B,发现成功插入一条数据,但是此时再次执行事务A的查询,发现还是4条,没有出现事务B新增的那一条,解决了幻读的情况。

参考文献:http://www.postgres.cn/docs/9.5/index.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: