다음을 통해 공유


낙관적 동시성

다중 사용자 환경에서는 데이터베이스의 데이터를 업데이트하는 두 가지 모델인 낙관적 동시성 및 비관적 동시성이 있습니다. 이 DataSet 객체는 데이터 원격 처리 및 데이터와의 상호작용과 같은 장기 실행 작업에 낙관적 동시성을 사용하도록 장려하기 위해 설계되었습니다.

비관적 동시성은 다른 사용자가 현재 사용자에게 영향을 주는 방식으로 데이터를 수정하지 못하도록 데이터 원본에서 행을 잠그는 것을 포함합니다. 비관적 모델에서 사용자가 잠금을 적용하는 작업을 수행할 때 잠금 소유자가 잠금을 해제할 때까지 다른 사용자는 잠금과 충돌하는 작업을 수행할 수 없습니다. 이 모델은 주로 데이터에 대한 경합이 많은 환경에서 사용되므로 잠금으로 데이터를 보호하는 비용은 동시성 충돌이 발생할 경우 트랜잭션을 롤백하는 비용보다 적습니다.

따라서 비관적 동시성 모델에서 행을 업데이트하는 사용자는 잠금을 설정합니다. 사용자가 업데이트를 완료하고 잠금을 해제할 때까지 다른 누구도 해당 행을 변경할 수 없습니다. 이러한 이유로 비관적 동시성은 레코드의 프로그래밍 방식 처리와 같이 잠금 시간이 짧을 때 가장 잘 구현됩니다. 비관적 동시성은 사용자가 데이터와 상호 작용하고 상대적으로 많은 시간 동안 레코드가 잠기는 경우 확장 가능한 옵션이 아닙니다.

비고

동일한 작업에서 여러 행을 업데이트해야 하는 경우 비관적 잠금을 사용하는 것보다 트랜잭션을 만드는 것이 더 확장 가능한 옵션입니다.

반면 낙관적 동시성을 사용하는 사용자는 읽을 때 행을 잠그지 않습니다. 사용자가 행을 업데이트하려는 경우 애플리케이션은 다른 사용자가 행을 읽은 이후 변경했는지 여부를 결정해야 합니다. 낙관적 동시성은 일반적으로 데이터에 대한 경합이 낮은 환경에서 사용됩니다. 낙관적 동시성은 레코드 잠금이 필요하지 않으며 레코드를 잠그려면 추가 서버 리소스가 필요하기 때문에 성능을 향상시킵니다. 또한 레코드 잠금을 유지하려면 데이터베이스 서버에 대한 영구 연결이 필요합니다. 낙관적 동시성 모델의 경우는 그렇지 않으므로 서버에 대한 연결은 더 적은 시간 안에 더 많은 수의 클라이언트를 무료로 제공할 수 있습니다.

낙관적 동시성 모델에서는 사용자가 데이터베이스에서 값을 받은 후 첫 번째 사용자가 수정을 시도하기 전에 다른 사용자가 값을 수정하는 경우 위반이 발생한 것으로 간주됩니다. 서버에서 동시성 위반을 해결하는 방법은 다음 예제를 먼저 설명하는 것이 가장 좋습니다.

다음 표는 낙관적 동시성의 예를 따릅니다.

오후 1:00에 User1은 다음 값을 사용하여 데이터베이스에서 행을 읽습니다.

CustID LastName FirstName

101 스미스 밥

열 이름 원래 값 현재 값 데이터베이스의 값
CustID 101 101 101
성씨 스미스 스미스 스미스
이름 (FirstName)

오후 1:01에 User2는 동일한 행을 읽습니다.

오후 1시 03분에 User2는 FirstName 을 "Bob"에서 "Robert"로 변경하고 데이터베이스를 업데이트합니다.

열 이름 원래 값 현재 값 데이터베이스의 값
CustID 101 101 101
성씨 스미스 스미스 스미스
이름 (FirstName) 로버트

업데이트 시 데이터베이스의 값이 User2의 원래 값과 일치하므로 업데이트가 성공합니다.

오후 1시 05분에 User1은 "Bob"의 이름을 "James"로 변경하고 행을 업데이트하려고 합니다.

열 이름 원래 값 현재 값 데이터베이스의 값
CustID 101 101 101
성씨 스미스 스미스 스미스
이름 (FirstName) 야고보 로버트

이 시점에서 User1은 데이터베이스의 값("Robert")이 User1이 예상한 원래 값("Bob")과 더 이상 일치하지 않으므로 낙관적 동시성 위반이 발생합니다. 동시성 위반은 단순히 업데이트가 실패했음을 알 수 있습니다. 이제 User2에서 제공한 변경 내용을 User1에서 제공한 변경 내용으로 덮어쓸지 아니면 User1에서 변경 내용을 취소할지 결정해야 합니다.

낙관적 동시성 위반 테스트

낙관적 동시성 위반을 테스트하기 위한 몇 가지 기술이 있습니다. 하나는 테이블에 타임스탬프 열을 포함하는 것입니다. 데이터베이스는 일반적으로 레코드가 마지막으로 업데이트된 날짜와 시간을 식별하는 데 사용할 수 있는 타임스탬프 기능을 제공합니다. 이 기술을 사용하면 타임스탬프 열이 테이블 정의에 포함됩니다. 레코드가 업데이트될 때마다 타임스탬프는 현재 날짜와 시간을 반영하도록 업데이트됩니다. 낙관적 동시성 위반을 테스트할 때, 테이블 내용을 쿼리할 때 시간표시 열이 함께 반환됩니다. 업데이트를 시도하면 데이터베이스의 타임스탬프 값이 수정된 행에 포함된 원래 타임스탬프 값과 비교됩니다. 일치하는 경우 업데이트가 수행되고 타임스탬프 열이 업데이트를 반영하기 위해 현재 시간으로 업데이트됩니다. 일치하지 않으면 낙관적 동시성 위반이 발생했습니다.

낙관적 동시성 위반을 테스트하는 또 다른 방법은 행의 모든 원래 열 값이 데이터베이스에 있는 값과 여전히 일치하는지 확인하는 것입니다. 예를 들어 다음과 같은 쿼리를 고려해 보겠습니다.

SELECT Col1, Col2, Col3 FROM Table1  

Table1에서 행을 업데이트할 때 낙관적 동시성 위반을 테스트하려면 다음 UPDATE 문을 실행합니다.

UPDATE Table1 Set Col1 = @NewCol1Value,  
              Set Col2 = @NewCol2Value,  
              Set Col3 = @NewCol3Value  
WHERE Col1 = @OldCol1Value AND  
      Col2 = @OldCol2Value AND  
      Col3 = @OldCol3Value  

원래 값이 데이터베이스의 값과 일치하는 한 업데이트가 수행됩니다. 값이 수정된 경우 WHERE 절에서 일치하는 항목을 찾을 수 없으므로 업데이트에서 행을 수정하지 않습니다.

항상 쿼리에서 고유한 기본 키 값을 반환하는 것이 좋습니다. 그렇지 않으면 위의 UPDATE 문이 둘 이상의 행을 업데이트할 수 있습니다. 이는 의도와 다를 수 있습니다.

데이터 원본의 열에서 null을 허용하는 경우 WHERE 절을 확장하여 로컬 테이블 및 데이터 원본에서 일치하는 null 참조를 확인해야 할 수 있습니다. 예를 들어 다음 UPDATE 문은 로컬 행의 null 참조가 여전히 데이터 원본의 null 참조와 일치하거나 로컬 행의 값이 여전히 데이터 원본의 값과 일치하는지 확인합니다.

UPDATE Table1 Set Col1 = @NewVal1  
  WHERE (@OldVal1 IS NULL AND Col1 IS NULL) OR Col1 = @OldVal1  

낙관적 동시성 모델을 사용할 때 덜 제한적인 조건을 적용하도록 선택할 수도 있습니다. 예를 들어 WHERE 절에서 기본 키 열만 사용하면 마지막 쿼리 이후 다른 열이 업데이트되었는지 여부에 관계없이 데이터를 덮어씁니다. 특정 열에만 WHERE 절을 적용할 수도 있습니다. 따라서 특정 필드가 마지막으로 쿼리된 이후 업데이트되지 않는 한 데이터를 덮어씁니다.

DataAdapter.RowUpdated 이벤트입니다

개체의 DataAdapter 이벤트는 앞에서 설명한 기술과 함께 사용하여 낙관적 동시성 위반의 애플리케이션에 알림을 제공할 수 있습니다. RowUpdatedDataSet에서 수정된 행을 업데이트하려고 할 때마다 발생합니다. 이렇게 하면 예외 발생 시 처리, 사용자 지정 오류 정보 추가, 재시도 논리 추가 등을 비롯한 특수 처리 코드를 추가할 수 있습니다. 개체는 RowUpdatedEventArgs 테이블의 수정된 행에 대한 특정 업데이트 명령의 영향을 받는 행 수를 포함하는 RecordsAffected 속성을 반환합니다. 업데이트 명령을 낙관적 동시성을 테스트하도록 설정하면, 낙관적 동시성 위반이 발생하여 레코드가 업데이트되지 않았을 때, RecordsAffected 속성은 0의 값을 반환합니다. 이 경우 예외가 발생합니다. RowUpdated 이벤트를 사용하면 UpdateStatus.SkipCurrentRow와 같은 적절한 RowUpdatedEventArgs.Status 값을 설정하여 이 발생을 처리하고 예외를 방지할 수 있습니다. RowUpdated 이벤트에 대한 자세한 내용은 DataAdapter 이벤트 처리를 참조하세요.

필요에 따라 업데이트를 호출하기 전에 DataAdapter.ContinueUpdateOnErrortrue로 설정하고 업데이트가 완료되면 특정 행의 RowError 속성에 저장된 오류 정보에 응답할 수 있습니다. 자세한 내용은 행 오류 정보를 참조하세요.

낙관적 동시성 예시

다음은 낙관적 동시성을 테스트하도록 DataAdapterUpdateCommand를 설정한 다음 RowUpdated 이벤트를 사용하여 낙관적 동시성 위반을 테스트하는 간단한 예제입니다. 낙관적 동시성 위반이 발생하면 애플리케이션은 낙관적 동시성 위반을 반영하기 위해 업데이트가 실행된 행의 RowError 를 설정합니다.

UPDATE 명령의 WHERE 절에 전달된 매개 변수 값은 해당 열의 원래 값에 매핑됩니다.

' Assumes connection is a valid SqlConnection.  
Dim adapter As SqlDataAdapter = New SqlDataAdapter( _  
  "SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID", _  
  connection)  
  
' The Update command checks for optimistic concurrency violations  
' in the WHERE clause.  
adapter.UpdateCommand = New SqlCommand("UPDATE Customers " &  
  "(CustomerID, CompanyName) VALUES(@CustomerID, @CompanyName) " & _  
  "WHERE CustomerID = @oldCustomerID AND CompanyName = " &  
  "@oldCompanyName", connection)  
adapter.UpdateCommand.Parameters.Add( _  
  "@CustomerID", SqlDbType.NChar, 5, "CustomerID")  
adapter.UpdateCommand.Parameters.Add( _  
  "@CompanyName", SqlDbType.NVarChar, 30, "CompanyName")  
  
' Pass the original values to the WHERE clause parameters.  
Dim parameter As SqlParameter = adapter.UpdateCommand.Parameters.Add( _  
  "@oldCustomerID", SqlDbType.NChar, 5, "CustomerID")  
parameter.SourceVersion = DataRowVersion.Original  
parameter = adapter.UpdateCommand.Parameters.Add( _  
  "@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName")  
parameter.SourceVersion = DataRowVersion.Original  
  
' Add the RowUpdated event handler.  
AddHandler adapter.RowUpdated, New SqlRowUpdatedEventHandler( _  
  AddressOf OnRowUpdated)  
  
Dim dataSet As DataSet = New DataSet()  
adapter.Fill(dataSet, "Customers")  
  
' Modify the DataSet contents.  
adapter.Update(dataSet, "Customers")  
  
Dim dataRow As DataRow  
  
For Each dataRow In dataSet.Tables("Customers").Rows  
    If dataRow.HasErrors Then
       Console.WriteLine(dataRow (0) & vbCrLf & dataRow.RowError)  
    End If  
Next  
  
Private Shared Sub OnRowUpdated( _  
  sender As object, args As SqlRowUpdatedEventArgs)  
   If args.RecordsAffected = 0  
      args.Row.RowError = "Optimistic Concurrency Violation!"  
      args.Status = UpdateStatus.SkipCurrentRow  
   End If  
End Sub  
// Assumes connection is a valid SqlConnection.  
SqlDataAdapter adapter = new SqlDataAdapter(  
  "SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID",  
  connection);  
  
// The Update command checks for optimistic concurrency violations  
// in the WHERE clause.  
adapter.UpdateCommand = new SqlCommand("UPDATE Customers Set CustomerID = @CustomerID, CompanyName = @CompanyName " +  
   "WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName", connection);  
adapter.UpdateCommand.Parameters.Add(  
  "@CustomerID", SqlDbType.NChar, 5, "CustomerID");  
adapter.UpdateCommand.Parameters.Add(  
  "@CompanyName", SqlDbType.NVarChar, 30, "CompanyName");  
  
// Pass the original values to the WHERE clause parameters.  
SqlParameter parameter = adapter.UpdateCommand.Parameters.Add(  
  "@oldCustomerID", SqlDbType.NChar, 5, "CustomerID");  
parameter.SourceVersion = DataRowVersion.Original;  
parameter = adapter.UpdateCommand.Parameters.Add(  
  "@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName");  
parameter.SourceVersion = DataRowVersion.Original;  
  
// Add the RowUpdated event handler.  
adapter.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated);  
  
DataSet dataSet = new DataSet();  
adapter.Fill(dataSet, "Customers");  
  
// Modify the DataSet contents.  
  
adapter.Update(dataSet, "Customers");  
  
foreach (DataRow dataRow in dataSet.Tables["Customers"].Rows)  
{  
    if (dataRow.HasErrors)  
       Console.WriteLine(dataRow [0] + "\n" + dataRow.RowError);  
}  
  
protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args)  
{  
  if (args.RecordsAffected == 0)
  {  
    args.Row.RowError = "Optimistic Concurrency Violation Encountered";  
    args.Status = UpdateStatus.SkipCurrentRow;  
  }  
}  

참고하십시오