SpringBoot + MyBatisPlus多数据源 + ShardingSphere读写分离和数据分片 + Seata分布式事务

1. 版本说明

SpringBoot:2.2.4.RELEASE
MyBatisPlus:3.5.1
ShardingSphere:5.1.1
Seata:1.4.2

<!-- spring-cloud版本 -->
<spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
<!-- spring-cloud-alibaba版本 -->
<spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version>

2. 相关依赖

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
	
	<dependencies>
		<!-- mybatis-plus -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.5.1</version>
		</dependency>

		<!-- mybatis-plus多数据源 -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
			<version>3.5.1</version>
		</dependency>

		<!-- mybatis-plus代码生成器 -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-generator</artifactId>
			<version>3.5.1</version>
		</dependency>

		<!-- ShardingSphere-JDBC -->
		<dependency>
			<groupId>org.apache.shardingsphere</groupId>
			<artifactId>shardingsphere-jdbc-core</artifactId>
			<version>5.1.1</version>
		</dependency>
		
		<!-- ShardingSphere事务管理 -->
		<dependency>
			<groupId>org.apache.shardingsphere</groupId>
			<artifactId>shardingsphere-transaction-core</artifactId>
			<version>5.1.1</version>
		</dependency>
		
		<!-- seata -->
		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-alibaba-seata</artifactId>
			<version>${spring-cloud-alibaba.version}</version>
			<exclusions>
				<exclusion>
					<groupId>io.seata</groupId>
					<artifactId>seata-all</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>io.seata</groupId>
			<artifactId>seata-all</artifactId>
			<version>1.4.2</version>
		</dependency>
	</dependencies>

3. 数据源配置

数据源使用配置类进行配置,不需要写在yml文件里。
包含:MyBatisPlus多数据源配置、Seata数据源代理配置、ShardingSphere读写分离和数据分片配置

数据源配置类
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import common.config.datasource.sharding.config.ShardingSphereAlgorithmConstants;
import common.config.datasource.sharding.table.SysLogShardingConfig;
import common.config.mybatisplus.CustomMetaObjectHandler;
import common.config.mybatisplus.MyBatisPlusConfig;
import common.model.enums.EnvironmentEnum;
import io.seata.rm.datasource.DataSourceProxy;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.shardingsphere.driver.api.ShardingSphereDataSourceFactory;
import org.apache.shardingsphere.infra.config.RuleConfiguration;
import org.apache.shardingsphere.infra.config.mode.ModeConfiguration;
import org.apache.shardingsphere.readwritesplitting.api.ReadwriteSplittingRuleConfiguration;
import org.apache.shardingsphere.readwritesplitting.api.rule.ReadwriteSplittingDataSourceRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.ShardingRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.rule.ShardingTableRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.strategy.sharding.StandardShardingStrategyConfiguration;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.*;

/**
 * 数据源配置<p>
 * 可切换的数据源有:<p>
 * 1. ShardingSphere数据源,包含master、slave01、slave02上的satisfactory数据库,读写分离、分库分表:@DS(DataSourceConstants.SHARDING_DATASOURCE)<p>
 * {@link DataSourceConstants#SHARDING_DATASOURCE}<p>
 * 2. master数据源,仅包含master主机上的satisfactory数据库,无数据分片和读写分离:@DS(DataSourceConstants.MASTER_DATASOURCE)<p>
 * {@link DataSourceConstants#MASTER_DATASOURCE}<p>
 * 3. 慢查询日志数据源,仅包含master主机上的mysql数据库,无数据分片和读写分离:@DS(DataSourceConstants.MYSQL_DATASOURCE)<p>
 * {@link DataSourceConstants#MYSQL_DATASOURCE}<p>
 * 4. information_schema数据源,仅包含master主机上的information_schema数据库,无数据分片和读写分离:@DS(DataSourceConstants.INFORMATION_SCHEMA_DATASOURCE)<p>
 * {@link DataSourceConstants#INFORMATION_SCHEMA_DATASOURCE}<p>
 *
 * @since 2023-7-28 下午 9:26
 */
@Configuration
@Slf4j
public class DataSourceConfig {

	/**
	 * 动态数据源
	 */
	private static final String DYNAMIC_DATASOURCE = "dynamic";

	/**
	 * 慢查询日志数据源(原始)
	 */
	private static final String ORIGINAL_MYSQL_DATASOURCE = "originalMysql";

	/**
	 * information_schema数据源(原始)
	 */
	private static final String ORIGINAL_INFORMATION_SCHEMA_DATASOURCE = "originalInformationSchema";

	/**
	 * 真实数据节点:master
	 */
	private static final String MASTER_RAW_DATASOURCE = "master";

	/**
	 * 真实数据节点:slave01
	 */
	private static final String SLAVE01_RAW_DATASOURCE = "slave01";

	/**
	 * 真实数据节点:slave02
	 */
	private static final String SLAVE02_RAW_DATASOURCE = "slave02";

	/**
	 * SqlSessionFactory
	 */
	private static final String DYNAMIC_SQL_SESSION_FACTORY = "dynamicSqlSessionFactory";

	@Value("${spring.application.name}")
	private String appName;

	@Resource
	private Environment environment;

	@Resource
	private CustomMetaObjectHandler metaObjectHandler;

	/**
	 * 配置见{@link MyBatisPlusConfig#mybatisPlusInterceptor()}
	 */
	@Resource
	private MybatisPlusInterceptor mybatisPlusInterceptor;

	/**
	 * 非读写分离数据源:mysql
	 */
	@Bean(name = ORIGINAL_MYSQL_DATASOURCE)
	public DataSource mysql() {
		return buildDataSource("3307", "mysql");
	}

	/**
	 * mysql数据源代理
	 */
	@Bean(DataSourceConstants.MYSQL_DATASOURCE)
	public DataSourceProxy dataSourceProxyMysql(@Qualifier(ORIGINAL_MYSQL_DATASOURCE) DataSource druidDataSource) {
		return new DataSourceProxy(druidDataSource);
	}

	/**
	 * 非读写分离数据源:information_schema
	 */
	@Bean(name = ORIGINAL_INFORMATION_SCHEMA_DATASOURCE)
	public DataSource informationSchema() {
		return buildDataSource("3307", "mysql");
	}

	/**
	 * information_schema数据源代理
	 */
	@Bean(DataSourceConstants.INFORMATION_SCHEMA_DATASOURCE)
	public DataSourceProxy dataSourceProxyInformationSchema(@Qualifier(ORIGINAL_INFORMATION_SCHEMA_DATASOURCE) DataSource druidDataSource) {
		return new DataSourceProxy(druidDataSource);
	}

	/**
	 * master数据源代理
	 */
	@Bean(DataSourceConstants.MASTER_DATASOURCE)
	public DataSourceProxy dataSourceProxyMaster(@Qualifier(MASTER_RAW_DATASOURCE) DataSource druidDataSource) {
		return new DataSourceProxy(druidDataSource);
	}

	/**
	 * 真实数据节点:master
	 */
	@Bean(name = MASTER_RAW_DATASOURCE)
	public DataSource master() {
		return buildDataSource("3307", "satisfactory");
	}

	/**
	 * 真实数据节点:slave01
	 */
	@Bean(name = SLAVE01_RAW_DATASOURCE)
	public DataSource slave01() {
		return buildDataSource("3308", "satisfactory");
	}

	/**
	 * 真实数据节点:slave02
	 */
	@Bean(name = SLAVE02_RAW_DATASOURCE)
	public DataSource slave02() {
		return buildDataSource("3309", "satisfactory");
	}

	private DataSource buildDataSource(String port, String databaseName) {
		DruidDataSource druidDataSource = new DruidDataSource();
		druidDataSource.setUrl("jdbc:mysql://localhost:" + port + "/" + databaseName + "?rewriteBatchedStatements=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai");
		druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		druidDataSource.setUsername("root");
		druidDataSource.setPassword("123456");
		return druidDataSource;
	}

	/**
	 * ShardingSphere数据源代理
	 */
	@Bean(DataSourceConstants.SHARDING_DATASOURCE)
	// 重要:必须在分片配置类加载后才能加载,否则无法获取分片算法
	@DependsOn({"sysLogShardingConfig"})
	public DataSourceProxy dataSource(@Qualifier(MASTER_RAW_DATASOURCE) DataSource master, @Qualifier(SLAVE01_RAW_DATASOURCE) DataSource slave01, @Qualifier(SLAVE02_RAW_DATASOURCE) DataSource slave02) throws SQLException {
		log.info("init sharding datasource");
		// 运行模式类型,可选配置:Memory、Standalone、Cluster,默认为Memory
		ModeConfiguration modeConfiguration = new ModeConfiguration("Memory", null, false);
		// 数据源配置
		Map<String, DataSource> dataSourceMap = new HashMap<>();
		dataSourceMap.put(MASTER_RAW_DATASOURCE, master);
		dataSourceMap.put(SLAVE01_RAW_DATASOURCE, slave01);
		dataSourceMap.put(SLAVE02_RAW_DATASOURCE, slave02);
		// 规则配置,可选配置:数据分片、读写分离、数据加密、影子库、SQL解析、混合规则
		Set<RuleConfiguration> configs = new HashSet<>();
		// 读写分离配置
		configs.add(getReadwriteSplittingRuleConfiguration());
		// 数据分片配置
		ShardingRuleConfiguration shardingRuleConfiguration = new ShardingRuleConfiguration();
		// 初始化所有算法
		shardingRuleConfiguration.getShardingAlgorithms().putAll(ShardingSphereAlgorithmConstants.SHARDING_ALGORITHMS);
		// 需要分片的表(需要分片的表都写在这里,目前只有一张表)
		shardingRuleConfiguration.getTables().add(getSysLogShardingConfiguration());
		configs.add(shardingRuleConfiguration);
		// 其他配置
		Properties properties = new Properties();
		// 在非生产环境打印日志
		if (environment.acceptsProfiles(Profiles.of(EnvironmentEnum.PRODUCT.getName()))) {
			properties.setProperty("sql-show", Boolean.FALSE.toString());
		} else {
			properties.setProperty("sql-show", Boolean.TRUE.toString());
		}
		// 创建数据源代理
		return new DataSourceProxy(ShardingSphereDataSourceFactory.createDataSource(modeConfiguration, dataSourceMap, configs, properties));
	}

	/**
	 * 动态数据源
	 */
	@Bean(DYNAMIC_DATASOURCE)
	public DataSource dynamicDataSource(@Qualifier(DataSourceConstants.SHARDING_DATASOURCE) DataSourceProxy shardingDatasource, @Qualifier(DataSourceConstants.MASTER_DATASOURCE) DataSourceProxy masterDatasource, @Qualifier(DataSourceConstants.MYSQL_DATASOURCE) DataSourceProxy mysqlDatasource, @Qualifier(DataSourceConstants.INFORMATION_SCHEMA_DATASOURCE) DataSourceProxy informationSchemaDatasource) {
		DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
		dynamicRoutingDataSource.addDataSource(DataSourceConstants.SHARDING_DATASOURCE, shardingDatasource);
		dynamicRoutingDataSource.addDataSource(DataSourceConstants.MASTER_DATASOURCE, masterDatasource);
		dynamicRoutingDataSource.addDataSource(DataSourceConstants.MYSQL_DATASOURCE, mysqlDatasource);
		dynamicRoutingDataSource.addDataSource(DataSourceConstants.INFORMATION_SCHEMA_DATASOURCE, informationSchemaDatasource);
		dynamicRoutingDataSource.setPrimary(DataSourceConstants.SHARDING_DATASOURCE);
		dynamicRoutingDataSource.setSeata(true);
		return dynamicRoutingDataSource;
	}

	/**
	 * 创建事务管理器,解决@Transactional注解失效的问题
	 */
	@Bean
	public DataSourceTransactionManager shardingTransactionManger(@Qualifier(DYNAMIC_DATASOURCE) DataSource dataSource) {
		return new DataSourceTransactionManager(dataSource);
	}

	/**
	 * 创建SqlSessionFactory
	 */
	@Bean(DYNAMIC_SQL_SESSION_FACTORY)
	public SqlSessionFactory sqlSessionFactory(@Qualifier(DYNAMIC_DATASOURCE) DataSource dataSource) throws Exception {
		// 这里用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否则 MyBatisPlus 不会生效
		MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
		mybatisSqlSessionFactoryBean.setDataSource(dataSource);
		mybatisSqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:" + "{id:common|" + appName + "}/mapper/xml/*.xml"));
		mybatisSqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
		// MyBatis-Plus 配置
		MybatisConfiguration mybatisConfiguration = new MybatisConfiguration();
		// MyBatis-Plus的分页、乐观锁等功能是通过MybatisPlusInterceptor插入的
		mybatisConfiguration.addInterceptor(mybatisPlusInterceptor);
		// 在非生产环境打印日志
		if (!environment.acceptsProfiles(Profiles.of(EnvironmentEnum.PRODUCT.getName()))) {
			mybatisConfiguration.setLogImpl(StdOutImpl.class);
		}
		// 返回为null的字段
		mybatisConfiguration.setCallSettersOnNulls(true);
		mybatisSqlSessionFactoryBean.setConfiguration(mybatisConfiguration);
		GlobalConfig globalConfig = new GlobalConfig();
		// 字段自动填充
		globalConfig.setMetaObjectHandler(metaObjectHandler);
		GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
		// 主键策略
		dbConfig.setIdType(IdType.AUTO);
		// 查询策略
		dbConfig.setWhereStrategy(FieldStrategy.DEFAULT);
		// 插入策略
		dbConfig.setInsertStrategy(FieldStrategy.DEFAULT);
		// 更新策略(IGNORED表示null值也会更新)
		dbConfig.setUpdateStrategy(FieldStrategy.IGNORED);
		globalConfig.setDbConfig(dbConfig);
		mybatisSqlSessionFactoryBean.setGlobalConfig(globalConfig);
		return mybatisSqlSessionFactoryBean.getObject();
	}

	/**
	 * 创建SqlSessionTemplate
	 */
	@Bean
	public SqlSessionTemplate sqlSessionTemplate(@Qualifier(DYNAMIC_SQL_SESSION_FACTORY) SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}

	/**
	 * 读写分离配置
	 */
	private ReadwriteSplittingRuleConfiguration getReadwriteSplittingRuleConfiguration() {
		// 负载均衡算法配置
		Properties dataSourceProperties = new Properties();
		// 写数据源名称
		dataSourceProperties.setProperty("write-data-source-name", MASTER_RAW_DATASOURCE);
		// 读数据源名称,多个从数据源用逗号分隔
		dataSourceProperties.setProperty("read-data-source-names", SLAVE01_RAW_DATASOURCE + "," + SLAVE02_RAW_DATASOURCE);
		// 自定义的读写分离数据源名称:shardingDatasource
		// 读写分离类型,如: Static,Dynamic
		ReadwriteSplittingDataSourceRuleConfiguration dataSourceRuleConfiguration = new ReadwriteSplittingDataSourceRuleConfiguration(DataSourceConstants.SHARDING_DATASOURCE, "Static", dataSourceProperties, ShardingSphereAlgorithmConstants.READWRITE_ALG_ROUND);
		return new ReadwriteSplittingRuleConfiguration(Collections.singleton(dataSourceRuleConfiguration), ShardingSphereAlgorithmConstants.READWRITE_ALGORITHMS);
	}

	/**
	 * sys_log表分片配置
	 * <p>
	 * 其中主键生成策略使用了MyBatis-Plus提供的主键策略{@link IdType#ASSIGN_ID}
	 */
	private ShardingTableRuleConfiguration getSysLogShardingConfiguration() {
		// 指定逻辑表名和真实数据节点
		ShardingTableRuleConfiguration tableRuleConfiguration = new ShardingTableRuleConfiguration(SysLogShardingConfig.LOGIC_TABLE_NAME, SysLogShardingConfig.ACTUAL_DATA_NODES);
		// 分库策略
//		tableRuleConfiguration.setDatabaseShardingStrategy(new StandardShardingStrategyConfiguration("user_id", ShardingSphereAlgorithmConstants.SHARDING_DATABASE_INLINE));
		// 分表策略
		tableRuleConfiguration.setTableShardingStrategy(new StandardShardingStrategyConfiguration(DataSourceConstants.ID_FIELD_NAME_IN_DATABASE, SysLogShardingConfig.SYS_LOG_TABLE_ALG));
		// 主键生成策略(未使用,而是使用了MyBatis-Plus提供的主键策略IdType.ASSIGN_ID)
//		Properties snowflakeProp = new Properties();
//		snowflakeProp.setProperty("worker-id", "1");
//		shardingRuleConfiguration.getKeyGenerators().put("snowflake", new ShardingSphereAlgorithmConfiguration("SNOWFLAKE", snowflakeProp));
//		tableRuleConfiguration.setKeyGenerateStrategy(new KeyGenerateStrategyConfiguration("id", "snowflake"));
		return tableRuleConfiguration;
	}

}

4. Seata配置文件

registry.conf(resources下)
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "seata"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
    password = ""
    cluster = "default"
    timeout = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    group = "SEATA_GROUP"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

file.conf(resources下)
transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  # NIO NATIVE
  server = "NIO"
  # enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  # thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    # auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}

service {
  # transaction service group mapping
  vgroupMapping.satisfactory_seata_group = "seata"
  # only support when registry.type=file, please don't set multiple addresses
  # 此配置作用参考:https://blog.csdn.net/weixin_39800144/article/details/100726116
  default.grouplist = "127.0.0.1:8091"
  # degrade, current not support
  enableDegrade = false
  # disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "seata_undo_log"
  }
  log {
    exceptionRate = 100
  }
}

seata.conf(resources下)

这个是ShardingSphere整合Seata需要的配置,单独使用Seata时不需要

client {
  ## 应用唯一主键
  application.id = gateway
  ## 所属事务组(与Seata中的配置一致)
  transaction.service.group = satisfactory_seata_group
}

5. ShardingSphere算法配置

包含:读写分离的负载均衡算法、数据分片算法

定义算法集合的类,容纳了ShardingSphere需要的所有算法
import org.apache.shardingsphere.infra.config.algorithm.ShardingSphereAlgorithmConfiguration;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

/**
 * ShardingSphere算法配置
 *
 * @since 2023-7-29 下午 7:07
 */
public class ShardingSphereAlgorithmConstants {

	/**
	 * 读写分离的负载均衡算法
	 */
	public static final Map<String, ShardingSphereAlgorithmConfiguration> READWRITE_ALGORITHMS = new HashMap<>();

	/**
	 * 算法名称:读写分离算法(ROUND_ROBIN)
	 */
	public static final String READWRITE_ALG_ROUND = "alg_round";

	/**
	 * 算法名称:读写分离算法(RANDOM)
	 */
	public static final String READWRITE_ALG_RANDOM = "alg_random";

	/**
	 * 算法名称:读写分离算法(WEIGHT)
	 */
	public static final String READWRITE_ALG_WEIGHT = "alg_weight";

	/**
	 * 数据分片算法
	 */
	public static final Map<String, ShardingSphereAlgorithmConfiguration> SHARDING_ALGORITHMS = new LinkedHashMap<>();

	static {
		// 负载均衡算法
		READWRITE_ALGORITHMS.put(READWRITE_ALG_ROUND, new ShardingSphereAlgorithmConfiguration("ROUND_ROBIN", null));
		READWRITE_ALGORITHMS.put(READWRITE_ALG_RANDOM, new ShardingSphereAlgorithmConfiguration("RANDOM", null));
		Properties properties = new Properties();
		properties.setProperty("slave01", "1");
		properties.setProperty("slave02", "2");
		READWRITE_ALGORITHMS.put(READWRITE_ALG_WEIGHT, new ShardingSphereAlgorithmConfiguration("WEIGHT", properties));
	}

}
每个表的数据分片算法(这里只有一张表sys_log需要分片)
import common.config.datasource.DataSourceConstants;
import common.config.datasource.sharding.config.ShardingSphereAlgorithmConstants;
import common.config.datasource.sharding.config.ShardingSphereAlgorithmUtil;
import org.springframework.stereotype.Component;

/**
 * sys_log表分片配置
 *
 * @since 2023-8-14 上午 11:47
 */
@Component("sysLogShardingConfig")
public class SysLogShardingConfig {

	/**
	 * 逻辑表名称
	 */
	public static final String LOGIC_TABLE_NAME = "sys_log";

	/**
	 * 真实数据节点
	 */
	public static final String ACTUAL_DATA_NODES = DataSourceConstants.SHARDING_DATASOURCE + ".sys_log_$->{1..2}";

	/**
	 * 算法名称
	 */
	public static final String SYS_LOG_TABLE_ALG = "sys_log_table_alg";
	public static final String SYS_LOG_TABLE_ALG_2 = "sys_log_table_alg_2";
	public static final String SYS_LOG_TABLE_ALG_3 = "sys_log_table_alg_3";

	static {
		ShardingSphereAlgorithmConstants.SHARDING_ALGORITHMS.put(SYS_LOG_TABLE_ALG, ShardingSphereAlgorithmUtil.buildInlineAlgorithmConfiguration("sys_log_$->{id % 2 + 1}"));
		ShardingSphereAlgorithmConstants.SHARDING_ALGORITHMS.put(SYS_LOG_TABLE_ALG_2, ShardingSphereAlgorithmUtil.buildHashModAlgorithmConfiguration("2"));
		ShardingSphereAlgorithmConstants.SHARDING_ALGORITHMS.put(SYS_LOG_TABLE_ALG_3, ShardingSphereAlgorithmUtil.buildInlineAlgorithmConfiguration("sys_log_$->{execution_time > 1000 ? 2 : 1}"));
	}

}

6. ShardingSphere分布式事务切面

需要显式设置分布式事务模式,也就是仅仅在注解里写TransactionType.BASE是不生效的,需要在方法执行前,调用TransactionTypeHolder.set(TransactionType.BASE),因此使用AOP拦截所有加了 @ShardingSphereTransactionType 注解的方法。

import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionTypeHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 设置ShardingSphere分布式事务类型
 *
 * @since 2023-8-10 上午 2:08
 */
@Component
@Aspect
// 如果一个方法被多个aop增强, 可以用@Order来控制增强顺序, 值越小优先级越高
@Order(-1)
@Slf4j
public class ShardingTransactionAspect {

	@Pointcut("@annotation(org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType)")
	public void pointCut() {
	}

	@Before("pointCut()")
	public void doBefore(JoinPoint point) {
		// 优先获取方法上的注解,如果方法上没有注解,则获取类上的注解
		MethodSignature signature = (MethodSignature) point.getSignature();
		Method method = signature.getMethod();
		ShardingSphereTransactionType transactionType = method.getAnnotation(ShardingSphereTransactionType.class);
		if (transactionType == null) {
			transactionType = point.getTarget().getClass().getAnnotation(ShardingSphereTransactionType.class);
		}
		if (transactionType == null) {
			log.warn("无法获取ShardingSphere分布式事务类型");
			return;
		}
		TransactionTypeHolder.set(transactionType.value());
		log.info("设置ShardingSphere分布式事务类型为:" + transactionType.value().toString());
	}

	@After("pointCut()")
	public void doAfter() {
		TransactionTypeHolder.clear();
	}

}

7. 解决ShardingSphere的兼容性问题

项目未使用shardingsphere-transaction-base-seata-at的依赖,原因是该包里的SeataTransactionHolder为package,外部无法调用其方法,故不引入此依赖,改为引入shardingsphere-transaction-core,但需要在项目java目录下新建org/apache/shardingsphere/transaction/base/seata/at包,放入以下三个类(这三个类来源于shardingsphere-transaction-base-seata-at依赖):

SeataATShardingSphereTransactionManager.java

修改了源码中isInTransaction()方法的一处判断

import com.google.common.base.Preconditions;
import io.seata.config.FileConfiguration;
import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.core.rpc.netty.RmNettyRemotingClient;
import io.seata.core.rpc.netty.TmNettyRemotingClient;
import io.seata.rm.RMClient;
import io.seata.rm.datasource.DataSourceProxy;
import io.seata.tm.TMClient;
import io.seata.tm.api.GlobalTransaction;
import io.seata.tm.api.GlobalTransactionContext;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.infra.database.type.DatabaseType;
import org.apache.shardingsphere.transaction.core.ResourceDataSource;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.apache.shardingsphere.transaction.rule.TransactionRule;
import org.apache.shardingsphere.transaction.spi.ShardingSphereTransactionManager;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Seata AT transaction manager.
 */
@Slf4j
public final class SeataATShardingSphereTransactionManager implements ShardingSphereTransactionManager {

	private final Map<String, DataSource> dataSourceMap = new HashMap<>();

	private final String applicationId;

	private final String transactionServiceGroup;

	private final boolean enableSeataAT;

	private final int globalTXTimeout;

	public SeataATShardingSphereTransactionManager() {
		FileConfiguration config = new FileConfiguration("seata.conf");
		enableSeataAT = config.getBoolean("sharding.transaction.seata.at.enable", true);
		applicationId = config.getConfig("client.application.id");
		transactionServiceGroup = config.getConfig("client.transaction.service.group", "default");
		globalTXTimeout = config.getInt("sharding.transaction.seata.tx.timeout", 60);
	}

	@Override
	public void init(final DatabaseType databaseType, final Collection<ResourceDataSource> resourceDataSources, final TransactionRule transactionRule) {
		if (enableSeataAT) {
			initSeataRpcClient();
			resourceDataSources.forEach(each -> dataSourceMap.put(each.getOriginalName(), new DataSourceProxy(each.getDataSource())));
		}
	}

	private void initSeataRpcClient() {
		Preconditions.checkNotNull(applicationId, "please config application id within seata.conf file.");
		TMClient.init(applicationId, transactionServiceGroup);
		RMClient.init(applicationId, transactionServiceGroup);
	}

	@Override
	public TransactionType getTransactionType() {
		return TransactionType.BASE;
	}

	@Override
	public boolean isInTransaction() {
		Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
		// 这里替换了源码中的判断方式,原方式会导致在commit和rollback时SeataTransactionHolder.get()返回null,报空指针
//		return null != RootContext.getXID();
		return null != SeataTransactionHolder.get();
	}

	@Override
	public Connection getConnection(final String dataSourceName) throws SQLException {
		Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
		return dataSourceMap.get(dataSourceName).getConnection();
	}

	@Override
	public void begin() {
		begin(globalTXTimeout);
	}

	@Override
	@SneakyThrows(TransactionException.class)
	public void begin(final int timeout) {
		if (timeout < 0) {
			throw new TransactionException("timeout should more than 0s");
		}
		Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
		GlobalTransaction globalTransaction = GlobalTransactionContext.getCurrentOrCreate();
		globalTransaction.begin(timeout * 1000);
		SeataTransactionHolder.set(globalTransaction);
	}

	@Override
	@SneakyThrows(TransactionException.class)
	public void commit(final boolean rollbackOnly) {
		Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
		try {
			SeataTransactionHolder.get().commit();
		} finally {
			SeataTransactionHolder.clear();
			RootContext.unbind();
		}
	}

	@Override
	@SneakyThrows(TransactionException.class)
	public void rollback() {
		Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
		try {
			SeataTransactionHolder.get().rollback();
		} finally {
			SeataTransactionHolder.clear();
			RootContext.unbind();
		}
	}

	@Override
	public void close() {
		dataSourceMap.clear();
		SeataTransactionHolder.clear();
		RmNettyRemotingClient.getInstance().destroy();
		TmNettyRemotingClient.getInstance().destroy();
	}
}
SeataTransactionHolder.java
import io.seata.tm.api.GlobalTransaction;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/**
 * Seata transaction holder.
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SeataTransactionHolder {

	private static final ThreadLocal<GlobalTransaction> CONTEXT = new ThreadLocal<>();

	/**
	 * Set seata global transaction.
	 *
	 * @param transaction global transaction context
	 */
	public static void set(final GlobalTransaction transaction) {
		CONTEXT.set(transaction);
	}

	/**
	 * Get seata global transaction.
	 *
	 * @return global transaction
	 */
	public static GlobalTransaction get() {
		return CONTEXT.get();
	}

	/**
	 * Clear global transaction.
	 */
	public static void clear() {
		CONTEXT.remove();
	}
}
TransactionalSQLExecutionHook.java
import io.seata.core.context.RootContext;
import org.apache.shardingsphere.infra.database.metadata.DataSourceMetaData;
import org.apache.shardingsphere.infra.executor.kernel.model.ExecutorDataMap;
import org.apache.shardingsphere.infra.executor.sql.hook.SQLExecutionHook;

import java.util.List;
import java.util.Map;

/**
 * Seata transactional SQL execution hook.
 */
public final class TransactionalSQLExecutionHook implements SQLExecutionHook {

	private static final String SEATA_TX_XID = "SEATA_TX_XID";

	private boolean seataBranch;

	@Override
	public void start(final String dataSourceName, final String sql, final List<Object> parameters,
	                  final DataSourceMetaData dataSourceMetaData, final boolean isTrunkThread, final Map<String, Object> shardingExecuteDataMap) {
		if (isTrunkThread) {
			if (RootContext.inGlobalTransaction()) {
				ExecutorDataMap.getValue().put(SEATA_TX_XID, RootContext.getXID());
			}
		} else if (!RootContext.inGlobalTransaction() && shardingExecuteDataMap.containsKey(SEATA_TX_XID)) {
			RootContext.bind((String) shardingExecuteDataMap.get(SEATA_TX_XID));
			seataBranch = true;
		}
	}

	@Override
	public void finishSuccess() {
		if (seataBranch) {
			RootContext.unbind();
		}
	}

	@Override
	public void finishFailure(final Exception cause) {
		if (seataBranch) {
			RootContext.unbind();
		}
	}
}

由于ShardingSphere是通过SPI方式加载的该类,故在子系统的resources目录下新建META-INF/services目录,新建两个文件:

文件1:
名为:org.apache.shardingsphere.infra.executor.sql.hook.SQLExecutionHook
内容为:org.apache.shardingsphere.transaction.base.seata.at.TransactionalSQLExecutionHook

文件2:
名为:org.apache.shardingsphere.transaction.spi.ShardingSphereTransactionManager
内容为:org.apache.shardingsphere.transaction.base.seata.at.SeataATShardingSphereTransactionManager

8. 数据分片表的主键生成策略

可以使用ShardingSphere内置的雪花算法,也可以使用MyBatisPlus提供的主键生成策略,这里用的后者。

在Java实体类中,可以这样定义主键字段:

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;

其中 IdType.ASSIGN_ID 是分配ID (主键类型为number或string),只有当插入对象id为空才自动填充,默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)

9. 如何使用事务控制

本地事务:@Transactional(rollbackFor = Exception.class) 即可

Seata全局事务:@Transactional(rollbackFor = Exception.class) 和 @ShardingSphereTransactionType(TransactionType.BASE) 同时使用

被feign调用的方法也需要加上相同的注解,其中ShardingSphereTransactionType的注解作用就是设置当前事务类型为BASE,这样shardingConnection里的transactionmanager就会放入SeataATShardingSphereTransactionManager,这一步会使本地事务转为分布式事务,并与seata通信。

10. 如何切换数据源

方法一:使用 @DS 切换数据源

@DS 可以注解在方法上或类上,同时存在就近原则,方法上的注解优先于类上的注解。

/**
 * serice实现类
 */
@Service
@DS("datasource1")
public class MysqlSlowLogServiceImpl extends ServiceImpl<MysqlSlowLogMapper, MySqlSlowLog> implements MysqlSlowLogService {

	@Resource
	private MysqlSlowLogMapper mapper;

	@Override
	public Page<MySqlSlowLog> pageSlowLog(PaginationRequest<SearchSlowLogRequest, MySqlSlowLog> paginationRequest) {
		return page(paginationRequest.buildPage(), paginationRequest.buildQueryWrapper());
	}

}

方法二:用 DynamicDataSourceContextHolder.push() 手动切换数据源
    @Override
    @DS("datasource1")
    public void getCopyDataInfo(String targetTableName, Long serverId, String dbName, Long taskSetId) {
        GetCopyDataInfoVO info = getCopyDataInfoOne(targetTableName, serverId, dbName, taskSetId);
        // 手动切换数据源
        DynamicDataSourceContextHolder.push("datasource2");
        saveCopyDataInfo(info.getSyncInfo(), info.getTaskSetId(), info.getSrTableInfo(), info.getTgtTableInfo(), info.getSrColInfoList(), info.getTgtColInfoList());
    }

11. 代码里涉及到的其他类

CustomMetaObjectHandler.java
MyBatisPlus自定义字段自动填充处理类

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import common.config.datasource.DataSourceConstants;
import common.config.jwt.JwtUtil;
import common.model.enums.EnvironmentEnum;
import common.util.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.Objects;
import java.util.function.Supplier;

/**
 * MyBatisPlus自定义字段自动填充处理类 - 实体类中使用 @TableField 注解<br/>
 * 3.3.0版本以后推荐使用strictInsertFill()和strictUpdateFill()进行填充,会判断字段是否加了自动填充的注解<br/>
 * setFieldValByName()和fillStrategy()不会判断字段是否加了自动填充的注解,需要自己判断<br/>
 * fillStrategy()不能填充乐观锁字段,因为默认字段有值就不进行填充<br/>
 */
@Component
@Slf4j
public class CustomMetaObjectHandler implements MetaObjectHandler {

	private static final String INSERT_DESC = "插入数据";
	private static final String UPDATE_DESC = "更新数据";
	@Value("${spring.application.name}")
	private String appName;
	@Resource
	private HttpServletRequest request;
	@Resource
	private Environment environment;

	/**
	 * 插入数据时
	 */
	@Override
	public void insertFill(MetaObject metaObject) {
		Date current = new Date();
		// 创建时间
		strictInsertFill(metaObject, DataSourceConstants.CREATE_TIME_FIELD_NAME_IN_JAVA_ENTITY, Date.class, current);
		printLog(INSERT_DESC, DataSourceConstants.CREATE_TIME_FIELD_NAME_IN_JAVA_ENTITY, DateUtil.format(current, DatePattern.NORM_DATETIME_MS_PATTERN));
		// 更新时间
		strictInsertFill(metaObject, DataSourceConstants.UPDATE_TIME_FIELD_NAME_IN_JAVA_ENTITY, Date.class, current);
		printLog(INSERT_DESC, DataSourceConstants.UPDATE_TIME_FIELD_NAME_IN_JAVA_ENTITY, DateUtil.format(current, DatePattern.NORM_DATETIME_MS_PATTERN));
		// 操作人
		switch (appName) {
			case Constants.SERVICE_NAME_GATEWAY:
			case Constants.SERVICE_NAME_SYSTEM:
			case Constants.SERVICE_NAME_GAME:
				String operator = JwtUtil.getUsernameFromRequest(request);
				strictInsertFill(metaObject, DataSourceConstants.OPERATOR_FIELD_NAME_IN_JAVA_ENTITY, String.class, operator);
				printLog(INSERT_DESC, DataSourceConstants.OPERATOR_FIELD_NAME_IN_JAVA_ENTITY, operator);
				break;
			case Constants.SERVICE_NAME_LOG:
			default:
				break;
		}
	}

	/**
	 * 更新数据时
	 */
	@Override
	public void updateFill(MetaObject metaObject) {
		Date current = new Date();
		// 更新时间
		strictUpdateFill(metaObject, DataSourceConstants.UPDATE_TIME_FIELD_NAME_IN_JAVA_ENTITY, Date.class, current);
		printLog(UPDATE_DESC, DataSourceConstants.UPDATE_TIME_FIELD_NAME_IN_JAVA_ENTITY, DateUtil.format(current, DatePattern.NORM_DATETIME_MS_PATTERN));
		// 操作人
		switch (appName) {
			case Constants.SERVICE_NAME_GATEWAY:
			case Constants.SERVICE_NAME_SYSTEM:
			case Constants.SERVICE_NAME_GAME:
				String operator = JwtUtil.getUsernameFromRequest(request);
				strictUpdateFill(metaObject, DataSourceConstants.OPERATOR_FIELD_NAME_IN_JAVA_ENTITY, String.class, operator);
				printLog(UPDATE_DESC, DataSourceConstants.OPERATOR_FIELD_NAME_IN_JAVA_ENTITY, operator);
				break;
			case Constants.SERVICE_NAME_LOG:
			default:
				break;
		}
	}

	/**
	 * 自定义严格模式填充策略:<br/>
	 * ① 覆盖原值<br/>
	 * ② 如果提供的值为null则不填充<br/>
	 */
	@Override
	public MetaObjectHandler strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal) {
		Object obj = fieldVal.get();
		if (Objects.nonNull(obj)) {
			metaObject.setValue(fieldName, obj);
		}
		return this;
	}

	/**
	 * 打印日志
	 *
	 * @param fillType  插入数据/更新数据
	 * @param fieldName 字段名
	 * @param fieldVal  字段值
	 */
	private void printLog(String fillType, String fieldName, Object fieldVal) {
		// 在非生产环境打印日志
		if (!environment.acceptsProfiles(Profiles.of(EnvironmentEnum.PRODUCT.getName()))) {
			log.info("MyBatisPlus自动填充【{}】 - {}:{}", fillType, fieldName, fieldVal);
		}
	}

}

MyBatisPlusConfig.java
MyBatisPlus插件配置

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatisPlus插件配置,注意:
 * 使用多个功能需要注意顺序关系,建议使用如下顺序
 * 多租户,动态表名
 * 分页,乐观锁
 * sql性能规范,防止全表更新与删除
 * 总结: 对sql进行单次改造的优先放入,不对sql进行改造的最后放入
 */
@Configuration
public class MyBatisPlusConfig {

	@Bean
	public MybatisPlusInterceptor mybatisPlusInterceptor() {
		MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
		// 分页
		interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
		// 乐观锁
		interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
		// 防止全表更新与删除
		interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
		return interceptor;
	}

}

DataSourceConstants.java
数据源相关的常量

import common.model.po.SysUser;
import common.util.ClassUtil;
import io.seata.common.exception.ShouldNeverHappenException;
import org.apache.shardingsphere.infra.database.DefaultSchema;

/**
 * 数据源相关的常量<p>
 * 其中数据源名称必须小写
 * 
 * @since 2023-7-29 下午 7:07
 */
public class DataSourceConstants {

	public static final String ID_FIELD_NAME_IN_DATABASE = "id";

	public static final String OPERATOR_FIELD_NAME_IN_DATABASE = "operator";

	public static final String CREATE_TIME_FIELD_NAME_IN_DATABASE = "create_time";

	public static final String UPDATE_TIME_FIELD_NAME_IN_DATABASE = "update_time";

	public static final String ID_FIELD_NAME_IN_JAVA_ENTITY = ClassUtil.getFieldName(SysUser::getId);

	public static final String OPERATOR_FIELD_NAME_IN_JAVA_ENTITY = ClassUtil.getFieldName(SysUser::getOperator);

	public static final String CREATE_TIME_FIELD_NAME_IN_JAVA_ENTITY = ClassUtil.getFieldName(SysUser::getCreateTime);

	public static final String UPDATE_TIME_FIELD_NAME_IN_JAVA_ENTITY = ClassUtil.getFieldName(SysUser::getUpdateTime);

	/**
	 * ShardingSphere数据源:读写分离 + 分库分表
	 * 数据源名称必须设置为{@link DefaultSchema#LOGIC_NAME},否则插入数据时报错{@link ShouldNeverHappenException}
	 */
	public static final String SHARDING_DATASOURCE = DefaultSchema.LOGIC_NAME;

	/**
	 * 慢查询日志数据源(代理)
	 */
	public static final String MYSQL_DATASOURCE = "mysql_proxy";

	/**
	 * information_schema数据源(代理)
	 */
	public static final String INFORMATION_SCHEMA_DATASOURCE = "informationSchema_proxy";

	/**
	 * master数据源(代理)
	 */
	public static final String MASTER_DATASOURCE = "master_proxy";

}

ClassUtil.java
Class工具类

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import java.beans.Introspector;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Class工具类
 *
 * @since 2022-12-31 下午 3:14
 */
@Slf4j
public class ClassUtil extends cn.hutool.core.util.ClassUtil {

	/**
	 * 字段缓存
	 */
	private static final Map<SerializableFunction<?, ?>, Field> CACHE = new ConcurrentHashMap<>();
	private static final String GET = "get";
	private static final String IS = "is";
	private static final String LAMBDA$ = "lambda$";

	private ClassUtil() {
		throw new AssertionError("不可实例化");
	}

	/**
	 * 根据getter方法获取字段名
	 */
	public static <T, R> String getFieldName(SerializableFunction<T, R> function) {
		Field field = getField(function);
		return field.getName();
	}

	/**
	 * 根据getter方法获取字段
	 */
	public static <T, R> Field getField(SerializableFunction<T, R> function) {
		return CACHE.computeIfAbsent(function, ClassUtil::findField);
	}

	private static <T, R> Field findField(SerializableFunction<T, R> function) {
		Field field = null;
		String fieldName = null;
		try {
			// 第1步:获取SerializedLambda
			Method method = function.getClass().getDeclaredMethod("writeReplace");
			method.setAccessible(Boolean.TRUE);
			SerializedLambda serializedLambda = (SerializedLambda) method.invoke(function);
			// 第2步:implMethodName 即为Field对应的Getter方法名
			String implMethodName = serializedLambda.getImplMethodName();
			if (implMethodName.startsWith(GET) && implMethodName.length() > GET.length()) {
				fieldName = Introspector.decapitalize(implMethodName.substring(GET.length()));
			} else if (implMethodName.startsWith(IS) && implMethodName.length() > IS.length()) {
				fieldName = Introspector.decapitalize(implMethodName.substring(IS.length()));
			} else if (implMethodName.startsWith(LAMBDA$)) {
				throw new IllegalArgumentException(SerializableFunction.class.getSimpleName() + "不能传递lambda表达式,只能使用方法引用");
			} else {
				throw new IllegalArgumentException(implMethodName + "不是Getter方法引用");
			}
			// 第3步:获取的Class是字符串,并且包名是“/”分割,需要替换成“.”,才能获取到对应的Class对象
			String declaredClass = serializedLambda.getImplClass().replace("/", ".");
			Class<?> aClass = Class.forName(declaredClass, false, ClassUtils.getDefaultClassLoader());
			// 第4步:Spring中的反射工具类获取Class中定义的Field
			field = ReflectionUtils.findField(aClass, fieldName);
		} catch (Exception e) {
			log.error("ClassUtil中findField()方法发生异常", e);
		}
		// 第5步:如果没有找到对应的字段应该抛出异常
		if (field == null) {
			throw new NoSuchFieldError(fieldName);
		}
		return field;
	}

}

ShardingSphereAlgorithmUtil.java
ShardingSphere算法工具类

import org.apache.shardingsphere.infra.config.algorithm.ShardingSphereAlgorithmConfiguration;

import java.util.Properties;

/**
 * ShardingSphere算法工具类
 *
 * @since 2023-7-29 下午 7:07
 */
public class ShardingSphereAlgorithmUtil {

	public static ShardingSphereAlgorithmConfiguration buildInlineAlgorithmConfiguration(String algorithmExpression) {
		Properties properties = new Properties();
		properties.setProperty("algorithm-expression", algorithmExpression);
		return new ShardingSphereAlgorithmConfiguration("INLINE", properties);
	}

	public static ShardingSphereAlgorithmConfiguration buildHashModAlgorithmConfiguration(String shardingCount) {
		Properties properties = new Properties();
		properties.setProperty("sharding-count", shardingCount);
		return new ShardingSphereAlgorithmConfiguration("HASH_MOD", properties);
	}

}

JwtUtil.java
Jwt-token工具类

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import common.model.dto.LoginToken;
import common.model.dto.ModifyEmailToken;
import common.model.dto.ModifyPasswordToken;
import common.util.Constants;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;

import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;

@Slf4j
public class JwtUtil {

	/**
	 * 从请求中获取token
	 */
	public static String getTokenStringFromRequest(HttpServletRequest request) {
		String bearerToken = request.getHeader(Constants.AUTHORIZATION_HEADER);
		if (StrUtil.isNotBlank(bearerToken)) {
			return bearerToken;
		}
		return null;
	}

	/**
	 * 从请求中获取当前登录用户的信息
	 */
	@Nullable
	public static LoginToken getLoginTokenFromRequest(HttpServletRequest request) {
		String token = getTokenStringFromRequest(request);
		// 如果获取不到token的信息,说明没有登录的用户
		if (StrUtil.isBlank(token)) {
			return null;
		}
		return parseLoginToken(token);
	}

	/**
	 * 从请求中获取当前登录用户的用户名
	 */
	@Nullable
	public static String getUsernameFromRequest(HttpServletRequest request) {
		LoginToken loginToken = getLoginTokenFromRequest(request);
		if (loginToken == null) {
			return null;
		}
		return loginToken.getUsername();
	}

}

12. 参考文章

spring boot:shardingsphere+druid多数据源整合seata分布式事务(spring boot 2.3.3):https://www.lmlphp.com/user/57801/article/item/2398339/

SpringBoot集成Shardingjdbc+seata AT模式:https://blog.csdn.net/qq_41563912/article/details/126305589

Shardingsphere-jdbc整合Feign、Seata AT 模式实现分布式事务的解决方案:https://blog.csdn.net/qq_42703153/article/details/129880291

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于springcloud+springboot+nacos+openFeign的分布式事务组件seata项目源码.zip 介绍 分布式事务组件seata的使用demo,AT模式、TCC模式,集成springbootspringcloud(nacos注册中心、openFeign服务调用、Ribbon负载均衡器)、spring jpa,数据库采用mysql demo中使用的相关版本号,具体请看代码。如果搭建个人demo不成功,验证是否是由版本导致,版本稍有变化可能出现相关组件的版本不一致便会出现许多奇怪问题 seata服务端 1.3 Nacos服务端 1.1.4 spring-cloud-alibaba-dependencies 2.1.0.RELEASE springboot 2.1.3.RELEASE springcloud Greenwich.RELEASE 软件架构 软件架构说明 springcloud-common 公共模块 springcloud-order-AT 订单服务 springcloud-product-AT 商品库存服务 springcloud-consumer-AT 消费调用者 springcloud-business-Tcc 工商银行服务 springcloud-merchants-Tcc 招商银行服务 springcloud-Pay-Tcc 消费调用者 AT模式:springcloud-order-AT,springcloud-product-AT,springcloud-consumer-AT为AT模式Dome;模拟场景用户购买商品下单; 调用流程springcloud-consumer-AT调用订单服务创建订单(新增一条数据到订单表);在调用商品库存服务扣减商品库存数量(修改商品库存表商品数量);最后出现异常则统一回滚,负责统一提交; 第一阶段:准备阶段(prepare)协调者通知参与者准备提交订单,参与者开始投票。协调者完成准备工作向协调者回应Yes。 第二阶段:提交(commit)/回滚(rollback)阶段协调者根据参与者的投票结果发起最终的提交指令。如果有参与者没有准备好则发起回滚指令。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值