Estamos viendo algunas condiciones de interbloqueo perniciosas, pero raras, en la base de datos Stack Overflow SQL Server 2005.
Adjunté el generador de perfiles, configuré un perfil de seguimiento utilizando este excelente artículo sobre solución de problemas de interbloqueos y capturé un montón de ejemplos. Lo extraño es que la escritura de interbloqueo es siempre la misma :
UPDATE [dbo].[Posts]
SET [AnswerCount] = @p1, [LastActivityDate] = @p2, [LastActivityUserId] = @p3
WHERE [Id] = @p0
La otra declaración de interbloqueo varía, pero generalmente es una especie de lectura simple y trivial de la tabla de publicaciones. Este siempre muere en el punto muerto. Aquí hay un ejemplo
SELECT
[t0].[Id], [t0].[PostTypeId], [t0].[Score], [t0].[Views], [t0].[AnswerCount],
[t0].[AcceptedAnswerId], [t0].[IsLocked], [t0].[IsLockedEdit], [t0].[ParentId],
[t0].[CurrentRevisionId], [t0].[FirstRevisionId], [t0].[LockedReason],
[t0].[LastActivityDate], [t0].[LastActivityUserId]
FROM [dbo].[Posts] AS [t0]
WHERE [t0].[ParentId] = @p0
Para ser perfectamente claros, no estamos viendo puntos muertos de escritura / escritura, sino de lectura / escritura.
Tenemos una mezcla de consultas LINQ y SQL parametrizadas en este momento. Hemos agregado with (nolock)
a todas las consultas SQL. Esto puede haber ayudado a algunos. También tuvimos una sola consulta de insignia (muy) mal escrita que arreglé ayer, que tardaba más de 20 segundos en ejecutarse cada vez, y además se ejecutaba cada minuto. ¡Esperaba que esta fuera la fuente de algunos de los problemas de bloqueo!
Desafortunadamente, recibí otro error de interbloqueo hace aproximadamente 2 horas. Los mismos síntomas exactos, el mismo culpable exacto escribe.
Lo realmente extraño es que la declaración SQL de escritura de bloqueo que ve arriba es parte de una ruta de código muy específica. Es solamente ejecuta cuando se añade una nueva respuesta a una pregunta - se actualiza la pregunta madre con el nuevo recuento de respuesta y última fecha / usuario. Esto, obviamente, no es tan común en relación con la gran cantidad de lecturas que estamos haciendo. Por lo que puedo decir, no estamos haciendo una gran cantidad de escrituras en ninguna parte de la aplicación.
Me doy cuenta de que NOLOCK es una especie de martillo gigante, pero la mayoría de las consultas que ejecutamos aquí no necesitan ser tan precisas. ¿Le importará si su perfil de usuario está desactualizado unos segundos?
Usar NOLOCK con Linq es un poco más difícil, como comenta Scott Hanselman aquí .
Estamos coqueteando con la idea de usar
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
en el contexto de la base de datos base para que todas nuestras consultas LINQ tengan este conjunto. Sin eso, tendríamos que envolver cada llamada LINQ que hacemos (bueno, las de lectura simple, que es la gran mayoría de ellas) en un bloque de código de transacción de 3-4 líneas, lo cual es feo.
Supongo que estoy un poco frustrado de que las lecturas triviales en SQL 2005 puedan bloquearse en las escrituras. Pude ver que los puntos muertos de escritura / escritura son un gran problema, pero ¿lee? No estamos ejecutando un sitio bancario aquí, no necesitamos una precisión perfecta siempre.
Ideas? Pensamientos?
¿Está creando una instancia de un nuevo objeto LINQ to SQL DataContext para cada operación o quizás está compartiendo el mismo contexto estático para todas sus llamadas?
Jeremy, estamos compartiendo un contexto de datos estáticos en el controlador base en su mayor parte:
private DBContext _db;
/// <summary>
/// Gets the DataContext to be used by a Request's controllers.
/// </summary>
public DBContext DB
{
get
{
if (_db == null)
{
_db = new DBContext() { SessionName = GetType().Name };
//_db.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
}
return _db;
}
}
¿Recomiendas que creemos un nuevo contexto para cada controlador, o por página, o ... más a menudo?
fuente
Respuestas:
Según MSDN:
http://msdn.microsoft.com/en-us/library/ms191242.aspx
Parece haber una leve penalización en el rendimiento por los gastos generales adicionales, pero puede ser insignificante. Deberíamos probar para asegurarnos.
Intente configurar esta opción y QUITAR todos los NOLOCK de las consultas de código a menos que sea realmente necesario. Los NOLOCK o el uso de métodos globales en el controlador de contexto de la base de datos para combatir los niveles de aislamiento de transacciones de la base de datos son una ayuda para resolver el problema. NOLOCKS enmascarará problemas fundamentales con nuestra capa de datos y posiblemente conducirá a la selección de datos no confiables, donde la selección / actualización automática de versiones de filas parece ser la solución.
ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON
fuente
NOLOCK y READ UNCOMMITTED son una pendiente resbaladiza. Nunca debe usarlos a menos que comprenda primero por qué ocurre el interbloqueo. Me preocuparía que dijera: "Hemos agregado con (nolock) a todas las consultas SQL". Necesitar agregar WITH NOLOCK en todas partes es una señal segura de que tiene problemas en su capa de datos.
La declaración de actualización en sí parece un poco problemática. ¿Determina el recuento antes en la transacción o simplemente lo extrae de un objeto?
AnswerCount = AnswerCount+1
cuando se agrega una pregunta es probablemente una mejor manera de manejar esto. Entonces no necesita una transacción para obtener el recuento correcto y no tiene que preocuparse por el problema de concurrencia al que se está exponiendo potencialmente.Una manera fácil de solucionar este tipo de problema de interbloqueo sin mucho trabajo y sin habilitar lecturas sucias es usar
"Snapshot Isolation Mode"
(nuevo en SQL 2005) que siempre le dará una lectura limpia de los últimos datos no modificados. También puede detectar y reintentar declaraciones interbloqueadas con bastante facilidad si desea manejarlas con elegancia.fuente
La pregunta de OP era preguntar por qué ocurrió este problema. Esta publicación espera responder a eso mientras deja posibles soluciones para que otros las resuelvan.
Probablemente se trate de un problema relacionado con el índice. Por ejemplo, digamos que la tabla Posts tiene un índice X no agrupado que contiene el ParentID y uno (o más) de los campos que se actualizan (AnswerCount, LastActivityDate, LastActivityUserId).
Se produciría un interbloqueo si el cmd SELECT realiza un bloqueo de lectura compartida en el índice X para buscar por el ParentId y luego necesita hacer un bloqueo de lectura compartida en el índice agrupado para obtener las columnas restantes mientras que el cmd UPDATE realiza una escritura exclusiva bloquear el índice agrupado y es necesario obtener un bloqueo exclusivo de escritura en el índice X para actualizarlo.
Ahora tiene una situación en la que A bloqueó X y está tratando de obtener Y mientras que B bloqueó Y y está tratando de obtener X.
Por supuesto, necesitaremos que el OP actualice su publicación con más información sobre qué índices están en juego para confirmar si esta es realmente la causa.
fuente
Estoy bastante incómodo con esta pregunta y las respuestas que la acompañan. Hay mucho "¡prueba este polvo mágico! ¡No ese polvo mágico!"
No veo en ninguna parte que haya analizado los bloqueos que se toman y determinado qué tipo exacto de bloqueos están bloqueados.
Todo lo que ha indicado es que se producen algunos bloqueos, no lo que es interbloqueo.
En SQL 2005, puede obtener más información sobre qué bloqueos se están quitando usando:
de modo que cuando se produzca el interbloqueo tendrá mejores diagnósticos.
fuente
¿Está creando una instancia de un nuevo objeto LINQ to SQL DataContext para cada operación o quizás está compartiendo el mismo contexto estático para todas sus llamadas? Originalmente probé el último enfoque y, por lo que recuerdo, causó un bloqueo no deseado en la base de datos. Ahora creo un nuevo contexto para cada operación atómica.
fuente
Antes de quemar la casa para atrapar una mosca con NOLOCK por todas partes, es posible que desee echar un vistazo a ese gráfico de punto muerto que debería haber capturado con Profiler.
Recuerde que un interbloqueo requiere (al menos) 2 bloqueos. La conexión 1 tiene la cerradura A, quiere la cerradura B, y viceversa para la conexión 2. Esta es una situación que no se puede resolver y alguien tiene que ceder.
Lo que ha mostrado hasta ahora se resuelve con un bloqueo simple, que Sql Server está feliz de hacer durante todo el día.
Sospecho que usted (o LINQ) está iniciando una transacción con esa declaración UPDATE en ella y SELECCIONANDO alguna otra información de antemano. Pero, realmente necesita retroceder a través del gráfico de interbloqueo para encontrar los bloqueos retenidos por cada hilo, y luego retroceder a través de Profiler para encontrar las declaraciones que causaron que se concedieran esos bloqueos.
Espero que haya al menos 4 declaraciones para completar este acertijo (o una declaración que tenga múltiples bloqueos, ¿quizás haya un disparador en la tabla de Publicaciones?).
fuente
No, eso es perfectamente aceptable. Establecer el nivel de aislamiento de transacciones base es probablemente la mejor y más limpia forma de hacerlo.
fuente
El bloqueo típico de lectura / escritura proviene del acceso a la orden de índice. Leer (T1) ubica la fila en el índice A y luego busca la columna proyectada en el índice B (generalmente agrupada). Escribir (T2) cambia el índice B (el clúster) luego tiene que actualizar el índice A. T1 tiene S-Lck en A, quiere S-Lck en B, T2 tiene X-Lck en B, quiere U-Lck en A. Interbloqueo , soplo. T1 muere. Esto es frecuente en entornos con mucho tráfico OLTP y solo un poco demasiados índices :). La solución es hacer que la lectura no tenga que saltar de A a B (es decir, la columna incluida en A o eliminar la columna de la lista proyectada) o que T2 no tenga que saltar de B a A (no actualice la columna indexada). Desafortunadamente, linq no es tu amigo aquí ...
fuente
@Jeff - Definitivamente no soy un experto en esto, pero he tenido buenos resultados al crear instancias de un nuevo contexto en casi todas las llamadas. Creo que es similar a crear un nuevo objeto Connection en cada llamada con ADO. La sobrecarga no es tan mala como podría pensar, ya que la agrupación de conexiones se seguirá utilizando de todos modos.
Solo uso un ayudante estático global como este:
y luego hago algo como esto:
var db = AppData.DB; var results = from p in db.Posts where p.ID = id select p;
Y haría lo mismo con las actualizaciones. De todos modos, no tengo tanto tráfico como tú, pero definitivamente me estaba bloqueando cuando usé un DataContext compartido desde el principio con solo un puñado de usuarios. No hay garantías, pero podría valer la pena intentarlo.
Actualizar : Por otra parte, al mirar su código, solo está compartiendo el contexto de datos durante la vida útil de esa instancia de controlador en particular, lo que básicamente parece estar bien a menos que de alguna manera se esté usando simultáneamente por múltiples llamadas dentro del controlador. En un hilo sobre el tema, ScottGu dijo:
De todos modos, puede que no sea así, pero de nuevo probablemente valga la pena intentarlo, quizás junto con algunas pruebas de carga.
fuente
P. ¿Por qué está almacenando
AnswerCount
elPosts
mesa en primer lugar?Un enfoque alternativo es eliminar la "escritura" en la
Posts
tabla al no almacenar elAnswerCount
en la tabla, sino calculando dinámicamente la cantidad de respuestas a la publicación según sea necesario.Sí, esto significará que está ejecutando una consulta adicional:
SELECT COUNT(*) FROM Answers WHERE post_id = @id
o más típicamente (si está mostrando esto para la página de inicio):
SELECT p.post_id, p.<additional post fields>, a.AnswerCount FROM Posts p INNER JOIN AnswersCount_view a ON <join criteria> WHERE <home page criteria>
pero esto generalmente da como resultado un
INDEX SCAN
y puede ser más eficiente en el uso de recursos que en el usoREAD ISOLATION
.Hay más de una forma de despellejar a un gato. La desnormalización prematura de un esquema de base de datos puede presentar problemas de escalabilidad.
fuente
Definitivamente desea que READ_COMMITTED_SNAPSHOT esté activado, lo cual no es predeterminado. Eso le da semántica MVCC. Es lo mismo que usa Oracle de forma predeterminada. Tener una base de datos MVCC es increíblemente útil, NO usar una es una locura. Esto le permite ejecutar lo siguiente dentro de una transacción:
Actualizar USUARIOS Set FirstName = 'foobar'; // decide dormir durante un año.
mientras tanto, sin cometer lo anterior, todos pueden continuar seleccionando de esa tabla sin problemas. Si no está familiarizado con MVCC, se sorprenderá de haber podido vivir sin él. Seriamente.
fuente
Establecer su valor predeterminado para leer sin confirmar no es una buena idea. Sin duda, introducirás inconsistencias y acabarás con un problema peor que el que tienes ahora. El aislamiento de instantáneas puede funcionar bien, pero es un cambio drástico en la forma en que funciona Sql Server y supone un gran carga para tempdb.
Esto es lo que debe hacer: use try-catch (en T-SQL) para detectar la condición de interbloqueo. Cuando suceda, simplemente vuelva a ejecutar la consulta. Esta es una práctica estándar de programación de bases de datos.
Hay buenos ejemplos de esta técnica en la Biblia Sql Server 2005 de Paul Nielson. .
Aquí hay una plantilla rápida que utilizo:
-- Deadlock retry template declare @lastError int; declare @numErrors int; set @numErrors = 0; LockTimeoutRetry: begin try; -- The query goes here return; -- this is the normal end of the procedure end try begin catch set @lastError=@@error if @lastError = 1222 or @lastError = 1205 -- Lock timeout or deadlock begin; if @numErrors >= 3 -- We hit the retry limit begin; raiserror('Could not get a lock after 3 attempts', 16, 1); return -100; end; -- Wait and then try the transaction again waitfor delay '00:00:00.25'; set @numErrors = @numErrors + 1; goto LockTimeoutRetry; end; -- Some other error occurred declare @errorMessage nvarchar(4000), @errorSeverity int select @errorMessage = error_message(), @errorSeverity = error_severity() raiserror(@errorMessage, @errorSeverity, 1) return -100 end catch;
fuente
Una cosa que me ha funcionado en el pasado es asegurarme de que todas mis consultas y actualizaciones accedan a los recursos (tablas) en el mismo orden.
Es decir, si una consulta se actualiza en el orden Tabla1, Tabla2 y una consulta diferente la actualiza en el orden de Tabla2, Tabla1, es posible que vea puntos muertos.
No estoy seguro de si es posible cambiar el orden de las actualizaciones ya que está utilizando LINQ. Pero es algo para mirar.
fuente
Definitivamente unos segundos serían aceptables. De todos modos, no parece que sea tan largo, a menos que una gran cantidad de personas estén enviando respuestas al mismo tiempo.
fuente
Estoy de acuerdo con Jeremy en este. Usted pregunta si debe crear un nuevo contexto de datos para cada controlador o por página; tiendo a crear uno nuevo para cada consulta independiente.
Estoy construyendo una solución en este momento que solía implementar el contexto estático como lo hace usted, y cuando lancé toneladas de solicitudes a la bestia de un servidor (más de un millón) durante las pruebas de estrés, también recibía bloqueos de lectura / escritura al azar.
Tan pronto como cambié mi estrategia para usar un contexto de datos diferente en el nivel LINQ por consulta, y confié en que el servidor SQL podría hacer su magia de agrupación de conexiones, los bloqueos parecieron desaparecer.
Por supuesto, estaba bajo cierta presión de tiempo, así que intenté varias cosas al mismo tiempo, así que no puedo estar 100% seguro de que eso fue lo que lo solucionó, pero tengo un alto nivel de confianza, digámoslo de esa manera. .
fuente
Debe implementar lecturas sucias.
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
Si no necesita absolutamente una integridad transaccional perfecta con sus consultas, debe usar lecturas sucias al acceder a tablas con alta concurrencia. Supongo que su tabla de Publicaciones sería una de esas.
Esto puede darle las llamadas "lecturas fantasmas", que es cuando su consulta actúa sobre los datos de una transacción que no se ha confirmado.
Usa lecturas sucias. Tiene razón en que no le darán una precisión perfecta, pero deberían aclarar sus problemas de bloqueo muerto.
Si implementa lecturas sucias en "el contexto de la base de datos base", siempre puede envolver sus llamadas individuales usando un nivel de aislamiento más alto si necesita la integridad transaccional.
fuente
Entonces, ¿cuál es el problema con la implementación de un mecanismo de reintento? Siempre existirá la posibilidad de que se produzca un punto muerto, así que ¿por qué no tener algo de lógica para identificarlo e intentarlo de nuevo?
¿No introducirán al menos algunas de las otras opciones penalizaciones de rendimiento que se aplican todo el tiempo cuando un sistema de reintento se activa raramente?
Además, no olvide algún tipo de registro cuando se produzca un reintento para no entrar en esa situación de rareza frecuente.
fuente
Ahora que veo la respuesta de Jeremy, creo que recuerdo haber escuchado que la mejor práctica es usar un nuevo DataContext para cada operación de datos. Rob Conery ha escrito varias publicaciones sobre DataContext, y siempre las informa en lugar de usar un singleton.
Este es el patrón que usamos para Video.Show ( enlace a la vista de fuente en CodePlex ):
Luego, en el nivel de servicio (o incluso más granular, para actualizaciones):
private VideoShowDataContext dataContext = DataContextFactory.DataContext(); public VideoSearchResult GetVideos(int pageSize, int pageNumber, string sortType) { var videos = from video in DataContext.Videos where video.StatusId == (int)VideoServices.VideoStatus.Complete orderby video.DatePublished descending select video; return GetSearchResult(videos, pageSize, pageNumber); }
fuente
Tendría que estar de acuerdo con Greg siempre que establecer el nivel de aislamiento para leer no confirmado no tenga efectos nocivos en otras consultas.
Me interesaría saber, Jeff, cómo la configuración en el nivel de la base de datos afectaría una consulta como la siguiente:
Begin Tran Insert into Table (Columns) Values (Values) Select Max(ID) From Table Commit Tran
fuente
Me parece bien si mi perfil está desactualizado incluso varios minutos.
¿Está volviendo a intentar la lectura después de que falla? Ciertamente es posible cuando se disparan una tonelada de lecturas aleatorias que algunas acertarán cuando no puedan leer. La mayoría de las aplicaciones con las que trabajo son muy pocas escrituras en comparación con la cantidad de lecturas y estoy seguro de que las lecturas no se acercan al número que está obteniendo.
Si implementar "READ UNCOMMITTED" no resuelve su problema, entonces es difícil ayudar sin saber mucho más sobre el procesamiento. Puede haber alguna otra opción de ajuste que ayude a este comportamiento. A menos que algún gurú de MSSQL venga al rescate, recomiendo enviar el problema al proveedor.
fuente
Seguiría afinando todo; ¿Cómo está funcionando el subsistema de disco? ¿Cuál es la longitud promedio de la cola de disco? Si las E / S están realizando copias de seguridad, es posible que el problema real no sean estas dos consultas que se bloquean, podría ser otra consulta la que está bloqueando el sistema; mencionaste una consulta que tarda 20 segundos que ha sido ajustada, ¿hay otras?
Concéntrese en acortar las consultas de larga duración, apuesto a que los problemas de interbloqueo desaparecerán.
fuente
Tuve el mismo problema y no puedo usar "IsolationLevel = IsolationLevel.ReadUncommitted" en TransactionScope porque el servidor no tiene DTS habilitado (!).
Eso es lo que hice con un método de extensión:
public static void SetNoLock(this MyDataContext myDS) { myDS.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); }
Entonces, para selecciones que usan tablas de concurrencia críticas, habilitamos el "nolock" de esta manera:
¡Las sugerencias son bienvenidas!
fuente