2009-07-28 102 views
21

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

考虑一个简单的SQL模式(约束和缺省值未示出。)等

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

意图是存储某种信用的一个用户,例如像stackoverflow的声誉。

如何处理该表的并发更新? 有几个选项:

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

    在这种情况下,应用retreived的电流值,计算出的新值(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;

    此情况下,就不需要担心并发更新的数据库发生一致性问题的关心,但有缺陷,creds将愉快地变成负数,这可能没有什么意义了一些应用。

所以,简单地说,接受的方法是什么,以处理上面概述的(很简单)问题,如果db引发错误会怎么样?

+0

如果您担心违反列上的约束条件,请在数据库中定义CONSTRAINTS。 – jva 2009-07-29 10:36:38

回答

25

使用交易:

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

一些重要注意事项:

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

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

+0

在这种情况下,您是否需要“选择更新”或至少一个SERIALIZABLE隔离级别? – nos 2009-07-31 23:49:42

+2

@nos,它取决于数据库。具有真实事务支持的数据库应该只提供与事务一致的快照,尽管可能不是默认情况。对于innodb,只要您在任何innodb表上进行选择,就会立即创建数据库状态的快照。 – bdonlan 2009-08-01 03:13:29

+0

虽然您可能确实需要设置隔离级别:http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html – bdonlan 2009-08-01 03:14:06

2

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

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

如果您在记录中存储上次更新时间戳,读取该值时还要读取时间戳。当你去更新记录时,检查以确保时间戳匹配。如果有人进来并在你之前更新,时间戳不匹配。

1

您可以设置一个排队机制,其中排序类型值的添加或减去排队等待某些作业的定期LIFO处理。如果需要关于排名“余额”的实时信息,则不适合,因为余额在未完成的排队条目调和之前不会计算,但如果它不需要立即调整,它可能会发挥作用。

这似乎反映出,至少在外部看来,像旧的坦克通用系列游戏如何处理个人动作。轮到一个玩家出现,他们宣布他们的动作。每个动作依次进行处理,并且没有冲突,因为每个动作都在队列中占据一席之地。

2

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

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

对于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),那么一个简单的选择,然后更新就足够了。 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 。

0

表可以修改如下,引入新的字段版本来处理乐观锁定。这是更具成本效益和有效的方式来获得更好的性能,而不是在数据库级别 使用锁创建表的学分( INT ID, INT creds, INT USER_ID, INT版本 );

从其中user_id = 1的信用中选择creds,user_id,version;

假定此返回 creds = 100和版本= 1

更新信用设置creds = creds * 10,版本=版本+ 1,其中USER_ID = 1和版本= 1;

务必将确保谁是有最新版本号只能更新此记录和肮脏的写入将不会被允许

6

结束语是不够的,在某些情况下,无论您定义的隔离级别的事务中的代码。

比方说你有这些步骤和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 

的事务可以防止您覆盖一个错误值vim的信任状值,但它不是够了,因为我不想失败任何错误。

我宁愿选择一个永远不会失败的较慢进程,我在获取数据(第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 
1

当您的用户的当前信用领域减少了,并且如果它成功减少了,那么在您的案例中有一个关键点,您执行其他操作并且问题在理论上是可以是许多并行请求减少操作例如,当用户有1个余额余额和5并行1个信用额度请求,他可以购买5件事情,如果请求将在同一时间完全发送,你最终得到-4个信用点用户的平衡。

为了避免这种情况你应该减少与请求量(在我们的例子1学分)和当前信用值还核对,如果当前值减去请求金额大于或等于零其中:

UPDATE学分SET creds = creds-1 WHERE creds-1> = 0和用户ID = 1

这将保证,如果他将DOS系统用户将不会购买下几学分很多东西。

这个查询,你应该运行ROW_COUNT(),它告诉如果当前用户的信用满足标准和行被更新后:

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

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

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

下行的WHERE贷记卡$量> = 0无行锁定的是:

更新后你一定知道一两件事,用户对贷方余额有足够量即使他尝试哟与许多请求骇客信用,但你不知道其他事情,如收费(更新)之前什么是信用和什么是收费后更新(更新)。

注意:

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

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

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