Mysql读锁定SELECT FOR UPDATE

时间:2014-02-12 06:21:23

标签: mysql node.js

修改

我使用node.js felixge-mysql并拥有一个的mysql连接。

ORIGINAL

我有一个mysql数据库,其中有2个表:

  1. “对话”,存储元数据:用户ID(2),主题,时间戳等
  2. “messages”,存储具有带对话的FK的消息.id
  3. 现在我总是这样做:

    1. SELECT“对话”
    2. 检查元数据是否允许请求的操作
    3. 对“对话”执行UPDATE(更改部分元数据,例如lastUpdatedTimestamp)
    4. 可能INSERT向“消息”发送消息。
    5. 在消息旁边,用户还可以block对话(从他身边!)

      对话UPDATE和可能的消息INSERT将在交易中发生。

      一个警告:在我SELECT会话行并检查应用程序级别的元数据后,可能是不允许请求的操作,导致UPDATEINSERT {}永远不会被执行!

      Q1

      现在如何从我选择的那一刻开始锁定会话行?但是当元数据导致“用户错误”时(例如,当前userId不是“this”对话中的userId)仍然能够释放锁定。

      Q2

      现在我正在使用redis'lock'数据库,它通过使用Lua锁定给定的id并使用node.js事件来释放此锁。这些redis锁具有超时功能。 (例如1000毫秒)。有没有办法在mysql锁上设置超时?

2 个答案:

答案 0 :(得分:2)

你正在寻找命名锁(小心,危险的东西,不要在生产服务器上试验锁:D)。

看看:

A1 :选择要锁定的唯一字符串并在其上使用GET_LOCK(例如GET_LOCK('conversation_' || [id]);如果它返回1则锁定是您的。做你想做的事情,然后调用RELEASE_LOCK(考虑所有可能的场景,包括错误)。

A2 GET_LOCK的第二个参数是的超时。如果操作超时GET_LOCK将返回0

来自官方文档

  

<强> GET_LOCK(str,timeout)

     

尝试使用字符串 str 获取名称锁定   超时 timeout 秒。如果获得了锁,则返回1   成功,0如果尝试超时(例如,因为另一个   客户端先前已锁定名称),如果发生错误,则为NULL   (例如内存耗尽或线程被杀死了   mysqladmin kill)。如果您使用GET_LOCK()获得锁定,则为   执行RELEASE_LOCK()执行新的GET_LOCK() 1 时发布,或者   您的连接终止(正常或异常) 2 。锁   使用GET_LOCK()获取的内容不与交易进行交互。那是,   提交事务不会释放任何此类锁定   在交易期间。

     

此函数可用于实现应用程序锁或   模拟记录锁。 名称在服务器范围内被锁定 3 。如果一个   name已被一个客户端锁定,GET_LOCK()阻止任何请求   另一个具有相同名称的锁的客户端。这使客户   同意给定的锁名称使用该名称来执行合作   咨询锁定。但请注意,它也可以使客户端成为   也不是一组合作客户锁定名称   无意或故意,从而阻止任何合作   锁定该名称的客户端。减少可能性的一种方法   这是使用特定于数据库的锁名称   专用。例如,使用表单的锁名称    db_name.str app_name.str

Bolds是我的:

  1. 意味着每个连接只能保留一个锁(对于您的用户来说不是问题)
  2. 表示一旦关闭连接
  3. ,就会释放锁定
  4. 意味着两个不同的连接(甚至来自同一个池)可能无法一次获得相同的外观。

答案 1 :(得分:1)

你想要完成什么并不完全清楚。但是,尽管我理解你的要求,但是没有任何原生的MySQL“锁定”机制可以满足您的需求。 (您希望会话能够“锁定”一行,以防止其被另一个会话“读取”(或修改)。

要完成您要执行的操作,这听起来像应用程序问题,而不是数据库完整性问题。

我将用来解决的方法是在表中添加两列:

locked_by   - uniquely identify the session holding the row lock
locked_at   - the date/time the row lock was placed 

对于尝试获取行锁定的会话,我将检查该行是否已被其他会话锁定,如果没有,则将该行标记为此会话锁定:

UPDATE mytable 
   SET locked_by = 'me'
     , locked_at = NOW()
 WHERE unique_row_identifer = someval
   AND locked_by IS NULL;

如果更新返回“零行更新”,则表示您没有获得锁定。 如果返回值非零,则表示您获得了一个锁(至少在一行上)。

检查我的会话是否已在该行上持锁:

SELECT 1
  FROM mytable t
 WHERE t.unique_row_identifier = someval
   AND locked_by = 'me';

一旦我知道我在行上有一个“锁定”,那么我可以用一个简单的SELECT

来检索它
SELECT ... WHERE unique_row_identifier = someval`

要释放锁定,会话会将locked_bylocked_at列设置为NULL。

“只读”会话可以通过检查locked_by列中的值来避免读取锁定的行:

SELECT t.*
  FROM mytable t
 WHERE t.unique_row_identifier = someval
   AND t.locked_by IS NULL

只有在没有锁定的情况下才会返回该行。

请注意,我会在单个语句中执行锁定和检查,以避免出现同时出现的竞争条件。如果我运行SELECT来进行检查,然后进行UPDATE,那么另外一个会话可能会在这两个单独的语句之间滑动......很难真正导致这种情况发生,而不会增加显着的延迟。但是,如果我们要打扰锁定行,我们最好这样做。

请注意,当我们要检查已经持有很长时间的锁时,存储在locked_at列中的值会起作用。也许会话占用了一些锁,并且该会话已经消失,并且这些锁永远不会被释放。可以安排一个单独的维护任务来查看表格中是否存在非常旧的locked_at值。

或者,您可以使用locked_at对锁进行更复杂的查找,并考虑过期的旧锁。

 WHERE ( locked_at IS NULL OR locked_at < (NOW() + INTERVAL 24 HOUR) )

===

注:

我之前从未在生产系统中使用过这种方法。我的团队通常关心的问题是“胜利中的最后一个”场景,其中更新可能会覆盖最近另一个会话所做的更改。但是我们解决的问题似乎与你想要完成的问题完全不同。

要解决“胜利中的最后一个”问题,我们在表中添加一个“版本”列(简单整数)。当我们检索一行时,我们检索版本列的当前值。当会话稍后想要更新该行时,它通过将先前检索的版本值与表中的当前值进行比较来验证没有对该行进行其他更新。如果版本号匹配,我们允许更新行,并将版本号增加1。 (我们在一个UPDATE语句中完成所有操作,因此操作是原子操作,以避免两个同时会话都不进行更新的竞争条件。我们使用此模式是因为我们真的不想让行被锁定一个会话,并永远保持锁定。我们只是阻止同时更新互相覆盖,这再次与你想要完成的声音不同。