Program Tip

Entity Framework의 SqlException-세션에서 실행중인 다른 스레드가 있으므로 새 트랜잭션이 허용되지 않습니다.

programtip 2020. 10. 3. 11:35
반응형

Entity Framework의 SqlException-세션에서 실행중인 다른 스레드가 있으므로 새 트랜잭션이 허용되지 않습니다.


현재이 오류가 발생합니다.

System.Data.SqlClient.SqlException : 세션에서 실행중인 다른 스레드가 있으므로 새 트랜잭션이 허용되지 않습니다.

이 코드를 실행하는 동안 :

public class ProductManager : IProductManager
{
    #region Declare Models
    private RivWorks.Model.Negotiation.RIV_Entities _dbRiv = RivWorks.Model.Stores.RivEntities(AppSettings.RivWorkEntities_connString);
    private RivWorks.Model.NegotiationAutos.RivFeedsEntities _dbFeed = RivWorks.Model.Stores.FeedEntities(AppSettings.FeedAutosEntities_connString);
    #endregion

    public IProduct GetProductById(Guid productId)
    {
        // Do a quick sync of the feeds...
        SyncFeeds();
        ...
        // get a product...
        ...
        return product;
    }

    private void SyncFeeds()
    {
        bool found = false;
        string feedSource = "AUTO";
        switch (feedSource) // companyFeedDetail.FeedSourceTable.ToUpper())
        {
            case "AUTO":
                var clientList = from a in _dbFeed.Client.Include("Auto") select a;
                foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
                {
                    var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
                    foreach (RivWorks.Model.Negotiation.AutoNegotiationDetails companyFeedDetail in companyFeedDetailList)
                    {
                        if (companyFeedDetail.FeedSourceTable.ToUpper() == "AUTO")
                        {
                            var company = (from a in _dbRiv.Company.Include("Product") where a.CompanyId == companyFeedDetail.CompanyId select a).First();
                            foreach (RivWorks.Model.NegotiationAutos.Auto sourceProduct in client.Auto)
                            {
                                foreach (RivWorks.Model.Negotiation.Product targetProduct in company.Product)
                                {
                                    if (targetProduct.alternateProductID == sourceProduct.AutoID)
                                    {
                                        found = true;
                                        break;
                                    }
                                }
                                if (!found)
                                {
                                    var newProduct = new RivWorks.Model.Negotiation.Product();
                                    newProduct.alternateProductID = sourceProduct.AutoID;
                                    newProduct.isFromFeed = true;
                                    newProduct.isDeleted = false;
                                    newProduct.SKU = sourceProduct.StockNumber;
                                    company.Product.Add(newProduct);
                                }
                            }
                            _dbRiv.SaveChanges();  // ### THIS BREAKS ### //
                        }
                    }
                }
                break;
        }
    }
}

모델 # 1-이 모델은 Dev Server의 데이터베이스에 있습니다. 모델 # 1 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/bdb2b000-6e60-4af0-a7a1-2bb6b05d8bc1/Model1.png

모델 # 2-이 모델은 Prod Server의 데이터베이스에 있으며 자동 피드를 통해 매일 업데이트됩니다. 대체 텍스트 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/4260259f-bce6-43d5-9d2a-017bd9a980d4/Model2.png

참고-모델 # 1의 빨간색 원으로 표시된 항목은 모델 # 2에 "매핑"하는 데 사용하는 필드입니다. 모델 # 2의 빨간색 원은 무시하십시오.이 질문은 제가 가지고 있던 다른 질문에 대한 답변입니다.

참고 : 클라이언트의 인벤토리에서 사라진 경우 DB1에서 소프트 삭제할 수 있도록 isDeleted 확인을 입력해야합니다.

이 특정 코드를 사용하여 내가 원하는 것은 DB1의 회사를 DB2의 클라이언트와 연결하고 DB2에서 제품 목록을 가져와 아직없는 경우 DB1에 삽입하는 것입니다. 처음으로 전체 인벤토리를 가져와야합니다. 밤새 새 인벤토리가 피드에 들어오지 않는 한 아무 일도 일어나지 않은 후에 실행될 때마다.

그래서 큰 질문-내가 얻는 거래 오류를 어떻게 해결합니까? 루프를 통과 할 때마다 내 컨텍스트를 삭제하고 다시 만들어야합니까 (이치에 맞지 않음)?


머리카락을 많이 뽑은 후 나는 foreach고리가 범인 이라는 것을 발견했습니다 . 해야 할 일은 EF를 호출하지만 IList<T>해당 대상 유형 으로 반환 한 다음 IList<T>.

예:

IList<Client> clientList = from a in _dbFeed.Client.Include("Auto") select a;
foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
{
   var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
    // ...
}

이미 확인했듯이 foreach활성 판독기를 통해 데이터베이스에서 여전히 드로잉중인에서 저장할 수 없습니다 .

ToList()또는 호출 ToArray()은 작은 데이터 세트에 적합하지만 수천 개의 행이 있으면 많은 양의 메모리를 소비하게됩니다.

행을 청크로로드하는 것이 좋습니다.

public static class EntityFrameworkUtil
{
    public static IEnumerable<T> QueryInChunksOf<T>(this IQueryable<T> queryable, int chunkSize)
    {
        return queryable.QueryChunksOfSize(chunkSize).SelectMany(chunk => chunk);
    }

    public static IEnumerable<T[]> QueryChunksOfSize<T>(this IQueryable<T> queryable, int chunkSize)
    {
        int chunkNumber = 0;
        while (true)
        {
            var query = (chunkNumber == 0)
                ? queryable 
                : queryable.Skip(chunkNumber * chunkSize);
            var chunk = query.Take(chunkSize).ToArray();
            if (chunk.Length == 0)
                yield break;
            yield return chunk;
            chunkNumber++;
        }
    }
}

위의 확장 메서드가 주어지면 다음과 같이 쿼리를 작성할 수 있습니다.

foreach (var client in clientList.OrderBy(c => c.Id).QueryInChunksOf(100))
{
    // do stuff
    context.SaveChanges();
}

이 메서드를 호출하는 쿼리 가능한 개체는 정렬되어야합니다. 이는 Entity Framework IQueryable<T>.Skip(int)가 정렬 된 쿼리 에서만 지원하기 때문입니다. 이는 서로 다른 범위에 대한 여러 쿼리가 순서가 안정적이어야한다고 생각할 때 의미가 있습니다. 순서가 중요하지 않은 경우 클러스터형 인덱스가있을 가능성이 높으므로 기본 키로 주문하십시오.

이 버전은 100 개의 배치로 데이터베이스를 쿼리합니다 SaveChanges(). 각 엔터티에 대해 호출됩니다.

처리량을 크게 향상 시키려면 전화 SaveChanges()횟수를 줄여야합니다. 대신 다음과 같은 코드를 사용하십시오.

foreach (var chunk in clientList.OrderBy(c => c.Id).QueryChunksOfSize(100))
{
    foreach (var client in chunk)
    {
        // do stuff
    }
    context.SaveChanges();
}

그 결과 데이터베이스 업데이트 호출이 100 배 감소합니다. 물론 이러한 각 호출은 완료하는 데 더 오래 걸리지 만 결국에는 여전히 앞서 나가게됩니다. 귀하의 마일리지는 다를 수 있지만 이것은 저에게 더 빠른 세상이었습니다.

그리고 그것은 당신이 본 예외를 우회합니다.

편집 SQL 프로파일 러를 실행 한 후이 질문을 다시 검토하고 성능을 향상시키기 위해 몇 가지 사항을 업데이트했습니다. 관심이있는 사람을 위해 DB에서 생성 된 내용을 보여주는 샘플 SQL이 있습니다.

첫 번째 루프는 아무것도 건너 뛸 필요가 없으므로 더 간단합니다.

SELECT TOP (100)                     -- the chunk size 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
FROM [dbo].[Clients] AS [Extent1]
ORDER BY [Extent1].[Id] ASC

후속 호출은 이전 결과 청크를 건너 뛰어야하므로 다음을 사용합니다 row_number.

SELECT TOP (100)                     -- the chunk size
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
FROM (
    SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], row_number()
    OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
    FROM [dbo].[Clients] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > 100   -- the number of rows to skip
ORDER BY [Extent1].[Id] ASC

이제 Connect에서 열린 버그 에 대한 공식 응답을 게시했습니다 . 권장되는 해결 방법은 다음과 같습니다.

이 오류는 Entity Framework가 SaveChanges () 호출 중에 암시 적 트랜잭션을 생성하기 때문에 발생합니다. 오류를 해결하는 가장 좋은 방법은 다른 패턴을 사용하거나 (즉, 읽는 동안 저장하지 않음) 트랜잭션을 명시 적으로 선언하는 것입니다. 다음은 세 가지 가능한 솔루션입니다.

// 1: Save after iteration (recommended approach in most cases)
using (var context = new MyContext())
{
    foreach (var person in context.People)
    {
        // Change to person
    }
    context.SaveChanges();
}

// 2: Declare an explicit transaction
using (var transaction = new TransactionScope())
{
    using (var context = new MyContext())
    {
        foreach (var person in context.People)
        {
            // Change to person
            context.SaveChanges();
        }
    }
    transaction.Complete();
}

// 3: Read rows ahead (Dangerous!)
using (var context = new MyContext())
{
    var people = context.People.ToList(); // Note that this forces the database
                                          // to evaluate the query immediately
                                          // and could be very bad for large tables.

    foreach (var person in people)
    {
        // Change to person
        context.SaveChanges();
    }
} 

(루프) context.SaveChanges()끝에 넣으 십시오 foreach.


실제로 foreachEntity Framework를 사용하여 C # 루프 내에 변경 내용을 저장할 수 없습니다 .

context.SaveChanges() 메서드는 일반 데이터베이스 시스템 (RDMS)에서 커밋처럼 작동합니다.

Entity Framework가 캐시 할 모든 변경 사항을 적용한 다음 SaveChanges()데이터베이스 커밋 명령과 같이 루프 (외부)를 호출하여 한 번에 모두 저장합니다 .

모든 변경 사항을 한 번에 저장할 수 있으면 작동합니다.


참고 : 책과 일부 줄이 유효하기 때문에 조정되었습니다.

SaveChanges () 메서드를 호출하면 반복이 완료되기 전에 예외가 발생하면 데이터베이스에 유지 된 모든 변경 사항을 자동으로 롤백하는 트랜잭션이 시작됩니다. 그렇지 않으면 트랜잭션이 커밋됩니다. 특히 대량의 항목을 업데이트하거나 삭제할 때 반복이 완료된 후가 아니라 각 항목 업데이트 또는 삭제 후에 메서드를 적용하고 싶을 수 있습니다.

모든 데이터가 처리되기 전에 SaveChanges ()를 호출하려고하면 "세션에서 실행중인 다른 스레드가 있기 때문에 새 트랜잭션이 허용되지 않습니다."예외가 발생합니다. 이 예외는 SQL Server가 SqlDataReader가 열려있는 연결에서 새 트랜잭션을 시작하는 것을 허용하지 않기 때문에 발생합니다. 연결 문자열에 의해 MARS (Multiple Active Record Set)가 활성화 된 경우에도 마찬가지입니다 (EF의 기본 연결 문자열은 MARS를 활성화 함).

때때로 일이 일어나는 이유를 이해하는 것이 더 좋습니다 ;-)


항상 선택 항목을 목록으로 사용

예 :

var tempGroupOfFiles = Entities.Submited_Files.Where(r => r.FileStatusID == 10 && r.EventID == EventId).ToList();

그런 다음 변경 사항을 저장하는 동안 컬렉션을 반복합니다.

 foreach (var item in tempGroupOfFiles)
             {
                 var itemToUpdate = item;
                 if (itemToUpdate != null)
                 {
                     itemToUpdate.FileStatusID = 8;
                     itemToUpdate.LastModifiedDate = DateTime.Now;
                 }
                 Entities.SaveChanges();

             }

나는이 같은 문제가 발생했지만 다른 상황에 처해 있습니다. 목록 상자에 항목 목록이 있습니다. 사용자는 항목을 클릭하고 삭제를 선택할 수 있지만 항목 삭제와 관련된 논리가 많기 때문에 저장된 프로 시저를 사용하여 항목을 삭제하고 있습니다. 저장된 프로 시저를 호출하면 삭제가 잘 작동하지만 이후 SaveChanges를 호출하면 오류가 발생합니다. 내 해결책은 EF 외부에서 저장된 proc을 호출하는 것이었고 이것은 잘 작동했습니다. 어떤 이유로 EF 방식을 사용하여 저장된 proc을 호출하면 무언가가 열린 상태로 남습니다.


여기에 각 루프에서 SaveChanges ()를 호출 할 수있는 또 다른 두 가지 옵션이 있습니다.

첫 번째 옵션은 하나의 DBContext를 사용하여 반복 할 목록 개체를 생성 한 다음 SaveChanges ()를 호출 할 두 번째 DBContext를 만드는 것입니다. 다음은 예입니다.

//Get your IQueryable list of objects from your main DBContext(db)    
IQueryable<Object> objects = db.Object.Where(whatever where clause you desire);

//Create a new DBContext outside of the foreach loop    
using (DBContext dbMod = new DBContext())
{   
    //Loop through the IQueryable       
    foreach (Object object in objects)
    {
        //Get the same object you are operating on in the foreach loop from the new DBContext(dbMod) using the objects id           
        Object objectMod = dbMod.Object.Find(object.id);

        //Make whatever changes you need on objectMod
        objectMod.RightNow = DateTime.Now;

        //Invoke SaveChanges() on the dbMod context         
        dbMod.SaveChanges()
    }
}

두 번째 옵션은 DBContext에서 데이터베이스 개체 목록을 가져 오지만 ID 만 선택하는 것입니다. 그런 다음 id 목록 (아마도 int)을 반복하고 각 int에 해당하는 객체를 가져 와서 SaveChanges ()를 호출합니다. 이 메서드의이면에있는 아이디어는 큰 정수 목록을 가져 와서 큰 db 개체 목록을 가져와 전체 개체에 대해 .ToList ()를 호출하는 것보다 훨씬 효율적입니다. 이 방법의 예는 다음과 같습니다.

//Get the list of objects you want from your DBContext, and select just the Id's and create a list
List<int> Ids = db.Object.Where(enter where clause here)Select(m => m.Id).ToList();

var objects = Ids.Select(id => db.Objects.Find(id));

foreach (var object in objects)
{
    object.RightNow = DateTime.Now;
    db.SaveChanges()
}

쿼리 가능한 목록을 .ToList ()로 만들면 제대로 작동합니다.


그래서 프로젝트에서 문제가 없었 foreach거나 .toList()실제로 우리가 사용한 AutoFac 구성에 있었던 것과 똑같은 문제가 발생 했습니다. 이로 인해 위의 오류가 발생했지만 다른 동등한 오류가 발생하는 이상한 상황이 발생했습니다.

이것이 우리의 수정이었습니다. 다음과 같이 변경되었습니다.

container.RegisterType<DataContext>().As<DbContext>().InstancePerLifetimeScope();
container.RegisterType<DbFactory>().As<IDbFactory>().SingleInstance();
container.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest();

에:

container.RegisterType<DataContext>().As<DbContext>().As<DbContext>();
container.RegisterType<DbFactory>().As<IDbFactory>().As<IDbFactory>().InstancePerLifetimeScope();
container.RegisterType<UnitOfWork>().As<IUnitOfWork>().As<IUnitOfWork>();//.InstancePerRequest();

거대한 ResultSet을 읽고 테이블의 일부 레코드를 업데이트해야했습니다. 나는 Drew Noakes답변 에서 제안한대로 덩어리를 사용하려고했습니다 .

불행히도 50000 개의 레코드 후에 OutofMemoryException이 발생했습니다. 대답 엔터티 프레임 워크 큰 데이터 세트, 메모리 부족 예외 설명,

EF는 변경 검색에 사용되는 데이터의 두 번째 복사본을 만듭니다 (데이터베이스에 대한 변경 사항을 유지할 수 있음). EF는 컨텍스트의 수명 동안이 두 번째 집합과 메모리 부족을 유발하는이 집합을 보유합니다.

각 배치마다 컨텍스트를 갱신하는 것이 좋습니다.

그래서 기본 키의 최소값과 최대 값을 검색했습니다-테이블에는 자동 증분 정수로 기본 키가 있습니다. 그런 다음 각 청크에 대한 컨텍스트를 열어 데이터베이스 레코드 청크에서 검색했습니다. 청크 컨텍스트를 처리 한 후 메모리를 닫고 해제합니다. 메모리 사용량이 증가하지 않도록합니다.

아래는 내 코드의 스 니펫입니다.

  public void ProcessContextByChunks ()
  {
        var tableName = "MyTable";
         var startTime = DateTime.Now;
        int i = 0;
         var minMaxIds = GetMinMaxIds();
        for (int fromKeyID= minMaxIds.From; fromKeyID <= minMaxIds.To; fromKeyID = fromKeyID+_chunkSize)
        {
            try
            {
                using (var context = InitContext())
                {   
                    var chunk = GetMyTableQuery(context).Where(r => (r.KeyID >= fromKeyID) && (r.KeyID < fromKeyID+ _chunkSize));
                    try
                    {
                        foreach (var row in chunk)
                        {
                            foundCount = UpdateRowIfNeeded(++i, row);
                        }
                        context.SaveChanges();
                    }
                    catch (Exception exc)
                    {
                        LogChunkException(i, exc);
                    }
                }
            }
            catch (Exception exc)
            {
                LogChunkException(i, exc);
            }
        }
        LogSummaryLine(tableName, i, foundCount, startTime);
    }

    private FromToRange<int> GetminMaxIds()
    {
        var minMaxIds = new FromToRange<int>();
        using (var context = InitContext())
        {
            var allRows = GetMyTableQuery(context);
            minMaxIds.From = allRows.Min(n => (int?)n.KeyID ?? 0);  
            minMaxIds.To = allRows.Max(n => (int?)n.KeyID ?? 0);
        }
        return minMaxIds;
    }

    private IQueryable<MyTable> GetMyTableQuery(MyEFContext context)
    {
        return context.MyTable;
    }

    private  MyEFContext InitContext()
    {
        var context = new MyEFContext();
        context.Database.Connection.ConnectionString = _connectionString;
        //context.Database.Log = SqlLog;
        return context;
    }

FromToRange 는 From 및 To 속성이있는 간단한 구조입니다.


foreach로 인해이 오류가 발생하고 실제로 하나의 엔티티를 루프 내부에 먼저 저장하고 생성 된 ID를 루프에서 추가로 사용해야하는 경우 가장 쉬운 해결책은 다른 DBContext를 사용하여 Id를 반환하고 사용할 엔티티를 삽입하는 것입니다. 외부 컨텍스트에서이 ID

예를 들면

    using (var context = new DatabaseContext())
    {
        ...
        using (var context1 = new DatabaseContext())
        {
            ...
               context1.SaveChanges();
        }                         
        //get id of inserted object from context1 and use is.   
      context.SaveChanges();
   }

나도 같은 문제에 직면했습니다.

여기에 원인과 해결책이 있습니다.

http://blogs.msdn.com/b/cbiyikoglu/archive/2006/11/21/mars-transactions-and-sql-error-3997-3988-or-3983.aspx

삽입, 업데이트와 같은 데이터 조작 명령을 실행하기 전에 이전의 모든 활성 SQL 판독기를 닫았는지 확인하십시오.

가장 일반적인 오류는 db에서 데이터를 읽고 값을 반환하는 함수입니다. 예를 들어 isRecordExist와 같은 함수.

이 경우 레코드를 발견하고 판독기를 닫는 것을 잊으면 즉시 함수에서 돌아옵니다.


제 경우에는 EF를 통해 저장 프로 시저를 호출 한 다음 나중에 SaveChanges에서이 예외를 throw 할 때 문제가 발생했습니다. 문제는 프로 시저를 호출하는 데 있었으며 열거자가 삭제되지 않았습니다. 다음과 같은 방법으로 코드를 수정했습니다.

public bool IsUserInRole(string username, string roleName, DataContext context)
{          
   var result = context.aspnet_UsersInRoles_IsUserInRoleEF("/", username, roleName);

   //using here solved the issue
   using (var en = result.GetEnumerator()) 
   {
     if (!en.MoveNext())
       throw new Exception("emty result of aspnet_UsersInRoles_IsUserInRoleEF");
     int? resultData = en.Current;

     return resultData == 1;//1 = success, see T-SQL for return codes
   }
}

아래 코드는 저에게 효과적입니다.

private pricecheckEntities _context = new pricecheckEntities();

...

private void resetpcheckedtoFalse()
{
    try
    {
        foreach (var product in _context.products)
        {
            product.pchecked = false;
            _context.products.Attach(product);
            _context.Entry(product).State = EntityState.Modified;
        }
        _context.SaveChanges();
    }
    catch (Exception extofException)
    {
        MessageBox.Show(extofException.ToString());

    }
    productsDataGrid.Items.Refresh();
}

나는 파티에 훨씬 늦었지만 오늘 나는 같은 오류에 직면했고 어떻게 해결했는지는 간단했습니다. 내 시나리오는 중첩 된 for-each 루프 내부에서 DB 트랜잭션을 만드는 주어진 코드와 유사했습니다.

문제는 단일 DB 트랜잭션이 for-each 루프보다 약간 더 오래 걸리므로 이전 트랜잭션이 완료되지 않으면 새로운 트랙션에서 예외가 발생하므로 솔루션은 for-each 루프에 새 객체를 만드는 것입니다. db 트랜잭션을 만드는 곳.

위에서 언급 한 시나리오의 경우 솔루션은 다음과 같습니다.

foreach (RivWorks.Model.Negotiation.AutoNegotiationDetails companyFeedDetail in companyFeedDetailList)
                {
private RivWorks.Model.Negotiation.RIV_Entities _dbRiv = RivWorks.Model.Stores.RivEntities(AppSettings.RivWorkEntities_connString);
                    if (companyFeedDetail.FeedSourceTable.ToUpper() == "AUTO")
                    {
                        var company = (from a in _dbRiv.Company.Include("Product") where a.CompanyId == companyFeedDetail.CompanyId select a).First();
                        foreach (RivWorks.Model.NegotiationAutos.Auto sourceProduct in client.Auto)
                        {
                            foreach (RivWorks.Model.Negotiation.Product targetProduct in company.Product)
                            {
                                if (targetProduct.alternateProductID == sourceProduct.AutoID)
                                {
                                    found = true;
                                    break;
                                }
                            }
                            if (!found)
                            {
                                var newProduct = new RivWorks.Model.Negotiation.Product();
                                newProduct.alternateProductID = sourceProduct.AutoID;
                                newProduct.isFromFeed = true;
                                newProduct.isDeleted = false;
                                newProduct.SKU = sourceProduct.StockNumber;
                                company.Product.Add(newProduct);
                            }
                        }
                        _dbRiv.SaveChanges();  // ### THIS BREAKS ### //
                    }
                }

조금 늦었지만이 오류도있었습니다. 업데이트하는 값이 어디에 있는지 확인하여 문제를 해결했습니다.

내 쿼리가 잘못되었고 250 개 이상의 편집이 보류중인 것을 알았습니다. 그래서 나는 내 쿼리를 수정했고 이제 올바르게 작동합니다.

따라서 내 상황 에서는 쿼리가 반환하는 결과를 디버깅하여 오류가 있는지 쿼리를 확인합니다. 그 후 쿼리를 수정하십시오.

이것이 미래의 문제를 해결하는 데 도움이되기를 바랍니다.


나는 그것이 오래된 질문이라는 것을 알고 있지만 오늘이 오류에 직면했습니다.

그리고 데이터베이스 테이블 트리거에 오류가 발생하면이 오류가 발생할 수 있음을 발견했습니다.

참고로이 오류가 발생하면 테이블 트리거도 확인할 수 있습니다.

참고 URL : https://stackoverflow.com/questions/2113498/sqlexception-from-entity-framework-new-transaction-is-not-allowed-because-ther

반응형