2012-10-01 23 views
3

作为注册新用户的一部分;我们从一个预编译列表(一个表)中为它们分配一个资源(在这种情况下是一个Solr核心)。不同的交易必须保证选择不同的项目;避免争用

如果5个用户注册后,它们必须被分配5个不同的核;如果用户成功注册,那么分配将成为最终的(参见下面的描述)。

但在现实世界中,同时注册新用户争夺同一行,不选择不同的行。如果X需要5秒注册,Y和Z的注册这是在X的“期限”将作为他们争夺同一行的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 statement locking all rows in a table的解决方案,但他们似乎表明降低隔离。这种方法是否正确?

+0

我认为这个例子不需要使用存储过程:http://stackoverflow.com/a/562744/1584772。至少,这是一个开始的好地方(关键是要让这一切都发生在一次交易中)。 – dan1111

回答

2

我想问的问题是,为什么它可能需要5秒,用户来完成。在START TRANSACTIONCOMMIT之间应该只有几分之一秒。

为了防止您在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 
+0

无论如何5秒钟都是10倍夸张:-)然而,尽管通过设置读取未提交的隔离并选择一个(选择一个空闲行作为子查询)进行更新,我仍然得到它的工作,这是比我更高的了。但是你的方法在SQLAlchemy ORM中“做”更简单。 – aitchnyu

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() 
相关问题