教程:实现地理分散的数据库

配置故障转移到远程区域所需的 Azure SQL 数据库和应用程序,然后测试故障转移计划。 你将学习如何执行以下操作:

  • 创建故障转移组
  • 运行 Java 应用程序以查询 Azure SQL 数据库
  • 测试故障转移

如果没有 Azure 订阅,可在开始前创建一个 1 元试用帐户

先决条件

Note

本文进行了更新,以便使用新的 Azure PowerShell Az 模块。 若要详细了解新的 Az 模块和 AzureRM 兼容性,请参阅新 Azure Powershell Az 模块简介。 有关安装说明,请参阅安装 Azure PowerShell

Important

PowerShell Azure 资源管理器模块仍受 Azure SQL 数据库的支持,但所有未来的开发都是针对 Az.Sql 模块的。 若要了解这些 cmdlet,请参阅 AzureRM.Sql。 Az 模块和 AzureRm 模块中的命令参数大体上是相同的。

若要完成本教程,请确保已安装以下项目:

Important

请务必设置防火墙规则,以便使用要在其上执行本教程中步骤的计算机的公共 IP 地址。 数据库级防火墙规则会自动复制到辅助服务器。

有关信息,请参阅创建数据库级防火墙规则;若要确定计算机的用于服务器级防火墙规则的 IP 地址,请参阅创建服务器级防火墙

创建故障转移组

使用 Azure PowerShell 创建故障转移组,以便在现有的 Azure SQL Server 和另一区域的全新 Azure SQL Server 之间进行故障转移。 然后,将示例数据库添加到故障转移组。

Important

本示例需要 Azure PowerShell 模块 5.1.1 或更高版本。 运行 Get-Module -ListAvailable AzureRM 即可查找版本。 如果需要进行安装或升级,请参阅安装 Azure PowerShell 模块

运行 Connect-AzureRmAccount -Environment AzureChinaCloud,创建与 Azure 的连接。

若要创建故障转移组,请运行以下脚本:

 # Set variables for your server and database
 $adminlogin = "<your admin>"
 $password = "<your password>"
 $myresourcegroupname = "<your resource group name>"
 $mylocation = "<your resource group location>"
 $myservername = "<your existing server name>"
 $mydatabasename = "<your database name>"
 $mydrlocation = "<your disaster recovery location>"
 $mydrservername = "<your disaster recovery server name>"
 $myfailovergroupname = "<your globally unique failover group name>"

 # Create a backup server in the failover region
 New-AzSqlServer -ResourceGroupName $myresourcegroupname `
    -ServerName $mydrservername `
    -Location $mydrlocation `
    -SqlAdministratorCredentials $(New-Object -TypeName System.Management.Automation.PSCredential `
       -ArgumentList $adminlogin, $(ConvertTo-SecureString -String $password -AsPlainText -Force))

 # Create a failover group between the servers
 New-AzSqlDatabaseFailoverGroup `
    -ResourceGroupName $myresourcegroupname `
    -ServerName $myservername `
    -PartnerServerName $mydrservername  `
    -FailoverGroupName $myfailovergroupname `
    -FailoverPolicy Automatic `
    -GracePeriodWithDataLossHours 2

 # Add the database to the failover group
 Get-AzSqlDatabase `
    -ResourceGroupName $myresourcegroupname `
    -ServerName $myservername `
    -DatabaseName $mydatabasename | `
  Add-AzSqlDatabaseToFailoverGroup `
    -ResourceGroupName $myresourcegroupname `
    -ServerName $myservername `
    -FailoverGroupName $myfailovergroupname

异地复制设置也可在 Azure 门户中更改,方法是:选择数据库,然后选择“设置” > “异地复制”。

异地复制设置

运行示例项目

  1. 在控制台中,使用以下命令创建一个 Maven 项目:

    mvn archetype:generate "-DgroupId=com.sqldbsamples" "-DartifactId=SqlDbSample" "-DarchetypeArtifactId=maven-archetype-quickstart" "-Dversion=1.0.0"
    
  2. 键入 Y,然后按 Enter

  3. 将目录切换到新项目。

    cd SqlDbSample
    
  4. 使用最常用的编辑器,打开项目文件夹中的 pom.xml 文件。

  5. 通过添加下面的 dependency 节来添加 Microsoft JDBC Driver for SQL Server 依赖项。 此依赖项必须粘贴在更大的 dependencies 节中。

    <dependency>
      <groupId>com.microsoft.sqlserver</groupId>
      <artifactId>mssql-jdbc</artifactId>
     <version>6.1.0.jre8</version>
    </dependency>
    
  6. properties 节添加到 dependencies 节之后,指定 Java 版本:

    <properties>
      <maven.compiler.source>1.8</maven.compiler.source>
      <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    
  7. build 节添加到 properties 节之后,为清单文件提供支持:

    <build>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.0.0</version>
          <configuration>
            <archive>
              <manifest>
                <mainClass>com.sqldbsamples.App</mainClass>
              </manifest>
            </archive>
         </configuration>
        </plugin>
      </plugins>
    </build>
    
  8. 保存并关闭 pom.xml 文件。

  9. 打开 ..\SqlDbSample\src\main\java\com\sqldbsamples 中的 App.java 文件,将内容替换为以下代码:

    package com.sqldbsamples;
    
    import java.sql.Connection;
    import java.sql.Statement;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.Timestamp;
    import java.sql.DriverManager;
    import java.util.Date;
    import java.util.concurrent.TimeUnit;
    
    public class App {
    
       private static final String FAILOVER_GROUP_NAME = "<your failover group name>";  // add failover group name
    
       private static final String DB_NAME = "<your database>";  // add database name
       private static final String USER = "<your admin>";  // add database user
       private static final String PASSWORD = "<your password>";  // add database password
    
       private static final String READ_WRITE_URL = String.format("jdbc:" +
          "sqlserver://%s.database.chinacloudapi.cn:1433;database=%s;user=%s;password=%s;encrypt=true;" +
          "hostNameInCertificate=*.database.chinacloudapi.cn;loginTimeout=30;", +
          FAILOVER_GROUP_NAME, DB_NAME, USER, PASSWORD);
       private static final String READ_ONLY_URL = String.format("jdbc:" +
          "sqlserver://%s.secondary.database.chinacloudapi.cn:1433;database=%s;user=%s;password=%s;encrypt=true;" +
          "hostNameInCertificate=*.database.chinacloudapi.cn;loginTimeout=30;", +
          FAILOVER_GROUP_NAME, DB_NAME, USER, PASSWORD);
    
       public static void main(String[] args) {
          System.out.println("#######################################");
          System.out.println("## GEO DISTRIBUTED DATABASE TUTORIAL ##");
          System.out.println("#######################################");
          System.out.println("");
    
          int highWaterMark = getHighWaterMarkId();
    
          try {
             for(int i = 1; i < 1000; i++) {
                 //  loop will run for about 1 hour
                 System.out.print(i + ": insert on primary " +
                    (insertData((highWaterMark + i))?"successful":"failed"));
                 TimeUnit.SECONDS.sleep(1);
                 System.out.print(", read from secondary " +
                    (selectData((highWaterMark + i))?"successful":"failed") + "\n");
                 TimeUnit.SECONDS.sleep(3);
             }
          } catch(Exception e) {
             e.printStackTrace();
       }
    }
    
    private static boolean insertData(int id) {
       // Insert data into the product table with a unique product name so we can find the product again
       String sql = "INSERT INTO SalesLT.Product " +
          "(Name, ProductNumber, Color, StandardCost, ListPrice, SellStartDate) VALUES (?,?,?,?,?,?);";
    
       try (Connection connection = DriverManager.getConnection(READ_WRITE_URL);
               PreparedStatement pstmt = connection.prepareStatement(sql)) {
          pstmt.setString(1, "BrandNewProduct" + id);
          pstmt.setInt(2, 200989 + id + 10000);
          pstmt.setString(3, "Blue");
          pstmt.setDouble(4, 75.00);
          pstmt.setDouble(5, 89.99);
          pstmt.setTimestamp(6, new Timestamp(new Date().getTime()));
          return (1 == pstmt.executeUpdate());
       } catch (Exception e) {
          return false;
       }
    }
    
    private static boolean selectData(int id) {
       // Query the data previously inserted into the primary database from the geo replicated database
       String sql = "SELECT Name, Color, ListPrice FROM SalesLT.Product WHERE Name = ?";
    
       try (Connection connection = DriverManager.getConnection(READ_ONLY_URL);
               PreparedStatement pstmt = connection.prepareStatement(sql)) {
          pstmt.setString(1, "BrandNewProduct" + id);
          try (ResultSet resultSet = pstmt.executeQuery()) {
             return resultSet.next();
          }
       } catch (Exception e) {
          return false;
       }
    }
    
    private static int getHighWaterMarkId() {
       // Query the high water mark id stored in the table to be able to make unique inserts
       String sql = "SELECT MAX(ProductId) FROM SalesLT.Product";
       int result = 1;
       try (Connection connection = DriverManager.getConnection(READ_WRITE_URL);
               Statement stmt = connection.createStatement();
               ResultSet resultSet = stmt.executeQuery(sql)) {
          if (resultSet.next()) {
              result =  resultSet.getInt(1);
             }
          } catch (Exception e) {
           e.printStackTrace();
          }
          return result;
       }
    }
    
  10. 保存并关闭 App.java 文件。

  11. 在命令控制台中运行以下命令:

    mvn package
    
  12. 启动应用程序。如果不手动停止,应用程序将运行大约 1 小时,让你有时间运行故障转移测试。

    mvn -q -e exec:java "-Dexec.mainClass=com.sqldbsamples.App"
    
    #######################################
    ## GEO DISTRIBUTED DATABASE TUTORIAL ##
    #######################################
    
    1. insert on primary successful, read from secondary successful
    2. insert on primary successful, read from secondary successful
    3. insert on primary successful, read from secondary successful
    ...
    

测试故障转移

运行以下脚本来模拟故障转移,观察应用程序结果。 注意在数据库迁移过程中某些插入和选择操作是如何失败的。

也可使用以下命令,检查灾难恢复服务器在测试过程中的角色:

(Get-AzSqlDatabaseFailoverGroup `
   -FailoverGroupName $myfailovergroupname `
   -ResourceGroupName $myresourcegroupname `
   -ServerName $mydrservername).ReplicationRole

若要测试某个故障转移,请执行以下操作:

  1. 启动对故障转移组的手动故障转移:

    Switch-AzSqlDatabaseFailoverGroup `
       -ResourceGroupName $myresourcegroupname `
       -ServerName $mydrservername `
       -FailoverGroupName $myfailovergroupname
    
  2. 将故障转移组还原到主服务器:

    Switch-AzSqlDatabaseFailoverGroup `
       -ResourceGroupName $myresourcegroupname `
       -ServerName $myservername `
       -FailoverGroupName $myfailovergroupname