如何处理数据库中的并发更新?
在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,不允许脏写入