小言_互联网的博客

数据库事务和锁详解(基于Postgresql)

301人阅读  评论(0)

1.在Postgresql里面事务以begin开始,以end结束。执行begin命令,则开启一个事务,之后的操作都属于该事务的操作,直到执行end时结束。一个连接会话只能开启一个事务,重复使用begin,则会报错。

一旦开启事务后,now这个函数不管执行多少次,它返回的时间都是固定的,这是因为它返回的时间是当前事务的开启时间。另外current_time,current_timestamp也是如此,而clock_timestamp则返回的是当前的真实时间:

2.回滚、保存点。看下面的一连串操作,这里我没有使用pgadmin4操作,而是使用pg自带的客户端进行的操作:

首先savepoint只能使用在事务中,上面的如果不进行rollback操作,那么使用commit命令,将是回滚操作,也就是1和2最终不会出现在数据库中。commit过后,就不需要end了,不使用commit也可以使用end结束事务。

这里推荐一个清屏命令:\! clear

3.隔离级别、不可重复读。

看下面的操作,我开两个客户端,一个进行事务操作,一个在事务操作期间插入一条数据:

你会发现第一个的事务操作期间,两次读取的数据结果不一致,原因是另外一个线程在期间修改了数据。这就是不可重复读。

开启三个客户端,一个开启默认隔离级别的事务,一个开启可重复读级别的事务,一个开启事务进行数据插入操作。你会发现,开启了可重复读隔离级别的事务,在修改数据事务提交后,读取到的数据并未受影响,而使用默认隔离级别,则会受影响。开启可重复读隔离级别的事务,命令为:begin isolation level repeatable read;当开启了可重复读,那么数据库会在第一次查询的时候保存当前数据库的一张快照(注意,这张快照是数据库的快照,不是某个表的,所以如果其他表的数据被修改,你仍然读不到最新的数据),后面所有的查询都是是从这个快照里面读取数据,不管这期间有没有其他的线程对数据进行了修改。

看下面的情况,在第二个回话插入数据之前,第一个事务并没有做任何操作,所以就不会保存快照,那么第二个回话的数据修改操作,依然会影响第一个回话的事务:

那么如何查看事务的隔离级别呢,主要用到两个命令:

a)select name,setting from pg_settings where name='default_transaction_isolation';     --查看系统默认的隔离级别。

b)show transaction_isolation;      --查看当前会话的事务隔离级别。

我们可以看到系统默认的事务隔离级别是读提交。

如果想查看有哪些隔离级别,使用\h begin;查看:

4.锁。

使用上面的可复读事务,我们很自然会想到一个问题,如果是两边同时增加,会怎样,看下面的例子:

上面的操作,不管是哪个事务限制性插入性语句,其结果都会导致重复,注意开启事务的那个不要忘记先执行一次查询,否则不会生成数据库快照。如果是手动开启事务的先执行,导致重复的原因是事务尚未提交,另一个回话读不到数据,因此数据库使用的默认事务隔离级别是读提交,这种情况下一个回话是读不到另一个事务尚未提交的数据的。如果是非手动开启事务的先执行,导致重复的原因是因为使用的事务隔离级别是可重复读,数据库读不到最新的数据。

这种结果显然是我们不想要的,第一个回话在事务里做了检查,如果不存在才插入,但是仍然多插入了一条数据进去,这种结果显然不是我们想要的结果,这就是开启了可重复读可能带来的问题。如果想解决这个问题,可以通过加锁的方式解决:

第一个回话的事务,通过lock,将表test锁住,第二个回话尝试修改test,会阻塞。直到第一个事务结束释放锁,它才会继续执行,所以这次的执行结果是正确的结果。

5.postgresql的咨询锁。

上面对test表加锁的操作,会让你很自然地想到,如果将整张表都锁住,那么当并发量很大的时候,将会阻塞大量的数据库操作,这样显示不合适的。那么有没有更细粒度的锁呢,这样就可以通过把锁的级别尽量降低些来提高系统的吞吐能力了,答案是肯定的。postgresql里面有个锁叫做咨询锁。看下面的操作:

这个锁和编程语言里面的锁差不多,对一个常量进行加锁,另外一个常量再对它进行加锁,就会阻塞,直到之前的线程释放了该锁。

这个锁不会随着事务的结束,自动释放,需要手动释放,这点需要特别注意。

它加锁的时候使用命令:select pg_advisory_lock(1);

释放锁的时候使用命令:select pg_advisory_unlock(1);

所以刚刚的问题,我们可以这样解决:

需要注意的是,咨询锁,只能锁整数。

6.辅助表。

上面采用咨询锁可以实现级别相对较低的锁,这里提供一个使用辅助表的方案,来实现用户级别的锁。

要说明的是,该种加锁的方案只能放到事务里面,同时要求能根据条件查询到数据,也就是辅助表里面必须有lockid=1这条数据,否则将不会起效果。锁会随着事务的结束自动释放,如果不希望阻塞,则加上nowait。如下:

当然,这个不一定非要使用辅助表实现,任何表都可以帮助实现这种加锁的方式,具体怎么加锁,要根据你的项目实际情况来定。

7.行级锁。

数据库本身会默认提供一个行级锁。看下面的操作:

当两个线程同时更新同一条数据的时候,由于行级锁的作用,会导致其中一个阻塞。

8.死锁。

由上面的行级锁,我们很容易想到,当两个线程更新的行有交叉,那么就很容易导致死锁:

这种情况下,数据库直接检测出了我们的操作存在死锁的情况,给我们返回了错误的结果。

应当说,在高并发环境下,死锁是很容易发生的,解决办法则根据实际情况来定。具体涉及到你的数据库操作策略,事务隔离级别等等。pg这个数据库在一些死锁的情况下会直接返回错误,而不是让线程一直卡着,这点我不知道其他的库是否也如此,也不太清楚pg是否能检测出所有的死锁情况。另外如果出现了死锁,在postgresql里面可以通过一些命令临时排出故障。

使用elect locktype,database,pid,relation ,mode from pg_locks;查看当前锁的使用情况:

使用select pg_terminate_backend(pid);终止某个线程:

我们看到正在进行的会话,如果被终止了,将会返回错误。另外pg还提供了其他的函数,来发现和解决死锁的问题,我提供的只是比较基础的方法。如果你有兴趣,可以翻看下pg的官方文档。

9.这一部分,我对上面提到的几种事务隔离级别做下稍微详细些的分析,看看他们都各适应些什么场景。

在Postgresql的官方文档里面,定义了四种在不同事务级别禁止的现象:

a)dirty read(脏读)

A transaction reads data written by a concurrent uncommitted transaction.

一个事务读到了另一个并发事务未提交的数据。

b)nonrepeatable read(不可重复读)

A transaction re-reads data it has previously read and finds that data has been modified by another transaction (that committed since the initial read).

一个事务重复读它之前读过一次的数据,发现数据被另一个事务修改了(该事务的提交时间是在之前的事务第一次读数据之后),即同一个事务期间,两次查到的数据不一致。

这里面包含了两个信息(另一个事务必须提交,另一个事务提交的时间在读数据事务第一次读之后)

c)phantom read(幻读)

A transaction re-executes a query returning a set of rows that satisfy a search condition and finds that the set of rows satisfying the condition has changed due to another recently-committed transaction.

一个事务重复执行一个能够得到一组满足某个查询条件数据的指令,发现这组满足查询条件的数据被另外一个最近提交的事务修改了。

所谓幻读是第一次没有读到,后面要根据前面读到的结果进行插入数据,满足条件就插入,在执行插入数据的时候,原本满足条件的数据被修改了,导致本不该插入的数据被插入了。它是由于读条件和执行指令之间有一个时间差导致的,但是这个时间差是没法避免的,虽然这个时间差可能很小,在高并发环境下,这个时间差范围内很可能有另外一个线程修改了数据。比如这句:

insert into user(id,name) select 1,'tom' where not exists(select * from user where id=1);

这句语句在执行的时候,要先去判断是否存在id=1的值,不存在才插入数据,如果在这期间另一个线程恰好插入了一条id=1的数据,那么数据库就出现两条id=1的数据了。

d)serialization anomaly(序列化异常)

The result of successfully committing a group of transactions is inconsistent with all possible orderings of running those transactions one at a time.

成功提交一组事务的结果与一次运行这些事务的所有可能顺序不一致。

下表列出了plsql支持的四种事务隔离级别,和它造成上述问题的可能性:

这四种是sql标准定义的事务隔离级别。注意看表格里面列出的信息,标识了“but not in PG”的表示在pg里面不会出现。所以在pg里面Read uncommitted和Read committed表现一样。


转载:https://blog.csdn.net/dap769815768/article/details/101101425
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场