Consulta sobre bloqueos

Acabo de hacer que una lista de restaurantes se pueda ordenar por nº de
visitas:

El nº de visitas se actualiza en cada visita, en el show:
@producto.update_attribute(‘veces_visto’, 1 + (@producto.veces_visto ||
0))

Pero el problema es que mientras se está actualizando, que es algo
bastante frecuente, se produce un bloqueo, y si se intenta acceder justo
en ese instante dará un error 500. De hecho, es fácil hasta provocarlo
uno mismo: basta con entrar en uno de los restaurantes con más visitas:

Le das a recargar (F5) quince veces seguidas, rápido, y te vas enseguida
a la lista ordenada por visitas… y enseguida tienes un error 500

Para tratar de solucionarlo, he intentado hacer que la actualización se
haga retrasada:
spawn do
@producto.update_attribute(‘veces_visto’, 1 + (@producto.veces_visto
|| 0))
end

Pero lo único que consigo es que el error 500 se produzca un poco
después… pero producirse, se produce igualmente.

¿Alguien se ha encontrado con este tipo de problemas? ¿Cómo pueden
solucionarse los conflictos de bloqueo, para campos que se actualizan
con mucha frecuencia?

PD: Y si encima se va a las últimas páginas, las probabilidades de
bloqueo se multiplican, pues MySQL recorre todos los registros hasta
llegar a los solicitados, y si alguno de los intermedios está
bloqueado… E500.

s2

On Jul 10, 2008, at 10:36 AM, Fernando C. wrote:

Pero lo único que consigo es que el error 500 se produzca un poco
después… pero producirse, se produce igualmente.

¿Alguien se ha encontrado con este tipo de problemas? ¿Cómo pueden
solucionarse los conflictos de bloqueo, para campos que se actualizan
con mucha frecuencia?

Puedes mandar el log de la aplicación cuando te da el error 500.

¿En que maquina tienes la aplicación funcionando?

Francesc E. wrote:

Puedes mandar el log de la aplicaci�n cuando te da el error 500.

FLIPANTE EL LOG!!!

Fijaos en esto:

Processing RestaurantesController#lista
Parameters: {“action”=>“lista”, “controller”=>“restaurantes”,
“orden”=>“visitas”}

El código es este:

pagina = (params[:page] ||= 1).to_i
orden = (params[:orden]) || "comentarios"
orden = orden.sub('visitas', 'veces_visto')
orden += " Desc" if 

[“puntuacion_media”,“contenidos_count”,“guardado_count”,
“veces_visto”].include?(orden)
join = @ver_provincia ? “inner join provincias p on p.id =
productos.provincia_id” : “”

@nproductos = Producto.count(:all, :joins => join )
@productos = Producto.paginate(:all, :per_page => 

Producto::PRODUCTOS_EN_LISTA, :page => pagina,
:select => “productos.nombre, productos.localidad,
#{@ver_provincia ? “p.nombre as provincia_,” : “”}
productos.puntuacion_media,
productos.contenidos_count, productos.guardado_count,
productos.id, productos.permalink,
productos.type, productos.anyo,
productos.id_publicado,
productos.veces_visto”,
:joins => join, :order => orden + “, nombre”,
:total_entries => @nproductos)

Normalmente esto funciona bien y genera esta consulta SQL:

SELECT productos.nombre, productos.localidad, p.nombre as provincia_,
productos.puntuacion_media, productos.contenidos_count,
productos.guardado_count, productos.id,
productos.permalink, productos.type, productos.anyo,
productos.id_publicado,
productos.veces_visto FROM productos inner join provincias p on
p.id = productos.provincia_id ORDER BY visitas, nombre LIMIT 0, 50

Y el error del log es este (lo he comprobado varias veces porque no me
lo creía):

ActiveRecord::StatementInvalid (Mysql::Error: Unknown column ‘visitas’
in ‘order clause’: SELECT productos.nombre, productos.localidad,
p.nombre as provincia_, productos.puntuacion_media,
productos.contenidos_count,
productos.guardado_count, productos.id,
productos.permalink, productos.type, productos.anyo FROM productos
inner join provincias p on p.id = productos.provincia_id WHERE
(productos.subtipo_id = 41 ) ORDER BY visitas, nombre LIMIT 0, 50)

Es decir, que cuando no puede acceder al campo veces_visto porque se
está actualizando, me recorta los dos últimos campos del SELECT
(productos.id_publicado, productos.veces_visto) y no me hace el orden =
orden.sub(‘visitas’, ‘veces_visto’). EN VEZ DE UN ERROR DE BASE DE
DATOS, LO QUE FALLA ES EL RAILS!!

�En que maquina tienes la aplicaci�n funcionando?

La aplicación corre sobre un servidor CentOS, en el que también está la
BBDD MySQL.

s2

2008/7/10 Fernando C. [email protected]:

Pero el problema es que mientras se está actualizando, que es algo
bastante frecuente, se produce un bloqueo, y si se intenta acceder justo
en ese instante dará un error 500.

Fernando averiguaste algo sobre esto?

Hola Fernando,

por lo que cuentas a mí me suena que puede ser un tema de que los
UPDATEs sobre la fila estén bloqueando los SELECT de esa misma fila.

Creo que es un pequeño error de diseño fácilmente subsanable si sacas
las estadísticas a otra tabla o incluso a otra base de datos:

Si lo sacas a otra tabla puedes cachear el valor del contador de
visitas para que el SELECT no se efectúe directamente sino contra el
valor cacheado.

Y si lo sacas a otra tabla puedes usar una fantástica función de
ActiveRecord un poco desconocida: stablish_connection.

Nosotros en unvlog.com, tenemos un modelo hit (página vista) definido
así:

class Hit < ActiveRecord::Base

Hit.establish_connection(‘stats’)

def self.create_hit!(post_id, user_id)
Hit.create(:post_id => post_id, :user_id => user_id)
end

end

Ten en cuenta que si está en otra tabla no puedes usar relaciones de
ActiveRecord, pero es un mal menor si defines métodos en las clases.

Espero que te sirva.

2008/7/10 Fernando C. [email protected]:

On Jul 13, 2008, at 9:21 AM, Fernando B. wrote:

Y si lo sacas a otra tabla puedes usar una fantástica función de
ActiveRecord un poco desconocida: stablish_connection.

Fernando … te falta el café de la mañana. :wink:

“Y si lo sacas a otra base de datos … un poco desconocida
establish_connection”

Me he colado, sí, es

Modelo.establish_connection

¿Te refieres a eso, no?

On Sun, Jul 13, 2008 at 9:34 AM, Francesc E.

On Jul 13, 2008, at 10:57 AM, Fernando B. wrote:

¿Te refieres a eso, no?

Si.

Fernando B. wrote:

Hola Fernando,

por lo que cuentas a mí me suena que puede ser un tema de que los
UPDATEs sobre la fila estén bloqueando los SELECT de esa misma fila.

Creo que es un pequeño error de diseño fácilmente subsanable si sacas
las estadísticas a otra tabla o incluso a otra base de datos:

Si lo sacas a otra tabla puedes cachear el valor del contador de
visitas para que el SELECT no se efectúe directamente sino contra el
valor cacheado.

Y si lo sacas a otra tabla puedes usar una fantástica función de
ActiveRecord un poco desconocida: stablish_connection.

Nosotros en unvlog.com, tenemos un modelo hit (página vista) definido
así:

class Hit < ActiveRecord::Base

Hit.establish_connection(‘stats’)

def self.create_hit!(post_id, user_id)
Hit.create(:post_id => post_id, :user_id => user_id)
end

end

Ten en cuenta que si está en otra tabla no puedes usar relaciones de
ActiveRecord, pero es un mal menor si defines métodos en las clases.

Espero que te sirva.

2008/7/10 Fernando C. [email protected]:

El tema de cachear ya me lo había planteado… de hecho, en realidad
para cachear no necesitaría ni siquiera otra tabla, me bastaría con otro
campo de la misma tabla: uno de los campos sería para escritura, y otro
sería el caché que utilizaría para lectura; y una vez al día, cargar los
datos al caché. Pero claro, al no ser en tiempo real, es una solución
menos buena…

Utilizar distintas tablas o incluso distintas BBDD, entiendo que sería
para añadir un registro por cada visita, y no como hasta ahora
incrementar un contador; pero entiendo que o vamos a cacheado (que se
puede hacer igual en la misma tabla, añadiendo un campo), o si vamos a
tiempo real puede que el rendimiento de consultar una tabla con tantos
registros sea muy bajo, ¿no?

s2

Gracias, me faltaban dos cafés, sí :slight_smile:

On Sun, Jul 13, 2008 at 10:59 AM, Francesc E.

On Jul 14, 2008, at 1:01 PM, Fernando C. wrote:

El tema de cachear ya me lo había planteado… de hecho, en realidad
para cachear no necesitaría ni siquiera otra tabla, me bastaría con
otro
campo de la misma tabla: uno de los campos sería para escritura, y
otro
sería el caché que utilizaría para lectura; y una vez al día, cargar
los
datos al caché. Pero claro, al no ser en tiempo real, es una solución
menos buena…

Y no has pensado en inesertar todas los visualizaciones en algun
sistema de colas
que vayas procesando cada X tiempo, no es en tiempo real, pero no es
mala
solución.

  1. Se visualiza una url.
  2. Metes esa visualización en la cola.
  3. Cada minuto procesas la cola.

Francesc E. wrote:

Y no has pensado en inesertar todas los visualizaciones en algun
sistema de colas
que vayas procesando cada X tiempo, no es en tiempo real, pero no es
mala
soluci�n.

  1. Se visualiza una url.
  1. Metes esa visualizaci�n en la cola.
  2. Cada minuto procesas la cola.

Lo de las colas suena bien… ¿tienes alguna URL donde expliquen cómo
funciona?

También hay que tener en cuenta que según el esquema de la tabla el
bloque se produce a nivel de fila (Innodb) o de tabla entera (MyISAM).

2008/7/14 Francesc E. [email protected]:

Fernando B. wrote:

También hay que tener en cuenta que según el esquema de la tabla el
bloque se produce a nivel de fila (Innodb) o de tabla entera (MyISAM).

2008/7/14 Francesc E. [email protected]:

En este caso el bloqueo es a nivel de fila, es Innodb, pero en una lista
en las que hay que consultar muchas filas la probabilidad de colisión no
es lo suficientemente baja como para darla por aceptable…

Nosotros en La Coctelera y unvlog.com tenemos Delayed Jobs:

http://blog.leetsoft.com/2008/2/17/delayed-job-dj

2008/7/14 Fernando C. [email protected]:

On Jul 14, 2008, at 8:30 PM, Fernando B. wrote:

Nosotros en La Coctelera y unvlog.com tenemos Delayed Jobs:

http://blog.leetsoft.com/2008/2/17/delayed-job-dj

Ostras … yo lo estuve valorando, pero es que me pareció bastante feo
hacer inserts de los jobs en la base de datos. Lo interesante de
Delayed Jobs es que al estar los jobs dentro de la base de datos es
“más fácil” meter sistemas de prioridades, horas de ejecución. Si a
veces ya hay problemas con el rendimiento de la base de datos, ¿meter
Delayed Jobs no es meterle un poco más de carga a la base de datos?

  • Que lo hayan extraido de Shopify es un punto a favor.
  • Que los jobs sean persistentes es un punto a favor.
  • Que esté en la base de datos … feo, pero tiene sus cosas buenas.

Despues de haber utilizado BackgroundRb … y haberme acordado del
desarrollador varias veces por cambios en la API, al final valoré las
opciones de Beanstalkd y Starling, y por necesidades de persistencia
al final opté por Starling. Como ya comenté extraje un plugin que se
llama simplified_starling [1] que creo que simplifica bastante la
gestion de colas y es super facil de integrar en una aplicación.

[1] http://github.com/fesplugas/simplified_starling/tree/master

Yo uso Amazon SQS, el sistema de colas que proporciona Amazon Web
Services [1], con la gema right_aws [2].

[1] http://aws.amazon.com/
[2] http://rightaws.rubyforge.org/

Salu2,

Por que es una cola solucion a este problema?

Xavier N. wrote:

Por que es una cola solucion a este problema?

Entiendo que la idea es que ya que se van a hacer muchísimas
actualizaciones, que puntualmente provocarán colisiones con lecturas de
forma más o menos aleatoria… si se pueden agrupar las actualizaciones,
y llevarlas a cabo en un momento de menor carga, se reduce la
probabilidad de colisiones.

De todos modos, está claro que meter una tabla específica con un
registro por cada página vista sería una solución más efectiva, pues
según lo veo yo la cola amortiguaría pero no resolvería del todo el
problema; pero falta ver si eso no sería un poco “matar moscas a
cañonazos”…

Aunque no tengo claro si realmente, mediante colas, se puede conseguir
una solución al 100% del problema, o sólo una mejora (como yo veo).

s2

On Jul 14, 2008, at 9:45 PM, Xavier N. wrote:

Por que es una cola solucion a este problema?

Cada vez que alguien carga una página se contabiliza y se guarda en la
base de datos. Eso no deberia ser un problema, pero Fernando C.
tiene problemas con los bloqueos de la base de datos.

Entre las opciones que se le han dado, yo comenté tener una cola donde
se fueran contabilizando todas estas consultas y que cada X minutos se
fuera procesando. Quizas una cola no es una solución a este problema,
pero es una opción para solucionarlo. ¿No?

Yo hice el comentario, Fernando B. comentó como lo hacen en La
Coctelera, y yo he continuado hablando de colas.

Lo que seria interesante saber es que tipo de tablas está utilizando,
como está el servidor de carga, etc …