数据库与持久化
你当前正在浏览历史版本 Vert.x 4.4.9 的文档。 访问本页面的 最新版本。

响应式 Microsoft SQL Server 客户端

Preview

响应式 MSSQL 客户端(Reactive MSSQL Client)是面向 Microsoft SQL Server 的客户端,具备直观易用的 API,专注于高伸缩性与低开销。

该客户端是响应式且非阻塞的,允许通过单个线程处理大量数据库连接。

特性

  • 事件驱动

  • 轻量级

  • 内置连接池

  • 内存直接映射到对象,无多余内存拷贝

  • 支持 Java 8 日期和时间 API

  • 支持 RxJava API

  • SSL/TLS

  • 游标(Cursor)

  • 行流式处理(Row streaming)

暂不支持的特性

  • 预编译查询缓存

使用方法

要使用响应式 MSSQL 客户端,请将以下依赖项添加到构建描述文件的 dependencies 区域:

  • Maven(在你的 pom.xml 中):

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-mssql-client</artifactId>
 <version>4.5.35</version>
</dependency>
  • Gradle(在你的 build.gradle 文件中):

dependencies {
 compile 'io.vertx:vertx-mssql-client:4.5.35'
}

快速入门

以下是连接数据库、执行查询和断开连接的最简单示例:

MSSQLConnectOptions connectOptions = new MSSQLConnectOptions()
  .setPort(1433)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池配置
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建客户端连接池
Pool client = MSSQLBuilder.pool()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .build();

// 简单查询
client
  .query("SELECT * FROM users WHERE id='julien'")
  .execute()
  .onComplete(ar -> {
    if (ar.succeeded()) {
      RowSet<Row> result = ar.result();
      System.out.println("Got " + result.size() + " rows ");
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }

    // 关闭连接池
    client.close();
  });

连接至 SQL Server 数据库

大多数情况下,你将使用连接池来连接 MSSQL:

MSSQLConnectOptions connectOptions = new MSSQLConnectOptions()
  .setPort(1433)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池配置
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建连接池客户端
Pool client = MSSQLBuilder.pool()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .using(vertx)
  .build();

连接池客户端内部使用连接池管理连接,任何操作都会先从连接池借取连接执行操作,并在完成后将其释放回连接池。

如果你在 Vert.x 环境中运行,可以传入你的 Vertx 实例:

MSSQLConnectOptions connectOptions = new MSSQLConnectOptions()
  .setPort(1433)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池配置
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);
// 创建连接池客户端
Pool client = MSSQLBuilder.pool()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .using(vertx)
  .build();

当不再需要连接池时,你需要释放它:

pool.close();

当你需要在同一连接上执行多次操作时,需要使用客户端`连接(Connection)`。

你可以轻松地从连接池中获取一个连接:

MSSQLConnectOptions connectOptions = new MSSQLConnectOptions()
  .setPort(1433)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池配置
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建连接池客户端
Pool client = MSSQLBuilder.pool()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .using(vertx)
  .build();

// 从连接池获取连接
client.getConnection().compose(conn -> {
  System.out.println("Got a connection from the pool");

  // 所有操作均在同一连接上执行
  return conn
    .query("SELECT * FROM users WHERE id='julien'")
    .execute()
    .compose(res -> conn
      .query("SELECT * FROM users WHERE id='emad'")
      .execute())
    .onComplete(ar -> {
      // 将连接释放回连接池
      conn.close();
    });
}).onComplete(ar -> {
  if (ar.succeeded()) {

    System.out.println("Done");
  } else {
    System.out.println("Something went wrong " + ar.cause().getMessage());
  }
});

一旦连接使用完毕,你必须将其关闭以归还给连接池,以便它可以被重复使用。

配置

数据对象

配置客户端的一种简单方式是指定 MSSQLConnectOptions 数据对象。

MSSQLConnectOptions connectOptions = new MSSQLConnectOptions()
  .setPort(1433)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池配置
PoolOptions poolOptions = new PoolOptions().setMaxSize(5);

// 通过数据对象创建连接池
Pool pool = MSSQLBuilder.pool()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .using(vertx)
  .build();

pool.getConnection()
  .onComplete(ar -> {
    // 处理你的连接
  });

连接 URI

作为使用 MSSQLConnectOptions 数据对象配置客户端的替代方案,你也可以使用连接 URI:

String connectionUri = "sqlserver://dbuser:secretpassword@database.server.com:1433/mydb";

// 通过连接 URI 创建连接池
Pool pool = MSSQLBuilder.pool()
  .connectingTo(connectionUri)
  .using(vertx)
  .build();

// 通过连接 URI 创建连接
MSSQLConnection.connect(vertx, connectionUri)
  .onComplete(res -> {
    // 处理你的连接
  });

客户端以如下规范格式定义连接 URI:

sqlserver://[user[:[password]]@]host[:port][/database][?<key1>=<value1>[&<key2>=<value2>]]
在连接 URI 中配置参数将覆盖默认属性。

当前客户端支持以下参数键名:

  • host

  • port

  • user

  • password

  • database

  • encrypt - 加密模式:strict(TDS 8.0)、true(启用 SSL 的 TDS 7.x)、false(不加密)

更多参数将被添加到 properties 中。

连接 URI 解析器会将所有属性键转换为小写。

连接重试

你可以配置客户端在连接建立失败时进行重试:

options
  .setReconnectAttempts(2)
  .setReconnectInterval(1000);

执行查询

当你不需要事务或仅运行单次查询时,可以直接在连接池上执行查询;连接池会自动从池中借调一个连接来执行该查询并向你返回结果。

以下演示如何执行简单查询:

client
  .query("SELECT * FROM users WHERE id='julien'")
  .execute()
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> result = ar.result();
    System.out.println("Got " + result.size() + " rows ");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

预编译查询(Prepared Query)

你同样可以对预编译查询执行类似操作。

SQL 字符串中可以通过位置引用参数,使用数据库特定的占位符语法 `@p1`, `@p2`, etc…​:

client
  .preparedQuery("SELECT * FROM users WHERE id=@p1")
  .execute(Tuple.of("julien"))
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    System.out.println("Got " + rows.size() + " rows ");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

查询方法返回异步的 RowSet 实例,适用于各类 SELECT 查询:

client
  .preparedQuery("SELECT first_name, last_name FROM users")
  .execute()
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    for (Row row : rows) {
      System.out.println("User " + row.getString(0) + " " + row.getString(1));
    }
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

对于数据变更操作,同样支持获取受影响的行数:

client
  .preparedQuery("INSERT INTO users (first_name, last_name) VALUES (@p1, @p2)")
  .execute(Tuple.of("Julien", "Viet"))
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    System.out.println(rows.rowCount());
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

Row 允许通过索引序号或列名访问数据字段:

System.out.println("User " + row.getString(0) + " " + row.getString(1));
System.out.println("User " + row.getString("first_name") + " " + row.getString("last_name"));

客户端不会对参数类型进行任何魔法猜测,所有的参数都应当严格遵守 SQL 类型系统。

你可以执行批处理预编译查询:

List<Tuple> batch = new ArrayList<>();
batch.add(Tuple.of("julien", "Julien Viet"));
batch.add(Tuple.of("emad", "Emad Alblueshi"));

// 执行批处理
client
  .preparedQuery("INSERT INTO USERS (id, name) VALUES (@p1, @p2)")
  .executeBatch(batch)
  .onComplete(res -> {
  if (res.succeeded()) {

    // 获取所有行结果
    RowSet<Row> rows = res.result();
  } else {
    System.out.println("Batch failed " + res.cause());
  }
});

使用 identity 自增列

在插入新数据后,你可以使用 OUTPUT 子句获取 identity 自增列的值:

client
  .preparedQuery("INSERT INTO movies (title) OUTPUT INSERTED.id VALUES (@p1)")
  .execute(Tuple.of("The Man Who Knew Too Much"))
  .onComplete(res -> {
    if (res.succeeded()) {
      Row row = res.result().iterator().next();
      System.out.println(row.getLong("id"));
    }
  });

使用独立连接

获取数据库连接

当你需要执行一系列顺序查询(不包含事务)时,可以创建新连接或直接从连接池借取连接。请牢记,在从池中获取连接到归还连接期间,你需要妥善维护该连接,因为它可能因服务器端空闲超时等原因被意外关闭:

pool
  .getConnection()
  .compose(connection ->
    connection
      .preparedQuery("INSERT INTO Users (first_name,last_name) VALUES (@p1, @p2)")
      .executeBatch(Arrays.asList(
        Tuple.of("Julien", "Viet"),
        Tuple.of("Emad", "Alblueshi")
      ))
      .compose(res -> connection
        // 处理返回的行数据
        .query("SELECT COUNT(*) FROM Users")
        .execute()
        .map(rows -> rows.iterator().next().getInteger(0)))
      // 将连接安全归还给连接池
      .eventually(() -> connection.close())
  ).onSuccess(count -> {
  System.out.println("Insert users, now the number of users is " + count);
});

同样可以创建并使用预编译查询(Prepared Query):

connection
  .prepare("SELECT * FROM users WHERE first_name LIKE @p1")
  .compose(pq ->
    pq.query()
      .execute(Tuple.of("Julien"))
      .eventually(() -> pq.close())
  ).onSuccess(rows -> {
  // 获取所有行数据
});

简化的连接管理 API

在使用连接池时,可以调用 withConnection 方法,传入一个在连接作用域内执行的函数。

该方法会自动从连接池借出连接,并将该连接作为参数调用你的函数。

传入的函数必须返回一个包含任意结果的 Future。

在该 Future 完成后,连接会被自动安全归还给连接池,并对外返回最终的处理结果:

pool.withConnection(connection ->
  connection
    .preparedQuery("INSERT INTO Users (first_name,last_name) VALUES (@p1, @p2)")
    .executeBatch(Arrays.asList(
      Tuple.of("Julien", "Viet"),
      Tuple.of("Emad", "Alblueshi")
    ))
    .compose(res -> connection
      // 处理查询返回的行数据
      .query("SELECT COUNT(*) FROM Users")
      .execute()
      .map(rows -> rows.iterator().next().getInteger(0)))
).onSuccess(count -> {
  System.out.println("Insert users, now the number of users is " + count);
});

使用事务

基于连接的事务管理

你可以使用 SQL BEGIN/COMMIT/ROLLBACK 语句手动执行事务;若这么做,你必须获取并自行管理 SqlConnection 连接。

或者,推荐使用 SqlConnection 提供的事务 API:

pool.getConnection()
  // 事务必须在独占的物理连接中运行
  .onSuccess(conn -> {
    // 开启事务
    conn.begin()
      .compose(tx -> conn
        // 执行一系列操作
        .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')")
        .execute()
        .compose(res2 -> conn
          .query("INSERT INTO Users (first_name,last_name) VALUES ('Emad','Alblueshi')")
          .execute())
        // 提交事务
        .compose(res3 -> tx.commit()))
      // 无论成功还是失败,均将连接归还至连接池
      .eventually(() -> conn.close())
      .onSuccess(v -> System.out.println("Transaction succeeded"))
      .onFailure(err -> System.out.println("Transaction failed: " + err.getMessage()));
  });

当数据库服务端判定当前事务失败时(例如出现著名的 current transaction is aborted, commands ignored until end of transaction block),事务会自动回滚,并且 completion Future 会以 TransactionRollbackException 异常宣告失败:

tx.completion()
  .onFailure(err -> {
    System.out.println("Transaction failed => rolled back");
  });

简化的事务管理 API

在使用连接池时,你可以调用 withTransaction,传入一个在事务内执行的函数。

该方法会自动从连接池借取连接、开启事务,并调用你的业务函数,由客户端在该事务的上下文中执行所有数据库操作。

该业务函数必须返回包含任意操作结果的 Future:

  • 当 Future 成功时,客户端会自动提交该事务;

  • 当 Future 失败时,客户端会自动回滚该事务。

事务执行完毕(无论提交或回滚)后,连接都会自动归还给连接池,并向调用方返回整体操作的最终结果:

pool.withTransaction(client -> client
  .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')")
  .execute()
  .flatMap(res -> client
    .query("INSERT INTO Users (first_name,last_name) VALUES ('Emad','Alblueshi')")
    .execute()
    // 转换为业务提示消息
    .map("Users inserted")))
  .onSuccess(v -> System.out.println("Transaction succeeded"))
  .onFailure(err -> System.out.println("Transaction failed: " + err.getMessage()));

游标(Cursor)与流式查询

默认情况下,执行预编译查询会一次性抓取所有数据行。你可以使用 Cursor 来精准控制每次读取的数据行数:

connection
  .prepare("SELECT * FROM users WHERE first_name LIKE @p1")
  .onComplete(ar0 -> {
  if (ar0.succeeded()) {
    PreparedStatement pq = ar0.result();

    // 游标要求在事务内部运行
    connection
      .begin()
      .onComplete(ar1 -> {
      if (ar1.succeeded()) {
        Transaction tx = ar1.result();

        // 创建游标
        Cursor cursor = pq.cursor(Tuple.of("julien"));

        // 读取 50 行
        cursor.read(50).onComplete(ar2 -> {
          if (ar2.succeeded()) {
            RowSet<Row> rows = ar2.result();

            // 检查是否还有剩余数据行?
            if (cursor.hasMore()) {
              // 重复读取流程...
            } else {
              // 全部数据行读取完毕 - 提交事务
              tx.commit();
            }
          }
        });
      }
    });
  }
});

当游标需要提前释放时,必须显式调用关闭方法:

cursor
  .read(50)
  .onComplete(ar2 -> {
  if (ar2.succeeded()) {
    // 关闭游标
    cursor.close();
  }
});

游标还提供了更方便的流式 API,尤其是在搭配 RxJava 版本时极为顺畅:

connection
  .prepare("SELECT * FROM users WHERE first_name LIKE @p1")
  .onComplete(ar0 -> {
  if (ar0.succeeded()) {
    PreparedStatement ps = ar0.result();

    // 数据流要求在事务内部运行
    connection
      .begin()
      .onComplete(ar1 -> {
      if (ar1.succeeded()) {
        Transaction tx = ar1.result();

        // 每次抓取 50 行
        RowStream<Row> stream = pq.createStream(50, Tuple.of("julien"));

        // 使用数据流
        stream.exceptionHandler(err -> {
          System.out.println("Error: " + err.getMessage());
          ps.close();
        });
        stream.endHandler(v -> {
          // 关闭流以释放数据库中的游标资源
          stream
            .close()
            .onComplete(closed -> {
            tx.commit()
              .onComplete(committed -> {
                System.out.println("End of stream");
                ps.close();
            });
          });
        });
        stream.handler(row -> {
          System.out.println("User: " + row.getString("last_name"));
        });
      }
    });
  }
});

数据流会以每次 50 行的批次读取并推送;当该批数据行全部传递给处理器后,再读取新的 50 行批次,以此类推。

该流支持随时暂停(Pause)或恢复(Resume);已加载的行将缓存在内存中直到被消费,在此期间游标会暂停迭代抓取。

追踪查询

当 Vert.x 启用了链路追踪(Tracing)时,SQL 客户端可以对查询执行过程进行全链路追踪。

客户端会上报以下 客户端(Client) Span:

  • 操作名称为 Query

  • 包含以下 Tags:

  • db.system:数据库管理系统产品名称

  • db.user:数据库用户名

  • db.instance:数据库实例

  • db.statement:SQL 查询语句

  • db.type:固定为 sql

默认的链路追踪策略为 PROPAGATE,客户端仅在参与现有活跃链路时才会创建 Span。

你可以通过 setTracingPolicy 更改客户端追踪策略,例如设置为 ALWAYS 以始终上报 Span:

options.setTracingPolicy(TracingPolicy.ALWAYS);

MSSQL 类型映射

当前客户端支持以下 SQL Server 数据类型:

SQL Server 类型 Java 类型 说明

TINYINT

java.lang.Short

SMALLINT

java.lang.Short

INT

java.lang.Integer

BIGINT

java.lang.Long

BIT

java.lang.Boolean

REAL

java.lang.Float

DOUBLE

java.lang.Double

NUMERIC / DECIMAL

java.math.BigDecimal

CHAR / VARCHAR

java.lang.String

NCHAR / NVARCHAR

java.lang.String

Unicode 字符串

DATE

java.time.LocalDate

TIME

java.time.LocalTime

SMALLDATETIME

java.time.LocalDateTime

DATETIME

java.time.LocalDateTime

DATETIME2

java.time.LocalDateTime

DATETIMEOFFSET

java.time.OffsetDateTime

BINARY / VARBINARY

io.vertx.core.buffer.Buffer

MONEY

java.math.BigDecimal

SMALLMONEY

java.math.BigDecimal

GUID

java.util.UUID

在存储值时,Tuple 解码会使用上述类型。

使用 Java enum 类型

SQL Server 没有原生的 ENUM 数据类型,但客户端可以将获取到的字符串/数值数据类型映射为枚举。

你可以像这样对 Java 枚举进行编码:

client
  .preparedQuery("INSERT INTO colors VALUES (@p1)")
  .execute(Tuple.of(Color.red))
  .onComplete(res -> {
    // ...
  });

你可以像这样对 Java 枚举进行解码:

client
  .preparedQuery("SELECT color FROM colors")
  .execute()
  .onComplete(res -> {
    if (res.succeeded()) {
      RowSet<Row> rows = res.result();
      for (Row row : rows) {
        System.out.println(row.get(Color.class, "color"));
      }
    }
  });

处理 NULL

如果你使用 Tuple 的 addXXX 方法之一添加数据,null 值将被自动透明处理。

客户端在执行预编译查询时能够推断出正确的 SQL 类型:

Tuple tuple = Tuple.tuple()
  .addInteger(17)
  .addString("The Man Who Knew Too Much")
  .addString(null);
client
  .preparedQuery("INSERT INTO movies (id, title, plot) VALUES (@p1, @p2, @p3)")
  .execute(tuple)
  .onComplete(res -> {
    // ...
  });

否则,你应当使用 NullValue 常量之一或 NullValue.of 方法显式声明类型:

Tuple tuple = Tuple.of(17, "The Man Who Knew Too Much", NullValue.String);
client
  .preparedQuery("INSERT INTO movies (id, title, plot) VALUES (@p1, @p2, @p3)")
  .execute(tuple)
  .onComplete(res -> {
    // ...
  });

收集器查询

你可以在查询 API 中结合使用 Java 收集器(Collector):

Collector<Row, ?, Map<Long, String>> collector = Collectors.toMap(
  row -> row.getLong("id"),
  row -> row.getString("last_name"));

// 配合收集器执行查询
client.query("SELECT * FROM users")
  .collecting(collector)
  .execute()
  .onComplete(ar -> {
    if (ar.succeeded()) {
      SqlResult<Map<Long, String>> result = ar.result();

      // 获取由收集器创建的 Map
      Map<Long, String> map = result.value();
      System.out.println("Got " + map);
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }
  });

收集器处理逻辑中绝不能保留对 Row 的引用,因为在处理整个结果集期间只复用了同一个 Row 实例。

Java 的 Collectors 提供了许多实用的预定义收集器,例如你可以轻松地直接从行结果集中拼接字符串:

Collector<Row, ?, String> collector = Collectors.mapping(
  row -> row.getString("last_name"),
  Collectors.joining(",", "(", ")")
);

// 配合收集器执行查询
client.query("SELECT * FROM users")
  .collecting(collector)
  .execute()
  .onComplete(ar -> {
    if (ar.succeeded()) {
      SqlResult<String> result = ar.result();

      // 获取由收集器创建的字符串
      String list = result.value();
      System.out.println("Got " + list);
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }
  });

信息性消息

SQL Server 可以在响应查询时向客户端发送信息性消息(Information messages)。 默认情况下,它们会以 WARN 级别记录日志。

你可以在连接上设置一个处理器(Handler)来捕获这些消息并进行相应处理:

connection.infoHandler(info -> {
  System.out.println("Received info " + info.getSeverity() + "" + info.getMessage());
});

使用 SSL/TLS

客户端同时支持 TDS 7.x 加密和 TDS 8.0 严格加密。

加密模式

客户端支持三种`加密模式`:

  • OFF - 不加密(默认值)

  • ON - 可选加密(等同于 ssl=true)

  • STRICT - 严格 TDS 8.0 加密

TDS 8.0 与严格加密

从 SQL Server 2022 开始,Microsoft 引入了 TDS 8.0,这是表格格式数据流(Tabular Data Stream)协议的新版本,要求从连接建立之初就对整个连接进行加密。

若要使用严格加密连接到 SQL Server 2022+ 实例:

MSSQLConnectOptions connectOptions = new MSSQLConnectOptions()
  .setEncryptionMode(EncryptionMode.STRICT)
  .setSslOptions(new ClientSSLOptions()
    // 严格加密模式要求进行有效的证书校验
    .setTrustOptions(new PemTrustOptions().addCertPath("/path/to/server-cert.pem")));

使用连接 URI:

sqlserver://localhost:1433/testdb?user=sa&password=xxx&encrypt=strict

TDS 8.0 要求进行严格的证书校验。 因此,在 STRICT 加密模式下不允许使用 trustAll 选项。

TDS 7.x 加密(传统方式)

加密级别协商

当使用 TDS 7.x(OFF 或 ON 模式)时,客户端与服务端会协商加密级别。

MSSQLConnectOptions 类保持了 encryptionMode 与 ssl 属性之间的同步:

  • 将 encryptionMode 设置为 OFF 会将 ssl 设置为 false

  • 将 encryptionMode 设置为 ON 会将 ssl 设置为 true

  • 将 ssl 设置为 false 会将 encryptionMode 设置为 OFF

  • 将 ssl 设置为 true 会将 encryptionMode 设置为 ON

这保证了传统的 ssl 属性与较新的 encryptionMode 属性之间的一致性。

协商后的级别取决于 MSSQLConnectOptions 中的客户端配置与服务端配置:

  • 不加密:若客户端选项中 ssl 设置为 false,且服务端不支持加密

  • 仅加密登录数据包:若客户端选项中 ssl 设置为 false,且服务端支持加密

  • 加密整个连接:若客户端选项中 ssl 设置为 true,或服务端强制要求加密

如果客户端选项中 ssl 设置为 true,而服务端不支持加密,则协商失败。 在这种情况下,客户端将终止连接。

配置

要在客户端选项中配置 ssl,请使用 setSsl 方法。 默认情况下,ssl 设置为 false。

MSSQLConnectOptions connectOptions = new MSSQLConnectOptions().setSsl(true);

当 ssl 设置为 false 时,客户端信任所有服务端证书。 否则,它会执行主机名验证。

如果客户端选项中 ssl 设置为 true,且服务端使用了自签名证书,可以禁用主机名验证:

MSSQLConnectOptions connectOptions = new MSSQLConnectOptions()
  .setSsl(true)
  .setTrustAll(true);

或者,可以通过 TrustOptions 配置客户端信任服务端证书。 例如,如果包含服务端证书的是 PEM 文件,可以使用 PemTrustOptions:

MSSQLConnectOptions connectOptions = new MSSQLConnectOptions()
  .setSsl(true)
  .setPemTrustOptions(new PemTrustOptions().addCertPath("/path/to/server-cert.pem"));

有关 Vert.x 中 SSL 支持的更多详细信息,请参阅 Vert.x Core 文档。

连接池共享

你可以在多个 Verticle 或同一 Verticle 的多个实例之间共享同一个连接池。此类连接池应在 Verticle 外部创建,否则当创建它的 Verticle 被卸载时,连接池也会被随之关闭:

Pool pool = Pool.pool(database, new PoolOptions().setMaxSize(maxSize));
vertx.deployVerticle(() -> new VerticleBase() {
  @Override
  public Future<?> start() throws Exception {
    // 使用连接池
    return super.start();
  }
}, new DeploymentOptions().setInstances(4));

你也可以直接在各个 Verticle 内部声明获取共享连接池:

vertx.deployVerticle(() -> new VerticleBase() {
  Pool pool;
  @Override
  public Future<?> start() throws Exception {
    // 获取或创建共享连接池
    // 本质上是获取对该池的一份租约(Lease)
    // 当该 Verticle 卸载时,租约会自动释放
    pool = MSSQLBuilder.pool()
      .with(new PoolOptions()
        .setMaxSize(maxSize)
        .setShared(true)
        .setName("my-pool"))
      .connectingTo(database)
      .using(vertx)
      .build();
    return super.start();
  }
}, new DeploymentOptions().setInstances(4));

首次创建共享连接池时,系统会为该连接池分配底层资源。随后的调用将直接复用该池并创建新的租约。当所有租约均被关闭后,底层系统资源才会被完全释放。

默认情况下,当连接池需要创建 TCP 物理连接时,会复用当前的 EventLoop。因此共享连接池会随机使用正在使用它的各 Verticle 的 EventLoop。

你也可以独立于调用上下文,为连接池显式分配指定数量的 EventLoop:

Pool pool = MSSQLBuilder.pool()
  .with(new PoolOptions()
    .setMaxSize(maxSize)
    .setShared(true)
    .setName("my-pool")
    .setEventLoopSize(4))
  .connectingTo(database)
  .using(vertx)
  .build();

高级连接池配置

服务器负载均衡

你可以为连接池配置多个服务器地址列表以实现负载均衡,而不是仅指定单一服务器:

Pool pool = MSSQLBuilder.pool()
  .with(options)
  .connectingTo(Arrays.asList(server1, server2, server3))
  .using(vertx)
  .build();

连接池在创建底层物理连接时采用轮询(Round-Robin)算法在不同服务器之间进行分配。

该负载均衡策略发生在连接创建阶段,而非从池中借取(Borrow)连接的阶段。

连接池初始化处理器

你可以使用 withConnectHandler 在物理连接创建完成后、放入连接池之前与其进行交互并执行初始化设置:

builder.withConnectHandler(conn -> {
  conn.query(sql).execute().onSuccess(res -> {
    // 将连接归还给池,准备供应用程序正常借用
    conn.close();
  });
});

一旦你完成对该连接的初始化操作,只需直接调用 close() 将其标记为就绪,以便连接池开始调度使用。

动态连接配置

你可以使用 Java Supplier 函数式接口动态提供连接配置详情,而非传入静态的 SqlConnectOptions 实例。

由于该 Supplier 是异步的,因此可用于实现连接配置的动态变更(例如数据库密码自动轮换):

Pool pool = MSSQLBuilder.pool()
  .with(poolOptions)
  .connectingTo(() -> {
    Future<SqlConnectOptions> connectOptions = retrieveOptions();
    return connectOptions;
  })
  .using(vertx)
  .build();