springboot动态切换多租户

场景

租户多且不固定且多服务场景动态实现

pom配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.3.7.RELEASE</version>
    </parent>

    <groupId>com.carsonlius</groupId>
    <artifactId>dynamic-datasource-project</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>pd-goods</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        
<!--        动态切换数据源头-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
<!--      连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.21</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.0</version>
        </dependency>
<!--        接口文档-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

<!--        配置中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
<!--        服务发现 可以搭配openfeign使用-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
    </dependencies>
</project>

创建2个演示的数据库

-- pd_auth

CREATE TABLE `pd_auth_user` (
  `id` bigint(20) NOT NULL COMMENT 'ID',
  `account` varchar(30) NOT NULL COMMENT '账号',
  `name` varchar(50) NOT NULL COMMENT '姓名',
  `org_id` bigint(20) DEFAULT NULL COMMENT '组织ID\n#c_core_org',
  `station_id` bigint(20) DEFAULT NULL COMMENT '岗位ID\n#c_core_station',
  `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
  `mobile` varchar(20) DEFAULT '' COMMENT '手机',
  `sex` varchar(1) DEFAULT 'N' COMMENT '性别\n#Sex{W:女;M:男;N:未知}',
  `status` bit(1) DEFAULT b'0' COMMENT '启用状态 1启用 0禁用',
  `avatar` varchar(255) DEFAULT '' COMMENT '头像',
  `work_describe` varchar(255) DEFAULT '' COMMENT '工作描述\r\n比如:  市长、管理员、局长等等   用于登陆展示',
  `password_error_last_time` datetime DEFAULT NULL COMMENT '最后一次输错密码时间',
  `password_error_num` int(11) DEFAULT '0' COMMENT '密码错误次数',
  `password_expire_time` datetime DEFAULT NULL COMMENT '密码过期时间',
  `password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  `create_user` bigint(20) DEFAULT '0' COMMENT '创建人id',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_user` bigint(20) DEFAULT '0' COMMENT '更新人id',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `UN_ACCOUNT` (`account`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='用户'

-- pd_goods
CREATE TABLE `pd_goods_info` (
  `id` bigint(20) NOT NULL COMMENT '商品ID',
  `code` char(16) COLLATE utf8mb4_bin NOT NULL COMMENT '商品编码',
  `name` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '商品名称',
  `bar_code` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '国条码',
  `brand_id` bigint(20) DEFAULT NULL COMMENT '品牌表ID',
  `one_category_id` bigint(20) DEFAULT NULL COMMENT '一级分类ID',
  `two_category_id` bigint(20) DEFAULT NULL COMMENT '二级分类ID',
  `three_category_id` bigint(20) DEFAULT NULL COMMENT '三级分类ID',
  `supplier_id` bigint(20) DEFAULT NULL COMMENT '商品的供应商ID',
  `price` decimal(8,2) NOT NULL COMMENT '商品售价价格',
  `average_cost` decimal(18,2) NOT NULL COMMENT '商品加权平均成本',
  `publish_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '上下架状态:0下架,1上架',
  `audit_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '审核状态: 0未审核,1已审核',
  `weight` float DEFAULT NULL COMMENT '商品重量',
  `length` float DEFAULT NULL COMMENT '商品长度',
  `height` float DEFAULT NULL COMMENT '商品重量',
  `width` float DEFAULT NULL COMMENT '商品宽度',
  `color` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '颜色',
  `production_date` datetime NOT NULL COMMENT '生产日期',
  `shelf_life` int(11) NOT NULL COMMENT '商品有效期',
  `descript` text COLLATE utf8mb4_bin COMMENT '商品描述',
  `update_time` datetime DEFAULT NULL,
  `update_user` bigint(20) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `create_user` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='商品信息表'

3. 创建2个简单的接口

3.1 用户列表接口
package com.carsonlius.controller;

import com.carsonlius.dto.BaseDto;
import com.carsonlius.entity.User;
import com.carsonlius.services.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @Author carsonlius
 * @Date 2022/3/6 17:43
 * @Version 1.0
 */
@RestController
@RequestMapping("/user")
@Api(value = "用户模块", tags = "用户模块")
public class UserController {
    @Autowired
    private UserService userService;

    /**
     * 获取users列表
     * */
    @GetMapping
    @ApiOperation(value = "用户列表", response = List.class, httpMethod = "GET")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "merchantId", value = "", dataType = "String", paramType = "query", required= true)
    })
    public List<User> lists(BaseDto baseDto){

        return userService.lists();
    }
}


3.2 商品列表接口
package com.carsonlius.controller;

import com.carsonlius.dto.BaseDto;
import com.carsonlius.entity.GoodsInfo;
import com.carsonlius.services.GoodsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @version V1.0
 * @author: liusen
 * @date: 2022年02月25日 15时55分
 * @contact 
 * @company
 */
@RestController
@RequestMapping("/goods")
@Api(value = "商品模块", tags = "商品模块")
public class GoodsController {
    @Autowired
    private GoodsService goodsService;

    @GetMapping("/list")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "merchantId", value = "", dataType = "String", paramType = "query", required= true)
    })
    @ApiOperation(value = "获取全量商品列表", httpMethod = "GET", response = List.class)
    public List<GoodsInfo> goods(BaseDto baseDto){
        return goodsService.getGoods();
    }
}

4. 定义基本上数据类型BaseDto (用来标识卖家信息 生产环境可以使用token替代)

package com.carsonlius.dto;

import lombok.Data;

/**
 * 基参
 * @Author carsonlius
 * @Date 2022/3/12 15:38
 * @Version 1.0
 */
@Data
public class BaseDto {
    /**
     * 租户ID
     * */
    public String merchantId;
}

创建切面

package com.carsonlius.aspects;

import com.alibaba.druid.util.StringUtils;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.carsonlius.config.PinDaConfig;
import com.carsonlius.dto.BaseDto;
import com.carsonlius.dto.DataSourceDTO;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.UUID;


/**
 * @Author carsonlius
 * @Date 2022/3/6 19:01
 * @Version 1.0
 */
@Component
@Aspect
public class DynamicChangeDatasource {
    private Logger logger = LoggerFactory.getLogger(DynamicChangeDatasource.class);

    @Autowired
    private PinDaConfig pinDaConfig;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private DefaultDataSourceCreator dataSourceCreator;

    @Pointcut("execution(* com.carsonlius.controller.*.*(..))")
    public void changeDatasource() {
    }

    @Around("changeDatasource()")
    public Object around(ProceedingJoinPoint joinPoint) {
        Object result = null;
        String poolName = "";
        try {

            //
            logger.info("开始加载数据库");
            poolName = setDataSource(joinPoint);
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        } finally {
            logger.info("执行切换数据源之后");
            removeDatasource(poolName);
            //Do Something useful, If you have
        }
        return result;
    }

    /**
     * 删除久数据源
     */
    private void removeDatasource(String poolName) {
        if (StringUtils.isEmpty(poolName)) {
            logger.error("没有加载到数据库:" + poolName);
            return;
        }

        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        ds.removeDataSource(poolName);
    }

    /**
     * 切换数据源头
     */
    private String setDataSource(ProceedingJoinPoint joinPoint) {

        BaseDto baseDto = null;
        for (Object arg : joinPoint.getArgs()) {
            System.out.println("arg" + arg);
            if (arg instanceof BaseDto) {
                baseDto = (BaseDto) arg;
                break;
            }
        }

        // todo 缺少必须参数
//        if (baseDto == null || StringUtils.isEmpty(baseDto.getMerchantId())) {
//
//            throw new BizException(ExceptionCode.ILLEGALA_ARGUMENT_EX.getCode(), ExceptionCode.ILLEGALA_ARGUMENT_EX.getMsg());
//        }
        String merchantId = "goods";

        if (baseDto != null) {
            merchantId = baseDto.getMerchantId();
        }


        // todo 这里要从数据库获取商家数据库配置 (测试这里可以写死)
        DataSourceDTO dto = new DataSourceDTO();
        dto.setPassword(pinDaConfig.getPassword());
        dto.setUsername(pinDaConfig.getUsername());
        String poolName = wrapperPoolName(merchantId);
        dto.setPoolName(poolName);

        String url = "";
        String ip = pinDaConfig.getIp();
        String port = pinDaConfig.getPort();
        if ("goods".equals(merchantId)) {

            url = "jdbc:mysql://" + ip + ":" + port + "/pd_goods?serverTimezone=CTT&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true";
        } else {
            url = "jdbc:mysql://" + ip + ":" + port + "/pd_auth?serverTimezone=CTT&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true";
        }

        dto.setUrl(url);

        // 注入数据源
        add(dto);

        //  手动切换数据源
        DynamicDataSourceContextHolder.push(poolName);

        return poolName;
    }

    /**
     * 保证PoolName
     *
     * @param merchantId
     * @return
     */
    private String wrapperPoolName(String merchantId) {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        return merchantId.concat("_").concat(uuid);
    }

    /**
     * 添加数据源
     * 通用数据源会根据maven中配置的连接池根据顺序依次选择 默认的顺序为druid>hikaricp>beecp>dbcp>spring basic
     */
    public void add(DataSourceDTO dto) {
        // 添加数据源
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
    }
}

实现原理

1. 节点: 进入impl实现之前设置数据源(可能需要先添加数据源); 离开impl之后 删除数据源

2. 新增数据源的实现: dynamic-datasource-spring-boot-starter 实现,操作简单

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值