spring boot 项目中多租户的实现

项目背景

项目中SaaS服务需要提供多租户基础功能,通过访问域名区分不同的客户,进而隔离数据源,即一个租户一个数据库。

AbstractRoutingDataSource

spring中对切换数据源提供了动态设置方法,通过determineCurrentLookupKey()设置值切换对应数据源。

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 源码的介绍:


基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,但是注意无法动态的增加数据源,只能在项目启动时加载。

实现逻辑

  1. 定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
  2. 启动时加载多个数据源,并配置到AbstractRoutingDataSource的defaultTargetDataSource和targetDataSources中。
  3. 对所有controller方法做aop,根据当前域名或者前端设置值修改本地线程动态数据源名称。

新建类 DynamicDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;


/**
 * 动态获取DataSource
 *
 * @author plsy
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSource();
    }

}

通过基于本地线程的上下文管理来切换数据源

import org.apache.log4j.Logger;

import java.util.LinkedList;

/**
 * 动态数据源上下文管理
 *
 * @author plsy
 */
public class DynamicDataSourceContextHolder {

    static Logger logger = Logger.getLogger(DynamicDataSourceContextHolder.class);

    /**
     * 存放当前线程使用的数据源类型信息
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    /**
     * 数据源使用顺序标识
     */
    public static LinkedList<String> dataSourceIds = new LinkedList<>();

    /**
     * 设置数据源
     *
     * @param dataSourceName
     */
    public static void setDataSource(String dataSourceName) {
        contextHolder.set(dataSourceName);
        dataSourceIds.add(dataSourceName);
    }

    /**
     * 获取数据源
     */
    public static String getDataSource() {
        if (contextHolder.get() == null) {
            logger.debug("数据源标识为空,使用默认的数据源");
        } else {
            logger.debug("使用数据源:" + contextHolder.get() + " 如果数据源不存在将使用默认数据源.");
        }
        return contextHolder.get();
    }

    /**
     * 清除数据源
     */
    public static void clearDataSource() {
        contextHolder.remove();
        dataSourceIds.clear();
    }

    /**
     * 返回上一次使用的数据源
     *
     * @return
     */
    public static void returnDataSource() {
        dataSourceIds.removeLast();
        setDataSource(dataSourceIds.getLast());
    }
}

初始化数据源

package com.gsoft.cos3.datasource;

import com.zaxxer.hikari.util.DriverDataSource;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

@Configuration
public class DataSourceConfig {

    private Logger logger = Logger.getLogger(DataSourceConfig.class);

    /**
     * 自定义数据源
     */
    public Map<Object, Object> customDataSources = new HashMap<>();

    @Value("${spring.datasource.url}")
    private String url;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Value("${spring.datasource.driver-class-name}")
    private String driver;

    private static final List<String> custNames = new ArrayList<>();

    @Primary
    @RefreshScope
    @Bean(name = "datasource")
    public DynamicDataSource dynamicDataSource() {
        logger.info("=====初始化动态数据源=====");
        Properties properties = new Properties();
        DataSource dataSource = new DriverDataSource(url, driver, properties, username, password);
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 默认数据源
        dynamicDataSource.setDefaultTargetDataSource(dataSource);
        // 配置多数据源
        Map<Object, Object> dsMap = new HashMap<Object, Object>();
        Map<Object, Object> customDataSources = new HashMap<>();
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            PreparedStatement preparedStatement = connection.prepareStatement("select C_CODE,C_DSURL,C_DSDRIVERCLASSNAME,C_DSUSERNAME,C_DSPASSWORD from COS_SAAS_CUSTOMER");
            ResultSet resultSet = preparedStatement.executeQuery();
            while (resultSet.next()) {
                String name = resultSet.getString(1);
                custNames.add(name);
                dsMap.put("url", resultSet.getString(2));
                dsMap.put("driver", resultSet.getString(3));
                dsMap.put("username", resultSet.getString(4));
                dsMap.put("password", resultSet.getString(5));
                DataSource ds = buildDataSource(dsMap);
                customDataSources.put(name, ds);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        dynamicDataSource.setTargetDataSources(customDataSources);
        this.customDataSources = customDataSources;
        logger.info("已加载租户库数据源" + custNames);
        return dynamicDataSource;
    }

    @Primary
    @Bean(name = "jdbcTemplate")
    public JdbcTemplate jdbcTemplate(@Qualifier("datasource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    public static DataSource buildDataSource(Map<Object, Object> dataSourceMap) {
        String driverClassName = dataSourceMap.get("driver").toString();
        String url = dataSourceMap.get("url").toString();
        String username = dataSourceMap.get("username").toString();
        String password = dataSourceMap.get("password").toString();
        // hikari配置参数 目前使用默认设置
        Properties properties = new Properties();
        DriverDataSource driverDataSource = new DriverDataSource(url, driverClassName, properties, username, password);
        return driverDataSource;
    }

}

AOP

import com.gsoft.cos3.util.Assert;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * DataSourceAspect
 *
 * @author plsy
 */
@Component
@Aspect
@Order(-1)
public class DataSourceAspect {

    Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    private void cutController() {
    }

    @Before("cutController()")
    public void before(JoinPoint joinPoint) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 租户标识
        String sign = request.getHeader("Site-info");
        if (Assert.isNotEmpty(sign)) {
            DynamicDataSourceContextHolder.setDataSource(sign);
        } else {
            DynamicDataSourceContextHolder.clearDataSource();
        }
    }
}

前端设置切换数据源标识header,切面取值设置到DynamicDataSourceContextHolder中,租户标识与数据源初始化时设置的name相同。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容