精华内容
下载资源
问答
  • 并行系统中并发问题永远不可忽视。尽管PHP语言原生没有提供多线程机制,那并不意味着所有的操作都是线程安全的。尤其是在操作诸如订单、支付等业务系统中,更需要注意操作数据库并发问题。接下来我通过一个案例...

    在并行系统中并发问题永远不可忽视。尽管PHP语言原生没有提供多线程机制,那并不意味着所有的操作都是线程安全的。尤其是在操作诸如订单、支付等业务系统中,更需要注意操作数据库的并发问题。

    接下来我通过一个案例分析一下PHP操作数据库时并发问题的处理问题。

    首先,我们有这样一张数据表:

    mysql> select * from counter;

    +----+-----+

    | id | num |

    +----+-----+

    |  1 | 0 |

    +----+-----+

    1 row in set (0.00 sec)

    这段代码模拟了一次业务操作:

    function dummy_business() {

    $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());

    mysqli_select_db($conn, 'test');

    for ($i = 0; $i < 10000; $i++) {

    mysqli_query($conn, 'UPDATE counter SET num = num + 1 WHERE id = 1');

    }

    mysqli_close($conn);

    }

    for ($i = 0; $i < 10; $i++) {

    $pid = pcntl_fork();

    if($pid == -1) {

    die('can not fork.');

    } elseif (!$pid) {

    dummy_business();

    echo 'quit'.$i.PHP_EOL;

    break;

    }

    }

    ?>

    上面的代码模拟了10个用户同时并发执行一项业务的情况,每次业务操作都会使得num的值增加1,每个用户都会执行10000次操作,最终num的值应当是100000。

    运行这段代码,num的值和我们预期的值是一样的:

    mysql> select * from counter;

    +----+--------+

    | id | num  |

    +----+--------+

    |  1 | 100000 |

    +----+--------+

    1 row in set (0.00 sec)

    这里不会出现问题,是因为单条UPDATE语句操作是原子的,无论怎么执行,num的值最终都会是100000。

    然而很多情况下,我们业务过程中执行的逻辑,通常是先查询再执行,并不像上面的自增那样简单:

    function dummy_business() {

    $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());

    mysqli_select_db($conn, 'test');

    for ($i = 0; $i < 10000; $i++) {

    $rs = mysqli_query($conn, 'SELECT num FROM counter WHERE id = 1');

    mysqli_free_result($rs);

    $row = mysqli_fetch_array($rs);

    $num = $row[0];

    mysqli_query($conn, 'UPDATE counter SET num = '.$num.' + 1 WHERE id = 1');

    }

    mysqli_close($conn);

    }

    for ($i = 0; $i < 10; $i++) {

    $pid = pcntl_fork();

    if($pid == -1) {

    die('can not fork.');

    } elseif (!$pid) {

    dummy_business();

    echo 'quit'.$i.PHP_EOL;

    break;

    }

    }

    ?>

    改过的脚本,将原来的原子操作UPDATE换成了先查询再更新,再次运行我们发现,由于并发的缘故程序并没有按我们期望的执行:

    mysql> select * from counter;

    +----+------+

    | id | num  |

    +----+------+

    |  1 | 21495|

    +----+------+

    1 row in set (0.00 sec)

    入门程序员特别容易犯的错误是,认为这是没开启事务引起的。现在我们给它加上事务:

    function dummy_business() {

    $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());

    mysqli_select_db($conn, 'test');

    for ($i = 0; $i < 10000; $i++) {

    mysqli_query($conn, 'BEGIN');

    $rs = mysqli_query($conn, 'SELECT num FROM counter WHERE id = 1');

    mysqli_free_result($rs);

    $row = mysqli_fetch_array($rs);

    $num = $row[0];

    mysqli_query($conn, 'UPDATE counter SET num = '.$num.' + 1 WHERE id = 1');

    if(mysqli_errno($conn)) {

    mysqli_query($conn, 'ROLLBACK');

    } else {

    mysqli_query($conn, 'COMMIT');

    }

    }

    mysqli_close($conn);

    }

    for ($i = 0; $i < 10; $i++) {

    $pid = pcntl_fork();

    if($pid == -1) {

    die('can not fork.');

    } elseif (!$pid) {

    dummy_business();

    echo 'quit'.$i.PHP_EOL;

    break;

    }

    }

    ?>

    依然没能解决问题:

    mysql> select * from counter;

    +----+------+

    | id | num  |

    +----+------+

    |  1 | 16328|

    +----+------+

    1 row in set (0.00 sec)

    请注意,数据库事务依照不同的事务隔离级别来保证事务的ACID特性,也就是说事务不是一开启就能解决所有并发问题。通常情况下,这里的并发操作可能带来四种问题:

    更新丢失:一个事务的更新覆盖了另一个事务的更新,这里出现的就是丢失更新的问题。

    脏读:一个事务读取了另一个事务未提交的数据。

    不可重复读:一个事务两次读取同一个数据,两次读取的数据不一致。

    幻象读:一个事务两次读取一个范围的记录,两次读取的记录数不一致。

    通常数据库有四种不同的事务隔离级别:

    隔离级别

    脏读

    不可重复读

    幻读

    Read uncommitted

    Read committed

    ×

    Repeatable read

    ×

    ×

    Serializable

    ×

    ×

    ×

    大多数数据库的默认的事务隔离级别是提交读(Read committed),而MySQL的事务隔离级别是重复读(Repeatable read)。对于丢失更新,只有在序列化(Serializable)级别才可得到彻底解决。不过对于高性能系统而言,使用序列化级别的事务隔离,可能引起死锁或者性能的急剧下降。因此使用悲观锁和乐观锁十分必要。

    并发系统中,悲观锁(Pessimistic Locking)和乐观锁(Optimistic Locking)是两种常用的锁:

    悲观锁认为,别人访问正在改变的数据的概率是很高的,因此从数据开始更改时就将数据锁住,直到更改完成才释放。悲观锁通常由数据库实现(使用SELECT…FOR UPDATE语句)。

    乐观锁认为,别人访问正在改变的数据的概率是很低的,因此直到修改完成准备提交所做的的修改到数据库的时候才会将数据锁住,完成更改后释放。

    上面的例子,我们用悲观锁来实现:

    function dummy_business() {

    $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());

    mysqli_select_db($conn, 'test');

    for ($i = 0; $i < 10000; $i++) {

    mysqli_query($conn, 'BEGIN');

    $rs = mysqli_query($conn, 'SELECT num FROM counter WHERE id = 1 FOR UPDATE');

    if($rs == false || mysqli_errno($conn)) {

    // 回滚事务

    mysqli_query($conn, 'ROLLBACK');

    // 重新执行本次操作

    $i--;

    continue;

    }

    mysqli_free_result($rs);

    $row = mysqli_fetch_array($rs);

    $num = $row[0];

    mysqli_query($conn, 'UPDATE counter SET num = '.$num.' + 1 WHERE id = 1');

    if(mysqli_errno($conn)) {

    mysqli_query($conn, 'ROLLBACK');

    } else {

    mysqli_query($conn, 'COMMIT');

    }

    }

    mysqli_close($conn);

    }

    for ($i = 0; $i < 10; $i++) {

    $pid = pcntl_fork();

    if($pid == -1) {

    die('can not fork.');

    } elseif (!$pid) {

    dummy_business();

    echo 'quit'.$i.PHP_EOL;

    break;

    }

    }

    ?>

    可以看到,这次业务以期望的方式正确执行了:

    mysql> select * from counter;

    +----+--------+

    | id | num  |

    +----+--------+

    |  1 | 100000 |

    +----+--------+

    1 row in set (0.00 sec)

    由于悲观锁在开始读取时即开始锁定,因此在并发访问较大的情况下性能会变差。对MySQL Inodb来说,通过指定明确主键方式查找数据会单行锁定,而查询范围操作或者非主键操作将会锁表。

    接下来,我们看一下如何使用乐观锁解决这个问题,首先我们为counter表增加一列字段:

    mysql> select * from counter;

    +----+------+---------+

    | id | num | version |

    +----+------+---------+

    | 1 | 1000 | 1000 |

    +----+------+---------+

    1 row in set (0.01 sec)

    实现方式如下:

    function dummy_business() {

    $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());

    mysqli_select_db($conn, 'test');

    for ($i = 0; $i < 10000; $i++) {

    mysqli_query($conn, 'BEGIN');

    $rs = mysqli_query($conn, 'SELECT num, version FROM counter WHERE id = 1');

    mysqli_free_result($rs);

    $row = mysqli_fetch_array($rs);

    $num = $row[0];

    $version = $row[1];

    mysqli_query($conn, 'UPDATE counter SET num = '.$num.' + 1, version = version + 1 WHERE id = 1 AND version = '.$version);

    $affectRow = mysqli_affected_rows($conn);

    if($affectRow == 0 || mysqli_errno($conn)) {

    // 回滚事务重新提交

    mysqli_query($conn, 'ROLLBACK');

    $i--;

    continue;

    } else {

    mysqli_query($conn, 'COMMIT');

    }

    }

    mysqli_close($conn);

    }

    for ($i = 0; $i < 10; $i++) {

    $pid = pcntl_fork();

    if($pid == -1) {

    die('can not fork.');

    } elseif (!$pid) {

    dummy_business();

    echo 'quit'.$i.PHP_EOL;

    break;

    }

    }

    ?>

    这次,我们也得到了期望的结果:

    mysql> select * from counter;

    +----+--------+---------+

    | id | num | version |

    +----+--------+---------+

    | 1 | 100000 | 100000 |

    +----+--------+---------+

    1 row in set (0.01 sec)

    由于乐观锁最终执行的方式相当于原子化UPDATE,因此在性能上要比悲观锁好很多。

    在高性能系统中处理并发问题,受限于后端数据库,无论何种方式加锁性能都无法高效处理如电商秒杀抢购量级的业务。使用NoSQL数据库、消息队列等方式才能更有效地完成业务的处理。

    参考文章

    展开全文
  • 数据库并发控制

    2020-11-24 17:39:08
    在多用户数据库系统中同一个时刻会有很多个并发运行的事务. 为什么数据库系统要采用并发控制? 1.提高吞吐量和资源利用率: 一个事务由多个步骤组成,一些步骤涉及I/O活动,另一些涉及CPU活动。计算机系统中CPU与...

    数据库是一个资源库,可以供多个用户使用. 允许多个用户同时使用同一个数据库的数据库系统称为多用户数据库系统.
    例如飞机订票数据库系统、银行数据库系统. 在多用户数据库系统中同一个时刻会有很多个并发运行的事务.

    为什么数据库系统要采用并发控制?

    1.提高吞吐量和资源利用率: 一个事务由多个步骤组成,一些步骤涉及I/O活动,另一些涉及CPU活动。计算机系统中CPU与磁盘可以并行运作。 因此,I/O活动可以与CPU处理并行执行。利用CPU与I/O系统的并行性,多个事务可以并行执行。当一个事务在一个磁盘上
    进行读写时,另一个事务可在CPU上运行,同时第三个事务由可以在另一个磁盘上进行读写。从而系统的吞吐量(throughout)增加,
    即给定时间内执行的事务数增加。相应的处理器与磁盘利用率也提高;处理器与磁盘空闲时间较少。

    2.减少等待时间: 系统中可能运行着各种各样的事务,短事务,长事务。如果事务串行执行,短事务可能要等待它前面的长事务完成,这可能导致难以预测的延迟。
    如果哥哥事务是针对数据库的不同部分进行操作,事务并发执行会更好,各个事务可以共享CPU周期与磁盘存取。并发执行可以减少不可预测的事务延迟执行。
    并行执行也可以减少平均响应时间(average response time),即一个事务从开始到完成所需的平均时间。

    世界是矛盾的。串行方式工作效率低,所以人们想到了并发方式,但是所有的并发都会在某种情况下带来问题。也就是所谓的并发问题。例如,在数据库中有第一类丢失更新问题,脏读问题、不可重复读问题、虚读问题、第二类丢失更新问题。所以,需要我们在开发程序的时候需要处理某些并发带来的问题。也就是说使用并发操作,但是不能有问题。通常采用总体并发,局部串行的方式。

    并发和并行的区别

    并发(concurrent)和并行(parallel)这两个概念,在数据库系统的资料中经常出现,然而有关它们的定义和区别却没有明确的说法。这里,我们根据这两个概念在资料中的使用,对它们的不同做一个说明。
    并发是指多个任务的同时执行,任务与任务之间没有联系。由于数据库系统要同时为许多用户提供服务,每个用户都可以发出自己的访问请求,一个请求就是一个任务。在一个时间点,数据库系统可能要同时处理多个任务。因此,数据库系统一定要具备并发处理能力。
    并行是指将一个任务划分为多个子任务,这些子任务同时执行。在所有子任务处理完成后,将它们的结果进行合并,就得到该任务的最终处理结果。在数据库系统中,如果要执行一个大的数据查询,为了提高速度、降低响应时间,用户可以通过系统配置或者在命令中,要求对该大数据量查询进行并行处理,将该查询划分成多个子查询。这些子查询同时执行,最后系统将所有子查询的处理结果进行合并,作为该查询处理的最终结果。现有的大型数据库系统都支持并行处理。
    需要说明的是,并发和并行与数据库系统采用多进程还是多线程体系结构无关。对采用多进程结构的数据库系统,所有的任务、子任务通过进程来处理;而对采用多线程结构的数据库系统,这些工作是由线程来完成。
    数据库系统的并发控制,涉及到任务的调度、数据的一致性及可靠性等,而数据库系统的并行处理,主要涉及任务的处理速度、系统性能等方面。

    并发控制概述

    事务是并发控制的基本单位. 数据库的并发操作可能会破坏食物的ACID特性. 为了保证食物的隔离性和一致性, 所以数据库管理系统需要对并发操作进行正确的调度.

    并发操作带来的数据不一致性(问题)

    1.丢失修改过的数据
    2.不可重复读
    3.读“脏”数据

    1.丢失修改过的数据

    事务T1和事物T2 同时并发读入一个数据num = 0, T1把num + 1 后num 为 1, T2 把num也加1, 原则上说num应该是2, 但是!!! num 还是1, 因为T2 和T1读入的num 都是0.

    2.不可重复读

    不可重复是指事物T1读取数据后,事务T2执行更新操作,使得T1无法再现前一次读取结果,(结果发生了改变,增加了,减少了,或者是丢失了)
    2.1事务T1 读取某一数据后,事务T2对其进行了修改,当事务T1再次读取该数据时,得到的与前一次不同的值。
    2.2事务T1安一定条件从数据库中读取了某些数据记录后,事务T2删除了其中部分记录,当T1再次按相同条件读取数据时,发现原来的数据中的某些记录消失了。
    2.3事务T1按一定条件从数据库中读取某些数据记录后,事务T2插入了一些记录,当T1再次按相同条件读取数据时,发现数据多了一些记录。

    3.读“脏”数据

    读“脏”数据是指事物T1修改某一数据并将其写回磁盘,事务T2读取同一数据后,T1 由于某种原因被撤销, 这是T1修改过的数据又恢复了原来的值,但是T2之前已经读了之前修改完但是没撤销的数据, 这就导致T2读到的数据和数据库中的数据不一致。 我们称T2读取到的数据为“脏”数据。

    产生上述三种问题的主要原因是并发操作破坏了事务的隔离性。 并发控制机制就是要用正确的方式调度并发操作,使一个用户事务的执行不受其他事务的干扰。 并发控制的主要技术包括:封锁(locking)、时间戳(timestamp)、乐观控制发(optimistic scheduler)和多版本并发控制(multi-version concurrency control, MVCC)等

    展开全文
  • 数据库并发处理

    2011-07-13 16:21:09
    数据库并发处理 一、并发处理数据库的特点就是数据的集中管理和共享。在通常情况下总是有若干个事务并发地运行,这些并行的事务可能并发地存取相同的数据。因此,数据库管理系统的一个重要任务就是要有一种机制去...
     

    数据库并发处理

     

    一、并发处理

    数据库的特点就是数据的集中管理和共享。在通常情况下总是有若干个事务并发地运行,这些并行的事务可能并发地存取相同的数据。因此,数据库管理系统的一个重要任务就是要有一种机制去保证这种并发的存取和修改不破坏数据的完整性,确保这些事务能正确地运行并取得正确的结果。
    我们知道,事务并发执行时若不加控制的话,将导致不正确的结果和数据库的不一致状态。为保证数据库数据正确地反映所有事务的更新,以及在一事务修改数据时其它事务不同时修改这个数据,数据库系统用锁来控制对数据的并发存取。

    二、ORACLE的并发处理机制

    无需任何说明,ORACLE自动提供行级锁,它允许用户在没有冲突的情况下更新表中不同的行。行级锁对联机事务处理非常有用。

    1、ORACLE锁

    ORACLE锁的类型在正常情况下,ORACLE会自动锁住需要加锁的资源以保护数据,这种锁是隐含的,叫隐含锁。然而,在一些条件下,这些自动的锁在实际应用时并不能满足需要,必须人工加一些锁。这些人工加的锁叫显示锁。
    下面指明了会产生隐含锁的SQL语句:

    INSERT;
    UPDATE;
    DELETE;
    DDL/DCL语句。

    下面指明了会产生显示锁的SQL语句:

    SELECT FOR UPDATE;
    LOCK TABLE INXXX MODE。

    解决读的不可重复性可以用下面的方法。在ORACLE中,用SELECT FOR UPDATE对预期要修改的记录加行排它锁(X),对表加行共享锁(RS)。它常用于要锁住一行,但不去真的修改这一行。锁之间是有相互作用的。
    例如,更新时会对表加RX锁,对行加X锁,而只有RS锁和RX锁允许再加RX锁。因此,当存在RS和RX锁时,表允许更新。再比如,当执行DDL和DCL语句时,会对表加排它锁X,而在存在X、RS、SRX、RX和S锁的前提下,都不能再加X锁。因此,当存在X,RS,SRX,RS或S锁时,不能对表做DCL和DDL操作。这样,数据库会自动防止一个用户更新表中的数据,而其他用户在同时修改表的结构。

    2、ORACLE只读事务

    ORACLE支持只读事务。只读事务有以下特点:
    *在事务中只允许查询
    *其它事务可修改和查询数据
    *在事务中,其它用户的任何修改都看不见

    只读事务的写法为:

    SET TRANS ACTION READONLY

    SQL语句

    COMMIT,ROLLBACK,DDL结束只读事务

    3、事务一致性的级别
    事务是定义和维护一致性的单位,封锁就是要保证这种一致性。如果对封锁的要求高会增加开销,降低并发性和效率;有的事务并不严格要求结果的质量(如用于统计的事务),如果加上严格的封锁则是不必要和不经济的。因此有必要进行进一步的分析,考察不同级别的一致性对数据库数据的质量及并行能力的影响。

    一致性级别定义为如下的几个条件:
    1)        事务不修改其它任何事务的脏数据。脏数据是被其它事务修改过,但尚未提交的数据。
    2)        在事务结束前不对被修改的资源解锁。
    3)        事务不读其它任何事务的脏数据。
    4)        在读前对数据加共享锁(RS)和行排它锁,直至事务结束。

    *满足条件1的事务叫第0级事务。
    *满足条件1和2的事务叫第1级一致性事务。
    *满足条件1、2和3的事务为2级一致性事务。
    ORACLE的读一致性保证了事务不读其它事务的脏数据。
    *满足条件1、2、3和4的事务叫第3级一致性事务。

    由ORACLE的三个性质:自动加隐式锁、在事务结束时释放锁和读一致性,使ORACLE成为自动满足以上的0、1和2级一致性事务。因此,ORACLE自动防止了脏读(写-读依赖)。但是,ORACLE不能自动防止丢失修改(写-写依赖),读的不可重复性(读-写依赖),彻底解决并发性中的问题还需满足第4个条件(3级一致性事务),这需要程序员根据实际情况编程。

    方法如下:
    *如果想在一段时间内使一些数据不被其它事务改变,且在本事务内仅仅查询数据,则可用SETTRANSACTIONREADONLY语句达到这一目的。
    *如果想在一事务内修改一数据,且避免丢失修改,则应在读这一数据前用SELECTFORUPDATE对该数据加锁。
    *如果想在一事务内读一数据,且想基于这一数据对其它数据修改,则应在读数据前对此数据用SELECTFORUPDATE加锁。对此种类型的应用,用这条SQL语句加锁最恰当。
    *如果想避免不可重复读现象,可在读前用SELECTFORUPDATE对数据加锁,或用SET TRANS ACTION READONLY设置只读事务。

    三、SYBASE的并发处理机制


    SYBASE的并发处理方法与ORACLE类似,但在很多方面不一样。SYBASE有两种粒度的封锁,一种的粒度是页,另一种的粒度是表。SYBASE根据SQL语句的情况决定用页封锁还是用表封锁。

    1、页级锁
    页级锁有以下所始的三类:
    *SHARED:在读操作时加共享锁。在缺省状态下,在读操作完成后释放共享锁。
    *EXCLUSIVE:在更新操作时加排它锁。在缺省状态下,在事务完成后释放排它锁。
    *UPDATE:在修改和删除操作的初期(读到被修改或删除的页时)加修改锁。在表上加了修改锁之后,还可以再加共享锁,但不能再加修改和排它锁。在进行修改和删除操作时,如果没有共享锁存在,修改锁则转化为排它锁。此锁的目的是为了防止死锁。SYBASE仅当在WHERE子句中包含索引列时才会使用页级的排它锁和修改锁。

    2、表级锁
    表级锁有以下所示的三类:
    *INTENT:当表中存在页级的排它锁和共享锁时,在表上加意向锁。在所有的页级锁释放后,意向锁随着释放。
    *SHARED:在读操作时加共享锁。在缺省状态下,在读操作完成后释放共享锁。
    *EXCLUSIVE:在更新操作时加排它锁。在缺省状态下,在事务完成后释放排它锁。

    3、请求锁
    请求锁用以防止共享锁一个接一个无休止地加在表上,从而写事务(要加排它锁)无法进行。

    4、SYBASE的封锁级别
    在SYBASE根据ANSI标准定义事务的封锁级别:

    (1)   级别1:脏读
    (2)   级别2:不可重复读
    (3)   光标带来的当前值混乱
    (4)   SYBASE的缺省一致性级别为1。

    如果要达到一致性级别2和3,必须使用HOLDLOCK关键字把共享锁持续到事务的结束。方法如下:

    SELECT*FROM AUTHS HOLDLOCK
    WHERE AUTHOR_CODE='A00001'

    SYBASE还可以通过T-SQL的SET命令改变SYBASE的一致性级别,从而使SYBASE自动在SELECT语句中加HOLDLOCK关键字:
    SET TRANS ACTION IS OLATION LEVEL3

    5、在SYBASE中提高并发效率的方法
    *避免在表中特定的页上多个用户过多的封锁。
    *避免在人机交互的应用中定义事务,这样会使某个用户长时间封锁住表(如去接电话),使其他用户持续等待。
    *使事务尽量的短。
    *仅当必要时才使用HOLDLOCK关键字。

     

    展开全文
  • 数据库并发控制详解

    千次阅读 2019-04-16 11:05:20
    概述 1. 什么是并发控制? 数据库是一个共享资源,可以提供多个用户使用。这些用户程序可以一个一个地串行执行...因此,为了充分利用数据库资源,发挥数据库共享资源的特点,应该允许多个用户并行地存取数据库。但这...

    概述

    1. 什么是并发控制?

    数据库是一个共享资源,可以提供多个用户使用。这些用户程序可以一个一个地串行执行,每个时刻只有一个用户程序运行,执行对数据库的存取,其他用户程序必须等到这个用户程序结束以后方能对数据库存取。但是如果一个用户程序涉及大量数据的输入/输出交换,则数据库系统的大部分时间处于闲置状态。因此,为了充分利用数据库资源,发挥数据库共享资源的特点,应该允许多个用户并行地存取数据库。但这样就会产生多个用户程序并发存取同一数据的情况,若对并发操作不加控制就可能会存取和存储不正确的数据,破坏数据库的一致性,所以数据库管理系统必须提供并发控制机制。并发控制机制的好坏是衡量一个数据库管理系统性能的重要标志之一。

    2. 为什么要进行并发控制?

    数据库是共享资源,通常有许多个事务同时在运行。当多个事务并发地存取数据库时就会产生同时读取和/或修改同一数据的情况。若对并发操作不加控制就可能会存取和存储不正确的数据,破坏数据库的一致性。所以数据库管理系统必须提供并发控制机制。
    3. 并发控制概述

    并发控制的单位――事务

    事务是数据库的逻辑工作单位,它是用户定义的一组操作序列。一个事务可以是一组SQL语句、一条SQL语句或整个程序。

    事务的开始和结束都可以由用户显示的控制,如果用户没有显式地定义事务,则由数据库系统按缺省规定自动划分事务。

    封锁

    封锁是事项并发控制的一个非常重要的技术。所谓封锁就是事务T在对某个数据对象,例如,在标、记录等操作之前,先向系统发出请求,对其加锁。加锁后事务T就对数据库对象有了一定的控制,在事务T释放它的锁之前,其他事务不能更新此数据对象。
    基本的封锁类型有两种:排它锁( Exclusive Locks ,简称 x 锁)和共享锁 ( Share Locks,简称 S 锁)。排它锁又称为写锁。若事务 T 对数据对象 A 加上 X 锁,则只允许 T 读取和修改 A ,其他任何事务都不能再对 A 加任何类型的锁,直到 T 释放 A 上的锁。这就保证了其他事务在 T 释放 A 上的锁之前不能再读取和修改 A 。共享锁又称为读锁。若事务 T 对数据对象 A 加上 S 锁,则事务 T 可以读 A但不能修改 A ,其他事务只能再对 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。这就保证了其他事务可以读 A ,但在 T 释放 A 上的 S 锁之前不能对 A 做任何修改。

    封锁的粒度
    X锁和S锁都是加在某一个数据对象上的。封锁的对象可以是逻辑单元,也可以是物理单元。例如,在关系数据库中,封锁对象可以是属性值、属性值集合、元组、关系、索引项、整个索引、整个数据库等逻辑单元;也可以是页(数据页或索引页)、块等物理单元。封锁对象可以很大,比如对整个数据库加锁,也可以很小,比如只对某个属性值加锁。封锁对象的大小称为封锁的粒度(granularity)。

    封锁粒度与系统的并发度和并发控制的开销密切相关。封锁的粒度越大,系统中能够被封锁的对象就越小,并发度也就越小,但同时系统开销也越小;相反,封锁的粒度越小,并发度越高,但系统开销也就越大。
    因此,如果在一个系统中同时存在不同大小的封锁单元供不同的事务选择使用是比较理想的。而选择封锁粒度时必须同时考虑封锁机构和并发度两个因素,对系统开销与并发度进行权衡,以求得最优的效果。一般说来,需要处理大量元组的用户事务可以以关系为封锁单元;需要处理多个关系的大量元组的用户事务可以以数据库为封锁单位;而对于一个处理少量元组的用户事务,可以以元组为封锁单位以提高并发度。

    封锁协议
    封锁的目的是为了保证能够正确地调度并发操作。为此,在运用X锁和S锁这两种基本封锁,对一定粒度的数据对象加锁时,还需要约定一些规则,例如,应何时申请X锁或S锁、持锁时间、何时释放等。我们称这些规则为封锁协议(locking protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议,它们分别在不同的程度上为并发操作的正确调度提供一定的保证。本节介绍保证数据一致性的三级封锁协议和保证并行调度可串行性的两段锁协议,下一节将介绍避免死锁的封锁协议。

    保证数据一致性的封锁协议――三级封锁协议
    ① 1级封锁协议

    1级封锁协议的内容是:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(commit)和非正常结束(rollback)。
    1级封锁协议可以防止丢失或覆盖更新,并保证事务T是可以恢复的。
    在1级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,所以它不能保证可重复读和脏读。

    ② 2级封锁协议

    2级封锁协议的内容是:1级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后即可释放S锁。
    2级封锁协议除防止了丢失或覆盖更新,还可进一步防止脏读。
    在2级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。

    ③ 3级封锁协议

    3级封锁协议的内容是:1级封锁协议加上事务T在读取数据之前必须先对其加S锁,直到事务结束才释放。
    3级封锁协议除防止丢失或覆盖更新和不脏读数据外,还进一步防止了不可重复读和幻想读。

    上述三级协议的主要区别在于什么操作需要申请封锁以及何时释放锁(即持锁时间)。

    死锁和活锁

    在这里插入图片描述
    如果事务 Tl 封锁了数据 R ,事务几又请求封锁 R ,于是几等待。几也请求封锁 R ,当 Tl 释放了 R 上的封锁之后系统首先批准了几的请求,几仍然等待。然后几又请求封锁 R ,当几释放了 R 上的封锁之后系统又批准了几的请求 … … 几有可能永远等待,这就是活锁的情形。活锁的含义是该等待事务等待时间太长,似乎被锁住了,实际上可能被激活。如果事务 Tl 封锁了数据 Rl ,几封锁了数据凡,然后 Tl 又请求封锁几,因几已封锁了几,于是 Tl 等待几释放几上的锁。接着几又申请封锁 Rl ,因 Tl 已封锁了 Rl ,几也只能等待 Tl 释放 Rl 上的锁。这样就出现了 Tl 在等待几,而几又在等待 T }的局面, T }和几两个事务永远不能结束,形成死锁。

    活锁产生的原因:当一系列封锁不能按照其先后顺序执行时,就可能导致一些事务无限期等待某个封锁,从而导致活锁。避免活锁的简单方法是采用先来先服务的策略。当多个事务请求封锁同一数据对象时,封锁子系统按请求封锁的先后次序对事务排队,数据对象上的锁一旦释放就批准申请队列中第一个事务获得锁。

    并发控制—锁和MVCC

    • 悲观并发控制
      控制不同的事务对同一份数据的获取是保证数据库的一致性的最根本方法,如果我们能够让事务在同一时间对同一资源有着独占的能力,那么就可以保证操作同一资源的不同事务不会相互影响。
      最简单的、应用最广的方法就是使用锁来解决,当事务需要对资源进行操作时需要先获得资源对应的锁,保证其他事务不会访问该资源后,在对资源进行各种操作;在悲观并发控制中,数据库程序对于数据被修改持悲观的态度,在数据处理的过程中都会被锁定,以此来解决竞争的问题。
      读写锁
      为了最大化数据库事务的并发能力,数据库中的锁被设计为两种模式,分别是共享锁和互斥锁。当一个事务获得共享锁之后,它只可以进行读操作,所以共享锁也叫读锁;而当一个事务获得一行数据的互斥锁时,就可以对该行数据进行读和写操作,所以互斥锁也叫写锁。

    共享锁和互斥锁除了限制事务能够执行的读写操作之外,它们之间还有『共享』和『互斥』的关系,也就是多个事务可以同时获得某一行数据的共享锁,但是互斥锁与共享锁和其他的互斥锁并不兼容,我们可以很自然地理解这么设计的原因:多个事务同时写入同一数据难免会发生各种诡异的问题。

    如果当前事务没有办法获取该行数据对应的锁时就会陷入等待的状态,直到其他事务将当前数据对应的锁释放才可以获得锁并执行相应的操作。

    两阶段锁协议
    两阶段锁协议(2PL)是一种能够保证事务可串行化的协议,它将事务的获取锁和释放锁划分成了增长(Growing)和缩减(Shrinking)两个不同的阶段。

    在增长阶段,一个事务可以获得锁但是不能释放锁;而在缩减阶段事务只可以释放锁,并不能获得新的锁,如果只看 2PL 的定义,那么到这里就已经介绍完了,但是它还有两个变种:

    Strict 2PL:事务持有的互斥锁必须在提交后再释放;
    Rigorous 2PL:事务持有的所有锁必须在提交后释放;

    虽然锁的使用能够为我们解决不同事务之间由于并发执行造成的问题,但是两阶段锁的使用却引入了另一个严重的问题,死锁;不同的事务等待对方已经锁定的资源就会造成死锁

    两个事务在刚开始时分别获取了 draven 和 beacon 资源上面的锁,然后再请求对方已经获得的锁时就会发生死锁,双方都没有办法等到锁的释放,如果没有死锁的处理机制就会无限等待下去,两个事务都没有办法完成。

    死锁的处理

    • 预防死锁
      有两种方式可以帮助我们预防死锁的出现,一种是保证事务之间的等待不会出现环,也就是事务之间的等待图应该是一张有向无环图,没有循环等待的情况或者保证一个事务中想要获得的所有资源都在事务开始时以原子的方式被锁定,所有的资源要么被锁定要么都不被锁定。

    但是这种方式有两个问题,在事务一开始时很难判断哪些资源是需要锁定的,同时因为一些很晚才会用到的数据被提前锁定,数据的利用率与事务的并发率也非常的低。一种解决的办法就是按照一定的顺序为所有的数据行加锁,同时与 2PL 协议结合,在加锁阶段保证所有的数据行都是从小到大依次进行加锁的,不过这种方式依然需要事务提前知道将要加锁的数据集。

    另一种预防死锁的方法就是使用抢占加事务回滚的方式预防死锁,当事务开始执行时会先获得一个时间戳,数据库程序会根据事务的时间戳决定事务应该等待还是回滚,在这时也有两种机制供我们选择,一种是 wait-die 机制:
    当执行事务的时间戳小于另一事务时,即事务 A 先于 B 开始,那么它就会等待另一个事务释放对应资源的锁,否则就会保持当前的时间戳并回滚。
    另一种机制叫做 wound-wait,这是一种抢占的解决方案,它和 wait-die 机制的结果完全相反,当前事务如果先于另一事务执行并请求了另一事务的资源,那么另一事务会立刻回滚,将资源让给先执行的事务,否则就会等待其他事务释放资源:
    两种方法都会造成不必要的事务回滚,由此会带来一定的性能损失,更简单的解决死锁的方式就是使用超时时间,但是超时时间的设定是需要仔细考虑的,否则会造成耗时较长的事务无法正常执行,或者无法及时发现需要解决的死锁,所以它的使用还是有一定的局限性。

    锁的粒度

    当我们拥有了不同粒度的锁之后,如果某个事务想要锁定整个数据库或者整张表时只需要简单的锁住对应的节点就会在当前节点加上显示(explicit)锁,在所有的子节点上加隐式(implicit)锁;虽然这种不同粒度的锁能够解决父节点被加锁时,子节点不能被加锁的问题,但是我们没有办法在子节点被加锁时,立刻确定父节点不能被加锁。

    在这时我们就需要引入意向锁来解决这个问题了,当需要给子节点加锁时,先给所有的父节点加对应的意向锁,意向锁之间是完全不会互斥的,只是用来帮助父节点快速判断是否可以对该节点进行加锁。

    乐观并发控制

    乐观并发控制也叫乐观锁,但是它并不是真正的锁,很多人都会误以为乐观锁是一种真正的锁,然而它只是一种并发控制的思想。

    • 基于时间戳的协议
      锁协议按照不同事务对同一数据项请求的时间依次执行,因为后面执行的事务想要获取的数据已将被前面的事务加锁,只能等待锁的释放,所以基于锁的协议执行事务的顺序与获得锁的顺序有关。在这里想要介绍的基于时间戳的协议能够在事务执行之前先决定事务的执行顺序。

    每一个事务都会具有一个全局唯一的时间戳,它即可以使用系统的时钟时间,也可以使用计数器,只要能够保证所有的时间戳都是唯一并且是随时间递增的就可以。

    基于时间戳的协议能够保证事务并行执行的顺序与事务按照时间戳串行执行的效果完全相同;每一个数据项都有两个时间戳,读时间戳和写时间戳,分别代表了当前成功执行对应操作的事务的时间戳。

    该协议能够保证所有冲突的读写操作都能按照时间戳的大小串行执行,在执行对应的操作时不需要关注其他的事务只需要关心数据项对应时间戳的值就可以了:

    无论是读操作还是写操作都会从左到右依次比较读写时间戳的值,如果小于当前值就会直接被拒绝然后回滚,数据库系统会给回滚的事务添加一个新的时间戳并重新执行这个事务。

    • 基于验证的协议

    乐观并发控制其实本质上就是基于验证的协议,因为在多数的应用中只读的事务占了绝大多数,事务之间因为写操作造成冲突的可能非常小,也就是说大多数的事务在不需要并发控制机制也能运行的非常好,也可以保证数据库的一致性;而并发控制机制其实向整个数据库系统添加了很多的开销,我们其实可以通过别的策略降低这部分开销。

    而验证协议就是我们找到的解决办法,它根据事务的只读或者更新将所有事务的执行分为两到三个阶段:

    在读阶段,数据库会执行事务中的全部读操作和写操作,并将所有写后的值存入临时变量中,并不会真正更新数据库中的内容;在这时候会进入下一个阶段,数据库程序会检查当前的改动是否合法,也就是是否有其他事务在 RAED PHASE 期间更新了数据,如果通过测试那么直接就进入 WRITE PHASE 将所有存在临时变量中的改动全部写入数据库,没有通过测试的事务会直接被终止。

    为了保证乐观并发控制能够正常运行,我们需要知道一个事务不同阶段的发生时间,包括事务开始时间、验证阶段的开始时间以及写阶段的结束时间;通过这三个时间戳,我们可以保证任意冲突的事务不会同时写入数据库,一旦由一个事务完成了验证阶段就会立即写入,其他读取了相同数据的事务就会回滚重新执行。

    作为乐观的并发控制机制,它会假定所有的事务在最终都会通过验证阶段并且执行成功,而锁机制和基于时间戳排序的协议是悲观的,因为它们会在发生冲突时强制事务进行等待或者回滚,哪怕有不需要锁也能够保证事务之间不会冲突的可能。

    多版本并发控制

    MySQL 中实现的多版本两阶段锁协议(Multiversion 2PL)将 MVCC 和 2PL 的优点结合了起来,每一个版本的数据行都具有一个唯一的时间戳,当有读事务请求时,数据库程序会直接从多个版本的数据项中具有最大时间戳的返回。

    MySQL 在InnoDB引擎下有当前读和快照读两种模式。
    1 当前读即加锁读,读取记录的最新版本号,会加锁保证其他并发事物不能修改当前记录,直至释放锁。插入/更新/删除操作默认使用当前读,显示的为select语句加lock in share mode或for update的查询也采用当前读模式。

    2 快照读:不加锁,读取记录的快照版本,而非最新版本,使用MVCC机制,最大的好处是读取不需要加锁,读写不冲突,用于读操作多于写操作的应用,因此在不显示加[lock in share mode]/[for update]的select语句,即普通的一条select语句默认都是使用快照读MVCC实现模式。所以楼主的为了让大家明白所做的演示操作,既有当前读也有快照读……

    • MVCC实现
      MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC. 不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制.
      InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),没开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID.

    INSERT
    InnoDB为新插入的每一行保存当前系统版本号作为版本号.

    SELECT
    InnoDB会根据以下两个条件检查每行记录:
    a.InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
    b.行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
    只有a,b同时满足的记录,才能返回作为查询结果.

    DELETE
    InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识.

    UPDATE
    InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间.

    展开全文
  • oracle数据库并发处理

    2012-03-28 15:10:43
    oracle数据库并发处理 一、并发处理 数据库的特点就是数据的集中管理和共享。在通常情况下总是有若干个事务并发地运行,这些并行的事务可能并发地存取相同的数据。因此,数据库管理系统的一个重要任务就是要有一种...
  • php 数据库并发处理

    2016-05-31 16:40:00
    并行系统中并发问题永远不可忽视。尽管PHP语言原生没有提供多线程机制,那并不意味着所有的操作都是线程安全的。尤其是在操作诸如订单、支付等业务系统中,更需要注意操作数据库并发问题。 接下来我通过一个案例...
  • 数据库并发控制技术

    2015-03-18 19:57:42
    数据库是一个共享资源,可以提供多个用户使用。这些用户程序可以一个一个地串行...因此,为了充分利用数据库资源,发挥数据库共享资源的特点,应该允许多个用户并行地存取数据库。但这样就会产生多个用户程序并发存取
  • 注意同一时间间隔(并发)和同一时刻(并行)的区别。在多道程序环境下,一定时间内,宏观上有多道程序在同时执行,而在每个时刻,单处理机环境下仅有一道程序能够执行,因此微观上这些程序仍是分时交替执行的。操作...
  • 浅尝数据库并发控制

    千次阅读 2012-03-15 20:30:39
    然而,对于大多数程序员来说,单处理机系统是我们接触最多的系统,运行在其上的数据库事务也并非真正意义上的并行,实际上它是这些并行事务的并行操作轮流交叉运行,这种并行执行方式称为交叉并发方式(Interleaved ...
  • 并行系统中并发问题永远不可忽视。尽管PHP语言原生没有提供多线程机制,那并不意味着所有的操作都是线程安全的。尤其是在操作诸如订单、支付等业务系统中,更需要注意操作数据库并发问题。 接下来我通过一个案例...
  • ①交叉并发方式:单处理机系统中,事务的并行执行实际上是这些并行事务的并行操作轮流交叉进行,这种并发执行的方式称为交叉并发方式。 ②同时并发方式:多处理机系统中,每个处理机运行一个事务,多个处理机同时...
  • 1、并发是什么,和并行的区别? 2、实现并发控制的方式有哪些? 3、总结分析 二、并发是什么,和并行的区别? 并发(concurrency)和并行(parallellism)是: 解释一:并行是指两个或者多个事件在同一时刻发生;而...
  • 并发并行的区别: 并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码...
  • 数据库是一个共享资源,可以提供多个用户使用。这些用户程序可以一个一个地串行...因此,为了充 分利用数据库资源,发挥数据库共享资源的特点,应该允许多个用户并行地存取数据库。但这样就会产生多个用户程序并...
  • 推荐教程:sql教程什么是数据库并发操作数据库是一个共享资源,可以提供多个用户使用。这些用户程序可以一个一个地串行执行,每个时刻只有一个用户程序运行,执行对数据库的存取,其他用户程序必须等到这个用户程序...
  • 关系数据库——并发控制

    千次阅读 多人点赞 2019-12-02 14:04:03
    并发控制 多用户数据库:允许多个用户同时...单处理机系统中,事务的并发执行实际上是这些并行事务的并行操作轮流交叉运行(不是真正的并发,但是提高了系统效率) 3.同时并发方式: 多处理机系统中,每个处理机...
  • 数据库是一个共享资源,可以提供多个用户使用。这些用户程序可以一个一个地串行...因此,为了充分利用数据库资源,发挥数据库共享资源的特点,应该允许多个用户并行地存取数据库。但这样就会产生多个用户程序并发存取
  • (二)并发操作的调度  计算机系统对并行事务中并行操作的调度是随机的,而不同的调度可能会产生不同的结果,那么哪个结果是正确的,哪个是不正确的呢?  如果一个事务运行过程中没有其他事务在同时运行,.....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,064
精华内容 425
关键字:

数据库并发并行