You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Remix+Drizzle ORM多租户数据库连接方案求生产环境指导

多租户数据库连接问题与方案验证

问题背景

我用Remix框架搭建系统,主库存储用户、租户、常量等通用数据,租户专属数据存在独立schema中,目前遇到两个数据库连接难题:

  • 不缓存连接到内存:频繁创建新连接触发「连接数过多」错误
  • 缓存连接到内存:连接超时失效后导致应用崩溃

初始解决方案代码

export const masterDbOptions = {
  host: zenv.DATABASE_HOST,
  port: zenv.DATABASE_PORT,
  user: zenv.DATABASE_USER,
  password: zenv.DATABASE_PASSWORD,
  database: zenv.DATABASE_NAME,
  connectionLimit: 10,
  enableKeepAlive: true,
} satisfies mysql2.ConnectionOptions;

//* _______ 主库连接 _______ */
/* 同步创建主库连接 */
const main = mysql2.createConnection({ ...masterDbOptions });
export const db = drizzle(main, { schema, mode: 'default' });

/* 异步创建租户库连接的工具函数 */
const createConn = async (db: string) => await mysql2promise.createConnection({ ...masterDbOptions, database: db });

/* 内存中缓存租户连接,实现复用 */
let tenantConnections: { [key: string]: Awaited<Promise<mysql2promise.Connection>> } = {};

const resumeConn = async (tenantName: string) => {
  /* 若连接已存在则销毁,重新创建并缓存,直到连接超时 */
  tenantConnections[tenantName]?.end();
  tenantConnections[tenantName] = await createConn(`tenant_${tenantName}`)
    .then((conn) => conn)
    .catch((err) => {
      console.error(err);
      throw new Error('创建租户连接失败');
    });

  return drizzle(tenantConnections[tenantName], {
    schema: tenancySchema,
    mode: 'default',
  });
};

//* _______ 租户库连接入口 _______ */
export const tdb = async (
  request: Request,
  {
    permissions,
  }: {
    permissions?: UserPermissions[];
  } = {},
) => {
  const { user, tenant } = await authenticate(request, {
    permissions,
  });

  const db = await resumeConn(tenant);

  type DB = typeof db & { ctx: { user: typeof user.username } };

  (db as DB).ctx = { user: user.username };

  return db as DB;
};

使用示例

const query = await tdb(request, {
    permissions: [UserPermissions.GetCustomers],
  }).then((db) =>
    db.query.customers.findFirst({
      where: eq(customers.id, Number(params.id)),
    }),
  );

修改后的resumeConn函数

const resumeConn = async (tenantName: string) => {
  /* 检测现有连接有效性,失效则重建并缓存 */
  const ping = await tenantConnections[tenantName]
    ?.ping()
    .then(() => true)
    .catch(() => false);

  if (!ping) {
    // tenantConnections[tenantName]?.end();
    tenantConnections[tenantName] = await createConn(`tenant_${tenantName}`)
      .then((conn) => conn)
      .catch((err) => {
        console.error(err);
        throw new Error('创建租户连接失败');
      });
  }

  return drizzle(tenantConnections[tenantName], {
    schema: tenancySchema,
    mode: 'default',
  });
};

方案方向与生产环境优化建议

你的核心思路是正确的:通过内存缓存租户连接避免连接数过载,同时用ping检测连接有效性,超时失效时重建连接,解决了连接崩溃的问题。但要适配生产环境,还需要补充以下关键细节:

1. 用连接池替代单连接

当前主库和租户库都用单连接,高并发下会出现请求阻塞,应该改用连接池:

  • 主库连接池配置示例:
    const mainPool = mysql2.createPool({ ...masterDbOptions });
    export const db = drizzle(mainPool, { schema, mode: 'default' });
    
  • 租户库也改用连接池缓存,单连接同一时间只能处理一个请求,连接池能自动管理连接复用,提升并发能力。

2. 配置连接闲置回收与保活

  • 给连接池添加idleTimeout(如30000ms),自动回收闲置过久的连接,避免占用数据库连接数;
  • 保留enableKeepAlive: true,同时配置keepAliveInitialDelay,维持长连接的有效性,减少连接重建开销。

3. 增加缓存连接的清理机制

当前租户连接缓存会无限增长,长期不活跃的租户连接会浪费资源:

  • 给每个缓存的租户连接池记录最后访问时间,定期(如每天)清理超过24小时无请求的连接池;
  • 或者通过连接池的统计数据(如活跃连接数、闲置连接数)判断是否闲置,自动销毁并移除缓存。

4. 强化错误处理的健壮性

  • ping失败后直接重建连接,但要添加有限次数的重试逻辑(如3次),避免单次网络波动导致请求失败;
  • 调用end()销毁连接时要包裹在try/catch中,避免已失效的连接调用销毁方法抛出未捕获错误。

5. 优化类型定义

tenantConnections的类型可以更严谨:

let tenantConnections: { [key: string]: mysql2promise.Pool } = {};

内容的提问来源于stack exchange,提问作者Kene

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 04:13:20