source

엔티티 프레임워크를 사용하여 테이블을 읽을 때 잠그려면 어떻게 해야 합니까?

manysource 2023. 6. 24. 09:18

엔티티 프레임워크를 사용하여 테이블을 읽을 때 잠그려면 어떻게 해야 합니까?

Entity Framework(4.1)를 사용하여 액세스하는 SQL 서버(2012)가 있습니다.데이터베이스에는 독립적인 프로세스가 새 URL을 입력하는 URL이라는 테이블이 있습니다.URL 테이블의 항목은 "새로 만들기", "처리 중" 또는 "처리됨" 상태일 수 있습니다.

다른 컴퓨터에서 URL 테이블에 액세스하고 상태가 "New"인 URL 항목을 확인한 후 첫 번째 항목을 가져와 "In Process"로 표시해야 합니다.

var newUrl = dbEntity.URLs.FirstOrDefault(url => url.StatusID == (int) URLStatus.New);
if(newUrl != null)
{
    newUrl.StatusID = (int) URLStatus.InProcess;
    dbEntity.SaveChanges();
}
//Process the URL

쿼리와 업데이트는 원자적인 것이 아니기 때문에 서로 다른 두 컴퓨터가 데이터베이스의 동일한 URL 항목을 읽고 업데이트하도록 할 수 있습니다.

이러한 충돌을 방지하기 위해 선택 후 업데이트 시퀀스를 원자성으로 만드는 방법이 있습니까?

수동으로 테이블에 잠금 명령문을 발행해야만 이 작업을 수행할 수 있었습니다.이것은 완전한 테이블 잠금 기능을 하므로 조심하세요!제 경우에는 여러 프로세스가 동시에 접촉하지 않도록 대기열을 생성하는 데 유용했습니다.

using (Entities entities = new Entities())
using (TransactionScope scope = new TransactionScope())
{
    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Complete();
}

업데이트 - Entity Framework 6에서, 특히async/await코드, 당신은 거래를 다르게 처리해야 합니다.이것은 몇 번의 전환 후에 우리에게 충격적이었습니다.

using (Entities entities = new Entities())
using (DbContextTransaction scope = entities.Database.BeginTransaction())
{
    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Commit();
}

@jocull이 제공한 답변은 훌륭합니다.다음과 같은 수정 사항을 제공합니다.

이 대신:

"SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)"

수행할 작업:

"SELECT TOP 0 NULL FROM MyTable WITH (TABLOCKX)"

이것은 더 일반적입니다.테이블 이름을 매개 변수로 사용하는 도우미 메소드를 만들 수 있습니다.데이터(열 이름이라고도 함)를 알 필요가 없으며 실제로 파이프 아래로 레코드를 검색할 필요가 없습니다).TOP 1)

안드레의 대답에 코멘트를 추가할 수는 없지만, "격리"라는 코멘트가 걱정됩니다.Level.RepeatableRead는 테이블 A가 스레드 1에서 읽었고 스레드 1이 트랜잭션을 완료하지 않은 경우 테이블 A에서 스레드 2가 읽을 수 없도록 모든 행에 잠금을 적용합니다."

반복 가능한 읽기는 트랜잭션이 끝날 때까지 모든 잠금을 유지한다는 내용만 표시됩니다.트랜잭션에서 이 분리 수준을 사용하고 행을 읽으면(예: 최대값) "공유" 잠금이 실행되고 트랜잭션이 완료될 때까지 유지됩니다.이 공유 잠금은 다른 스레드가 행을 업데이트하는 것을 방지합니다(업데이트는 해당 행에 배타적 잠금을 적용하고 기존 공유 잠금에 의해 차단됨).그러나 다른 스레드가 값을 읽을 수 있도록 허용합니다(두 번째 스레드는 행에 다른 공유 잠금을 설정합니다. 이것이 허용됩니다(공유 잠금이라고 함).그래서 위의 진술을 정확하게 하기 위해서는 "격리"라고 말해야 합니다.Level.RepeatableRead는 테이블 A를 스레드 1에서 읽고 스레드 1이 트랜잭션을 완료하지 않은 경우 스레드 2가 테이블 A를 업데이트할 수 없도록 읽는 모든 행에 잠금을 적용합니다."

원래 질문의 경우 두 프로세스가 동일한 값을 읽고 업데이트하지 못하도록 반복 가능한 읽기 분리 수준을 사용하고 잠금을 배타적 잠금으로 에스컬레이션해야 합니다.모든 솔루션에는 EF를 사용자 정의 SQL에 매핑하는 작업이 포함됩니다(잠금 유형이 EF에 내장되지 않음).jocull 응답을 사용하거나 출력 절이 있는 업데이트를 사용하여 행을 잠글 수 있습니다(update 문은 항상 배타적 잠금을 가지며 2008년 또는 그 이상에서는 결과 집합을 반환할 수 있습니다).

UPDLOCK 힌트를 데이터베이스에 전달하고 특정 행만 잠글 수 있습니다.따라서 업데이트하기 위해 선택한 항목은 변경 사항을 저장할 수 있도록 배타적 잠금을 요구합니다(처음에 다시 잠금을 획득하는 것이 아니라 나중에 저장할 때 업그레이드를 시도합니다).위의 jocull이 제안한 Holdlock도 좋은 생각입니다.

private static TestEntity GetFirstEntity(Context context) {
return context.TestEntities
          .SqlQuery("SELECT TOP 1 Id, Value FROM TestEntities WITH (UPDLOCK)")
          .Single();
}

저는 낙관적인 동시성을 고려하는 것을 강력히 추천합니다: https://www.entityframeworktutorial.net/EntityFramework5/handle-concurrency-in-entity-framework.aspx

위의 답변을 제가 조합하여 공유하고자 합니다.

public class Repository<TEntity, TKey>
    : IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>
{
    protected readonly DbContext DbContext;

    ...

    private sealed class Transaction<TTransactionEntity> : IDisposable
    {
        private readonly IDbContextTransaction dbTransaction;

        public Transaction(DbContext context)
        {
            var tableName = context.Model
                .FindEntityType(typeof(TTransactionEntity))
                .GetTableName();

            this.dbTransaction = context.Database
                .BeginTransaction(IsolationLevel.RepeatableRead);

            context.Database
                .ExecuteSqlRaw($"SELECT TOP 0 NULL FROM {tableName} WITH (TABLOCKX)");
        }

        public void Dispose()
        {
            this.dbTransaction.Commit();
            this.dbTransaction.Dispose();
        }
    }

    public IDisposable LockingTransaction()
        => new Transaction<TEntity>(this.DbContext);
}

용도:

using (this.unitOfWork.MyRepository.LockingTransaction())
{
    ...
}

언급URL : https://stackoverflow.com/questions/13404061/how-can-i-lock-a-table-on-read-using-entity-framework