在SQL数据库中处理并发更新的常用方法是什么?
考虑一个简单的SQL模式(约束和默认值未显示..),如
create table credits (
int id,
int creds,
int user_id
);
意图是为用户存储某种信用,例如,类似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抛出错误怎么办?
答案 0 :(得分:26)
使用交易:
BEGIN WORK;
SELECT creds FROM credits WHERE userid = 1;
-- do your work
UPDATE credits SET creds = 150 WHERE userid = 1;
COMMIT;
一些重要的注释:
将事务与SQL存储过程相结合可以使后一部分更容易处理;应用程序只会在事务中调用单个存储过程,并在事务中止时重新调用它。
答案 1 :(得分:16)
对于MySQL InnoDB表,这实际上取决于您设置的隔离级别。
如果使用默认级别3(REPEATABLE READ),则需要锁定影响后续写入的任何行,即使您处于事务中也是如此。在您的示例中,您需要:
SELECT FOR UPDATE creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;
如果您使用的是4级(SERIALIZABLE),那么简单的SELECT后跟更新就足够了。 InnoDB中的第4级是通过读取锁定的每一行来实现的。
SELECT creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;
但是在这个具体的例子中,由于计算(添加信用)很简单,可以在SQL中完成,简单:
UPDATE credits set creds = creds - 150 where userid=1;
将等同于SELECT FOR UPDATE,后跟UPDATE。
答案 2 :(得分:11)
在事务中包装代码在某些情况下是不够的,无论您定义的隔离级别如何(例如,您已将代码部署到生产中的2个不同服务器中)。
假设您有这些步骤和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
事务阻止你用错误的值覆盖creds值,但这还不够,因为我不想让任何错误失败。
我更喜欢一个永不失败的慢速进程,我在获取数据的时候用“数据库行锁定”解决了问题(步骤2),防止其他线程读取同一行直到我完成它
在SQL Server中有几种方法可以做,其中之一是:
SELECT creds FROM credits WITH (UPDLOCK) WHERE userid = 1;
如果我使用此改进重新创建上一个时间线,您将获得以下内容:
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
答案 3 :(得分:2)
对于第一个场景,您可以在where子句中添加另一个条件,以确保不会覆盖并发用户所做的更改。例如。
update credits set creds= 150 where userid = 1 AND creds = 0;
答案 4 :(得分:2)
使用新的timestamp
列进行乐观锁定可以解决此并发问题。
UPDATE credits SET creds = 150 WHERE userid = 1 and modified_data = old_modified_date
答案 5 :(得分:1)
您可以设置一种排队机制,其中排名类型值的添加或减少将排队等待某些作业定期进行LIFO处理。如果需要关于等级“余额”的实时信息,则这不适合,因为在核对未完成的队列条目之前,余额不会计算,但如果它不需要立即协调,则可以提供。
这似乎反映了,至少在外面看,像旧的Panzer General系列游戏如何处理个人动作。一名球员出现了,他们宣布了他们的举动。依次处理每个移动,并且没有冲突,因为每个移动都在队列中占有一席之地。
答案 6 :(得分:1)
表可以修改如下,引入新的字段版本来处理乐观锁定。这是实现更好性能的更具成本效益和更有效的方式,而不是在数据库级别使用锁 创建表信用( int id, int creds, int user_id, int版本 );
从credit_id = 1;
中选择信用卡,user_id,版本假设这会返回 creds = 100和version = 1
更新信用设置creds = creds * 10,version = version + 1其中user_id = 1且version = 1;
始终确保拥有最新版本号的人只能更新此记录并且不允许脏写
答案 7 :(得分:1)
当您将用户的当前信用额减少一个请求数量时,您的情况有一个关键点,如果它成功减少,您执行其他操作,理论上问题 可以是许多并行请求减少操作当例如用户有1个余额的余额和5个并行的1个信用费用请求时他可以购买5个东西,如果请求将在同一时间完全发送并且你最终得到 - 用户余额4个学分。
为了避免这种你应该减少当前信用额度和要求金额(在我们的示例中为1个信用额度),还要检查当前值减去请求金额是否大于或等于零< /强>:
UPDATE credit SET creds = creds-1 WHERE creds-1&gt; = 0 和userid = 1
这样可以保证,如果用户使用您的系统,用户将永远不会购买很少数量的东西。
在此查询之后,您应该运行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&gt; = 0的下行是:
更新后你肯定知道一个用户有足够的信用余额,即使他尝试与许多请求你的黑客信用,但你不知道其他的东西,如充电前的信用(更新)收费后的信用额度(更新)。
注意:
请勿在不提供最新行数据的事务级别内使用此策略。
如果您想知道更新前后的价值,请不要使用此策略。
只是试着依靠信用成功收费而不低于零的事实。
答案 8 :(得分:0)
如果存储记录的最后更新时间戳,则在读取值时,也请读取时间戳。当您更新记录时,请检查以确保时间戳匹配。如果有人进来并在你面前更新,那么时间戳将不匹配。