文章目录

  • 1 楔子
  • 2 分析
  • 3 代码实现
    • 3.1 管理库关键库表设计
    • 3.2 租户库关键库表设计
    • 3.3 新建一个SpringBootWeb项目
    • 3.4 添加maven依赖
    • 3.5 创建初始化数据库工具类
    • 3.6 创建动态数据源配置类
    • 3.7 创建登录代码
    • 3.8 创建数据源元数据服务类
    • 3.9 创建saas服务基础父类
  • 4 示例演示
    • 4.1 下载示例代码
    • 4.2 执行resources下初始化数据库脚本init.sql
    • 4.3 修改resources下application.yml中数据库配置
    • 4.4 通过DynamicApplication启动项目
    • 4.5 测试
      • 4.5.1 添加数据库
      • 4.5.2 初始化数据库
      • 4.5.2 多租户测试

✨这里是第七人格的博客✨小七,欢迎您的到来~✨
系列专栏:【工作小札】
✈️本篇内容: 利用动态数据源实现Sass化✈️
本篇收录完整代码地址:https://gitee.com/diqirenge/sheep-web-demo/tree/master/sheep-web-demo-dynamicDataSource

1 楔子

针对Sass多租户,业内有许多解决方案。一般来说,如果做的简单一点,直接用一个表字段区分租户,所有db操作都带上这个标识即可。如果做的稍微好一点,我们可以考虑分库,即每个租户都拥有自己的数据库,且可以将数据库部署在本地。

2 分析

基于分库的需求,我们可以做以下技术拆分:

1、需要有一个管理中心,管理所有租户的数据库,这个应该是一个单独的库,租户的库又是其他单独的库。

2、从管理中心页面上,要能够对租户的库进行管理,比如动态建库建表。

3、后台只用一套代码,所以要动态适配数据源。

4、租户登录之后,应该就要适配到适合自己的库。

3 代码实现

以下是关键代码的实现,如果读者不感兴趣,可以直接看第4章。

3.1 管理库关键库表设计

库名随意,我这里取dynamic

CREATE DATABASE `dynamic` ;

作为管理库,肯定要管理其他库的数据库元数据,那么抽象出哪些元数据比较合适呢?观察以下配置

url: jdbc:mysql://localhost:3306/dynamic?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=trueusername: rootpassword: 123456

我们发现连接Mysql时,需要配置url、username以及password,为了方便切库我们多抽象设计一个schema(即问号前面的dynamic部分)。这里给出一个简单的参考表如下:

CREATE TABLE `data_source_meta`  (                                 `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID',                                 `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '名称',                                 `url` varchar(127) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'mysql地址',                                 `mysql_schema` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'mysql库名',                                 `user_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'mysql用户名',                                 `user_password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'mysql密码',                                 PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

3.2 租户库关键库表设计

租户主要是业务表,我们这里就随便设计一个地区表area

CREATE TABLE `area`  (                         `area_id` int(0) NOT NULL AUTO_INCREMENT,                         `area_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,                         PRIMARY KEY (`area_id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

3.3 新建一个SpringBootWeb项目

3.4 添加maven依赖

为了实现我们的需求,需要添加以下2个关键依赖

    <!--动态数据源-->    <dependency>        <groupId>com.baomidou</groupId>        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>        <version>3.5.0</version>    </dependency>    <!--加入数据库连接池-->    <dependency>        <groupId>com.alibaba</groupId>        <artifactId>druid</artifactId>        <version>1.2.9</version>    </dependency>

3.5 创建初始化数据库工具类

/** * 初始化数据库工具 * * @author 第七人格 * @date 2023/04/13 */@Slf4jpublic class InitDBUtil {    /**     * jdbc url模板     */    private static final String jdbcUrlTemplate = "jdbc:mysql://#{mysqlUrl}/#{schema}?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";    /**     * 驱动程序类     */    private static final String driverClass = "com.mysql.cj.jdbc.Driver";    /**     * 删除sql模板     */    private static final String dropSchemaSqlTemplate = "DROP DATABASE IF EXISTS #{schema}";    /**     * 创建sql模板     */    private static final String createSchemaSqlTemplate = "CREATE DATABASE `#{schema}` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'; ";    /**     * 使用sql模板     */    private static final String useSchemaSqlTemplate = "use `#{schema}`;";    /**     * 初始化数据库     *     * @param mysqlUrl mysql url     * @param schema   模式     * @param username 用户名     * @param password 密码     * @return boolean     */    public static boolean initDB(String mysqlUrl,String schema,String username,String password){        Connection connection = null;        try{            Class.forName(driverClass);            connection = DriverManager.getConnection(jdbcUrlTemplate.replace("#{mysqlUrl}",mysqlUrl).replace("#{schema}","mysql"), username, password);            Statement statement = connection.createStatement();            statement.execute(dropSchemaSqlTemplate.replace("#{schema}",schema));            statement.execute(createSchemaSqlTemplate.replace("#{schema}",schema));            statement.execute(useSchemaSqlTemplate.replace("#{schema}",schema));            ScriptRunner scriptRunner = new ScriptRunner(connection);            scriptRunner.setStopOnError(true);            ClassPathResource classPathResource = new ClassPathResource("sqlTemplate.sql");            InputStream inputStream = classPathResource.getInputStream();            InputStreamReader isr = new InputStreamReader(inputStream);            scriptRunner.runScript(isr);            return true;        }catch(Exception e){            log.error("初始化数据库失败,{}",e.getMessage());            return false;        }finally {            if(null != connection){                try {                    connection.commit();                    connection.close();                } catch (SQLException ignored) {                }            }        }    }    public static boolean tryConnectDB(String mysqlUrl,String schema,String username,String password){        Connection connection = null;        try{            Class.forName(driverClass);            connection = DriverManager.getConnection(jdbcUrlTemplate.replace("#{mysqlUrl}",mysqlUrl).replace("#{schema}",schema), username, password);            return true;        }catch(Exception e){            log.error("尝试连接数据库失败,{}",e.getMessage());            return false;        }finally {            if(null != connection){                try {                    connection.commit();                    connection.close();                } catch (SQLException ignored) {                }            }        }    }    /**     * 得到初始化数据库配置     *     * @return {@link DruidDataSource}     */    public static DruidDataSource getInitDBConfig(){        DruidDataSource dataSource = new DruidDataSource();        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");        dataSource.setInitialSize(5);        dataSource.setMinIdle(5);        dataSource.setMaxActive(20);        dataSource.setMaxWait(60000);        dataSource.setTimeBetweenEvictionRunsMillis(60000);        dataSource.setMinEvictableIdleTimeMillis(300000);        dataSource.setValidationQuery("select 1 from dual");        dataSource.setTestWhileIdle(true);        dataSource.setTestOnBorrow(false);        dataSource.setTestOnReturn(false);        dataSource.setPoolPreparedStatements(true);        dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);        return dataSource;    }}

3.6 创建动态数据源配置类

关键代码是 DynamicRoutingDataSource 的 api 的使用

/** * 动态数据源配置 * * @author 第七人格 * @date 2023/04/13 */@Component@Slf4jpublic class DynamicDataSourceConfig {    /**     * 缓存     */    private final Map<String, String> cache = new HashMap<>();    /**     * 数据源     */    @Resource    private DynamicRoutingDataSource dataSource;    /**     * 加载所有数据库     */    @PostConstruct    public void loadAllDB(){        cache.put("master","管理中心");        // todo 这里可以做成,项目一启动就去读取管理库的数据库元数据,加载到缓存之中    }    /**     * 动态添加数据库     *     * @param datasourceMeta 数据源元     */    public void addDB(DataSourceMeta datasourceMeta){        DruidDataSource tmpdb = InitDBUtil.getInitDBConfig();        tmpdb.setUsername(datasourceMeta.getUsername());        tmpdb.setPassword(datasourceMeta.getPassword());        tmpdb.setUrl("jdbc:mysql://"+ datasourceMeta.getUrl()+"/"+ datasourceMeta.getMysqlSchema()+"?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true");        dataSource.addDataSource(datasourceMeta.getMysqlSchema(),tmpdb);        log.info("======动态添加数据库完成:mysqlSchema={}",datasourceMeta.getMysqlSchema());        cache.put(datasourceMeta.getMysqlSchema(), datasourceMeta.getName());    }    /**     * 动态删除数据库     *     * @param datasourceMeta 数据源元     */    public void deleteDB(DataSourceMeta datasourceMeta){        dataSource.removeDataSource(datasourceMeta.getMysqlSchema());        log.info("======动态删除数据库完成:mysqlSchema={}",datasourceMeta.getMysqlSchema());        cache.remove(datasourceMeta.getMysqlSchema());    }    /**     * 通过schema获取在元数据中的名称     *     * @param schema 模式     * @return {@link String}     */    public String getNameBySchema(String schema){        return cache.getOrDefault(schema, "");    }}

3.7 创建登录代码

关键代码是将前端传入的schema,放到浏览器session中

/** * 登录控制器 * * @author 第七人格 * @date 2023/04/13 */@RestController@RequestMapping(value = "/admin")public class LoginController {    /**     * 登录     *     * @param schema  模式     * @param request 请求     * @return {@link String}     */    @GetMapping("/login/{schema}")    public String login(@PathVariable String schema, HttpServletRequest request) {        // 存入session,用于切库        request.getSession().setAttribute("schema",schema);        return "登录成功!";    }}

3.8 创建数据源元数据服务类

关键代码是使用com.baomidou.dynamic.datasource.annotation.DS注解

@DS(“master”),标明使用的是管理库

/** * 数据源元数据服务 * * @author 第七人格 * @date 2023/04/13 */@Service@DS("master")public class DataSourceMetaService {    /**     * 数据源元映射器     */    @Resource    private DataSourceMetaMapper datasourceMetaMapper;    /**     * 选择可用数据     *     * @param dataSourceMeta 数据源元     * @return {@link List}     */    public List<DataSourceMeta> selectAvailable(DataSourceMeta dataSourceMeta) {        return new LambdaQueryChainWrapper<>(datasourceMetaMapper)                .eq(DataSourceMeta::getId, dataSourceMeta.getId())                .eq(DataSourceMeta::getUrl, dataSourceMeta.getUrl())                .list();    }    public void add(DataSourceMeta dataSourceMeta) {        datasourceMetaMapper.insert(dataSourceMeta);    }    public void update(DataSourceMeta dataSourceMeta) {        datasourceMetaMapper.updateById(dataSourceMeta);    }    public void delete(int dataSourceMetaId) {        datasourceMetaMapper.deleteById(dataSourceMetaId);    }    public boolean initDB(DataSourceMeta dataSourceMeta) {        return InitDBUtil.initDB(dataSourceMeta.getUrl(),dataSourceMeta.getMysqlSchema(),dataSourceMeta.getUsername(),dataSourceMeta.getPassword());    }    public boolean tryConnectDB(DataSourceMeta dataSourceMeta) {        return InitDBUtil.tryConnectDB(dataSourceMeta.getUrl(),dataSourceMeta.getMysqlSchema(),dataSourceMeta.getUsername(),dataSourceMeta.getPassword());    }}

3.9 创建saas服务基础父类

关键代码师使用com.baomidou.dynamic.datasource.annotation.DS注解

@DS(“#session.schema”), 该接口下的所有数据操作默认根据session中的schema进行路由,其他业务服务类都要继承他

/** * saas服务 * 该接口下的所有数据操作默认根据session中的schema进行路由。 * * @author 第七人格 * @date 2023/04/13 */@DS("#session.schema")public class SaasService {}

业务实现类例子

/** * 区域服务impl * * @author 第七人格 * @date 2023/04/13 */@Servicepublic class AreaServiceImpl extends SaasService {    /**     * 区域映射器     */    @Resource    private AreaMapper areaMapper;    /**     * 选择所有     *     * @param area 区域     * @return {@link List}     */    public List<Area> selectAll(Area area) {        return new LambdaQueryChainWrapper<>(areaMapper)                .eq(Area::getAreaId, area.getAreaId())                .list();    }}

4 示例演示

4.1 下载示例代码

https://gitee.com/diqirenge/sheep-web-demo/tree/master/sheep-web-demo-dynamicDataSource

4.2 执行resources下初始化数据库脚本init.sql

4.3 修改resources下application.yml中数据库配置

4.4 通过DynamicApplication启动项目

4.5 测试

测试方法皆可在http-test-api.http文件中查看

4.5.1 添加数据库

4.5.2 初始化数据库

初始化数据库dy_test_1

初始化后,可在本地库中看到新建的数据库

修改area表area_name的数据为重庆测试

初始化数据库dy_test

4.5.2 多租户测试

模拟dy_test登录

模拟业务请求

模拟dy_test_1登录

模拟业务请求