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

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

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

create table credits ( int id, int creds, int user_id ); 

其目的是为用户存储某种types的信用,例如像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; 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引发错误,怎么办?

使用交易:

 BEGIN WORK; SELECT creds FROM credits WHERE userid = 1; -- do your work UPDATE credits SET creds = 150 WHERE userid = 1; COMMIT; 

一些重要的说明:

  • 并非所有数据库types都支持事务 特别是,mysql的默认数据库typesMyISAM没有。 如果你在MySQL上使用InnoDB。
  • 交易可能因您无法控制的原因而中止。 如果发生这种情况,您的申请必须准备从BEGIN WORK开始重新开始。
  • 您需要将隔离级别设置为SERIALIZABLE,否则第一个select可以读取其他事务尚未提交的数据(事务不像编程语言中的互斥体)。 有些数据库会在发生并行正在进行的SERIALIZABLE事务时引发错误,您必须重新启动事务。
  • 一些DBMS提供了SELECT .. FOR UPDATE,它将lockingselect所返回的行,直到事务结束。

将事务与SQL存储过程结合起来可以使后者更容易处理; 应用程序只会调用事务中的单个存储过程,并在事务中止时重新调用它。

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

如果使用默认级别3(REPEATABLE READ),那么即使您处于事务中,也需要locking影响后续写入的行。 在你的例子中,你将需要:

 SELECT FOR UPDATE creds FROM credits WHERE userid = 1; -- calculate -- UPDATE credits SET creds = 150 WHERE userid = 1; 

如果你正在使用4级(SERIALIZABLE),那么一个简单的SELECT和更新就足够了。 InnoDB中的级别4是通过读取locking读取的每一行来实现的。

 SELECT creds FROM credits WHERE userid = 1; -- calculate -- UPDATE credits SET creds = 150 WHERE userid = 1; 

然而在这个特定的例子中,由于计算(join信用)足够简单,可以在SQL中完成,所以简单:

 UPDATE credits set creds = creds - 150 where userid=1; 

将相当于SELECT FOR UPDATE,后跟UPDATE。

在事务内部封装代码在某些情况下是不够的,无论您定义的隔离级别如何。

假设你有这些步骤和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 

而这段时间:

 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 

交易可以防止你用一个错误的值覆盖信用值,但这还不够,因为我不想让任何错误失败。

我宁愿select一个永远不会失败的较慢进程,我在获取数据(第2步)时解决了“数据库行locking”的问题,以防止其他线程在完成之前读取同一行。

在SQL Server中有几种方法可以做到这一点:

 SELECT creds FROM credits WITH (UPDLOCK) WHERE userid = 1; 

如果我使用这种改进重新创build之前的时间线,可以得到如下所示的内容:

 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 

使用新的timestamp列进行乐观locking可以解决此并发问题。

 UPDATE credits SET creds = 150 WHERE userid = 1 and modified_data = old_modified_date 

对于第一种情况,您可以在where子句中添加另一个条件,以确保不会覆盖并发用户所做的更改。 例如

 update credits set creds= 150 where userid = 1 AND creds = 0; 

你可以build立一个排队机制,在sorting机制中,sortingtypes值的增加或减less将排队等待某个工作的定期LIFO处理。 如果需要关于排名的“余额”的实时信息,则不适合,因为在未结清的排队条目被调和之前,余额将不计算,但是如果它不需要立即调整,则它可以起作用。

这似乎反映了,至less在外部看来,像旧的装甲通用系列游戏如何处理个人动作。 轮到一个玩家,他们宣布他们的动作。 每个动作依次处理,并没有冲突,因为每个动作都在队列中的位置。

如果您将最后一次更新时间戳记存储在logging中,那么当您读取该值时,请同时读取时间戳记。 当你去更新logging时,检查以确保时间戳匹配。 如果有人进来,在你之前更新,时间戳不匹配。

在你的情况下,当你减less用户的当前信用领域的一个要求的数量是一个关键点,如果它减less了成功,你做其他操作和问题在理论上可以有许多并行请求减less操作时,例如用户余额为1学分,并且有5个平行的1个信用额度请求,如果请求将在同一时间完全发送,那么他可以购买5件东西,最终用户余额为-4个学分。

为了避免这种情况, 您应该减less当前信用值与请求金额 (在我们的示例1信贷), 并检查当前值减去请求金额大于或等于零的地方

更新信用SET creds = creds-1 WHERE creds-1> = 0和userid = 1

这将保证用户永远不会购买很less的东西,如果他将你的系统。

在这个查询之后,您应该运行ROW_COUNT(),告诉当前用户信用是否符合条件并且行已经更新:

 UPDATE credits SET creds = creds-1 WHERE creds-1>=0 and userid = 1 IF (ROW_COUNT()>0) THEN --IF WE ARE HERE MEANS USER HAD SURELY ENOUGH CREDITS TO PURCHASE THINGS END IF; 

类似的事情在PHP中可以做到这一点:

 mysqli_query ("UPDATE credits SET creds = creds-$amount WHERE creds-$amount>=0 and userid = $user"); if (mysqli_affected_rows()) { \\do good things here } 

在这里,我们也没有使用SELECT … FOR UPDATE既没有TRANSACTION,但是如果你把这段代码放到事务中,只要确保事务级别总是提供来自行的最新数据(包括已经提交的其他事务)。 如果ROW_COUNT()= 0,也可以用户ROLLBACK

没有行锁的WHERE credit – $ amount> = 0的下行是:

更新后,你肯定知道一个用户有足够的余额信用余额,即使他尝试哟黑客信用与许多请求,但你不知道什么是信用前收费(更新)和什么是信用后收费(更新)其他的东西。

警告:

在不提供最近行数据的事务级别内不要使用这个策略。

如果您想了解更新前后的价值,请不要使用此策略。

只要试图依靠信贷成功收取而不低于零的事实。

表可以修改如下,引入新的字段版本来处理乐观锁。 这是更具成本效益和更有效的方法来实现更好的性能,而不是在数据库级别使用锁创build表信用(int id,int creds,int user_id,int版本);

从其中user_id = 1的信用中selectcreds,user_id,version;

假设这返回信用= 100和版本= 1

更新信用设置creds = creds * 10,version = version + 1其中user_id = 1和version = 1;

始终确保拥有最新版本号的用户只能更新此logging,不允许脏写入