如何使用强一致性事务与柔性事务
分布式环境下,事务就会变得比较复杂。这在分布式事务章节会进行详细分析。本章节只是针对ShardingShere中的分布式事务处理方案。
ShardingSphere 中的分布式事务
在 ShardingSphere 中,除本地事务之外,还提供针对分布式事务的两种实现方案,分别是 XA 事务和柔性事务。这点可以从事务类型枚举值 TransactionType 中得到验证:
public enum TransactionType {
LOCAL, XA, BASE
}
XA 事务
XA 事务提供基于两阶段提交协议的实现机制。所谓两阶段提交,顾名思义分成两个阶段,一个是准备阶段,一个是执行阶段。在准备阶段中,协调者发起一个提议,分别询问各参与者是否接受。在执行阶段,协调者根据参与者的反馈,提交或终止事务。如果参与者全部同意则提交,只要有一个参与者不同意就终止。
目前,业界在实现 XA 事务时也存在一些主流工具库,包括 Atomikos、Narayana 和 Bitronix。ShardingSphere 对这三种工具库都进行了集成,并默认使用 Atomikos 来完成两阶段提交。
BASE 事务
与 XA 事务这种“刚性”不同,柔性事务则遵循 BASE 设计理论,追求的是最终一致性。这里的 BASE 来自基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventual Consistency)这三个概念。
关于如何实现基于 BASE 原则的柔性事务,业界也存在一些优秀的框架,例如阿里巴巴提供的 Seata。ShardingSphere 内部也集成了对 Seata 的支持。当然,我们也可以根据需要,集成其他分布式事务类开源框架,并基于微内核架构嵌入到 ShardingSphere 运行时环境中。
使用XA事务
在 Spring 应用程序中添加对 XA 事务的支持相对简单,无论是 Spring 框架,还是 ShardingSphere 自身,都为我们提供了低成本的开发机制。
开发环境准备
配置maven依赖
要想使用 XA 事务,我们首先要在 pom 文件中添加 sharding-jdbc-core 和 sharding-transaction-xa-core 这两个依赖:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-xa-core</artifactId>
</dependency>
编写配置文件
在今天的案例中,我们将演示如何在分库环境下实现分布式事务,因此我们需要在 Spring Boot 中创建一个 .properties 文件,并包含分库需要的所有配置项信息:
spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://localhost:3306/ds0
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=root
spring.shardingsphere.datasource.ds0.autoCommit: false
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://localhost:3306/ds1
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=root
spring.shardingsphere.datasource.ds0.autoCommit: false
spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2}
spring.shardingsphere.sharding.binding-tables=health_record,health_task
spring.shardingsphere.sharding.broadcast-tables=health_level
spring.shardingsphere.sharding.tables.health_record.actual-data-nodes=ds$->{0..1}.health_record
spring.shardingsphere.sharding.tables.health_record.key-generator.column=record_id
spring.shardingsphere.sharding.tables.health_record.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.health_record.key-generator.props.worker.id=33
spring.shardingsphere.sharding.tables.health_task.actual-data-nodes=ds$->{0..1}.health_task
spring.shardingsphere.sharding.tables.health_task.key-generator.column=task_id
spring.shardingsphere.sharding.tables.health_task.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.health_task.key-generator.props.worker.id=33
spring.shardingsphere.props.sql.show=true
实现 XA 事务
通过分库配置,我们将获取 SQL 执行的目标 DataSource。由于我们使用 Spring 框架而不是使用原生的 JDBC 进行事务管理,所以需要将 DataSource 与 Spring 中的事务管理器 PlatformTransactionManager 关联起来。
另一方面,为了更好地集成 ShardingSphere 中的分布式事务支持,我们可以通过 Spring 框架提供的 JdbcTemplate 模板类来简化 SQL 的执行过程。一种常见的做法是创建一个事务配置类来初始化所需的 PlatformTransactionManager 和 JdbcTemplate 对象:
@Configuration
@EnableTransactionManagement
public class TransactionConfiguration {
@Bean
public PlatformTransactionManager txManager(final DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public JdbcTemplate jdbcTemplate(final DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
下面使用jdbcTemplate进行处理。
使用jdbcTemplate有两种方式进行操作,第一种方式是采用ShardingTransactionType 注解。第二种方式是使用自己通过ThreadLocal保存设定的TransactionType。
使用ShardingTransactionType
为了在业务代码中以最少的开发成本嵌入分布式事务机制,ShardingSphere 也专门提供了一个 @ShardingTransactionType 注解来配置所需要执行的事务类型:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ShardingTransactionType {
TransactionType value() default TransactionType.LOCAL;
}
我们知道,ShardingSphere 提供的事务类型有三种,分别是 LOCAL、XA 和 BASE,默认使用的是 LOCAL。所以如果需要用到分布式事务,需要在业务方法上显式的添加这个注解:
@Transactional
@ShardingTransactionType(TransactionType.XA)
public void insert(){
…
}
使用自定义工具类
另一种设置 TransactionType 的方式是使用 TransactionTypeHolder 工具类。TransactionTypeHolder 类中通过 ThreadLocal 来保存 TransactionType:
public final class TransactionTypeHolder {
private static final ThreadLocal<TransactionType> CONTEXT = new ThreadLocal<TransactionType>() {
@Override
protected TransactionType initialValue() {
return TransactionType.LOCAL;
}
};
public static TransactionType get() {
return CONTEXT.get();
}
public static void set(final TransactionType transactionType) {
CONTEXT.set(transactionType);
}
public static void clear() {
CONTEXT.remove();
}
}
可以看到,TransactionTypeHolder 中默认采用的是本地事务,我们可以通过 set 方法来改变初始设置:
TransactionTypeHolder.set(TransactionType.XA);
下面是采用TransactionTypeHolder实现的业务代码。
@Override
public void processWithXA() throws SQLException {
TransactionTypeHolder.set(TransactionType.XA);
insertHealthRecords();
}
private List<Long> insertHealthRecords() throws SQLException {
List<Long> result = new ArrayList<>(10);
jdbcTemplate.execute((ConnectionCallback<Object>) connection-> {
connection.setAutoCommit(false);
try {
for (Long i = 1L; i <= 10; i++) {
HealthRecord healthRecord = createHealthRecord(i);
insertHealthRecord(healthRecord, connection);
HealthTask healthTask = createHealthTask(i, healthRecord);
insertHealthTask(healthTask, connection);
result.add(healthRecord.getRecordId());
}
connection.commit();
} catch (final SQLException ex) {
connection.rollback();
throw ex;
}
return connection;
});
return result;
}
private void insertHealthRecord(HealthRecord healthRecord, Connection connection) throws SQLException {
try (PreparedStatement preparedStatement = connection.prepareStatement(sql_health_record_insert, Statement.RETURN_GENERATED_KEYS)) {
preparedStatement.setLong(1, healthRecord.getUserId());
preparedStatement.setLong(2, healthRecord.getLevelId() % 5 );
preparedStatement.setString(3, "Remark" + healthRecord.getUserId());
preparedStatement.executeUpdate();
try (ResultSet resultSet = preparedStatement.getGeneratedKeys()) {
if (resultSet.next()) {
healthRecord.setRecordId(resultSet.getLong(1));
}
}
}
}
首先通过 Connection 对象构建一个 PreparedStatement。请注意,由于我们需要通过 ShardingSphere 的主键自动生成机制,所以在创建 PreparedStatement 时需要进行特殊地设置:
connection.prepareStatement(sql_health_record_insert, Statement.RETURN_GENERATED_KEYS)
通过这种方式,在 PreparedStatement 完成 SQL 执行之后,我们就可以获取自动生成的主键值:
try (ResultSet resultSet = preparedStatement.getGeneratedKeys()) {
if (resultSet.next()) {
healthRecord.setRecordId(resultSet.getLong(1));
}
}
现在,让我们模拟事务失败的场景,可以在代码执行过程中故意抛出一个异常来做到这一点:
try {
for (Long i = 1L; i <= 10; i++) {
HealthRecord healthRecord = createHealthRecord(i);
insertHealthRecord(healthRecord, connection);
HealthTask healthTask = createHealthTask(i, healthRecord);
insertHealthTask(healthTask, connection);
result.add(healthRecord.getRecordId());
//手工抛出异常
throw new SQLException("数据库执行异常!");
}
connection.commit();
} catch (final SQLException ex) {
connection.rollback();
throw ex;
}
再次执行 processWithXA 方法,基于 connection 提供的 rollback 方法,我们发现已经执行的部分 SQL 并没有提交到任何一个数据库中。
使用BASE 事务
这里,我们将基于阿里巴巴提供的 Seata 框架来演示如何使用 BASE 事务。
开发环境准备
同样,要想使用基于 Seata 的 BASE 事务,我们首先需要在 pom 文件中添加对 sharding-jdbc-core 和 sharding-transaction-base-seata-at 这两个依赖:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-base-seata-at</artifactId>
</dependency>
因为用到了 Seata 框架,所以也需要引入 Seate 框架的相关组件:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-rm-datasource</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-tm</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-codec-all</artifactId>
</dependency>
然后,我们下载并启动 Seata 服务器,这个过程需要设置 Seata 服务器 config 目录下的 registry.conf,以便指定注册中心,这里使用 ZooKeeper 来充当注册中心。关于如何启动 Seata 服务器的过程可以参考 Seata 的官方文档。请注意,按照 Seata 的运行要求,我们需要在每一个分片数据库实例中创建一张 undo_log 表。然后,我们还需要在代码工程中 classpath 中增加一个 seata.conf 配置文件:
client {
application.id = health-base
transaction.service.group = health-base-group
}
现在,在 src/main/resources 目录下的文件组织形式应该是这样:
实现 BASE 事务
基于 ShardingSphere 提供的分布式事务的抽象,我们从 XA 事务转到 BASE 事务唯一要做的事情就是重新设置 TransactionType,也就是修改一行代码:
@Override
public void processWithBASE() throws SQLException {
TransactionTypeHolder.set(TransactionType.BASE);
insertHealthRecords();
}
重新执行测试用例,我们发现在正常提交和异常回滚的场景下,基于 Seata 的分布式事务同样发挥了效果。
ShardingSphere如何数据脱敏
这里,我将从敏感数据的存储方式、敏感数据的加解密过程以及在业务代码中嵌入加解密的过程这三个维度来抽象数据脱敏。
针对每一个维度,我也将基于 ShardingSphere 给出这个框架的具体抽象过程,从而方便你理解使用它的方法和技巧,让我们来一起看一下。
敏感数据如何存储?
我们先来考虑第一种情况。对于一些敏感数据而言,我们显然应该直接以密文的形式将加密之后的数据进行存储,防止有任何一种途径能够从数据库中获取这些数据明文。 在这类敏感数据中,最典型的就是用户密码,我们通常会采用 MD5 等不可逆的加密算法对其进行加密,而使用这些数据的方法也只是依赖于它的密文形式,不会涉及对明文的直接处理。
但对于用户姓名、手机号等信息,由于统计分析等方面的需要,显然我们不能直接采用不可逆的加密算法对其进行加密,还需要将明文信息进行处理**。**一种常见的处理方式是将一个字段用两列来进行保存,一列保存明文,一列保存密文,这就是第二种情况。
显然,我们可以将第一种情况看作是第二种情况的特例。也就是说,在第一种情况中没有明文列,只有密文列。
ShardingSphere 同样基于这两种情况进行了抽象,它将这里的明文列命名为 plainColumn,而将密文列命名为 cipherColumn。其中 plainColumn 属于选填,而 cipherColumn 则是必填。同时,ShardingSphere 还提出了一个逻辑列 logicColumn 的概念,该列代表一种虚拟列,只面向开发人员进行编程使用:
敏感数据如何加解密?
传统的加解密方式有两种,一种是对称加密,常见的包括 DEA 和 AES;另一种是非对称加密,常见的包括 RSA。
ShardingSphere 内部也抽象了一个 ShardingEncryptor 组件专门封装各种加解密操作:
public interface ShardingEncryptor extends TypeBasedSPI {
//初始化
void init();
//加密
String encrypt(Object plaintext);
//解密
Object decrypt(String ciphertext);
}
目前,ShardingSphere 内置了 AESShardingEncryptor 和 MD5ShardingEncryptor 这两个具体的 ShardingEncryptor 实现。当然,由于 ShardingEncryptor 扩展了 TypeBasedSPI 接口,所以开发人员完全可以基于微内核架构和 JDK 所提供的 SPI 机制来实现和动态加载自定义的各种 ShardingEncryptor。我们会在“微内核架构:ShardingSphere 如何实现系统的扩展性?”这个课时中对 ShardingSphere 中的微内核架构和 SPI 机制进行详细的讨论。
业务代码中如何嵌入数据脱敏?
首先,ShardingSphere 通过对从应用程序传入的 SQL 进行解析,并依据开发人员提供的脱敏配置对 SQL 进行改写,从而实现对明文数据的自动加密,并将加密后的密文数据存储到数据库中。当我们查询数据时,它又从数据库中取出密文数据,并自动对其解密,最终将解密后的明文数据返回给用户。ShardingSphere 提供了自动化+透明化的数据脱敏过程,业务开发人员可以像使用普通数据那样使用脱敏数据,而不需要关注数据脱敏的实现细节。
实现步骤
这个过程主要有三个步骤:准备数据脱敏、配置数据脱敏和执行数据脱敏。
准备数据脱敏
为了演示数据脱敏功能,我们重新定义一个 EncryptUser 实体类,该类中定义了与数据脱敏相关的常见用户名、密码等字段,这些字段与数据库中 encrypt_user 表的列是一一对应的:
public class EncryptUser {
//用户Id
private Long userId;
//用户名(密文)
private String userName;
//用户名(明文)
private String userNamePlain;
//密码(密文)
private String pwd;
…
}
接下来,我们有必要提一下 EncryptUserMapper 中关于 resultMap 和 insert 语句的定义,如下所示:
<mapper namespace="com.tianyilan.shardingsphere.demo.repository.EncryptUserRepository">
<resultMap id="encryptUserMap" type="com.tianyilan.shardingsphere.demo.entity.EncryptUser">
<result column="user_id" property="userId" jdbcType="INTEGER"/>
<result column="user_name" property="userName" jdbcType="VARCHAR"/>
<result column="pwd" property="pwd" jdbcType="VARCHAR"/>
</resultMap>
<insert id="addEntity">
INSERT INTO encrypt_user (user_id, user_name, pwd) VALUES (#{userId,jdbcType=INTEGER}, #{userName,jdbcType=VARCHAR}, #{pwd,jdbcType=VARCHAR})
</insert>
…
</mapper>
请注意,我们在 resultMap 中并没有指定 user_name_plain 字段,同时,insert 语句中同样没有指定这个字段。
有了 Mapper,我们就可以构建 Service 层组件。在这个 EncryptUserServiceImpl 类中,我们分别提供了 processEncryptUsers 和 getEncryptUsers 方法来插入用户以及获取用户列表。
@Service
public class EncryptUserServiceImpl implements EncryptUserService {
@Autowired
private EncryptUserRepository encryptUserRepository;
@Override
public void processEncryptUsers() throws SQLException {
insertEncryptUsers();
}
private List<Long> insertEncryptUsers() throws SQLException {
List<Long> result = new ArrayList<>(10);
for (Long i = 1L; i <= 10; i++) {
EncryptUser encryptUser = new EncryptUser();
encryptUser.setUserId(i);
encryptUser.setUserName("username_" + i);
encryptUser.setPwd("pwd" + i);
encryptUserRepository.addEntity(encryptUser);
result.add(encryptUser.getUserId());
}
return result;
}
@Override
public List<EncryptUser> getEncryptUsers() throws SQLException {
return encryptUserRepository.findEntities();
}
}
配置数据脱敏
在整体架构上,和分库分表以及读写分离一样,数据脱敏对外暴露的入口也是一个符合 JDBC 规范的 EncryptDataSource 对象。如下面的代码所示,ShardingSphere 提供了 EncryptDataSourceFactory 工厂类,完成了 EncryptDataSource 对象的构建:
public final class EncryptDataSourceFactory {
public static DataSource createDataSource(final DataSource dataSource, final EncryptRuleConfiguration encryptRuleConfiguration, final Properties props) throws SQLException {
return new EncryptDataSource(dataSource, new EncryptRule(encryptRuleConfiguration), props);
}
}
可以看到,这里存在一个 EncryptRuleConfiguration 类,该类中包含了两个 Map,分别用来配置加解密器列表以及加密表配置列表:
//加解密器配置列表
private final Map<String, EncryptorRuleConfiguration> encryptors;
//加密表配置列表
private final Map<String, EncryptTableRuleConfiguration> tables;
其中 EncryptorRuleConfiguration 集成了 ShardingSphere 中的一个通用抽象类 TypeBasedSPIConfiguration,包含了 type 和 properties 这两个字段:
//类型(例如MD5/AES加密器)
private final String type;
//属性(例如AES加密器用到的Key值)
private final Properties properties;
而 EncryptTableRuleConfiguration 内部是一个包含多个 EncryptColumnRuleConfiguration 的 Map,这个 EncryptColumnRuleConfiguration 就是 ShardingSphere 中对加密列的配置,包含了 plainColumn、cipherColumn 的定义:
public final class EncryptColumnRuleConfiguration {
//存储明文的字段
private final String plainColumn;
//存储密文的字段
private final String cipherColumn;
//辅助查询字段
private final String assistedQueryColumn;
//加密器名字
private final String encryptor;
}
作为总结,我们通过一张图罗列出各个配置类之间的关系,以及数据脱敏所需要配置的各项内容:
现在回到代码,为了实现数据脱敏,我们首先需要定义一个数据源,这里命名为 dsencrypt:
spring.shardingsphere.datasource.names=dsencrypt
spring.shardingsphere.datasource.dsencrypt.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.dsencrypt.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.dsencrypt.jdbc-url=jdbc:mysql://localhost:3306/dsencrypt
spring.shardingsphere.datasource.dsencrypt.username=root
spring.shardingsphere.datasource.dsencrypt.password=root
配置成功之后,我们再配置加密器,这里定义 name_encryptor 和 pwd_encryptor 这两个加密器,分别用于对 user_name 列和 pwd 列进行加解密。注意,在下面这段代码中,对于 name_encryptor,我们使用了对称加密算法 AES;而对于 pwd_encryptor,我们则直接使用不可逆的 MD5 散列算法:
spring.shardingsphere.encrypt.encryptors.name_encryptor.type=aes
spring.shardingsphere.encrypt.encryptors.name_encryptor.props.aes.key.value=123456
spring.shardingsphere.encrypt.encryptors.pwd_encryptor.type=md5
接下来,我们需要完成脱敏表的配置。针对案例中的场景,我们可以选择对 user_name 列设置 plainColumn、cipherColumn 以及 encryptor 属性,而对于 pwd 列而言,由于我们不希望在数据库中存储明文,所以只需要配置 cipherColumn 和 encryptor 属性就可以了。
spring.shardingsphere.encrypt.tables.encrypt_user.columns.user_name.plainColumn=user_name_plain
spring.shardingsphere.encrypt.tables.encrypt_user.columns.user_name.cipherColumn=user_name
spring.shardingsphere.encrypt.tables.encrypt_user.columns.user_name.encryptor=name_encryptor
spring.shardingsphere.encrypt.tables.encrypt_user.columns.pwd.cipherColumn=pwd
spring.shardingsphere.encrypt.tables.encrypt_user.columns.pwd.encryptor=pwd_encryptor
最后,ShardingSphere 还提供了一个属性开关,当底层数据库表里同时存储了明文和密文数据后,该属性开关可以决定是直接查询数据库表里的明文数据进行返回,还是查询密文数据并进行解密之后再返回:
spring.shardingsphere.props.query.with.cipher.comlum=true
执行数据脱敏
在这个过程中,ShardingSphere 会把原始的 SQL 语句转换为用于数据脱敏的目标语句:
然后,我们再来执行查询语句并获取控制台日志:
2020-05-30 15:10:59.174 INFO 31808 --- [ main] ShardingSphere-SQL : Rule Type: encrypt
2020-05-30 15:10:59.174 INFO 31808 --- [ main] ShardingSphere-SQL : SQL: SELECT * FROM encrypt_user;
user_id: 1, user_name: username_1, pwd: 99024280cab824efca53a5d1341b9210
user_id: 2, user_name: username_2, pwd: 36ddda5af915d91549d3ab5bff1bafec
…
可以看到这里的路由类型为“encrypt”,获取的 user_name 是经过解密之后的明文而不是数据库中存储的密文,这就是 spring.shardingsphere.props.query.with.cipher.comlum=true 配置项所起到的作用。如果将这个配置项设置为 false,那么返回的就是密文。
ShardingSphere的配置中心
采用配置中心也就意味着采用集中式配置管理的设计思想。在集中式配置中心内,开发、测试和生产等不同的环境配置信息统一保存在配置中心内,这是一个维度。另一个维度就是需要确保分布式集群中同一类服务的所有服务实例保存同一份配置文件并且能够同步更新。配置中心的示意图如下所示:
在 ShardingSphere 中,提供了多种配置中心的实现方案,包括主流的 ZooKeeeper、Etcd、Apollo 和 Nacos。开发人员也可以根据需要实现自己的配置中心并通过 SPI 机制加载到 ShardingSphere 运行时环境中。
另一方面,配置信息不是一成不变的。对修改后的配置信息的统一分发,是配置中心可以提供的另一个重要能力。配置中心中配置信息的任何变化都可以实时同步到各个服务实例中。在 ShardingSphere 中,通过配置中心可以支持数据源、数据表、分片以及读写分离策略的动态切换。
同时,在集中式配置信息管理方案的基础上,ShardingSphere 也支持从本地加载配置信息的实现方案。如果我们希望以本地的配置信息为准,并将本地配置覆盖配置中心的配置,通过一个开关就可以做到这一点。
配置中心的前提是要注册客户端,下面说下shardingshpere中的注册中心。
注册中心与配置中心的不同之处在于两者保存的数据类型。配置中心管理的显然是配置数据,但注册中心存放的是 ShardingSphere 运行时的各种动态/临时状态数据,最典型的运行时状态数据就是当前的 Datasource 实例。那么,保存这些动态和临时状态数据有什么用呢?我们来看一下这张图:
注册中心一般都提供了分布式协调机制。在注册中心中,所有 DataSource 在指定路径根目录下创建临时节点,所有访问这些 DataSource 的业务服务都会监听该目录。当有新 DataSource 加入时,注册中心实时通知到所有业务服务,由业务服务做相应路由信息维护;而当某个 DataSource 宕机时,业务服务通过监听机制同样会收到通知。
基于这种机制,我们就可以提供针对 DataSource 的治理能力,包括熔断对某一个 DataSource 的数据访问,或禁用对从库 DataSource 的访问等。
在 ShardingSphere 中,注册中心更多地面向框架内部使用,普通场景下不需要过多了解注册中心的使用方法。目前,ShardingSphere 针对注册中心所打造的面向开发人员的功能也还比较有限。因此,今天我们重点关注配置中心的使用方式,关于注册中心的讨论,我们放在源码解析部分进行展开。接下来,我将带领你完成集成配置中心的系统改造工作。
如何集成配置中心
准备开发环境
为了集成配置中心,第一步需要引入 ShardingSphere 中与编排治理相关的依赖包。在 Spring Boot 环境中,这个依赖包是 sharding-jdbc-orchestration-spring-boot-starter:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-orchestration-spring-boot-starter</artifactId>
</dependency>
在接下来的内容中,我们将演示如何基于 ZooKeeeper 这款分布式协调工具来实现配置中心。而在 ShardingSphere 中,集成的 ZooKeeeper 客户端组件是 Curator,所以也需要引入 sharding-orchestration-reg-zookeeper-curator 组件:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-orchestration-reg-zookeeper-curator</artifactId>
</dependency>
当然,如果我们使用的是 Nacos,那么也需要添加相关的依赖包:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-orchestration-reg-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</dependency>
掌握配置项
针对配置中心,ShardingSphere 提供了一系列的 DataSource,包括用于数据分片的 OrchestrationShardingDataSource、用于读写分离的 OrchestrationMasterSlaveDataSource 以及用于数据脱敏的 OrchestrationEncryptDataSource。围绕这些 DataSource,也存在对应的 DataSourceFactory 工厂类。这里以 OrchestrationMasterSlaveDataSourceFactory 为例来看创建 DataSource 所需要的配置类:
public final class OrchestrationMasterSlaveDataSourceFactory {
public static DataSource createDataSource(final Map<String, DataSource> dataSourceMap, final MasterSlaveRuleConfiguration masterSlaveRuleConfig,
final Properties props, final OrchestrationConfiguration orchestrationConfig) throws SQLException {
if (null == masterSlaveRuleConfig || null == masterSlaveRuleConfig.getMasterDataSourceName()) {
return createDataSource(orchestrationConfig);
}
MasterSlaveDataSource masterSlaveDataSource = new MasterSlaveDataSource(dataSourceMap, new MasterSlaveRule(masterSlaveRuleConfig), props);
return new OrchestrationMasterSlaveDataSource(masterSlaveDataSource, orchestrationConfig);
}
…
}
可以看到,这里存在一个治理规则配置类 OrchestrationConfiguration,而在其他的 DataSourceFactory 中所使用的也是这个配置类:
public final class OrchestrationConfiguration {
//治理规则名称
private final String name;
//注册(配置)中心配置类
private final RegistryCenterConfiguration regCenterConfig;
//本地配置是否覆写服务器配置标志位
private final boolean overwrite;
}
在 OrchestrationConfiguration 中我们看到了用于指定本地配置是否覆写服务器配置的 overwrite 标志位,也看到了一个注册中心的配置子类 RegistryCenterConfiguration。RegistryCenterConfiguration 包的含内容比较多,我们截取最常见最通用的部分配置项:
public final class RegistryCenterConfiguration extends TypeBasedSPIConfiguration {
//配置中心服务器列表
private String serverLists;
//命名空间
private String namespace;
…
}
这里包含了配置中心服务器列表 serverLists 以及用于标识唯一性的命名空间 namespace。因为 RegistryCenterConfiguration 继承了 TypeBasedSPIConfiguration,所以也就自动带有 type 和 properties 这两个配置项。
实现配置中心
现在,我们来实现基于 ZooKeeper 的配置中心。首先需要下载 ZooKeeper 服务器组件,并确保启动成功。如果采用默认配置,那么 ZooKeeper 会在 2181 端口启动请求监听。
然后创建一个配置文件并输入配置项,由于还是以读写分离为例进行演示,因此,在配置文件中,我们设置了一主两从一共三个数据源,这部分配置项在介绍读写分离机制时已经介绍过,这里不再展开:
spring.shardingsphere.datasource.names=dsmaster,dsslave0,dsslave1
spring.shardingsphere.datasource.dsmaster.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.dsmaster.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.dsmaster.jdbc-url=jdbc:mysql://localhost:3306/dsmaster
spring.shardingsphere.datasource.dsmaster.username=root
spring.shardingsphere.datasource.dsmaster.password=root
spring.shardingsphere.datasource.dsslave0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.dsslave0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.dsslave0.jdbc-url=jdbc:mysql://localhost:3306/dsslave0
spring.shardingsphere.datasource.dsslave0.username=root
spring.shardingsphere.datasource.dsslave0.password=root
spring.shardingsphere.datasource.dsslave1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.dsslave1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.dsslave1.jdbc-url=jdbc:mysql://localhost:3306/dsslave1
spring.shardingsphere.datasource.dsslave1.username=root
spring.shardingsphere.datasource.dsslave1.password=root
spring.shardingsphere.masterslave.load-balance-algorithm-type=random
spring.shardingsphere.masterslave.name=health_ms
spring.shardingsphere.masterslave.master-data-source-name=dsmaster
spring.shardingsphere.masterslave.slave-data-source-names=dsslave0,dsslave1
spring.shardingsphere.props.sql.show=true
接下来指定配置中心,我们将 overwrite 设置为 true,这意味着前面的这些本地配置项会覆盖保存在 ZooKeeper 服务器上的配置项,也就是说我们采用的是本地配置模式。然后我们设置配置中心类型为 zookeeper,服务器列表为 localhost:2181,并将命名空间设置为 orchestration-health_ms。
spring.shardingsphere.orchestration.name=health_ms
spring.shardingsphere.orchestration.overwrite=true
spring.shardingsphere.orchestration.registry.type=zookeeper
spring.shardingsphere.orchestration.registry.server-lists=localhost:2181
spring.shardingsphere.orchestration.registry.namespace=orchestration-health_ms
现在,让我们启动服务,控制台会出现与 ZooKeeper 进行通信的相关日志信息:
2020-05-30 18:13:45.954 INFO 20272 --- [ main] org.apache.zookeeper.ZooKeeper : Initiating client connection, connectString=localhost:2181 sessionTimeout=60000 watcher=org.apache.curator.ConnectionState@585ac855
2020-05-30 18:13:46.011 INFO 20272 --- [0:0:0:0:1:2181)] org.apache.zookeeper.ClientCnxn : Opening socket connection to server 0:0:0:0:0:0:0:1/0:0:0:0:0:0:0:1:2181. Will not attempt to authenticate using SASL (unknown error)
2020-05-30 18:13:46.012 INFO 20272 --- [0:0:0:0:1:2181)] org.apache.zookeeper.ClientCnxn : Socket connection established to 0:0:0:0:0:0:0:1/0:0:0:0:0:0:0:1:2181, initiating session
2020-05-30 18:13:46.029 INFO 20272 --- [0:0:0:0:1:2181)] org.apache.zookeeper.ClientCnxn : Session establishment complete on server 0:0:0:0:0:0:0:1/0:0:0:0:0:0:0:1:2181, sessionid = 0x10022dd7e680001, negotiated timeout = 40000
2020-05-30 18:13:46.034 INFO 20272 --- [ain-EventThread] o.a.c.f.state.ConnectionStateManager : State change: CONNECTED
同时,ZooKeeper 服务器端也对来自应用程序的请求作出响应。我们可以使用一些 ZooKeeper 可视化客户端工具来观察目前服务器上的数据。这里,我使用了 ZooInspector 这款工具,由于 ZooKeeper 本质上就是树状结构,现在所以在根节点中就新增了配置信息:
由于我们在本地配置文件中将 spring.shardingsphere.orchestration.overwrite 配置项设置为 true,本地配置的变化就会影响到服务器端配置,进而影响到所有使用这些配置的应用程序。如果不希望产生这种影响,而是统一使用位于配置中心上的配置,应该怎么做呢?
很简单,我们只需要将 spring.shardingsphere.orchestration.overwrite 设置为 false 即可。将这个配置开关进行关闭,意味着我们将只从配置中心读取配置,也就是说,本地不需要保存任何配置信息,只包含指定配置中心的相关内容了:
spring.shardingsphere.orchestration.name=health_ms
spring.shardingsphere.orchestration.overwrite=false
spring.shardingsphere.orchestration.registry.type=zookeeper
spring.shardingsphere.orchestration.registry.server-lists=localhost:2181
spring.shardingsphere.orchestration.registry.namespace=orchestration-health_ms
执行测试用例后,会发现读写分离规则同样生效。
如果你选择使用其他的框架来构建配置中心服务器,比如阿里巴巴的 Nacos,那么也很简单,只需要将 spring.shardingsphere.orchestration.registry.type 设置成 nacos 并提供对应的 server-lists 就可以了:
spring.shardingsphere.orchestration.name=health_ms
spring.shardingsphere.orchestration.overwrite=true
spring.shardingsphere.orchestration.registry.type=nacos
spring.shardingsphere.orchestration.registry.server-lists=localhost:8848
spring.shardingsphere.orchestration.registry.namespace=