【问题标题】:Entity Framework 6 MySQL Change Value Within TransactionEntity Framework 6 MySQL 在事务中更改值
【发布时间】:2014-10-09 09:41:56
【问题描述】:

我想在事务中将1 添加到我的数据库中的值。我想确保记录正确更新并且在此期间没有被其他人更改。

我有以下我认为可以工作的代码,但我仍然可以在调试期间暂停,将数据库中的记录更改为不同的内容,然后它变得不一致。

这是我的代码:

using (var transaction = this.Context.Database.BeginTransaction())
{
    try
    {

        if (quiz.PasswordRequiredToTakeQuiz())
        {
            // Check password exists for quiz
            bool passwordIsValid = quiz.QuizPasswords.Any(x => x.Password.ToLower() == model.QuizPassword.ToLower() && !x.Deleted);
            QuizPassword quizPassword = quiz.QuizPasswords.Where(x => x.Password.ToLower() == model.QuizPassword.ToLower() && !x.Deleted).First();
            string passwordError = "Sorry the password you provided has expired or is not valid for this quiz";

            if (!passwordIsValid)
            {
                ViewData.ModelState.AddModelError("QuizPassword", passwordError);
            }
            else
            {
                // Password is valid for use with this quiz, but can it be used?
                if (quizPassword.RemainingUses < 1 && quizPassword.UnlimitedUses != true)
                {
                    // Password cannot be used
                    ViewData.ModelState.AddModelError("QuizPassword", passwordError);
                }
                else
                {
                    // Password CAN be used
                    if (!quizPassword.UnlimitedUses)
                    {
                        quizPassword.RemainingUses--;
                    }
                    // Increase use count
                    quizPassword.UseCount++;

                    this.Context.EntitySet<QuizPassword>().Attach(quizPassword);
                    this.Context.Entry(quizPassword).State = EntityState.Modified;

                    // I can change the record UseCount value in the database at this point
                    // then when it saves, it becomes inconsistent with other's use of
                    // the password

                    this.Context.SaveChanges();
                }
            }
        }

        // Commit the changes
        transaction.Commit();

    }
    catch(Exception)
    {
        transaction.Rollback();
    }
    finally
    {
        transaction.Dispose();
    }
}

事情的转折:

  1. 最初,数据库中的 UseCount = 0
  2. 我将代码运行到 SaveChanges() 之前
  3. 我进入数据库并将 UseCount 更改为5
  4. 我允许调用 SaveChanges()(不应该被阻止)
  5. 数据库中的UseCount 值为1

通常我会使用SELECT FOR UPDATE 来临时锁定记录,但我最初使用的是 PHP + MySQL。

我读到锁是不可能的,所以我想知道如何实现。

这很重要,因为我不希望人们使用密码的次数超过设定的次数!如果有人可以同时更改该值,则不能保证正确的使用次数。

【问题讨论】:

  • 简单的UPDATE SET SomeField=SomeField+1 有什么问题? SELECT FOR UPDATE 或试图持有锁是一种气味,表明您以错误的方式使用数据库。过去 20 年使用数据库的可扩展方式(即超过 10 个用户)是乐观并发,它检查一行的 ROW_VERSION 以检测是否有人修改了它。
  • 原因是我想利用实体框架而不是使用原始查询......也许原始查询是实现这种行为的唯一方法。
  • 一开始为什么要使用 ORM?您在这里没有 OO 行为,只有您操作的数据。此外,问题在于使用了错误的并发模型(悲观与乐观)。悲观并发(即事务)会导致阻塞,应该避免。所有数据访问方法都可以正常工作,但悲观并发严重影响性能。最后,与单个更新语句相比,让任何 ORM 发出 Field+1 语句太麻烦了
  • 我选择使用 ORM 是因为它在我的整个应用程序中使用它从数据库中获取对象。这段代码并不是我的解决方案中唯一的一段代码,哈哈。我想我必须为此创建一个存储过程,因为我需要检查不同的变量并根据情况更改它们。如您所见,此代码取决于处于特定状态的两个值
  • 重点是——你不是在这里处理对象。 ORM 涵盖一种场景(将单行映射到类实例以供离线使用),但不适用于许多其他场景,例如批处理、报告、分析或简单的值操作

标签: c# mysql transactions entity-framework-6 acid


【解决方案1】:

一种解决方案是使用普通 sql (ado.net) 和悲观锁定。

BEGIN TRANSACTION

SELECT usecount, unlimiteduses FROM quizpassword WITH (UPDLOCK, HOLDLOCK) WHERE id = x;

// check usecount here

// only do this if unlimitedUses == false
UPDATE quizpassword SET usecount = usecount + 1 WHERE id = x;

UPDATE quizpassword SET remaininguses = remaininguses -1 WHERE id = x;


COMMIT TRANSACTION // (lock is released)

【讨论】:

  • 这个问题是我需要在增加之前检查 UseCount 的值。我想知道使用原始 SQL 是否是实现这一目标的唯一方法。
  • 然后在包含它的更新中添加WHERE 语句。您已经使用您拥有的代码检查了RemainingUses,但您从未检查过UseCount
  • RemainingUses 上的负一呢?
  • 我明白了你的意思并修改了我的答案。这仍然是一个普通的 sql 解决方案。悲观锁只锁定受影响的行,因此不会导致严重的性能问题。
【解决方案2】:

我创建了一个存储过程,它返回了我想要返回的值的 SELECT 语句,即 Success int

DROP PROCEDURE IF EXISTS UsePassword;

DELIMITER //
CREATE PROCEDURE UsePassword (QuizId INT(11), PasswordText VARCHAR(25))
BEGIN

  /* Get current state of password */
    SELECT RemainingUses, UnlimitedUses, Deleted INTO @RemainingUses, @UnlimitedUses, @Deleted FROM QuizPassword q WHERE q.QuizId = QuizId AND `Password` = PasswordText AND Deleted = 0 LIMIT 0,1 FOR UPDATE;

    IF FOUND_ROWS() = 0 OR @Deleted = 1 THEN

        /* Valid password not found for quiz */
        SET @Success = 0;   

    ELSEIF @UnlimitedUses = 1 THEN

        UPDATE QuizPassword SET UseCount = UseCount + 1 WHERE QuizId = QuizId AND `Password` = PasswordText;
        SET @Success = ROW_COUNT();

    ELSEIF @RemainingUses > 0 AND @UnlimitedUses = 0 THEN

        UPDATE QuizPassword SET UseCount = UseCount + 1, RemainingUses = RemainingUses - 1 WHERE QuizId = QuizId AND `Password` = PasswordText;
        SET @Success = ROW_COUNT();

    ELSE

        SET @Success = 0;

    END IF;

  /* Return rows changed rows */
  SELECT @Success AS Success;

END //
DELIMITER;

我必须创建一个新对象来保存这些值,我只有一个字段,但你可以放更多。

// Class to hold return values from stored procedure
public class UsePasswordResult
{
    public int Success { get; set; }

    // could have more fields...
}

我将我的最终代码简化为这样,它调用存储过程并将值分配给对象内的成员变量:

using (var transaction = this.Context.Database.BeginTransaction())
{
    try
    {

        if (quiz.PasswordRequiredToTakeQuiz())
        {
            // Attempt to use password
            UsePasswordResult result = this.Context.Database.SqlQuery<UsePasswordResult>("CALL UsePassword({0}, {1})", quiz.Id, model.QuizPassword).FirstOrDefault();

            // Check the result of the password use
            if (result.Success != 1)
            {
                // Failed to use the password
                ViewData.ModelState.AddModelError("QuizPassword", "Sorry the password you provided has expired or is not valid for this quiz");
            }
        }

        // Is model state still valid after password checks?
        if (ModelState.IsValid)
        {
            // Do stuff
        }

        transaction.Commit();
    }
    catch(Exception)
    {
        transaction.Rollback();
    }
    finally
    {
        transaction.Dispose();
    }
}

您从存储过程返回的值必须与它将生成的类中的值完全相同。

因为我在我的事务using 语句中调用procedure,所以该语句锁定记录,因为我已将其选为SELECT... FOR UPDATE,直到transaction.Commit()/Rollback()/Dispose() 为在代码中调用...从而防止任何人尝试使用密码而其他人。

【讨论】:

    猜你喜欢
    • 2013-10-23
    • 2015-09-25
    • 1970-01-01
    • 2014-08-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-13
    • 1970-01-01
    相关资源
    最近更新 更多