【问题标题】:Safely decrease User balance column. Should I use optimistic locking?安全地减少用户余额栏。我应该使用乐观锁定吗?
【发布时间】:2018-01-29 15:48:55
【问题描述】:

我有一个带有 MySQL/Doctrine ORM 的简单 Silex Web 应用程序。每个用户都有balance(这是一个简单的应用程序,所以只是列就可以了),我需要在一些操作后减少它(当然检查它是否> 0)。

据我了解,我可以使用乐观锁定来避免冲突/漏洞。我已阅读文档http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html,但找不到任何有关使用它的完整示例。

从哪里获得“预期版本”?我是否需要将其作为输入(隐藏表单字段)传递?还是有更好的方法?文档说了一些关于会话的内容,但我不知道如何将它存储在那里(更新每个请求的会话?)。

此外,如果我将其作为输入传递,那么据我所知,在捕获OptimisticLockException 后没有办法自动重复查询而不通知用户? (例如,如果用户打开两个选项卡并在其中一个一个提交请求)

我的目标只是防止用户同时发送多个请求并且余额仅减少一次等潜在问题。因此,能够在不涉及用户的情况下在锁定错误时自动重复它会很好。因为如果我通过表单传递它,那么很可能会因为多个选项卡而出现此错误。所以看起来有点复杂,也许除了乐观锁定还有别的东西?

【问题讨论】:

  • 有点宽泛的问题。我没有适合你的工作示例。基本思想是,您需要先保存实体的当前版本,然后再将其传递给用户进行编辑。您可以使用隐藏的表单元素来做到这一点,但您会冒着一些快乐的小丑用户更改它的价值的风险。将其存储在会话中可以消除这种风险。当然,您必须根据每个请求对其进行更新。当版本不再匹配时该怎么做取决于您。在不通知用户的情况下丢弃用户的更改似乎有点粗鲁。
  • @Cerad 谢谢 :) 就我而言,我没有什么可丢弃的,它只是一个开始(排队)某些操作的按钮。但我的目标只是防止用户同时发送多个请求并且余额仅减少一次等潜在问题。因此,能够在不涉及用户的情况下自动重复锁定错误会很好。因为如果我通过表单(我猜也是在会话中)传递它,那么很可能由于多个选项卡而出现此错误。所以看起来有点复杂,也许我应该使用其他东西而不是乐观锁定?
  • 我认为 CQRS 和事件溯源更接近你想要的。 kenneth-truyers.net/2013/12/05/…cqrs.nu 乐观锁定实际上是关于两个或更多用户尝试同时更新同一记录。
  • 请提供SHOW CREATE TABLE Optimistic 何时使用和何时不使用。我正在寻找一个有助于“乐观”算法的额外专栏(可能是TIMESTAMP)。
  • @RickJames 你是什么意思?我打算像上面的文档一样使用version 整数列。

标签: php mysql sql concurrency doctrine-orm


【解决方案1】:

您应该只对不能以原子方式执行的操作使用锁定。因此,如果可能,请避免查询对象、检查金额然后更新它。如果你这样做:

update user set balance = (balance + :amount) 
where (balance + :amount) >= 0 
and id = :user_id

您将在一次操作中检查和更新,如果检查通过并且余额已更新,则更新的行数将为 1,否则为 0。

【讨论】:

  • 是的,好主意,似乎适合我的情况(除了我需要减少,而不是增加余额)。 :) 只需要弄清楚如何在 Doctrine 中获得该数量...
  • 金额可以是负数,这就是我把支票放在那里的原因。
【解决方案2】:

在“user”表中创建一个名为“version”的列,并使其成为“timestamp”列(具有“on update CURRENT_TIMESTAMP”属性)。因此,“用户” ORM 类将如下所示:

class User
{
    // ...
    /** @Version @Column(type="timestamp") */
    private $version;
    // ...
}

现在,读取当前记录及其“版本”。

$theEntityId = YOUR ENTITY ID;
$entity = $em->find('User', $theEntityId);
$expectedVersion = entity->version;
try {
   // assert version
    $em->lock($entity, LockMode::OPTIMISTIC, $expectedVersion);

    // do the work

    $em->flush();
} 
catch(OptimisticLockException $e) {
    echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
}

【讨论】:

  • 你确定它是安全的吗?似乎它类似于文档中描述的案例来解释为什么我们需要通过表单传递它或存储在会话中。
  • 是的。它是安全的。因为,在任何时候,只有一个用户可以在数据选择和更新之间拥有相同的版本号。我不建议通过表单或会话传递它。我建议在表中创建一个“版本”列,如果版本号因为另一个用户而更新,它将确保更新操作按顺序发生或正常失败。
  • @sudip 不。这不是乐观锁定。当您将实体发送出去进行编辑并希望防止其他人出现并在第一个用户午休时更新实体的可能性时,将应用乐观锁定。 docs.doctrine-project.org/projects/doctrine-orm/en/latest/… 不涉及显式锁定。这个问题令人费解的是,有某种“排队”的事情正在发生。没有更多细节是不可能得到答案的。
  • @Cerad 它与队列无关,我只需要在用户单击按钮(向服务器发送请求)时将用户的余额减少一些(或返回错误)并为指定实体设置一些状态(稍后服务器将检测该状态并执行某些任务)。之后服务器做什么并不重要。基本上,“平衡”只是限制了用户,因此他们不会经常重复该操作。
【解决方案3】:

乐观锁将允许并发访问来读取实体(意味着可能有一些线程会读取过期数据),而悲观锁将在有人对该注册表执行操作时锁定读取。

取决于您希望并发访问的准确性如何?! 可以读取过时的数据吗?

例如:

{OTIMISTIC LOCK}
Thread1 -> read(balance1[200$][version=1])
Thread2 -> read(balance1[200$][version=1])
Thread1 -> balance.add(100$).save()[300$ total and version=2]
Thread2 -> balance.add(50$).save()[OtimisticLockError Version-> 2 != 1]

{PESSIMISTIC LOCK}
Thread1 -> read(balance1[200$]) [lock for update | select for update |... depends on DB])
Thread2 -> read(balance1) [Pessimistic lock exception]
Thread1 -> balance.add(100$).save()[300$ total]
Thread1 -> release lock balance1
Thread2 -> read(balance1[300$]) Ok

OPTIMISTIC LOCK

PESSIMISTIC LOCK

PESSIMISTIC vs. OPTIMISTIC

【讨论】:

    猜你喜欢
    • 2013-05-29
    • 1970-01-01
    • 1970-01-01
    • 2017-02-06
    • 2013-08-12
    • 1970-01-01
    • 1970-01-01
    • 2010-09-12
    • 2018-04-27
    相关资源
    最近更新 更多