Red de conocimiento informático - Consumibles informáticos - Cómo lograr la programación sin bloqueos tanto como sea posible en condiciones de alta concurrencia

Cómo lograr la programación sin bloqueos tanto como sea posible en condiciones de alta concurrencia

En un juego online de 2k, la concurrencia cada segundo es aterradora. La biblioteca tradicional de complementos directos de hibernación básicamente no es factible. Deduciré paso a paso una operación de base de datos sin bloqueo.

1. Cómo estar libre de bloqueos en concurrencia.

Una idea muy simple, convertir la concurrencia en un solo hilo. El Disruptor de Java es un buen ejemplo. Si usa la clase concurrentCollection de Java para hacerlo, el principio es iniciar un subproceso y ejecutar una cola. Durante la concurrencia, las tareas se insertan en la cola y los subprocesos se entrenan para leer la cola y luego ejecutarlos uno por uno.

Bajo este patrón de diseño, cualquier concurrencia se convertirá en una operación de un solo subproceso y será muy rápida. Los node.js actuales, o los servidores ARPG más comunes, tienen este diseño, una arquitectura de "gran bucle".

De esta manera, nuestro sistema original tiene dos entornos: el entorno concurrente y el entorno de "bucle grande".

El entorno concurrente es nuestro entorno tradicional bloqueado con bajo rendimiento.

El entorno "Big Loop" es un entorno sin bloqueo de un solo subproceso desarrollado por nosotros utilizando Disruptor, con un rendimiento potente.

2. Cómo mejorar el rendimiento del procesamiento en un entorno de "bucle grande".

Una vez que la concurrencia se convierte en un solo subproceso, una vez que uno de los subprocesos tiene un problema de rendimiento, todo el procesamiento inevitablemente se ralentizará. Por lo tanto, cualquier operación en un solo subproceso no debe implicar procesamiento de IO. ¿Qué pasa con las operaciones de bases de datos?

Aumentar el almacenamiento en caché. La idea es muy simple. Leer directamente desde la memoria definitivamente será más rápido. En cuanto a las operaciones de escritura y actualización, utilice una idea similar: envíe la operación a una cola y luego ejecute un subproceso separado para obtener las bibliotecas de inserción una por una. Esto garantiza que no haya operaciones IO involucradas en el "gran bucle".

El problema vuelve a surgir:

Si nuestro juego sólo tiene un bucle grande, es fácil de solucionar, porque proporciona una sincronización perfecta y sin bloqueos.

Pero el entorno real del juego coexiste con la concurrencia y el "gran bucle", que son los dos entornos mencionados anteriormente. Entonces, no importa cómo lo diseñemos, inevitablemente encontraremos que habrá un bloqueo en el caché.

3. ¿Cómo resolver la concurrencia y el "gran bucle" y eliminar los bloqueos?

Sabemos que si desea evitar operaciones de bloqueo en un "gran bucle", utilice "asíncrono" y entregue la operación al hilo para su procesamiento. Combinando estas dos características, cambié ligeramente la estructura de la base de datos.

La capa de caché original debe tener bloqueos, por ejemplo:

TableCache público

{

privado HashMaplt, Objectgt cachés; = new ConcurrentHashMaplt; String, Objectgt ();

}

Esta estructura es inevitable y garantiza que el caché se pueda operar con precisión en un entorno concurrente. Sin embargo, el "gran bucle" no puede operar directamente este caché para modificarlo, por lo que se debe iniciar un hilo para actualizar el caché, por ejemplo:

private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();

EXECUTOR.execute(new LatencyProcessor(logs));

la clase LatencyProcessor implementa Runnable

{

public void run()

{

//Aquí puedes modificar los datos de la memoria de forma arbitraria. Se utiliza asíncrono.

}

}

Vale, se ve precioso. Pero surge otro problema. En el proceso de acceso de alta velocidad, es muy probable que el caché aún no se haya actualizado, y otras solicitudes lo obtengan nuevamente y se obtengan los datos antiguos.

4. ¿Cómo garantizar que los datos almacenados en caché sean únicos y correctos en un entorno concurrente?

Sabemos que si solo hay operaciones de lectura y no de escritura, entonces este comportamiento no requiere bloqueo.

Utilizo esta técnica para agregar una capa de caché encima del caché para convertirlo en un "caché de nivel uno", y el original naturalmente se convertirá en un "caché de nivel dos". Un poco como una CPU, ¿verdad?

El caché de primer nivel solo se puede modificar mediante "bucles grandes", pero se puede adquirir al mismo tiempo y se pueden adquirir "bucles grandes" al mismo tiempo, por lo que no se requiere bloqueo.

Cuando se producen cambios en la base de datos, se dan dos situaciones:

1) Cambios en la base de datos en un entorno concurrente, permitimos la existencia de bloqueos, por lo que no hay problema en operar directamente la segunda caché de nivel.

2) Cuando la base de datos cambia en un entorno de "bucle grande", primero almacenamos los datos modificados en el caché de primer nivel, luego los enviamos a la corrección asincrónica del caché de segundo nivel y eliminamos el caché de primer nivel después de la corrección.

De esta manera, no importa en qué entorno se lean los datos, el caché de primer nivel se juzga primero y el caché de segundo nivel no.

Esta arquitectura garantiza la absoluta precisión de los datos de la memoria.

Y lo más importante: disponemos de un espacio eficiente y sin bloqueos para implementar cualquiera de nuestras lógicas de negocio.

Por último, hay algunos consejos para mejorar el rendimiento.

1. Dado que las operaciones de nuestra base de datos se han procesado de forma asincrónica, es posible que sea necesario insertar una gran cantidad de datos en la base de datos en un momento determinado al ordenar las tablas, las claves principales y los tipos de operaciones. Podemos eliminar algunas operaciones no válidas. Por ejemplo:

a) Para múltiples actualizaciones para la misma tabla y la misma clave principal, se seleccionará la última.

b) En la misma tabla y la misma clave principal, mientras aparezca Eliminar, todas las operaciones anteriores no serán válidas.

2. Dado que queremos ordenar las operaciones, debe haber una clasificación basada en el tiempo. ¿Cómo garantizar que no haya bloqueos? Utilice

private final static AtomicLong _seq = new AtomicLong(0);

para garantizar un incremento globalmente único y sin bloqueos, como una serie de tiempo.