En este documento, se describen métodos para que los administradores de bases de datos y los desarrolladores de aplicaciones generen secuencias numéricas únicas en las aplicaciones que usan Spanner.
Introducción
Suelen darse situaciones en las que una empresa requiere un ID numérico único y simple como, por ejemplo, un número de empleado o un número de factura. Las bases de datos relacionales convencionales a menudo incluyen una función para generar secuencias únicas y crecientes de forma monótona. Estas secuencias se usan a fin de generar identificadores únicos (claves de filas) para los objetos almacenados en la base de datos.
Sin embargo, el uso de valores que aumentan (o descienden) de forma monótona, como claves de filas, podría no seguir las prácticas recomendadas en Spanner, ya que crea hotspots en la base de datos, lo que lleva a un de rendimiento. En este documento, se proponen mecanismos a fin de implementar un generador de secuencias mediante una tabla de base de datos de Spanner y una lógica de capa de aplicación.
Como alternativa, Spanner admite un generador de secuencias integrado en bits invertido. Para obtener más información sobre el generador de secuencias de Spanner, consulta Crea y administra secuencias.
Requisitos para un generador de secuencias
Cada generador de secuencias debe generar un valor único para cada transacción.
Según el caso práctico, también es posible que un generador de secuencias necesite crear secuencias con las siguientes características:
- Estar ordenado: Los valores más bajos en la secuencia no deben emitirse después de los valores más altos.
- No tener espacios: No debe haber espacios en la secuencia.
El generador de secuencias también debe generar valores con la frecuencia que requiere la aplicación.
Puede ser difícil cumplir con todos estos requisitos, en especial en un sistema distribuido. Si es necesario para cumplir con los objetivos de rendimiento, puedes dejar de lado los requisitos de orden y ausencia de espacios de la secuencia.
Otros motores de base de datos tienen maneras de manejar estos requisitos. Por ejemplo, las secuencias en las columnas PostgreSQL y AUTO_INCREMENT en MySQL pueden generar valores únicos para transacciones diferentes, pero no pueden producir valores sin espacios si las transacciones se revierten. Para obtener más información, consulta Notas en la documentación de PostgreSQL y, también, Implicaciones de AUTO_INCREMENT en MySQL.
Generadores de secuencias mediante filas de tablas de bases de datos
La aplicación puede implementar un generador de secuencias mediante el uso de una tabla de base de datos en la que se almacenan los nombres de las secuencias y el siguiente valor en la secuencia.
Leer y aumentar la celda next_value
de la secuencia dentro de una transacción de base de datos genera valores únicos, sin necesidad de ninguna otra sincronización entre los procesos de la aplicación.
Primero, define la tabla de la siguiente manera:
CREATE TABLE sequences (
name STRING(64) NOT NULL,
next_value INT64 NOT NULL,
) PRIMARY KEY (name)
Puedes crear secuencias si insertas una fila en la tabla con el nombre de la secuencia nueva y el valor de inicio; por ejemplo, ("invoice_id", 1)
. Sin embargo, debido a que la celda next_value
se incrementa para cada valor de secuencia generado, el rendimiento está limitado por la frecuencia con la que se puede actualizar la fila.
Las bibliotecas cliente de Spanner usan transacciones que se pueden reintentar para resolver conflictos. Si las celdas (valores de columna) que se leen durante una transacción de lectura y escritura se modifican en otra transacción, la transacción de lectura y escritura se bloqueará hasta que se complete la otra transacción. Luego, se anulará y se volverá a intentar para que se puedan leer los valores actualizados. Esto minimiza la duración de los bloqueos de escritura, pero también significa que una transacción se puede intentar varias veces antes de que se confirme de forma correcta.
Debido a que solo puede llevarse a cabo una transacción a la vez en una fila, la frecuencia máxima de emisión de valores de secuencia es inversamente proporcional a la latencia total de la transacción.
Esta latencia total de transacción depende de varios factores, como la latencia entre la aplicación cliente y los nodos de Spanner, la latencia entre los nodos Spanner y la incertidumbre de TrueTime. Por ejemplo, la configuración multirregional tiene una latencia de transacción más alta, ya que debe esperar que se genere un quórum de confirmaciones de escritura de los nodos en diferentes regiones para completarse.
Por ejemplo, si una transacción de actualización de lectura en una sola celda (una columna en una fila) tiene una latencia de 10 milisegundos (ms), en teoría, la frecuencia máxima de emisión de valores de secuencia es 100 por segundo. Este máximo se aplica a toda la base de datos, sin importar la cantidad de instancias de la aplicación cliente o la cantidad de nodos de la base de datos. Esto se debe a que un solo nodo siempre administra una sola fila.
En la siguiente sección, se describen las formas de solucionar esta limitación.
Implementación en el lado de la aplicación
El código de la aplicación debe leer y actualizar la celda next_value
en la base de datos. Existen varias formas de hacerlo, cada una de las cuales presenta diferentes desventajas y características de rendimiento.
Generador de secuencias simple dentro de la transacción
La forma más simple de manejar la generación de secuencias es incrementar el valor de la columna dentro de la transacción cada vez que la aplicación necesite un valor secuencial nuevo.
En una sola transacción, la aplicación hace lo siguiente:
- Lee la celda
next_value
del nombre de la secuencia que se usará en la aplicación. - Aumenta y actualiza la celda
next_value
para el nombre de la secuencia. - Usa el valor recuperado para cualquier valor de columna que la aplicación necesite.
- Completa el resto de la transacción de la aplicación.
Con este proceso, se genera una secuencia ordenada y sin espacios. Si nada actualiza la celda next_value
de la base de datos a un valor menor, la secuencia también será única.
Debido a que el valor de la secuencia se recupera como parte de la transacción de aplicación más amplia, la frecuencia máxima de generación de secuencias depende de la complejidad de la transacción completa de la aplicación. Una transacción compleja tendrá una latencia más alta y, por lo tanto, una menor frecuencia máxima posible.
En un sistema distribuido, es posible que se intenten muchas transacciones al mismo tiempo, lo que genera una alta competencia en el valor de la secuencia. Debido a que la celda next_value
se actualiza dentro de la transacción de la aplicación, cualquier otra transacción que intente aumentar la celda next_value
al mismo tiempo se bloqueará por esta primera transacción y se volverá a intentar.
Esto genera grandes aumentos en el tiempo que la aplicación necesita para completar la transacción de forma correcta, lo que puede causar problemas de rendimiento.
El siguiente código proporciona un ejemplo de un generador de secuencias simple en la transacción que muestra solo un valor de secuencia por transacción. Esta restricción existe porque las escrituras dentro de una transacción realizadas mediante la API de mutación no son visibles hasta que se confirma la transacción. Ni siquiera las lecturas de la misma transacción pueden verlas. Por lo tanto, si se llama a esta función varias veces en la misma transacción, siempre se mostrará el mismo valor de secuencia.
En el siguiente código de ejemplo, se muestra cómo implementar una función getNext()
síncrona:
En el siguiente código de ejemplo, se muestra cómo se usa la función getNext()
síncrona en una transacción:
Generador de secuencias síncrono mejorado dentro de la transacción
Puedes modificar la abstracción anterior para que se produzcan varios valores dentro de una sola transacción si haces un seguimiento de los valores de secuencia emitidos dentro de una transacción.
En una sola transacción, la aplicación hace lo siguiente:
- Lee la celda
next_value
del nombre de la secuencia que se usará en la aplicación. - Almacena este valor como una variable de forma interna.
- Cada vez que se solicita un valor de secuencia nuevo, se incrementa la variable
next_value
almacenada y se almacena en búfer una escritura que establece el valor de la celda actualizada en la base de datos. - Completa el resto de la transacción de la aplicación.
Si usas una abstracción, el objeto para esta abstracción debe crearse dentro de la transacción. El objeto realiza una sola lectura cuando se solicita el primer valor. El objeto realiza un seguimiento interno de la celda next_value
para que se pueda generar más de un valor.
Las mismas advertencias relacionadas con la latencia y la competencia que se aplicaron a la versión anterior también se aplican a esta versión.
En el siguiente código de ejemplo, se muestra cómo implementar una función getNext()
síncrona:
En el siguiente código de ejemplo, se muestra cómo usar la función getNext()
síncrona en una solicitud de dos valores de secuencia:
Generador de secuencias fuera de la transacción (asíncrono)
En las dos implementaciones anteriores, el rendimiento del generador depende de la latencia de la transacción de la aplicación. Puedes mejorar la frecuencia máxima, a expensas de tolerar espacios en la secuencia, si incrementas la secuencia en una transacción independiente. (Este es el enfoque que usa PostgreSQL). Debes recuperar los valores de secuencia que se usarán primero, antes de que la aplicación inicie su transacción.
La aplicación hace lo siguiente:
- Crea una primera transacción para obtener y actualizar el valor de la secuencia:
- Lee la celda
next_value
del nombre de la secuencia que se usará en la aplicación. - Almacena este valor como una variable.
- Incrementa y actualiza la celda
next_value
del nombre de la secuencia en la base de datos. - Completa la transacción.
- Lee la celda
- Usa el valor mostrado en una transacción independiente.
La latencia de esta transacción independiente será cercana a la latencia mínima, con un rendimiento que se aproximará a la frecuencia teórica máxima de 100 valores por segundo (si suponemos que la latencia de transacción es de 10 ms). Debido a que los valores de secuencia se recuperan por separado, la latencia de la transacción de la aplicación en sí no se modifica y la competencia se minimiza.
Sin embargo, si se solicita un valor de secuencia y no se usa, queda un espacio en la secuencia porque no es posible revertir los valores de secuencia solicitados. Esto puede ocurrir si la aplicación se anula o falla durante la transacción después de solicitar un valor de secuencia.
En el siguiente código de ejemplo, se muestra cómo implementar una función que recupera y aumenta la celda next_value
en la base de datos:
Puedes usar esta función con facilidad para recuperar un solo valor de secuencia nuevo, como se muestra en la siguiente implementación de una función getNext()
asíncrona:
En el siguiente código de ejemplo, se muestra cómo usar la función getNext()
asíncrona en una solicitud de dos valores de secuencia:
En el ejemplo de código anterior, puedes ver que los valores de secuencia se solicitan fuera de la transacción de la aplicación. Esto se debe a que Cloud Spanner no admite la ejecución de una transacción dentro de otra transacción en el mismo subproceso (lo que también se conoce como transacciones anidadas).
Para solucionar esta restricción, solicita el valor de secuencia mediante un subproceso en segundo plano y espera el resultado:
Generador de secuencias por lotes
Puedes obtener una mejora significativa del rendimiento si también descartas el requisito que establece que los valores de la secuencia deben estar en orden. Esto le permite a la aplicación reservar un lote de valores de secuencia y emitirlos de forma interna. Las instancias de aplicación individuales tienen su propio lote de valores independiente, por lo que los valores que se emiten no están en orden. Además, las instancias de aplicación que no usan todo su lote de valores (por ejemplo, si la instancia de aplicación se cierra) dejarán espacios de valores sin usar en la secuencia.
La aplicación hará lo siguiente:
- Mantendrá un estado interno para cada secuencia que contenga el valor de inicio y el tamaño del lote, además del siguiente valor disponible.
- Solicitará un valor de secuencia del lote.
- Si no hay valores restantes en el lote, haz lo siguiente:
- Crea una transacción para leer y actualizar el valor de la secuencia.
- Lee la celda
next_value
de la secuencia. - Almacena este valor de forma interna como el valor inicial del lote nuevo.
- Incrementa la celda
next_value
en la base de datos de modo que quede establecida una cantidad equivalente al tamaño del lote. - Completa la transacción.
- Muestra el siguiente valor disponible y aumenta el estado interno.
- Usa el valor que se muestra en la transacción.
Con este método, las transacciones que usan un valor de secuencia experimentarán un aumento en la latencia solo cuando se deba reservar un lote de valores de secuencia nuevo.
La ventaja es que cuando aumentas el tamaño del lote, el rendimiento se puede aumentar a cualquier nivel, ya que el factor limitante se convierte en la cantidad de lotes emitidos por segundo.
Por ejemplo, con un tamaño de lote de 100, si suponemos que la latencia es de 10 ms para obtener un lote nuevo y que, por lo tanto, hay un máximo de 100 lotes por segundo, se pueden emitir 10,000 valores de secuencia por segundo.
En el siguiente código de ejemplo, se muestra cómo implementar una función getNext()
mediante el uso de lotes. Ten en cuenta que el código vuelve a usar la función getAndIncrementNextValueInDB()
definida con anterioridad para recuperar nuevos lotes de valores de secuencia de la base de datos.
En el siguiente código de ejemplo, se muestra cómo usar la función getNext()
asíncrona en una solicitud de dos valores de secuencia:
Otra vez, los valores deben solicitarse fuera de la transacción (o mediante un subproceso en segundo plano), ya que Spanner no admite transacciones anidadas.
Generador asíncrono de secuencias por lotes
En el caso de las aplicaciones de alto rendimiento en las que no se acepta un aumento de latencia, puedes mejorar el rendimiento del generador por lotes anterior si cuentas con un nuevo lote de valores preparado para el momento en que se agote el lote de valores actual.
Para lograrlo, establece un umbral que indique cuándo la cantidad de valores de secuencia restantes en un lote es demasiado baja. Cuando se alcanza el umbral, el generador de secuencias comienza a solicitar un nuevo lote de valores en un subproceso en segundo plano.
Al igual que con la versión anterior, los valores no se emiten en orden y habrá espacios de valores que no se usan en la secuencia si las transacciones fallan o si las instancias de aplicación se cierran.
La aplicación hará lo siguiente:
- Mantendrá un estado interno para cada secuencia, que contenga el valor inicial del lote y el siguiente valor disponible.
- Solicitará un valor de secuencia del lote.
- Si los valores restantes del lote son menores que el umbral, haz lo siguiente en un subproceso en segundo plano:
- Crea una transacción para leer y actualizar el valor de la secuencia.
- Lee la celda
next_value
del nombre de la secuencia que se usará en la aplicación. - Almacena este valor de forma interna como el valor inicial del siguiente lote.
- Incrementa la celda
next_value
en la base de datos de modo que quede establecida una cantidad equivalente al tamaño del lote. - Completa la transacción.
- Si no hay valores restantes en el lote, recupera el valor de inicio del siguiente lote desde el subproceso en segundo plano (espera que se complete si es necesario) y crea un lote nuevo en el que uses el valor de inicio que recuperaste como siguiente valor.
- Muestra el siguiente valor y, también, incrementa el estado interno.
- Usa el valor que se muestra en la transacción.
Para obtener un rendimiento óptimo, el subproceso en segundo plano debe iniciarse y completarse antes de que se agoten los valores de secuencia en el lote actual. De lo contrario, la aplicación deberá esperar al siguiente lote, y la latencia aumentará. Por lo tanto, deberás ajustar el tamaño del lote y el umbral bajo, según la frecuencia con la que se emiten los valores de secuencia.
Por ejemplo, supongamos que el tiempo de transacción es de 20 ms para recuperar un nuevo lote de valores, que el tamaño de lote es de 1, 000 y que la frecuencia máxima de emisión de secuencias es de 500 valores por segundo (un valor cada 2 ms). Durante los 20 ms en los que se emite un nuevo lote de valores, se emiten 10 valores de secuencia. Por lo tanto, el umbral para el número de valores de secuencia restantes debe ser superior a 10, de modo que el siguiente lote esté disponible cuando se lo necesite.
En el siguiente código de ejemplo, se muestra cómo implementar una función getNext()
mediante el uso de lotes. Ten en cuenta que el código usa la función getAndIncrementNextValueInDB()
definida con anterioridad para recuperar un lote de valores de secuencia mediante un subproceso en segundo plano.
En el siguiente código de ejemplo, se muestra cómo se usa la función getNext()
por lotes y asíncrona en una solicitud de dos valores que se usarán en la transacción:
Ten en cuenta que, en este caso, los valores se pueden solicitar dentro de la transacción, ya que la recuperación de un nuevo lote de valores se produce en un subproceso en segundo plano.
Resumen
En la siguiente tabla, se comparan las características de los cuatro tipos de generadores de secuencias:
Síncrono | Asíncrono | Lote | Por lotes y asíncrono | |
---|---|---|---|---|
Valores únicos | Sí | Sí | Sí | Sí |
Valores ordenados de forma global | Sí | Sí | No Sin embargo, con una carga bastante alta y un tamaño de lote bastante pequeño, los valores estarán cerca el uno del otro. |
No Sin embargo, con una carga bastante alta y un tamaño de lote bastante pequeño, los valores estarán cerca el uno del otro. |
Sin espacios | Sí | No | No | No |
Rendimiento | Latencia de 1 por transacción, (~25 valores por segundo) |
Entre 50 y 100 valores por segundo | Entre 50 y 100 lotes de valores por segundo | Entre 50 y 100 lotes de valores por segundo |
Aumento de latencia | > 10 ms Mucho mayor con alta competencia (cuando una transacción lleva mucho tiempo) |
10 ms en cada transacción Mucho mayor con alta competencia |
10 ms, pero solo cuando se recupera un nuevo lote de valores | Cero, si el tamaño del lote y el umbral bajo se establecen en los valores adecuados |
En la tabla anterior, también se ilustra el hecho de que es posible que debas dejar de lado los requisitos que establecen que los valores deben estar ordenados de manera global y que las series de valores no deben tener espacios a fin de generar valores únicos, a la vez que cumples con los requisitos de rendimiento general.
Pruebas de rendimiento
Puedes usar una herramienta de prueba o análisis de rendimiento, que se encuentra en el mismo repositorio de GitHub que las clases de generador de secuencias anteriores, para probar cada uno de estos generadores de secuencias y demostrar las características de rendimiento y latencia. La herramienta simula una latencia de transacción de la aplicación de 10 ms y ejecuta varios subprocesos de forma simultánea que solicitan valores de secuencia.
Para las pruebas de rendimiento, solo se necesita una instancia de Spanner de nodo único en la que realizar la prueba, ya que solo se modifica una fila.
Por ejemplo, en la siguiente salida, se muestra una comparación entre el rendimiento y la latencia en modo síncrono con 10 subprocesos:
$ ITERATIONS=2000
$ MODE=SYNC
$ NUMTHREADS=10
$ java -jar sequence-generator.jar \
$INSTANCE_ID $DATABASE_ID $MODE $ITERATIONS $NUMTHREADS
2000 iterations (10 parallel threads) in 58739 milliseconds: 34.048928 values/s
Latency: 50%ile 27 ms
Latency: 75%ile 31 ms
Latency: 90%ile 1189 ms
Latency: 99%ile 2703 ms
En la siguiente tabla, se comparan los resultados de varios modos y números de subprocesos paralelos, incluida la cantidad de valores que se pueden emitir por segundo y la latencia en los percentiles 50, 90 y 99:
Modo y parámetros | Cantidad de subprocesos | Valores por segundo | Latencia en percentil 50 (ms) | Latencia en percentil 90 (ms) | Latencia en percentil 99 (ms) |
---|---|---|---|---|---|
SYNC | 10 | 34 | 27 | 1,189 | 2,703 |
SYNC | 50 | 30.6 | 1,191 | 3,513 | 5,982 |
ASYNC | 10 | 66.5 | 28 | 611 | 1,460 |
ASYNC | 50 | 78.1 | 29 | 1,695 | 3,442 |
BATCH (tamaño 200) |
10 | 494 | 18 | 20 | 38 |
BATCH (tamaño del lote 200) | 50 | 1,195 | 27 | 55 | 168 |
ASYNC BATCH (tamaño del lote 200, LT 50) |
10 | 512 | 18 | 20 | 30 |
ASYNC BATCH (tamaño del lote 200, LT 50) |
50 | 1,622 | 24 | 28 | 30 |
Puedes ver que en el modo síncrono (SYNC), con el aumento de la cantidad de subprocesos, se produce un aumento de la competencia. Esto genera latencias de transacción mucho más altas.
En el modo asíncrono (ASYNC), debido a que la transacción para obtener la secuencia es más pequeña y, además, es independiente de la transacción de la aplicación, hay menos competencia, y la frecuencia es mayor. Sin embargo, la competencia aún puede ocurrir, lo que lleva a latencias superiores al percentil 90.
En el modo por lotes (BATCH), la latencia se reduce bastante, excepto por el percentil 99, que corresponde al momento en el que el generador necesita solicitar de forma síncrona otro lote de valores de secuencia de la base de datos. El rendimiento es muchas veces mayor en el modo BATCH que en el modo ASYNC.
El modo por lotes de 50 subprocesos tiene una latencia más alta porque las secuencias se emiten tan rápido que el factor limitante es la potencia de la instancia de máquina virtual (VM) (en este caso, una máquina de 4 CPU virtuales se ejecutó al 350% de CPU durante la prueba). Usar varias máquinas y varios procesos mostraría resultados generales similares al modo por lotes de 10 subprocesos.
En el modo ASYNC BATCH, la variación en latencia es mínima y el rendimiento es mayor, incluso con grandes cantidades de subprocesos, porque la latencia de solicitar un nuevo lote de la base de datos es completamente independiente de la transacción de la aplicación.
¿Qué sigue?
- Obtén información sobre las prácticas recomendadas para el diseño de esquemas en Spanner.
- Obtén detalles sobre cómo elegir índices y claves para las tablas de Spanner.
- Explora arquitecturas de referencia, diagramas y prácticas recomendadas sobre Google Cloud. Consulta nuestro Cloud Architecture Center.