Skip to content

配置和实现

相关配置

在控制台打印SQL语句

提示

database.yml文件中acuity.database.p6spy = true时,启动程序时dynamic-datasource-spring-boot-starter组件将会使用P6spy数据源代理Druid的数据源,以实现SQL语句的输出功能

yaml
acuity:
  database:
    # 生产环境请设置p6spy = false  
    p6spy: true
acuity:
  database:
    # 生产环境请设置p6spy = false  
    p6spy: true
java
/**
 * 抽象连接池创建器
 * <p>
 * 这里主要处理一些公共逻辑,如脚本和事件等
 */
@Slf4j
public abstract class AbstractDataSourceCreator implements DataSourceCreator {
    private DataSource wrapDataSource(DataSource dataSource, DataSourceProperty dataSourceProperty) {
        String name = dataSourceProperty.getPoolName();
        DataSource targetDataSource = dataSource;

      	// acuity.database.p6spy = true 时,P6DataSource 会包装原生DataSource
        Boolean enabledP6spy = properties.getP6spy() && dataSourceProperty.getP6spy();
        if (enabledP6spy) {
            targetDataSource = new P6DataSource(dataSource);
            log.debug("dynamic-datasource [{}] wrap p6spy plugin", name);
        }

        // ...
        return new ItemDataSource(name, dataSource, targetDataSource, enabledP6spy, enabledSeata, seataMode);
    }
}
/**
 * 抽象连接池创建器
 * <p>
 * 这里主要处理一些公共逻辑,如脚本和事件等
 */
@Slf4j
public abstract class AbstractDataSourceCreator implements DataSourceCreator {
    private DataSource wrapDataSource(DataSource dataSource, DataSourceProperty dataSourceProperty) {
        String name = dataSourceProperty.getPoolName();
        DataSource targetDataSource = dataSource;

      	// acuity.database.p6spy = true 时,P6DataSource 会包装原生DataSource
        Boolean enabledP6spy = properties.getP6spy() && dataSourceProperty.getP6spy();
        if (enabledP6spy) {
            targetDataSource = new P6DataSource(dataSource);
            log.debug("dynamic-datasource [{}] wrap p6spy plugin", name);
        }

        // ...
        return new ItemDataSource(name, dataSource, targetDataSource, enabledP6spy, enabledSeata, seataMode);
    }
}

修改DATASOURCE模式的租户数据库前缀

yaml
acuity:
  mysql: 
    database: test_defaults       # 启动时连接的默认库
  database:
    initDatabasePrefix: 
      - test_base                 # 启动时连接的租户库前缀  务必与 ContextConstants.TENANT_BASE_POOL_NAME_HEADER 保持一致
acuity:
  mysql: 
    database: test_defaults       # 启动时连接的默认库
  database:
    initDatabasePrefix: 
      - test_base                 # 启动时连接的租户库前缀  务必与 ContextConstants.TENANT_BASE_POOL_NAME_HEADER 保持一致
java
public final class ContextConstants {
    // 切换数据源时租户前缀   务必与 acuity.database.initDatabasePrefix 保持一致
    public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
}
public final class ContextConstants {
    // 切换数据源时租户前缀   务必与 acuity.database.initDatabasePrefix 保持一致
    public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
}
text
- 微服务版
acuity-system-server/src/main/resources/schema/{数据库类型}/acuity_base.sql 修改为 test_base.sql
acuity-system-server/src/main/resources/data/{数据库类型}/acuity_base.sql 修改为 test_base.sql
- Boot
acuity-boot-server/src/main/resources/schema/{数据库类型}/acuity_base.sql 修改为 test_base.sql
acuity-boot-server/src/main/resources/data/{数据库类型}/acuity_base.sql 修改为 test_base.sql
- 微服务版
acuity-system-server/src/main/resources/schema/{数据库类型}/acuity_base.sql 修改为 test_base.sql
acuity-system-server/src/main/resources/data/{数据库类型}/acuity_base.sql 修改为 test_base.sql
- Boot
acuity-boot-server/src/main/resources/schema/{数据库类型}/acuity_base.sql 修改为 test_base.sql
acuity-boot-server/src/main/resources/data/{数据库类型}/acuity_base.sql 修改为 test_base.sql

服务运行时,操作多个租户库步骤

提示

下面配置表示该服务在启动时,除了acuity_defautls库,还会同时连3*N个租户库, N表示租户表(def_tenant)有多少租户,3表示下面配置中的test_base、test_extend、test_xxx。

修改 database.yml
yaml
acuity:
  database:
    initDatabasePrefix: 
      - test_base    # 启动时连接的租户库前缀  务必与TENANT_BASE_POOL_NAME_HEADER保持一致
      - test_extend  # 启动时连接的租户库前缀2 务必与TENANT_EXTEND_POOL_NAME_HEADER保持一致  
      - test_xxx     # 启动时连接的租户库前缀3  需要在ContextConstants中新增一个常量:  TENANT_XXX_POOL_NAME_HEADER
acuity:
  database:
    initDatabasePrefix: 
      - test_base    # 启动时连接的租户库前缀  务必与TENANT_BASE_POOL_NAME_HEADER保持一致
      - test_extend  # 启动时连接的租户库前缀2 务必与TENANT_EXTEND_POOL_NAME_HEADER保持一致  
      - test_xxx     # 启动时连接的租户库前缀3  需要在ContextConstants中新增一个常量:  TENANT_XXX_POOL_NAME_HEADER
修改 ContextConstants
java
public final class ContextConstants {
    // [内置常量] 切换数据源时租户前缀
    public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
		// [内置常量]
    public static final String TENANT_EXTEND_POOL_NAME_HEADER = "test_extend";
		// [自行新增]
    public static final String TENANT_XXX_POOL_NAME_HEADER = "test_xxx";
}
public final class ContextConstants {
    // [内置常量] 切换数据源时租户前缀
    public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
		// [内置常量]
    public static final String TENANT_EXTEND_POOL_NAME_HEADER = "test_extend";
		// [自行新增]
    public static final String TENANT_XXX_POOL_NAME_HEADER = "test_xxx";
}
修改 ContextUtil
java
public final class ContextUtil {

    public static void setTenantId(Object tenantId) {
        set(ContextConstants.TENANT_ID_HEADER, tenantId);
        setTenantBasePoolName(tenantId);
        setTenantExtendPoolName(tenantId);
        // 需要自己新增该方法
        setTenantXxxPoolName(tenantId);
    }

    /**
     * 切换xxx库
     *
     * @param tenantId
     */
    public static void setTenantXxxPoolName(Object tenantId) {
        set(ContextConstants.TENANT_XXX_POOL_NAME_HEADER, tenantId);
    }
}
public final class ContextUtil {

    public static void setTenantId(Object tenantId) {
        set(ContextConstants.TENANT_ID_HEADER, tenantId);
        setTenantBasePoolName(tenantId);
        setTenantExtendPoolName(tenantId);
        // 需要自己新增该方法
        setTenantXxxPoolName(tenantId);
    }

    /**
     * 切换xxx库
     *
     * @param tenantId
     */
    public static void setTenantXxxPoolName(Object tenantId) {
        set(ContextConstants.TENANT_XXX_POOL_NAME_HEADER, tenantId);
    }
}
修改 DsConstant
java
public interface DsConstant {
    /**
     * 默认数据源
     */
    String DEFAULTS = "0";
    /**
     * [内置常量] 动态租户数据源
     */
    String BASE_TENANT = "#thread." + ContextConstants.TENANT_BASE_POOL_NAME_HEADER;
    String EXTEND_TENANT = "#thread." + ContextConstants.TENANT_EXTEND_POOL_NAME_HEADER;
    // [自行新增]
    String XXX_TENANT = "#thread." + ContextConstants.TENANT_XXX_POOL_NAME_HEADER;
}
public interface DsConstant {
    /**
     * 默认数据源
     */
    String DEFAULTS = "0";
    /**
     * [内置常量] 动态租户数据源
     */
    String BASE_TENANT = "#thread." + ContextConstants.TENANT_BASE_POOL_NAME_HEADER;
    String EXTEND_TENANT = "#thread." + ContextConstants.TENANT_EXTEND_POOL_NAME_HEADER;
    // [自行新增]
    String XXX_TENANT = "#thread." + ContextConstants.TENANT_XXX_POOL_NAME_HEADER;
}
编写ServiceImpl代码
java
@DS(DsConstant.BASE_TENANT)
public class ServiceImpl {
  	// 方法上没有@DS注解就使用类上的
    public void test1() {
        // CRUD 操作 test_base 库
    }

    @DS(DsConstant.DEFAULTS)
    public void test2() {
        // CRUD 操作 test_defaults 库
    }
    @DS(DsConstant.EXTEND_TENANT)
    public void test3() {
        // CRUD 操作 test_extend 库
    }
    @DS(DsConstant.XXX_TENANT)
    public void test4() {
        // CRUD 操作 test_xxx 库
    }
}
@DS(DsConstant.BASE_TENANT)
public class ServiceImpl {
  	// 方法上没有@DS注解就使用类上的
    public void test1() {
        // CRUD 操作 test_base 库
    }

    @DS(DsConstant.DEFAULTS)
    public void test2() {
        // CRUD 操作 test_defaults 库
    }
    @DS(DsConstant.EXTEND_TENANT)
    public void test3() {
        // CRUD 操作 test_extend 库
    }
    @DS(DsConstant.XXX_TENANT)
    public void test4() {
        // CRUD 操作 test_xxx 库
    }
}
添加SQL脚本
  • 微服务
    • 在 acuity-system-server/src/main/resources/schema/{数据库类型}/ 目录添加 test_extend.sql
    • 在 acuity-system-server/src/main/resources/data/{数据库类型}/ 目录添加 test_xxx.sql
  • boot

    在 acuity-boot-server/src/main/resources/schema/{数据库类型}/ 目录添加 test_extend.sql 在 acuity-boot-server/src/main/resources/data/{数据库类型}/ 目录添加 test_xxx.sql

代码层面如何切换数据库

提示

使用dynamic-datasource-spring-boot-starter组件来实现操作多数据源,受到这个组件的限制,在需要代码层面上需要切换数据源时,切换的方法上不能加本地事务注解@Transactional

java
public class OrderService {
  	@Resource
		private UserService userService;
  	@Resource
		private ProductService productService;

		// 正例  
  	public void save1() {
      	// 保存 默认库
      	userService.save();
      	ContextUtil.setTenantBasePoolName(1L);
      	// 保存 租户库: acuity_base_1
      	productService.save();
      
        // 保存 租户库:acuity_base_2
      	ContextUtil.setTenantBasePoolName(2L);      
      	productService.save();
      	int a = 1/0;  // 虽然能切库成功,但此方法没有事务,报错后数据不会回滚。
    }
  
  	// 反例 涉及分布式事务需要切库时,不能在调用方方法上加 本地事务,想要解决事务问题,请使用seata分布式事务
  	@Transactional(rollbackFor = Exception.class)  // 这个注解会导致切库失败
  	public void save2() {
      	// 保存 默认库
      	userService.save();
      	ContextUtil.setTenantBasePoolName(1L);
      	// 保存 租户库: acuity_base_1
      	productService.save();
      
        // 保存 租户库:acuity_base_2
      	ContextUtil.setTenantBasePoolName(2L);      
      	productService.save();
      	int a = 1/0;
    }
}
public class OrderService {
  	@Resource
		private UserService userService;
  	@Resource
		private ProductService productService;

		// 正例  
  	public void save1() {
      	// 保存 默认库
      	userService.save();
      	ContextUtil.setTenantBasePoolName(1L);
      	// 保存 租户库: acuity_base_1
      	productService.save();
      
        // 保存 租户库:acuity_base_2
      	ContextUtil.setTenantBasePoolName(2L);      
      	productService.save();
      	int a = 1/0;  // 虽然能切库成功,但此方法没有事务,报错后数据不会回滚。
    }
  
  	// 反例 涉及分布式事务需要切库时,不能在调用方方法上加 本地事务,想要解决事务问题,请使用seata分布式事务
  	@Transactional(rollbackFor = Exception.class)  // 这个注解会导致切库失败
  	public void save2() {
      	// 保存 默认库
      	userService.save();
      	ContextUtil.setTenantBasePoolName(1L);
      	// 保存 租户库: acuity_base_1
      	productService.save();
      
        // 保存 租户库:acuity_base_2
      	ContextUtil.setTenantBasePoolName(2L);      
      	productService.save();
      	int a = 1/0;
    }
}
java
public class ProductService {
  	// 本地事务   #thread.xxx 表示动态获取 acuity_base的数据源
  	@DS("#thread.acuity_base")
  	@Transactional(rollbackFor = Exception.class)
  	public void save(Product product) {
        dao.insert(product);
    }
}
public class ProductService {
  	// 本地事务   #thread.xxx 表示动态获取 acuity_base的数据源
  	@DS("#thread.acuity_base")
  	@Transactional(rollbackFor = Exception.class)
  	public void save(Product product) {
        dao.insert(product);
    }
}
java
public class UserService {
    // 本地事务  0表示在yaml中配置的固定数据源
		@DS("0")
  	@Transactional(rollbackFor = Exception.class)
  	public void save(User user) {
        dao.insert(user);
    }
}
public class UserService {
    // 本地事务  0表示在yaml中配置的固定数据源
		@DS("0")
  	@Transactional(rollbackFor = Exception.class)
  	public void save(User user) {
        dao.insert(user);
    }
}

修改自动拼接的SQL中租户ID的字段名

字段模式适用

yaml
# database.yml
acuity:
  database: 
    tenantIdColumn: tenant_id
# database.yml
acuity:
  database: 
    tenantIdColumn: tenant_id

修改自动拼接的SQL中租户ID的值

字段模式适用

  • 方法1:全局配置修改
  • 方法2:在代码中临时修改
全局配置修改
java
@Configuration
@Slf4j
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {UTIL_PACKAGE, BUSINESS_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {

    public MybatisAutoConfiguration(final DatabaseProperties databaseProperties) {
        super(databaseProperties);
    }

    @Override
    protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
        List<InnerInterceptor> list = new ArrayList<>();
        // COLUMN 模式 多租户插件
        TenantLineInnerInterceptor tli = new TenantLineInnerInterceptor();
        tli.setTenantLineHandler(new MultiTenantLineHandler() {
          	//  租户字段的数据库字段名
            @Override
            public String getTenantIdColumn() {
                return databaseProperties.getTenantIdColumn();
            }

            @Override
            public boolean ignoreTable(String tableName) {
                return false
            }
						
						// 自动拼接的租户值
            @Override
            public Expression getTenantId() {
            		// 1. 可以修改这里
                return new LongValue(ContextUtil.getBasePoolNameHeader());
            }
        });
        list.add(tli);
        return list;
    }
}
@Configuration
@Slf4j
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {UTIL_PACKAGE, BUSINESS_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {

    public MybatisAutoConfiguration(final DatabaseProperties databaseProperties) {
        super(databaseProperties);
    }

    @Override
    protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
        List<InnerInterceptor> list = new ArrayList<>();
        // COLUMN 模式 多租户插件
        TenantLineInnerInterceptor tli = new TenantLineInnerInterceptor();
        tli.setTenantLineHandler(new MultiTenantLineHandler() {
          	//  租户字段的数据库字段名
            @Override
            public String getTenantIdColumn() {
                return databaseProperties.getTenantIdColumn();
            }

            @Override
            public boolean ignoreTable(String tableName) {
                return false
            }
						
						// 自动拼接的租户值
            @Override
            public Expression getTenantId() {
            		// 1. 可以修改这里
                return new LongValue(ContextUtil.getBasePoolNameHeader());
            }
        });
        list.add(tli);
        return list;
    }
}
在代码中临时修改
java
public class UserServiceImpl {
  
  	public void save() {
  			// 2 调用ContextUtil.setBasePoolNameHeader这个方法,就能影响拼接到SQL中的租户ID值
      	ContextUtil.setBasePoolNameHeader(4L);
      	dao.insert(user);
    }
}
public class UserServiceImpl {
  
  	public void save() {
  			// 2 调用ContextUtil.setBasePoolNameHeader这个方法,就能影响拼接到SQL中的租户ID值
      	ContextUtil.setBasePoolNameHeader(4L);
      	dao.insert(user);
    }
}

代码层面如何切换数据库

切库时使用mybatis-plus组件来实现拦截SQL语句,动态添加租户条件的目的

java
public class UserServiceImpl {
  
  	@Transactional(rollbackFor = Exception.class)   // 事务不影响 COLUMN模式 切租户
  	public void save() {
  			// insert into user (id, tenant_id, name) values (1, 4, 'xxx');
      	ContextUtil.setBasePoolNameHeader(4L);
      	dao.insert(user);
      
        // insert into user (id, tenant_id, name) values (2, 5, 'xxx');
        ContextUtil.setBasePoolNameHeader(5L);
      	dao.insert(user);
    }
}
public class UserServiceImpl {
  
  	@Transactional(rollbackFor = Exception.class)   // 事务不影响 COLUMN模式 切租户
  	public void save() {
  			// insert into user (id, tenant_id, name) values (1, 4, 'xxx');
      	ContextUtil.setBasePoolNameHeader(4L);
      	dao.insert(user);
      
        // insert into user (id, tenant_id, name) values (2, 5, 'xxx');
        ContextUtil.setBasePoolNameHeader(5L);
      	dao.insert(user);
    }
}
java
public interface UserMapper {
  	public int insert(User user);
}
public interface UserMapper {
  	public int insert(User user);
}

欢迎使用天源云Saas快速开发系统