您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

SpringBoot整合多数据源,并支持动态新增与切换

时间:2023-07-18 17:40:39  来源:微信公众号  作者:程序猿小杨

一、概述

      在项目的开发过程中,遇到了需要从数据库中动态查询新的数据源信息并切换到该数据源做相应的查询操作,这样就产生了动态切换数据源的场景。为了能够灵活地指定具体的数据库,本文基于注解和AOP的方法实现多数据源自动切换。在使用过程中,只需要添加注解就可以使用,简单方便。(代码获取方式:见文章底部(开箱即用))

二、构建核心代码

2.1、AbstractRoutingDataSource构建

ackage com.wonders.dynamic;
import org.springframework.beans.factory.InitializingBean;import org.springframework.jdbc.datasource.AbstractDataSource;import org.springframework.jdbc.datasource.lookup.DataSourceLookup;import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;import org.springframework.lang.Nullable;import org.springframework.util.Assert;import org.springframework.util.CollectionUtils;
import JAVAx.sql.DataSource;import java.sql.Connection;import java.sql.SQLException;import java.util.Map;
/** * @Description: TODO:抽象类AbstractRoutingDataSource,实现动态数据源切换 * @Author: yyalin * @CreateDate: 2023/7/16 14:40 * @Version: V1.0 */public abstract class AbstractRoutingDataSource extends AbstractDataSource        implements InitializingBean {    //目标数据源map集合,存储将要切换的多数据源bean信息    @Nullable    private Map<Object, Object> targetDataSources;    //未指定数据源时的默认数据源对象    @Nullable    private Object defaultTargetDataSource;    private boolean lenientFallback = true;    //数据源查找接口,通过该接口的getDataSource(String dataSourceName)获取数据源信息    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();    //解析targetDataSources之后的DataSource的map集合    @Nullable    private Map<Object, DataSource> resolvedDataSources;    @Nullable    private DataSource resolvedDefaultDataSource;
    //将targetDataSources的内容转化一下放到resolvedDataSources中,将defaultTargetDataSource转为DataSource赋值给resolvedDefaultDataSource    public void afterPropertiesSet() {        //如果目标数据源为空,会抛出异常,在系统配置时应至少传入一个数据源        if (this.targetDataSources == null) {            throw new IllegalArgumentException("Property 'targetDataSources' is required");        } else {            //初始化resolvedDataSources的大小            this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());            //遍历目标数据源信息map集合,对其中的key,value进行解析            this.targetDataSources.forEach((key, value) -> {                //resolveSpecifiedLookupKey方法没有做任何处理,只是将key继续返回                Object lookupKey = this.resolveSpecifiedLookupKey(key);                //将目标数据源map集合中的value值(德鲁伊数据源信息)转为DataSource类型                DataSource dataSource = this.resolveSpecifiedDataSource(value);                //将解析之后的key,value放入resolvedDataSources集合中                this.resolvedDataSources.put(lookupKey, dataSource);            });            if (this.defaultTargetDataSource != null) {                //将默认目标数据源信息解析并赋值给resolvedDefaultDataSource                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);            }
        }    }
    protected Object resolveSpecifiedLookupKey(Object lookupKey) {        return lookupKey;    }
    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {        if (dataSource instanceof DataSource) {            return (DataSource)dataSource;        } else if (dataSource instanceof String) {            return this.dataSourceLookup.getDataSource((String)dataSource);        } else {            throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);        }    }
    //因为AbstractRoutingDataSource继承AbstractDataSource,而AbstractDataSource实现了DataSource接口,所有存在获取数据源连接的方法    public Connection getConnection() throws SQLException {        return this.determ.NETargetDataSource().getConnection();    }
    public Connection getConnection(String username, String password) throws SQLException {        return this.determineTargetDataSource().getConnection(username, password);    }
    protected DataSource determineTargetDataSource() {        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");        //调用实现类中重写的determineCurrentLookupKey方法拿到当前线程要使用的数据源的名称        Object lookupKey = this.determineCurrentLookupKey();        //去解析之后的数据源信息集合中查询该数据源是否存在,如果没有拿到则使用默认数据源resolvedDefaultDataSource        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {            dataSource = this.resolvedDefaultDataSource;        }
        if (dataSource == null) {            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");        } else {            return dataSource;        }    }
    @Nullable    protected abstract Object determineCurrentLookupKey();}

2.2、DynamicDataSource类

** * @Description: TODO:动态数据源 * @Author: yyalin * @CreateDate: 2023/7/16 14:46 * @Version: V1.0 *//** * * 调用AddDefineDataSource组件的addDefineDynamicDataSource()方法,获取原来targetdatasources的map, * 并将新的数据源信息添加到map中,并替换targetdatasources中的map * 切换数据源时可以使用@DataSource(value = "数据源名称"),或者DynamicDataSourceContextHolder.setContextKey("数据源名称") */@Data@AllArgsConstructor@NoArgsConstructorpublic class DynamicDataSource extends AbstractRoutingDataSource {    //备份所有数据源信息,    private Map<Object, Object> defineTargetDataSources;
    /**     * 决定当前线程使用哪个数据源     */    @Override    protected Object determineCurrentLookupKey() {        return DynamicDataSourceHolder.getDynamicDataSourceKey();    }
}

2.3、DynamicDataSourceHolder

** * @Description: TODO:数据源切换处理 * DynamicDataSourceHolder类主要是设置当前线程的数据源名称, * 移除数据源名称,以及获取当前数据源的名称,便于动态切换 * @Author: yyalin * @CreateDate: 2023/7/16 14:51 * @Version: V1.0 */@Slf4jpublic class DynamicDataSourceHolder {    /**     * 保存动态数据源名称     */    private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();
    /**     * 设置/切换数据源,决定当前线程使用哪个数据源     */    public static void setDynamicDataSourceKey(String key){        log.info("数据源切换为:{}",key);        DYNAMIC_DATASOURCE_KEY.set(key);    }
    /**     * 获取动态数据源名称,默认使用mater数据源     */    public static String getDynamicDataSourceKey(){        String key = DYNAMIC_DATASOURCE_KEY.get();        return key == null ? DbsConstant.MySQL_db_01 : key;    }
    /**     * 移除当前数据源     */    public static void removeDynamicDataSourceKey(){        log.info("移除数据源:{}",DYNAMIC_DATASOURCE_KEY.get());        DYNAMIC_DATASOURCE_KEY.remove();    }
}

2.4、数据源工具类

** * @Description: TODO:数据源工具类 * @Author: yyalin * @CreateDate: 2023/7/16 15:00 * @Version: V1.0 */@Slf4j@Componentpublic class DataSourceUtils {    @Resource    DynamicDataSource dynamicDataSource;
    /**     * @Description: 根据传递的数据源信息测试数据库连接     * @Author zhangyu     */    public DruidDataSource createDataSourceConnection(DataSourceInfo dataSourceInfo) {        DruidDataSource druidDataSource = new DruidDataSource();        druidDataSource.setUrl(dataSourceInfo.getUrl());        druidDataSource.setUsername(dataSourceInfo.getUserName());        druidDataSource.setPassword(dataSourceInfo.getPassword());        druidDataSource.setDriverClassName(dataSourceInfo.getDriverClassName());        druidDataSource.setBreakAfterAcquireFAIlure(true);        druidDataSource.setConnectionErrorRetryAttempts(0);        try {            druidDataSource.getConnection(2000);            log.info("数据源连接成功");            return druidDataSource;        } catch (SQLException throwables) {            log.error("数据源 {} 连接失败,用户名:{},密码 {}",dataSourceInfo.getUrl(),dataSourceInfo.getUserName(),dataSourceInfo.getPassword());            return null;        }    }
    /**     * @Description: 将新增的数据源加入到备份数据源map中     * @Author zhangyu     */    public void addDefineDynamicDataSource(DruidDataSource druidDataSource, String dataSourceName){        Map<Object, Object> defineTargetDataSources = dynamicDataSource.getDefineTargetDataSources();        defineTargetDataSources.put(dataSourceName, druidDataSource);        dynamicDataSource.setTargetDataSources(defineTargetDataSources);        dynamicDataSource.afterPropertiesSet();    }

2.5、DynamicDataSourceConfig

** * @Description: TODO:数据源信息配置类,读取数据源配置信息并注册成bean。 * @Author: yyalin * @CreateDate: 2023/7/16 14:54 * @Version: V1.0 */@Configuration@MApperScan("com.wonders.mapper")@Slf4jpublic class DynamicDataSourceConfig {    @Bean(name = DbsConstant.mysql_db_01)    @ConfigurationProperties("spring.datasource.mysqldb01")    public DataSource masterDataSource() {        log.info("数据源切换为:{}",DbsConstant.mysql_db_01);        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();        return dataSource;    }
    @Bean(name = DbsConstant.mysql_db_02)    @ConfigurationProperties("spring.datasource.mysqldb02")    public DataSource slaveDataSource() {        log.info("数据源切换为:{}",DbsConstant.mysql_db_02);        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();        return dataSource;    }

    @Bean(name = DbsConstant.oracle_db_01)    @ConfigurationProperties("spring.datasource.oracledb01")    public DataSource oracleDataSource() {        log.info("数据源切换为oracle:{}",DbsConstant.oracle_db_01);        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();        return dataSource;    }    @Bean    @Primary    public DynamicDataSource dynamicDataSource(){        Map<Object, Object> dataSourceMap = new HashMap<>(3);        dataSourceMap.put(DbsConstant.mysql_db_01,masterDataSource());        dataSourceMap.put(DbsConstant.mysql_db_02,slaveDataSource());        dataSourceMap.put(DbsConstant.oracle_db_01,oracleDataSource());        //设置动态数据源        DynamicDataSource dynamicDataSource = new DynamicDataSource();        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());        dynamicDataSource.setTargetDataSources(dataSourceMap);        //将数据源信息备份在defineTargetDataSources中        dynamicDataSource.setDefineTargetDataSources(dataSourceMap);        return dynamicDataSource;    }
}

 

三、测试代码

/** * @Description: TODO * @Author: yyalin * @CreateDate: 2023/7/16 15:02 * @Version: V1.0 */@Slf4j@Api(tags="动态切换多数据源测试")@RestControllerpublic class TestController {    @Resource    DataSourceUtils dataSourceUtils;    @Autowired    private StudentMapper studentMapper;    @ApiOperation(value="动态切换多数据源测试", notes="test")    @GetMapping("/test")    public Map<String, Object> dynamicDataSourceTest(String id){        Map<String, Object> map = new HashMap<>();        //1、默认库中查询数据        Student student=studentMapper.selectById(id);        map.put("1、默认库中查询到的数据",student);        //2、指定库中查询的数据        DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);        Student student02=studentMapper.selectById(id);        map.put("2、指定库中查询的数据",student02);        //3、从数据库获取连接信息,然后获取数据        //模拟从数据库中获取的连接        DataSourceInfo dataSourceInfo = new DataSourceInfo(                "jdbc:mysql://127.0.0.1:3308/test02?useUnicode=true&characterEncoding=utf-8&useSSL=false",                 "root",                "root",                "mysqldb03",                "com.mysql.cj.jdbc.Driver");        map.put("dataSource",dataSourceInfo);        log.info("数据源信息:{}",dataSourceInfo);        //测试数据源连接        DruidDataSource druidDataSource = dataSourceUtils.createDataSourceConnection(dataSourceInfo);        if (Objects.nonNull(druidDataSource)){            //将新的数据源连接添加到目标数据源map中            dataSourceUtils.addDefineDynamicDataSource(druidDataSource,dataSourceInfo.getDatasourceKey());            //设置当前线程数据源名称-----代码形式            DynamicDataSourceHolder.setDynamicDataSourceKey(dataSourceInfo.getDatasourceKey());            //在新的数据源中查询用户信息            Student student03=studentMapper.selectById(id);            map.put("3、动态数据源查询的数据",student03);            //关闭数据源连接            druidDataSource.close();        }        //4、指定oracle库中查询的数据        DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.oracle_db_01);        Student student04=studentMapper.selectById(id);        map.put("4、指定oracle库中查询的数据",student04);        return map;    }}

测试结果如下:

       从结果中可以明显的看出,通过切换不同的数据源,可以从不同的库中获取不同的数据,包括:常见库Mysql、oracle、sqlserver等数据库相互切换。也可以从数据库的某张表中获取连接信息,实现动态切换数据库。

四、使用注解方式切换数据源

      从上述TestController 中代码不难看出,若要想切换数据源需要在mapper调用之前调用:

  • DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);

不够简洁优雅,所以下面推荐使用注解的方式来动态进行数据源的切换。

4.1、创建注解类DataSource

/** * @Description: TODO:自定义多数据源切换注解 * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 * @Author: yyalin * @CreateDate: 2023/7/17 14:00 * @Version: V1.0 */@Target({ ElementType.METHOD, ElementType.TYPE })@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface DataSource {    //切换数据源名称,默认mysql_db_01    public String value() default DbsConstant.mysql_db_01;}

4.2、创建切面DataSourceAspect类

** * @Description: TODO:创建切面DataSourceAspect类 * @Author: yyalin * @CreateDate: 2023/7/17 14:03 * @Version: V1.0 */@Aspect@Componentpublic class DataSourceAspect {    // 设置DataSource注解的切点表达式    @Pointcut("@annotation(com.wonders.dynamic.DataSource)")    public void dynamicDataSourcePointCut(){}
    //环绕通知    @Around("dynamicDataSourcePointCut()")    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{        String key = getDefineAnnotation(joinPoint).value();        DynamicDataSourceHolder.setDynamicDataSourceKey(key);        try {            return joinPoint.proceed();        } finally {            DynamicDataSourceHolder.removeDynamicDataSourceKey();        }    }    /**     * 功能描述:先判断方法的注解,后判断类的注解,以方法的注解为准     * @MethodName: getDefineAnnotation     * @MethodParam: [joinPoint]     * @Return: com.wonders.dynamic.DataSource     * @Author: yyalin     * @CreateDate: 2023/7/17 14:09     */    private DataSource getDefineAnnotation(ProceedingJoinPoint joinPoint){        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();        DataSource dataSourceAnnotation = methodSignature.getMethod().getAnnotation(DataSource.class);        if (Objects.nonNull(methodSignature)) {            return dataSourceAnnotation;        } else {            Class<?> dsClass = joinPoint.getTarget().getClass();            return dsClass.getAnnotation(DataSource.class);        }    }}

4.3、进行数据源切换

/@Mapper 与 启动类的@MapperScan({"com.example.demo.mapper"}) 二选一即可@Repositorypublic interface StudentMapper extends BaseMapper<Student> {    /**     * 功能描述:在mysql_db_01中查询数据     * @MethodName: findStudentById     * @MethodParam: [id]     * @Return: com.wonders.entity.Student     * @Author: yyalin     * @CreateDate: 2023/7/17 14:20     */    @DataSource(value = DbsConstant.oracle_db_01)    Student findStudentById(String id);}

或在service层

Servicepublic class StudentServiceImpl implements StudentService{    @Autowired    private StudentMapper studentMapper;    //注解加在实现层才能生效    @DataSource(value = DbsConstant.mysql_db_01)    @Override    public Student findStudentById(String id) {        return studentMapper.selectById(id);    }}

4.3、测试效果

ApiOperation(value="使用注解方式动态切换多数据源", notes="test02")    @GetMapping("/test02")    public Student test02(String id){        Student student=studentMapper.findStudentById(id);        return student;    }

--结果如下:



Tags:SpringBoot   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  Search: SpringBoot  点击:(10)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19  Search: SpringBoot  点击:(86)  评论:(0)  加入收藏
SpringBoot3+Vue3 开发高并发秒杀抢购系统
开发高并发秒杀抢购系统:使用SpringBoot3+Vue3的实践之旅随着互联网技术的发展,电商行业对秒杀抢购系统的需求越来越高。为了满足这种高并发、高流量的场景,我们决定使用Spring...【详细内容】
2024-01-14  Search: SpringBoot  点击:(90)  评论:(0)  加入收藏
公司用了六年的 SpringBoot 项目部署方案,稳得一批!
本篇和大家分享的是springboot打包并结合shell脚本命令部署,重点在分享一个shell程序启动工具,希望能便利工作。 profiles指定不同环境的配置 maven-assembly-plugin打发布压...【详细内容】
2024-01-10  Search: SpringBoot  点击:(163)  评论:(0)  加入收藏
简易版的SpringBoot是如何实现的!!!
SpringBoot作为目前最流行的框架之一,同时是每个程序员必须掌握的知识,其提供了丰富的功能模块和开箱即用的特性,极大地提高了开发效率和降低了学习成本,使得开发人员能够更专注...【详细内容】
2023-12-29  Search: SpringBoot  点击:(132)  评论:(0)  加入收藏
用 SpringBoot+Redis 解决海量重复提交问题
前言 一:搭建redis的服务Api 二:自定义注解AutoIdempotent 三:token创建和检验 四:拦截器的配置 五:测试用例 六:总结前言:在实际的开发项目中,一个对外暴露的接口往往会面临很多...【详细内容】
2023-12-20  Search: SpringBoot  点击:(53)  评论:(0)  加入收藏
SpringBoot中如何优雅地个性化定制Jackson
当使用 JSON 格式时,Spring Boot 将使用ObjectMapper实例来序列化响应和反序列化请求。在本教程中,我们将了解配置序列化和反序列化选项的最常用方法。一、默认配置默认情况下...【详细内容】
2023-12-20  Search: SpringBoot  点击:(132)  评论:(0)  加入收藏
springboot-如何集成Validation进行参数校验
一、步骤概览 二、步骤说明1.引入依赖包在 pom.xml 文件中引入 validation 组件,它提供了在 Spring Boot 应用程序中进行参数校验的支持。<!-- WEB 程序依赖包 --><dependen...【详细内容】
2023-12-13  Search: SpringBoot  点击:(156)  评论:(0)  加入收藏
优雅的springboot参数校验,你学会了吗?
前言在后端的接口开发过程,实际上每一个接口都或多或少有不同规则的参数校验,有一些是基础校验,如非空校验、长度校验、大小校验、格式校验;也有一些校验是业务校验,如学号不能重...【详细内容】
2023-11-29  Search: SpringBoot  点击:(198)  评论:(0)  加入收藏
Springboot扩展点之BeanDefinitionRegistryPostProcessor,你学会了吗?
前言通过这篇文章来大家分享一下,另外一个Springboot的扩展点BeanDefinitionRegistryPostProcessor,一般称这类扩展点为容器级后置处理器,另外一类是Bean级的后置处理器;容器级...【详细内容】
2023-11-27  Search: SpringBoot  点击:(174)  评论:(0)  加入收藏
▌简易百科推荐
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  步步运维步步坑    Tags:架构   点击:(5)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27    InfoQ  Tags:架构模式   点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  哈啰技术  微信公众号  Tags:架构   点击:(10)  评论:(0)  加入收藏
DDD 与 CQRS 才是黄金组合
在日常工作中,你是否也遇到过下面几种情况: 使用一个已有接口进行业务开发,上线后出现严重的性能问题,被老板当众质疑:“你为什么不使用缓存接口,这个接口全部走数据库,这怎么能扛...【详细内容】
2024-03-27  dbaplus社群    Tags:DDD   点击:(11)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13    阿里云开发者  Tags:高并发   点击:(6)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  二进制跳动  微信公众号  Tags:架构设计   点击:(36)  评论:(0)  加入收藏
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  ijunfu  今日头条  Tags:SpringBoot   点击:(10)  评论:(0)  加入收藏
PHP+Go 开发仿简书,实战高并发高可用微服务架构
来百度APP畅享高清图片//下栽のke:chaoxingit.com/2105/PHP和Go语言结合,可以开发出高效且稳定的仿简书应用。在实现高并发和高可用微服务架构时,我们可以采用一些关键技术。首...【详细内容】
2024-01-14  547蓝色星球    Tags:架构   点击:(115)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11    王建立  Tags:Spring Boot   点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  互联网架构小马哥    Tags:Spring Boot   点击:(115)  评论:(0)  加入收藏
站内最新
站内热门
站内头条