spring boot+mybatis plus 实现动态数据源

开篇说明
  • 如果在这里获得过启发和思考,希望点赞支持!对于内容有不同的看法欢迎来信交流。
  • 技术栈 >> java
  • 邮箱 >> 15673219519@163.com
描述

由于项目开发中设计到游戏运营平台的搭建,游戏中每一个不同的区服使用的是不同的数据库存储数据。 如:
区服1:包括 game_1_data(游戏数据数据库),game_1_log(游戏日志数据库);
区服2:包括 game_2_data(游戏数据数据库),game_2_log(游戏日志数据库);
… 并且之后会持续增多
除了以上的数据库还包含一些,游戏全局的库等,以及平台本身的数据库;那么在单体项目中如何切换每一个请求应该查询那个数据库,成为一个难点。

  • 最终实现的效果如下
/**
 * 查询指定区服的聊天记录 game_{0}_log 数据库名称格式,
 * ChatMonitorSearchDTO.xyServerId 请求参数中指定的区服ID
 * 若ChatMonitorSearchDTO.xyServerId=1, 则查询数据库game_1_log
 */
@SelectDB(dbName = "game_{0}_log", serverFiled = "ChatMonitorSearchDTO.xyServerId")
public Future<IPage<ChatMonitorListVO>> pageList(ChatMonitorSearchDTO DTO) throws Exception{
    IPage<ChatMonitorListVO> page = new Page<>(DTO.getCurrent(), DTO.getSize());
    page = chatMonitorMapper.pageList(page, DTO);
    return new AsyncResult(page);
}
我的思路
  • 项目其中时初始化本平台的数据源;初始化成功后查询其他游戏服数据库的数据源进行初始化。全部添加到指定的 Map<Object, Object> dataSources = new HashMap<>()中。
  • 利用AbstractRoutingDataSource+ThreadLocal+AOP配合使用,确保可以修改每条线程的数据源。
  • 为了确保主线程中开启事务的情况下,依然能够切换数据源查询游戏库,@SelectDB会新开线程执行。
  • 在aop中实现,新服数据库添加的逻辑。确保开启新的区服后会新增game_x_data,game_x_log两个库,也能够正常查询。
实现步骤
  • 第一步:初始化数据源
/**
 * 初始化数据源
 */
@Component
public class JavaCodeDataSourceProvider implements ApplicationListener<ContextRefreshedEvent> {
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;

    @Autowired
    private DataSourceInfoServiceImpl dataSourceInfoService;

    // 初始化本平台的数据源
    @PostConstruct
    public void init() {
        DynamicDataSourceService.addDataSource(DynamicDataSource.DEFAULT_DB_KEY, url, username, password);
    }

    // 查询本平台数据库中,配置的游戏数据库的连接信息,并加载到 Map<Object, Object> dataSources = new HashMap<>()中
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        dataSourceInfoService.buildDynamicDataSourceFromDB();
    }
}
@Service
public class DataSourceInfoServiceImpl {
    @Autowired
    private DataSourceInfoMapper dataSourceInfoMapper;
    /**
     * 从数据库中查询配置的数据库链接信息,构建动态的数据源
     */
    public void buildDynamicDataSourceFromDB(){
        List<DataSourceInfo> dataSourceInfos = dataSourceInfoMapper.selectList(Wrappers.lambdaQuery(DataSourceInfo.class));
        for (DataSourceInfo info : dataSourceInfos){
            DynamicDataSourceService.addDataSource(info.getDbName(), info.getUrl(), info.getUsername(), info.getPassword());
        }
    }
}
/**
 * 维护动态数据源
 */
@Slf4j
public class DynamicDataSourceService {

    private static final Map<Object, Object> dataSources = new HashMap<>();
    private static final ThreadLocal<String> dbKeys = ThreadLocal.withInitial(() -> null);

    /**
     * 构建DataSource
     * @param url 数据库地址
     * @param username 用户名
     * @param password 用户密码
     * @return DataSource
     */
    public static DataSource buildDataSource(String url, String username, String password) {
        DataSourceBuilder<?> builder = DataSourceBuilder.create();
        builder.driverClassName("com.mysql.cj.jdbc.Driver");
        builder.username(username);
        builder.password(password);
        builder.url(url);
        return builder.build();
    }

    /**
     * 动态添加一个数据源
     * @param name       数据源的key
     * @param dataSource 数据源对象
     */
    public static void addDataSource(String name, DataSource dataSource) {
        DynamicDataSource dynamicDataSource = SpringUtils.getBean(DynamicDataSource.class);
        dataSources.put(name, dataSource);
        dynamicDataSource.setTargetDataSources(dataSources);
        dynamicDataSource.afterPropertiesSet();
        log.info("添加了数据源:{}", name);
    }

    /**
     * 动态添加一个数据源
     * @param dbName 数据源的key
     * @param url 数据库地址
     * @param username 用户名
     * @param password 用户密码
     */
    public static void addDataSource(String dbName, String url, String username, String password){
        DataSource dataSource = buildDataSource(url, username, password);
        addDataSource(dbName, dataSource);
    }

    /**
     * 是否存在数据源
     */
    public static boolean exist(String dbKey) {
        return dataSources.get(dbKey) != null;
    }

    /**
     * 切换数据源
     */
    public static void switchDb(String dbKey) {
        dbKeys.set(dbKey);
    }

    /**
     * 重置数据源
     */
    public static void resetDb() {
        dbKeys.remove();
    }

    /**
     * 获取当前数据源
     */
    public static String currentDb() {
        return dbKeys.get();
    }
}
  • 第二步:基本数据库配置 mybatisPlus
@Configuration
public class DynamicDataSourceConfig {
    /**
     * 动态数据源
     */
    @Bean
    public DynamicDataSource dynamicDataSource() {
        DynamicDataSource dataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource());
        sqlSessionFactoryBean.setTypeAliasesPackage("com.qykj.xyj.**.entity");
        sqlSessionFactoryBean.setTypeEnumsPackage("com.qykj.xiyouji.**.enums");
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*/*Mapper*.xml"));

        GlobalConfig globalConfig = new GlobalConfig();
        // 配置自定义填充器 MyMetaObjectHandler
        globalConfig.setMetaObjectHandler(new MybatisPlusMetaObjectHandler() );
        sqlSessionFactoryBean.setGlobalConfig(globalConfig);

        // 逻辑删除配置
        GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
        dbConfig.setLogicDeleteField("del");
        dbConfig.setLogicDeleteValue("1");
        dbConfig.setLogicNotDeleteValue("0");
        globalConfig.setDbConfig(dbConfig);

        // 设置XML
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        // 设置sql日志
        configuration.setLogImpl(StdOutImpl.class);
        // 设置枚举处理器
        configuration.setDefaultEnumTypeHandler(EnumValueTypeHandler.class);
        sqlSessionFactoryBean.setConfiguration(configuration);

        // 配置分页插件
        sqlSessionFactoryBean.setPlugins(mybatisPlusInterceptor());

        // 配置事务管理器
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 配置分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInnerInterceptor.setDialect(new MySqlDialect());
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
}
  • 第三步:基本数据库配置 AbstractRoutingDataSource
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    public static final String DEFAULT_DB_KEY = "game_base";
    
    @Override
    protected Object determineCurrentLookupKey() {
        String currentDb = DynamicDataSourceService.currentDb();
        log.info("currentDb:"+currentDb);
        if (currentDb == null) {
            return DEFAULT_DB_KEY;
        }
        return currentDb;
    }
}
  • 第四步:注解@SelectDB的编写

@Async(ThreadPoolConfig.THREAD_POOL),确保切换数据源之后为另一个线程。

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Async(ThreadPoolConfig.THREAD_POOL)
public @interface SelectDB {

    // 数据库名称
    String dbName();
    
    // 查询参数中标识区服的字段
    String serverFiled() default "-1";
}
@Order(3)
@Slf4j
@Aspect
@Component
public class SelectDBAspect {

    @Value("${gjxy.db.url}")
    private String url;
    @Value("${gjxy.db.username}")
    private String username;
    @Value("${gjxy.db.password}")
    private String password;

    @Pointcut("@annotation(com.xxx.aspect.SelectDB)")
    public void pointcut(){}

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 获取接口上 SelectDB 注解
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        SelectDB annotation = method.getAnnotation(SelectDB.class);
        String dbName = annotation.dbName();
        String serverFiled = annotation.serverFiled();

        if("-1".equals(serverFiled)){
            // 切换到对应的库,不区分区服的库
            DynamicDataSourceService.switchDb(dbName);
        } else {
            // 切换到对应的库,根据区服切换到不同的库
            Object[] strArr = AnalyzeParamsUtils.analyzeParams(point, serverFiled);
            dbName = MessageFormat.format(dbName, strArr);
            boolean exist = DynamicDataSourceService.exist(dbName);
            if(exist){
                DynamicDataSourceService.switchDb(dbName);
            }else {
                // 确保开启新的区服后会新增game_x_data,game_x_log两个库,也能够正常查询
                String urlFormat = MessageFormat.format(url, dbName);
                DynamicDataSourceService.addDataSource(dbName, urlFormat, username, password);
                DynamicDataSourceService.switchDb(dbName);
            }
        }
        Object proceed = point.proceed();
        DynamicDataSourceService.resetDb();
        return proceed;
    }
}
  • AnalyzeParamsUtils.java 解析方法入参,获取实际值
public class AnalyzeParamsUtils {

    // 解析参数,赋予实际的数据值
    public static Object[] analyzeParams(JoinPoint point, String params) {
        if(params == null || params.length() == 0) return new Object[0];
        List<Object> list = new ArrayList<>();
        Arrays.stream(params.split("\\,")).filter(s -> s.length() > 0).forEach(s -> {
            try {
                list.add(getParamValue(point, s));
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return list.toArray();
    }

    // 从参数列表中根据参数链获取值
    private static Object getParamValue(JoinPoint point, String params) throws Exception {
        String[] split = params.split("\\.");
        int length = split.length;

        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        String[] argNames =  methodSignature.getParameterNames();
        Object[] argValues = point.getArgs();
        int argLength = argNames.length;

        for (int i = 0; i < argLength;i++){
            Object arg = argValues[i];
            final Class<?> clazz = arg.getClass();
            final String name = clazz.getSimpleName();
            if(name.equals(split[0])){ // 判断是否是指定类
                if(length == 1 ) {
                    ObjectMapper objectMapper = new ObjectMapper();
                    objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
                    return objectMapper.writeValueAsString(arg);
                }else{
                    return getParamValue(arg, split[1]);
                }
            }else if(argNames[i].equals(split[0])){
                return arg;
            }
        }
        return new Object();
    }

    // 获取指定对象的指定字段的值
    private static Object getParamValue(Object argValue, String param) throws Exception {
        String getMethodName = "get" + firstUpperCase(param);
        Method declaredMethod = argValue.getClass().getMethod(getMethodName);
        return declaredMethod.invoke(argValue);
    }

    // 首字母大写转换
    private static String firstUpperCase(String field) {
        if (!StringUtils.isEmpty(field)) {
            char[] cs = field.toCharArray();
            cs[0] -= 32;
            return String.valueOf(cs);
        } else {
            return field;
        }
    }
}
  • 至此,动态数据源功能实现完成。核心就是利用AbstractRoutingDataSource+ThreadLocal+AOP配合使用,确保可以修改每条线程的数据源。
  • ThreadLocal中存放的就是,本次查询需要使用到的数据库名称,对应的就是Map<Object, Object> dataSources = new HashMap<>()中的key,value就是指定的数据源。
  • 一般设计到这种水平分库分表的情况,建议使用类似 sharding-jdbc这种优秀的第三方库去实现。我这里没有使用的原因是因为,sharding-jdbc设计到一个分片策略的问题。游戏中某些操作将会打破这个规则,如合服时两个水平库将会进行合并。会导致数据规则错乱。无法正确路由到指定的库,导致查询不到数据,或所有库查询。
  • 本例中这种方案,考虑到游戏服无法进行修改。做出的妥协方案。缺点:每次查询只能查询一个区服的数据。若需要多个区服查询,则需要复杂的数据汇总逻辑。
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论
### 回答1: 基于 Spring BootMyBatis Plus和Vue的开发框架是现代Web应用开发的主要选择之一。Spring Boot是一个轻量级的Java开发框架,提供了快速构建独立应用程序所需的所有功能。它简化了传统的Spring应用程序开发过程,让开发者能够更专注于业务逻辑。 MyBatis Plus是一个对MyBatis进行封装的工具,提供了更简单、更方便的方式来操作数据库。它使用注解和代码生成器来减少开发时间,使得数据库操作更加高效和可维护。 Vue是一个流行的JavaScript框架,用于构建用户界面。它的特点是轻量级、易于学习和使用,并且具有非常高的性能。Vue可以与服务器端的后端框架(如Spring Boot)无缝集成,在前后端分离的项目中,提供了极佳的开发体验。 使用基于Spring BootMyBatis Plus和Vue的开发框架,开发者可以快速搭建一个完整的Web应用。首先,使用Spring Boot来创建后端应用,配置数据源和集成MyBatis Plus以简化数据库操作。然后,使用Vue来创建用户界面,通过REST API与后端进行通信。 这个开发框架的优点是集成了强大的Java后端和灵活的JavaScript前端,开发者可以使用多种技术栈来完成各种功能。同时,Spring Boot提供了很好的项目结构和配置管理,MyBatis Plus简化了数据库操作,Vue提供了丰富的界面组件和开发工具,整合后的开发框架提供了高效、可维护和可扩展的开发环境。 总之,基于Spring BootMyBatis Plus和Vue的开发框架提供了一种现代化、高效和灵活的Web应用开发方式。它适用于各种规模的项目,可以提高开发效率,降低维护成本,并且提供良好的开发体验。 ### 回答2: 基于Spring BootMyBatis Plus和Vue框架的开发可以实现一个完整的前后端分离的应用。以下是具体方面的解释: 1. Spring Boot是一个快速开发的Java框架,可以轻松搭建后端服务器。它提供了自动配置、快速开发等功能,大大简化了Java后端开发的步骤。 2. MyBatis Plus是一个基于MyBatis的增强工具,可以简化与数据库的交互。它提供了更简单的API、更强大的查询功能和代码生成等特性,可以极大提高后端开发的效率。 3. Vue是一个流行的前端框架,用于构建用户界面。它具有简单易学、灵活和高性能等特点,可以帮助我们编写交互式、响应式的前端应用。 基于这三个框架的组合,可以实现前后端完全分离的开发模式。后端使用Spring Boot搭建服务器,处理业务逻辑,并与数据库进行交互。MyBatis Plus可以简化与数据库的交互,提供了更简单的CRUD操作和强大的查询功能。 前端使用Vue进行开发,可以实现灵活的用户界面。通过Restful API与后端进行通信,实现数据的交互。 总而言之,基于Spring BootMyBatis Plus和Vue的开发模式,可以高效地实现前后端分离的应用。后端使用Spring Boot提供服务,MyBatis Plus简化数据库交互,前端使用Vue构建用户界面。这种组合可以提高开发效率,让开发人员更专注于业务逻辑的实现。 ### 回答3: 基于Spring BootMyBatis Plus和Vue的开发,可以实现一个全栈的Web应用程序。 Spring Boot是一个基于Spring框架的快速开发工具,它简化了Spring项目的配置,提供了一套约定大于配置的开发模式。使用Spring Boot可以快速搭建一个Web应用程序的后端,处理数据层和业务逻辑。 MyBatis Plus是基于MyBatis框架之上的增强工具,它进一步简化了在Java应用程序中操作数据库的流程。MyBatis Plus提供了很多常用的功能和特性,如代码生成器、自动填充、分页插件等,可以极大地提高开发效率。 Vue是一种现代化的JavaScript框架,用于构建用户界面。它提供了一套响应式组件系统和强大的工具集,可以帮助开发者快速构建单页应用程序。Vue可以与其他后端技术,如Spring BootMyBatis Plus一起使用,实现前后端分离的开发模式。 在基于这些技术的开发过程中,可以将后端的数据处理和业务逻辑实现放在Spring Boot中,使用MyBatis Plus进行数据库操作。前端使用Vue构建用户界面,并通过异步请求与后端进行数据交互。通过这种方式,可以实现一个功能完善的全栈Web应用程序。 使用Spring BootMyBatis Plus可以快速搭建后端框架,并提供强大的数据库操作能力。而Vue作为前端框架,可以提供友好的用户界面和交互体验。通过这些技术的结合,可以实现高效、灵活的全栈开发,为用户提供优质的应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

放码过来_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值