3

作为注册新用户的一部分;我们从预编译列表(表)中为它们分配资源(在本例中为 Solr 核心)。

如果有 5 个用户注册,则必须为他们分配 5 个不同的核心;如果用户成功注册,则分配是最终的(见下面我的描述)。

但在现实世界中,同时注册的新用户竞争同一行,而不是选择不同的行。如果 X 需要 5 秒来注册,那么在 X 的“持续时间”内的 Y 和 Z 的注册将失败,因为它们与 X 竞争同一行。

问题:如何在每秒100个注册这样的高并发下,让交易选择没有争用?

table: User
user_id   name  core   
      1    Amy h1-c1
      2    Anu h1-c1
      3    Raj h1-c1
      4    Ron h1-c2
      5    Jon h1-c2

table: FreeCoreSlots
core_id  core status   
      1 h1-c1   used
      2 h1-c1   used
      3 h1-c1   used
      4 h1-c2   used
      5 h1-c2   used #these went to above users already
      6 h1-c2   free
      7 h1-c2   free
      8 h1-c2   free
      9 h1-c2   free

如果东西被隔离了,则为伪代码:

sql = SQLTransaction()
core_details = sql.get("select * from FreeCoreSlots limit 1")
sql.execute("update FreeCoreSlots set status = 'used' where id = {id}".format(
   id = core_details["id"]))
sql.execute("insert into users (name,core) values ({name},{core})".format(
   name = name,
   id   = core_details["id"]))
sql.commit()

如果每秒发生 100 次注册,他们将争夺第一行FreeCoreSlots导致严重失败。

有一个 select... for update 如InnoDB SELECT ... FOR UPDATE 语句锁定表中的所有行作为解决方案,但他们似乎建议降低隔离。这种方法正确吗?

4

3 回答 3

5

通过将事务隔离级别设置为可序列化。

http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html

实际上,这会阻止其他事务在事务处理时更改更新的表,从而确保您的数据以原子、一致的方式更新。

于 2012-10-01T12:07:09.440 回答
2

我要问的问题是为什么用户可能需要 5 秒才能完成。在START TRANSACTION和之间COMMIT应该只有几分之一秒。

为防止您再次将同一行分配给FreeCoreSlots同一用途,您必须使用SELECT for UPDATE. 在我看来,锁定级别并不是真正的问题。您设计数据库下一个空闲行的FreeCoreSlots方式实际上是锁定的,直到事务完成。请参阅下面的测试结果。而且我确实认为即使每秒 100 个新用户也应该足够了。但是如果你甚至想克服这个问题,你必须找到一种方法来锁定另一个下一个空闲行FreeCoreSlots给每一位用户。不幸的是,没有“选择第一行,除非它有锁”的功能。也许改为使用一些随机或模数逻辑。但正如我已经说过的,我认为这不应该是你的问题,即使每秒有 100 个新用户是不可想象的。如果我对此有误,请随时发表评论,我愿意再看一遍。

这是我的测试结果。默认 InnoDB 锁定级别:可重复读取,无需FOR UPDATE. 这样它不起作用。

User 1:
    START TRANSACTION
    SELECT * FROM FreeCoreSlots WHERE status = 'FREE' LIMIT 1 -- returns id 1
User 2:
    START TRANSACTION
    SELECT * FROM FreeCoreSlots WHERE status = 'FREE' LIMIT 1 -- also returns id 1 !!!
User 1:
    UPDATE FreeCoreSlots SET status = 'USED' where ID = 1;
User 2:
    UPDATE FreeCoreSlots SET status = 'USED' where ID = 1; -- WAITS !!!
User 1:
    INSERT INTO user VALUES (...
    COMMIT;
USER 2:
    wait ends and updates also ID = 1 which is WRONG

锁定级别可重复读取,但带有FOR UPDATE. 这样它就可以工作了。

User 1:
    START TRANSACTION
    SELECT * FROM FreeCoreSlots WHERE status = 'FREE' LIMIT 1 FOR UPDATE -- returns id 1
User 2:
    START TRANSACTION
    SELECT * FROM FreeCoreSlots WHERE status = 'FREE' LIMIT 1 FOR UPDATE -- WAITS
User 1:
    UPDATE FreeCoreSlots SET status = 'USED' where ID = 1;
User 2:
    still waits
User 1:
    INSERT INTO user VALUES (...
    COMMIT;
USER 2:
    Gets back Id 2 from the select as the next free Id
于 2012-10-06T10:43:49.943 回答
0

将分配推迟到最后可能的时刻。不要在注册过程中尝试绑定它。

将状态列用作并发策略。应用一个谓词,以确保您仅在该行仍然“空闲”时才更新该行。使用默认隔离级别,读取将是非阻塞的。您的更新将更新 1 行或不更新。如果不成功,您可以循环,并限制尝试次数(以防空闲插槽用尽)。您还可以预取 1 个以上的免费 ID,以节省您尝试的往返次数。最后,这作为一个返回新分配槽的 id 的存储过程会更好。在这种情况下,您可以在存储过程中使用类似的策略,在其中获取/更新直到更新成功。在高竞争环境中,如果串行分配不是很重要,我会从前 N 个空闲的随机行中选择一个。

sql = SQLTransaction()

core_details = sql.get("select * from FreeCoreSlots where status = 'free' limit 1")
sql.execute("update FreeCoreSlots set status = 'used' where id = {id} and status = 'free'".format(
   id = core_details["id"]))

//CHECK ROW COUNT HERE (I don't know what language or library you are using.
//Perhaps sql.execute returns the number of rows affected).
//REPEAT IF ROW COUNT < 1

sql.execute("insert into users (name,core) values ({name},{core})".format(
   name = name,
   id   = core_details["id"]))
sql.commit()
于 2012-10-07T14:50:28.367 回答