Ir al contenido
wannacode

Pools de conexiones, copias de seguridad y permisos

En resumen: Una base de datos detrás de un sitio web en marcha necesita un pool que reutilice conexiones abiertas, copias de seguridad de las que de verdad se pueda restaurar y un usuario con solo los permisos necesarios.

Problema

Viernes por la noche

La tienda lanza una promoción y en cinco minutos llegan 400 personas. El sitio se detiene y el registro dice:

FATAL: sorry, too many clients already

El programa abre una conexión nueva a la base de datos para cada página y la cierra al final. Abrir una no es gratis: un viaje por la red, un inicio de sesión con contraseña y, en PostgreSQL, un proceso nuevo en el servidor. Y el servidor acepta un número limitado de conexiones (max_connections, 100 por defecto). Cuatrocientas páginas a la vez son cuatrocientas conexiones.

La idea

Un pool de conexiones

Un pool mantiene unas cuantas conexiones abiertas. Una página toma una libre, la usa y la devuelve, y la siguiente toma la misma. Abrir se paga una vez, y la base de datos nunca ve más conexiones que el tamaño del pool.

Aquí hay un pequeño pool de tres conexiones, escrito a mano para que veas qué hace. borrow da una conexión libre y, si no hay ninguna, abre una nueva, pero nunca más de tres. giveBack la devuelve:

static final int MAX = 3;
static final Deque<Connection> free = new ArrayDeque<>();
static int opened = 0;
static Connection borrow() throws SQLException {
if (!free.isEmpty()) return free.pop();
if (opened == MAX) throw new SQLException("el pool está vacío");
opened++;
Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:");
setup(conn);
return conn;
}
static void giveBack(Connection conn) {
free.push(conn);
}
static String productName(int id) throws SQLException {
Connection conn = borrow();
try (PreparedStatement ps = conn.prepareStatement("SELECT name FROM products WHERE id = ?")) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return "no existe el producto"; // la conexión nunca se devuelve
String name = rs.getString("name");
giveBack(conn);
return name;
}
}
}
for (int id : new int[] { 1, 99, 5, 98, 97, 7 }) {
try {
System.out.println("producto " + id + ": " + productName(id));
} catch (SQLException e) {
System.out.println("producto " + id + ": error: " + e.getMessage());
}
}
System.out.println("conexiones abiertas: " + opened + ", libres: " + free.size());

Míralo

Una conexión perdida

La función del nombre de un producto tiene dos salidas. Cuando el producto existe, devuelve la conexión. Cuando no existe, sale antes de tiempo y la conexión sigue ocupada:

static String productName(int id) throws SQLException {
Connection conn = borrow();
try (PreparedStatement ps = conn.prepareStatement("SELECT name FROM products WHERE id = ?")) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return "no existe el producto"; // la conexión nunca se devuelve
String name = rs.getString("name");
giveBack(conn);
return name;
}
}
}
Salida
producto 1: Pan blanco
producto 99: no existe el producto
producto 5: Leche fresca
producto 98: no existe el producto
producto 97: no existe el producto
producto 7: error: el pool está vacío
conexiones abiertas: 3, libres: 0

Tres búsquedas de un producto que no existe y el pool queda vacío. La sexta petición es perfectamente normal, pero no recibe ninguna conexión. Un pool de verdad no falla al instante: espera una conexión libre (hasta 30 segundos en HikariCP) y la página se cuelga. Por eso una fuga se nota primero como «el sitio va lento» y después como «el sitio no responde».

La idea

Devolverla va en finally

La solución es devolver la conexión salga como salga la función:

static String productName(int id) throws SQLException {
Connection conn = borrow();
try (PreparedStatement ps = conn.prepareStatement("SELECT name FROM products WHERE id = ?")) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString("name") : "no existe el producto";
}
} finally {
giveBack(conn);
}
}
Salida
producto 1: Pan blanco
producto 99: no existe el producto
producto 5: Leche fresca
producto 98: no existe el producto
producto 97: no existe el producto
producto 7: Queso blanco
conexiones abiertas: 1, libres: 1

Seis peticiones con una sola conexión: cada una la devuelve y la siguiente la toma. Con las bibliotecas reales no escribes un pool tú mismo. Tomas una conexión del pool y la cierras como en la primera lección del módulo, y cerrar la devuelve al pool en lugar de cerrarla de verdad:

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/shop");
config.setUsername("shop_app");
config.setPassword(System.getenv("SHOP_DB_PASSWORD"));
config.setMaximumPoolSize(10);
HikariDataSource pool = new HikariDataSource(config); // una vez, al arrancar
try (Connection conn = pool.getConnection()) { // close() la devuelve al pool
// las consultas
}
Más precisamente, para curiosos

Estos ejemplos no los ejecuta la comprobación del sitio, porque necesitan PostgreSQL y los paquetes del pool. Los tamaños por defecto son: HikariCP 10, Npgsql 100, SQLAlchemy 5 (y 10 más si hace falta), pg 10.

Un pool más grande no significa un sitio más rápido. La base de datos tiene los núcleos que tiene, y 200 consultas a la vez solo se esperan unas a otras. La guía de HikariCP sugiere empezar con unas dos veces el número de núcleos del servidor de la base de datos. Si el sitio corre en cinco servidores con 20 conexiones cada uno, la base de datos ve 100. Cuando hay demasiadas conexiones, delante de PostgreSQL se pone un pool aparte como PgBouncer.

La idea

Una copia de seguridad

Algún día la base de datos se dañará: un disco, una consulta equivocada, una migración con un error. Una copia es el único camino de vuelta. En SQLite VACUUM INTO escribe toda la base de datos en un archivo nuevo mientras está en marcha, y ATTACH abre ese archivo como una segunda base de datos en la misma conexión:

SQLbase de datos: tienda

Aquí se ve para qué sirve la copia. Un script borra por error las reseñas del café, y la copia de hace un minuto las devuelve:

SQLbase de datos: tienda

Ejecuta el cuadro por segunda vez y VACUUM INTO se detiene, porque el archivo ya existe. En la vida real es igual: cada copia es un archivo nuevo con la fecha y la hora en el nombre.

Más precisamente, para curiosos

En PostgreSQL pg_dump hace una copia de una base de datos, y pg_basebackup con un archivo WAL permite restaurar a un momento exacto («a las 14:14, antes del script»). En MySQL es mysqldump. Una regla habitual es 3-2-1: tres copias, en dos tipos de soporte, una de ellas en otro sitio.

Pruébalo

Comprobar después de restaurar

Una copia de la que nunca has restaurado es solo una esperanza. Después de cada restauración (al menos en una base de pruebas) compruebas que la base de datos está entera:

SQLbase de datos: tienda

y cuántas filas tiene cada tabla, para compararlo con los números de antes de la copia.

Para cada una de las seis tablas de la tienda muestra su nombre (columna table_name) y su número de filas (columna row_count).

SQLbase de datos: tienda

Ejercicio

Recupera lo borrado sin perder lo nuevo

La copia de anoche se ha restaurado en la tabla reviews_2026_10_24. Esta mañana un script borró reseñas de reviews y después un cliente escribió una reseña nueva. Recupera las reseñas borradas desde la copia sin perder la nueva.

SQLbase de datos: tienda

La idea

Tantos privilegios como haga falta

SQLite no tiene usuarios: quien puede abrir el archivo puede hacerlo todo. Las bases de datos de servidor sí los tienen, y el sitio entra con su propio usuario, que solo puede hacer lo que necesita:

CREATE ROLE shop_app LOGIN PASSWORD '…';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO shop_app;

Sin DROP, sin ALTER y sin acceso a las demás bases de datos del servidor. Si alguien entra en el sitio, puede hacer tanto como shop_app. Las migraciones se ejecutan con otro usuario, dueño de las tablas, y las dos contraseñas quedan fuera del código, como en la primera lección del módulo.

Test

Ponte a prueba

1. El sitio abre una conexión nueva a PostgreSQL para cada página. ¿Qué pasa en una hora punta?
2. El pool tiene 10 conexiones. Una función sale antes de tiempo sin devolver su conexión. ¿Qué pasa?
3. ¿Cuándo sabes que una copia de seguridad funciona?
4. ¿Qué privilegios necesita el usuario con el que entra el sitio?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·