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

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

一、概述

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

二、构建核心代码

2.1、AbstractRoutingDataSource构建


package 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.determineTargetDataSource().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
@NoArgsConstructor
public 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
 */
@Slf4j
public 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
@Component
public 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")
@Slf4j
public 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="动态切换多数据源测试")
@RestController
public 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
@Inherited
public @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
@Component
public 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"}) 二选一即可
@Repository
public 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层

@Service
public 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.4、测试效果


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

–结果如下:

图片

五、功能点

1、使用注解的方式来动态进行数据源的切换;

2、支持动态新增新的数据源;

3、支持oracle\mysql等常见数据库切换。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/286036.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

图解Kafka Producer常用性能优化配置参数

1 基本参数 bootstrap.servers&#xff1a;Kafka broker服务器地址列表&#xff0c;,分开&#xff0c;可不必写全&#xff0c;Kafka内部有自动感知Kafka broker的机制 client.dns.lookup&#xff1a;客户端寻找bootstrap地址的方式&#xff0c;支持两种方式&#xff1a; resol…

现在学鸿蒙开发有前途吗?能找到工作吗?

鸿蒙开发前景肯定是有的&#xff0c;我们可以从市场的情况来分析。 1、鸿蒙开发不兼容安卓 23年9月举办的华为新品发布会中&#xff0c;华为方面宣布开始启用原生鸿蒙应用&#xff0c;并不再提供安卓代码的兼容性。涵盖了资讯、社交、工具、金融、生活、美食、游戏等多品类的…

多技术融合在生态系统服务功能社会价值评估中的应用及论文写作、拓展分析

生态系统服务是人类从自然界中获得的直接或间接惠益&#xff0c;可分为供给服务、文化服务、调节服务和支持服务4类&#xff0c;对提升人类福祉具有重大意义&#xff0c;且被视为连接社会与生态系统的桥梁。自从启动千年生态系统评估项目&#xff08;Millennium Ecosystem Asse…

【Bootstrap学习 day8】

加载器 使用Bootstrap读取图标以表示元件加载状态&#xff0c;这些读取图标完全使用HTML,CSS。要创建spinner/加载器&#xff0c;可以使用.spinner-border类 <div class"spinner-border"></div>可以使用文本颜色类设置不同的颜色&#xff1a; <div …

关于Github部分下载的方法

一、问题 在Github中&#xff0c;我需要下载部分文件&#xff0c;而github只有下载最原始文件夹和单独文件的功能。 比如我想下载头四个文件&#xff0c;难以操作。 二、方法 推荐使用谷歌浏览器&#xff0c;进入扩展程序界面&#xff1a; 在应用商店获取GitZip for github…

Python数据科学应用从入门到精通--Python读取、合并SPSS数据文件

在很多情况下&#xff0c;我们需要调用SPSS软件产生的数据&#xff0c;下面通过示例来进行讲解。首先需要将本书提供的数据文件存储在安装spyder-py3的默认路径位置&#xff08;C:/Users/Administrator/.spyder-py3/&#xff0c;注意具体的安装路径可能与此不同&#xff09;&am…

PDF文档转换工具箱流量主小程序开发

PDF转换小助手&#xff0c;不仅是文件格式转换的利器&#xff0c;更是一位得力的助手。它精通PDF与各类文档间的自由转换&#xff0c;如Word、Excel、PowerPoint等。 转换选项丰富多样&#xff0c;满足您对文件保护、页面设置、图像品质等细致要求。处理大量文件&#xff1f;…

canvas绘制圆点示例

查看专栏目录 canvas示例教程100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

格式转换工具,一键转换文件格式

有时候&#xff0c;为了满足工作或学习的需要&#xff0c;我们需要将文件从一种格式转换为另一种格式。传统的单文件转换方式不仅费时&#xff0c;而且容易出错。有没有便捷的方法可以解决这个问题&#xff1f;答案是肯定的&#xff0c;那就是使用【文件批量改名高手】来批量操…

Python实现简单的JS逆向解密, 实现翻译软件+语音播报

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 环境使用: python 3.8 pycharm 第三方模块使用: requests --> pip install requests execjs --> pip install PyExecJS ttkbootstrap --> pip inst…

时隔五天,重温Redis基础总结

目录 字符串操作命令 Redis 字符串类型常用命令SET key value 设置指定key的值 ​编辑GET key 获取指定key的值 ​编辑SETEX key seconds value 设置指定key的值&#xff0c;并将 key 的过期时间设为 seconds 秒 SETNX key value 只有在key不存在时设置key的值 哈希操作命…

epoll原理及服务器代码实现

epoll 是 Linux 下用于实现高性能事件通知机制的系统调用。它相对于传统的 select 和 poll 具有更好的性能和可伸缩性&#xff0c;特别适用于需要处理大量并发连接的场景&#xff0c;比如网络编程中的服务器。 #include <sys/epoll.h> // 创建一个新的epoll实例。在内核中…

安全与认证Week3

Key Management 密钥管理 密钥交换、证书 密钥的类别 密钥管理方面 密钥分发问题 密钥分发方案 简单的密钥分发&#xff1a;允许安全通信&#xff0c;但不存在先前或之后的密钥。 带机密性和身份验证的密钥分发&#xff1a;提供更高级别的安全性。 混合密钥分发 公钥分发 公开…

Node.js使用jemalloc内存分配器显著减少内存使用

前言 Node.js 默认使用的是 ptmalloc(glibc) 内存分配器&#xff0c;而&#xff1a; 在服务端领域「不会选择默认的 malloc」是一个常识。&#xff08; 来源 &#xff09; ptmalloc 的分配效率较低&#xff08; 来源 &#xff09;&#xff0c;对于 长时间、多核 / 多线程 运行…

惠普打印机---共享打印机安装 --连接

1. 远程连接 输入 winR ,再输入共享打印机的连接的IP 2.进入 连接 界面 3.右击打印机 &#xff0c;点击连接 &#xff0c;就可以添加打印机设备 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/ba03aea8156642d58982fd2ce0934b45.png 方法二、 添加打印机 2.…

金和OA jc6 ntko-upload 任意文件上传漏洞复现

0x01 产品简介 金和OA协同办公管理系统软件(简称金和OA),本着简单、适用、高效的原则,贴合企事业单位的实际需求,实行通用化、标准化、智能化、人性化的产品设计,充分体现企事业单位规范管理、提高办公效率的核心思想,为用户提供一整套标准的办公自动化解决方案,以帮助…

论最近热门的AI绘画技术—从小白绘画到文创手账设计【文末送书-13】

文章目录 &#x1f3c0;前言⚽AI绘图技术栈⚾️简单的代码实现案例&#x1f3c8;iPad萌系简笔画&#xff1a;从小白绘画到文创手账设计【文末送书-13】⛳粉丝福利&#xff1a;文末推荐与福利免费包邮送书&#xff01; &#x1f3c0;前言 AI绘画技术&#xff0c;也称为人工智能…

网络安全好就业吗?会不会容易被淘汰

研究生网安&#xff0c;本科信安。 研究生几个同专业的人里面&#xff0c;考公的考公&#xff0c;考编制的考编制&#xff0c;进国企的进国企。只有一个进互联网公司做安全。 本科的90%都考研了&#xff0c;加上保研的&#xff0c;基本都上岸了&#xff0c;本科信息安全的学生…

ClickHouse基础知识(三):ClickHouse 数据类型全解

1 整型 固定长度的整型&#xff0c;包括有符号整型或无符号整型。 整型范围&#xff08;-2n-1~2n-1-1&#xff09;&#xff1a; 无符号整型范围&#xff08;0~2n-1&#xff09;&#xff1a; 使用场景&#xff1a; 个数、数量、也可以存储型 id。 2 浮点型 Float32 - float …

Windows电脑引导损坏?按照这个教程能修复

前言 Windows系统的引导一般情况下是不会坏的&#xff0c;小伙伴们可以不用担心。发布这个帖子是因为要给接下来的文章做点铺垫。 关注小白很久的小伙伴应该都知道&#xff0c;小白的文章都讲得比较细。而且文章与文章之间的关联度其实还是蛮高的。在文章中&#xff0c;你会遇…