如何处理数据库中的并发更新?

Lee*_*roy 29 sql concurrency

在SQL数据库中处理并发更新的常用方法是什么?

考虑一个简单的SQL模式(约束和默认值未显示..)之类的

create table credits (
  int id,
  int creds,
  int user_id
);
Run Code Online (Sandbox Code Playgroud)

目的是为用户存储某种信用,例如stackoverflow的声誉.

如何处理该表的并发更新?一些选择:

  • update credits set creds= 150 where userid = 1;

    在这种情况下,应用程序检索当前值,计算新值(150)并执行更新.如果其他人同时做同样的事情,那么这就是灾难.我猜测包装当前值的撤销和事务中的更新将解决这个问题,例如,Begin; select creds from credits where userid=1; do application logic to calculate new value, update credits set credits = 160 where userid = 1; end;在这种情况下,您可以检查新信用是否<0,如果负信用没有意义,则将其截断为0.

  • update credits set creds = creds - 150 where userid=1;

    这种情况不需要担心并发更新,因为数据库负责一致性问题,但是有一些缺陷,即信用很高兴会变成负面的,这可能对某些应用程序没有意义.

那么简单地说,处理上面提到的(非常简单的)问题的公认方法是什么,如果db抛出错误怎么办?

bdo*_*lan 26

使用交易:

BEGIN WORK;
SELECT creds FROM credits WHERE userid = 1;
-- do your work
UPDATE credits SET creds = 150 WHERE userid = 1;
COMMIT;
Run Code Online (Sandbox Code Playgroud)

一些重要的说明:

  • 并非所有数据库类型都支持事务 特别是,mysql的旧默认数据库引擎(版本5.5.5之前的默认值)MyISAM没有.如果您使用的是mysql,请使用InnoDB(新的默认值).
  • 由于您无法控制的原因,交易可能会中止.如果发生这种情况,您的申请必须准备从BEGIN WORK重新开始.
  • 您需要将隔离级别设置为SERIALIZABLE,否则第一个选择可以读取其他事务尚未提交的数据(事务不像编程语言中的互斥锁).如果存在并发的SERIALIZABLE事务,某些数据库将抛出错误,您将不得不重新启动事务.
  • 一些DBMS提供SELECT .. FOR UPDATE,它将锁定由select检索的行,直到事务结束.

将事务与SQL存储过程相结合可以使后一部分更容易处理; 应用程序只会在事务中调用单个存储过程,并在事务中止时重新调用它.

  • 在这种情况下,您不需要“选择更新”,或者至少需要一个可串行化的隔离级别吗? (2认同)
  • @nos,这取决于数据库.具有真正事务支持的数据库应该只为事务提供一致的快照,尽管可能不是默认情况.对于innodb,只要在任何innodb表上执行选择,就会生成数据库状态的快照. (2认同)
  • @Ankush,大多数SQL服务器将识别死锁并回滚其中一个解决死锁的txns.根据您的SQL服务器,您还可以使用"SELECT ... FOR UPDATE"或类似命令从一开始就获取写锁定. (2认同)
  • @bdonlan确实,SQL会识别死锁并挑选受害者,但这会严重损害您的产品性能,特别是如果上面的代码位于关键路径上. (2认同)

小智 17

对于MySQL InnoDB表,这实际上取决于您设置的隔离级别.

如果使用默认级别3(REPEATABLE READ),则需要锁定影响后续写入的任何行,即使您处于事务中也是如此.在您的示例中,您将需要:

SELECT FOR UPDATE creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;
Run Code Online (Sandbox Code Playgroud)

如果您使用的是4级(SERIALIZABLE),那么简单的SELECT后跟更新就足够了.InnoDB中的第4级是通过读取锁定的每一行来实现的.

SELECT creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;
Run Code Online (Sandbox Code Playgroud)

但是在这个具体的例子中,由于计算(添加信用)很简单,可以在SQL中完成,这很简单:

UPDATE credits set creds = creds - 150 where userid=1;
Run Code Online (Sandbox Code Playgroud)

将等同于SELECT FOR UPDATE,后跟UPDATE.


Abe*_*ROS 12

将代码包装在事务中,在某些情况下,无论您定义的隔离级别如何,都是不够的.

假设您有这些步骤和2个并发线程:

1) open a transaction
2) fetch the data (SELECT creds FROM credits WHERE userid = 1;)
3) do your work (credits + amount)
4) update the data (UPDATE credits SET creds = ? WHERE userid = 1;)
5) commit
Run Code Online (Sandbox Code Playgroud)

这个时间线:

Time =  0; creds = 100
Time =  1; ThreadA executes (1) and creates Txn1
Time =  2; ThreadB executes (1) and creates Txn2
Time =  3; ThreadA executes (2) and fetches 100
Time =  4; ThreadB executes (2) and fetches 100
Time =  5; ThreadA executes (3) and adds 100 + 50
Time =  6; ThreadB executes (3) and adds 100 + 50
Time =  7; ThreadA executes (4) and updates creds to 150
Time =  8; ThreadB tries to executes (4) but in the best scenario the transaction
          (depending of isolation level) won't allow it and you get an error
Run Code Online (Sandbox Code Playgroud)

事务阻止您使用错误的值覆盖creds值,但这还不够,因为我不想让任何错误失败.

我更喜欢一个永不失败的慢速进程,我在获取数据时采用"数据库行锁定"来解决问题(步骤2),以防止其他线程读取同一行,直到我完成它为止.

在SQL Server中有几种方法可以做,这是其中之一:

SELECT creds FROM credits WITH (UPDLOCK) WHERE userid = 1;
Run Code Online (Sandbox Code Playgroud)

如果我使用此改进重新创建上一个时间线,您将获得以下内容:

Time =  0; creds = 100
Time =  1; ThreadA executes (1) and creates Txn1
Time =  2; ThreadB executes (1) and creates Txn2
Time =  3; ThreadA executes (2) with lock and fetches 100
Time =  4; ThreadB tries executes (2) but the row is locked and 
                   it's has to wait...

Time =  5; ThreadA executes (3) and adds 100 + 50
Time =  6; ThreadA executes (4) and updates creds to 150
Time =  7; ThreadA executes (5) and commits the Txn1

Time =  8; ThreadB was waiting up to this point and now is able to execute (2) 
                   with lock and fetches 150
Time =  9; ThreadB executes (3) and adds 150 + 50
Time = 10; ThreadB executes (4) and updates creds to 200
Time = 11; ThreadB executes (5) and commits the Txn2
Run Code Online (Sandbox Code Playgroud)


Ovi*_*pas 5

使用新timestamp列的乐观锁可以解决这个并发问题。

UPDATE credits SET creds = 150 WHERE userid = 1 and modified_data = old_modified_date
Run Code Online (Sandbox Code Playgroud)