SpringBoot——动态数据源(多数据源自动切换)

news2024/11/20 18:32:35

前言

日常的业务开发项目中只会配置一套数据源,如果需要获取其他系统的数据往往是通过调用接口, 或者是通过第三方工具比如kettle将数据同步到自己的数据库中进行访问。

但是也会有需要在项目中引用多数据源的场景。比如如下场景:

  • 自研数据迁移系统,至少需要新、老两套数据源,从老库读取数据写入新库
  • 自研读写分离中间件,系统流量增加,单库响应效率降低,引入读写分离方案,写入数据是一个数据源,读取数据是另一个数据源

某系统除了需要从自己的主要数据库上读取和管理数据外,还有一部分业务涉及到其他多个数据库,要求可以在任何方法上可以灵活指定具体要操作的数据库。

为了在开发中以最简单的方法使用,本文基于注解和AOP的方法实现,在spring boot框架的项目中,添加本文实现的代码类后,只需要配置好数据源就可以直接通过注解使用,简单方便。

一、原理

关键类说明

忽略掉controller/service/entity/mapper/xml介绍。

  • jdbc.properties: 数据源配置文件。虽然可以配置到Spring boot的默认配置文件application.properties/application.yml文件当中,但是如果数据源比较多的话,根据实际使用,最佳的配置方式还是独立配置比较好。
  • DynamicDataSourceConfig:数据源配置类
  • DynamicDataSource:动态数据源配置类
  • DataSourceRouting:动态数据源注解
  • DynamicDataSourceAspect:动态数据源设置切面
  • DynamicDataSourceContextHolder:当前线程持有的数据源key
  • DataSourceConstants:数据源key常量类

开发流程

 

动态数据源流程

Spring Boot 的动态数据源,本质上是把多个数据源存储在一个 Map 中,当需要使用某个数据源时,从 Map 中获取此数据源进行处理。

 

在 Spring 中已提供了抽象类 AbstractRoutingDataSource 来实现此功能,继承AbstractRoutingDataSource类并覆写其determineCurrentLookupKey()方法即可,该方法只需要返回数据源key即可,也就是存放数据源的Map的key。

因此,我们在实现动态数据源的,只需要继承它,实现自己的获取数据源逻辑即可。AbstractRoutingDataSource顶级继承了DataSource,所以它也是可以做为数据源对象,因此项目中使用它作为主数据源。

 

AbstractRoutingDataSource原理

AbstractRoutingDataSource中有一个重要的属性:

  • argetDataSources:目标数据源,即项目启动的时候设置的需要通过AbstractRoutingDataSource管理的数据源。
  • defaultTargetDataSource:默认数据源,项目启动的时候设置的默认数据源,如果没有指定数据源,默认返回改数据源。
  • resolvedDataSources:也是存放的数据源,是对targetDataSources进行处理后进行存储的。可以看一下源码。

 

 

  • resolvedDefaultDataSource: 对默认数据源进行了二次处理,源码如上图最后的两行代码。

AbstractRoutingDataSource中所有的方法和属性:

 

比较重要的是determineTargetDataSource方法。

protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = determineCurrentLookupKey();
    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 + "]");
    }
    return dataSource;
}
 
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Nullable
protected abstract Object determineCurrentLookupKey();

这个方法主要就是返回一个DataSource对象,主要逻辑就是先通过方法determineCurrentLookupKey获取一个Object对象的lookupKey,然后通过这个lookupKey到resolvedDataSources中获取数据源(resolvedDataSources就是一个Map,上面已经提到过了);如果没有找到数据源,就返回默认的数据源。determineCurrentLookupKey就是程序员配置动态数据源需要自己实现的方法。

二、实现

引入Maven依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.10.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--如果要用传统的xml或properties配置,则需要添加此依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>

    <dependency>
        <groupId>tk.mybatis</groupId>
        <artifactId>mapper-spring-boot-starter</artifactId>
        <version>2.1.5</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <!-- swagger -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>

    <!-- spring security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- jwt -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    <!-- fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.70</version>
    </dependency>


    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>

        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.6</version>
            <configuration>
                <configurationFile>
                    ${basedir}/src/main/resources/generator/generatorConfig.xml
                </configurationFile>
                <overwrite>true</overwrite>
                <verbose>true</verbose>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>5.1.41</version>
                </dependency>
                <dependency>
                    <groupId>tk.mybatis</groupId>
                    <artifactId>mapper</artifactId>
                    <version>4.1.5</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

主要实现步骤:一配置二使用

  • 启动类注册动态数据源
  • 配置文件中配置多个数据源
  • 在需要的方法上使用注解指定数据源
  • 1、在启动类添加 @Import({DynamicDataSourceRegister.class, MProxyTransactionManagementConfiguration.class})

// 注册动态多数据源
@Import({DynamicDataSourceRegister.class})
@MapperScan("com.yibo.mapper")//扫描Mapper接口
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}
  • 2、配置文件配置内容为:
# 默认数据源
spring.datasource.url=jdbc:mysql://localhost:3306/user_center?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.username=root
spring.datasource.hikari.password=yibo

# 更多数据源
custom.datasource.names=ds1,ds2
custom.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
custom.datasource.ds1.url=jdbc:mysql://localhost:3306/content_center?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
custom.datasource.ds1.username=root
custom.datasource.ds1.password=yibo
custom.datasource.ds2.driver-class-name=com.mysql.cj.jdbc.Driver
custom.datasource.ds2.url=jdbc:mysql://localhost:3306/trade?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
custom.datasource.ds2.username=root
custom.datasource.ds2.password=yibo

mybatis.type-aliases-package: com.yibo.center.domain.entity
mybatis.mapper-locations: classpath:mapper/*.xml
mapper.identity: MYSQL
mapper.not-empty: false

#是否激活 swagger true or false
swagger.enable=true
  • 3、使用方法
import com.yibo.center.domain.entity.Share;
import com.yibo.datasource.anno.TargetDataSource;
import com.yibo.mapper.ShareMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;



@Service
public class ShareService {

    @Autowired
    private ShareMapper shareMapper;

    @TargetDataSource(name = "ds1")
    @Transactional
    public List<Share> findAll(){
        return shareMapper.selectAll();
    }
}

import com.yibo.center.domain.entity.TradeGoods;
import com.yibo.center.domain.vo.TradeGoodsAO;
import com.yibo.datasource.anno.TargetDataSource;
import com.yibo.mapper.TradeGoodsMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;

/**
 * @Description:
 */

@Service
public class TradeGoodsService {

    @Autowired
    private TradeGoodsMapper tradeGoodsMapper;

    @TargetDataSource(name = "ds2")
    @Transactional
    public List<TradeGoods> findAll(){
        return tradeGoodsMapper.selectAll();
    }

    @TargetDataSource(name = "ds2")
    @Transactional
    public String addTradeGoods(TradeGoodsAO tradeGoodsAO){
        TradeGoods tradeGoods = new TradeGoods();
        BeanUtils.copyProperties(tradeGoodsAO,tradeGoods);
        tradeGoods.setAddTime(new Date());
        tradeGoodsMapper.insert(tradeGoods);
        return "SUCCESS";
    }
}

import com.yibo.center.domain.entity.User;
import com.yibo.center.domain.vo.UserAo;
import com.yibo.mapper.UserMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;

/**
 * @Description:
 */

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public List<User> findAll(){
        return userMapper.selectAll();
    }

    @Transactional
    public User findById(Integer id){
        User user = new User();
        user.setId(id);
        return userMapper.selectOne(user);
    }

    @Transactional
    public String addUser(UserAo userAo){
        User user = new User();
        BeanUtils.copyProperties(userAo,user);
        user.setCreateTime(new Date());
        user.setUpdateTime(new Date());
        userMapper.insert(user);
        return "SUCCESS";
    }
}

要注意的是,在使用MyBatis时,注解@TargetDataSource 不能直接在接口类Mapper上使用。

请将下面几个类放到Spring Boot项目中。

  • DynamicDataSource.java
  • DynamicDataSourceAspect.java
  • DynamicDataSourceContextHolder.java
  • DynamicDataSourceRegister.java
  • TargetDataSource.java

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

/**
 * @Description: 继承Spring AbstractRoutingDataSource实现路由切换
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

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

import com.yibo.datasource.DynamicDataSourceContextHolder;
import com.yibo.datasource.anno.TargetDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * @Description: 动态数据源通知
 */
@Aspect
//保证该AOP在@Transactional之前执行
@Order(-1)
@Component
@Slf4j
public class DynamicDataSourceAspect {

    /**
     * @Description 在方法执行之前执行  @annotation(ds) 会拦截有ds这个注解的方法即有 TargetDataSource这个注解的
     * @param @param point
     * @param @param ds
     * @param @throws Throwable 参数
     * @return void 返回类型
     * @throws
     */
    @Before("@annotation(ds)")
    public void changeDataSource(JoinPoint point, TargetDataSource ds)
            throws Throwable {
        String dsId = ds.name();
        if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
            log.error("数据源[{}]不存在,使用默认数据源 > {}", ds.name(), point.getSignature());
        }
        else {
            log.debug("Use DataSource : {} > {}", ds.name(),point.getSignature());
            DynamicDataSourceContextHolder.setDataSourceType(ds.name());
        }
    }

    /**
     * @Description 在方法执行之后执行  @annotation(ds) 会拦截有ds这个注解的方法即有 TargetDataSource这个注解的
     * @param @param point
     * @param @param ds 参数
     * @return void 返回类型
     * @throws
     */
    @After("@annotation(ds)")
    public void restoreDataSource(JoinPoint point, TargetDataSource ds) {
        log.debug("Revert DataSource : {} > {}", ds.name(), point.getSignature());
        DynamicDataSourceContextHolder.clearDataSourceType();
    }
}

import java.util.ArrayList;
import java.util.List;

/**
 * @Description: 动态数据源上下文管理
 */

public class DynamicDataSourceContextHolder {

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

    //存放数据源id
    public static List<String> dataSourceIds = new ArrayList<String>();

    //设置数据源
    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    //获取数据源
    public static String getDataSourceType() {
        return contextHolder.get();
    }

    //清除数据源
    public static void clearDataSourceType() {
        contextHolder.remove();
    }

    /**
     * 判断指定DataSrouce当前是否存在
     *
     * @param dataSourceId
     * @return
     */
    public static boolean containsDataSource(String dataSourceId){
        return dataSourceIds.contains(dataSourceId);
    }
}

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description: 注册动态数据源
 *  初始化数据源和提供了执行动态切换数据源的工具类
 *  EnvironmentAware(获取配置文件配置的属性值)
 */

@Slf4j
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    //指定默认数据源(springboot2.0默认数据源是hikari如何想使用其他数据源可以自己配置)
    private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";

    //默认数据源
    private DataSource defaultDataSource;

    //用户自定义数据源
    private Map<String, DataSource> customDataSources  = new HashMap<>();

    /**
     * 加载多数据源配置
     * @param env
     */
    @Override
    public void setEnvironment(Environment env) {
        initDefaultDataSource(env);
        initCustomDataSources(env);
    }



    /**
     * 初始化主数据源
     * @param env
     */
    private void initDefaultDataSource(Environment env) {
        // 读取主数据源
        Map<String, Object> dsMap = new HashMap<>();
        dsMap.put("driver", env.getProperty("spring.datasource.hikari.driver-class-name"));
        dsMap.put("url", env.getProperty("spring.datasource.url"));
        dsMap.put("username", env.getProperty("spring.datasource.hikari.username"));
        dsMap.put("password", env.getProperty("spring.datasource.hikari.password"));
        defaultDataSource = buildDataSource(dsMap);
    }


    /**
     * 初始化更多数据源
     * @param env
     */
    private void initCustomDataSources(Environment env) {
        // 读取配置文件获取更多数据源
        String dsPrefixs = env.getProperty("custom.datasource.names");
        for (String dsPrefix : dsPrefixs.split(",")) {
            // 多个数据源
            Map<String, Object> dsMap = new HashMap<>();
            dsMap.put("driver", env.getProperty("custom.datasource." + dsPrefix + ".driver-class-name"));
            dsMap.put("url", env.getProperty("custom.datasource." + dsPrefix + ".url"));
            dsMap.put("username", env.getProperty("custom.datasource." + dsPrefix + ".username"));
            dsMap.put("password", env.getProperty("custom.datasource." + dsPrefix + ".password"));
            DataSource ds = buildDataSource(dsMap);
            customDataSources.put(dsPrefix, ds);
        }
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        // 将主数据源添加到更多数据源中
        targetDataSources.put("dataSource", defaultDataSource);
        DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
        // 添加更多数据源
        targetDataSources.putAll(customDataSources);
        for (String key : customDataSources.keySet()) {
            DynamicDataSourceContextHolder.dataSourceIds.add(key);
        }

        // 创建DynamicDataSource
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(DynamicDataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv = beanDefinition.getPropertyValues();
        mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
        mpv.addPropertyValue("targetDataSources", targetDataSources);
        registry.registerBeanDefinition("dataSource", beanDefinition); // 注册到Spring容器中

        log.info("Dynamic DataSource Registry");
    }

    /**
     * 创建DataSource
     * @param dsMap
     * @return
     */
    @SuppressWarnings("unchecked")
    public DataSource buildDataSource(Map<String, Object> dsMap) {
        try {
            Object type = dsMap.get("type");
            if (type == null)
                type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource

            Class<? extends DataSource> dataSourceType;
            dataSourceType = (Class<? extends DataSource>)Class.forName((String)type);
            log.info("dsMap:{}",dsMap);
            System.out.println(dsMap);
            String driverClassName = dsMap.get("driver").toString();
            String url = dsMap.get("url").toString();
            String username = dsMap.get("username").toString();
            String password = dsMap.get("password").toString();
            // 自定义DataSource配置
            DataSourceBuilder factory = DataSourceBuilder.create()
                    .driverClassName(driverClassName)
                    .url(url)
                    .username(username)
                    .password(password)
                    .type(dataSourceType);
            return factory.build();
        }catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

import java.lang.annotation.*;

/**
 * @Description: 作用于类、接口或者方法上
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String name();
}

本文代码博主是经过测试后没有问题才发出来共享给大家的。对于连接池参数配置会应用到所有数据源上。

比如配置一个:

spring.datasource.maximum-pool-size=80

那么我们所有的数据源都会自动应用上。

补充:

如果你使用的是SpringMVC,并集成了Shiro,一般按网上的配置你可能是:

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
    <property name="proxyTargetClass" value="true" />
</bean>

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>

那么你请不要这样做,请按下面方法配置:

<!-- AOP式方法级权限检查  -->
<!-- 不要使用 DefaultAdvisorAutoProxyCreator 会出现二次代理的问题,这里不详述。 mark by shanhy 2016-05-15 -->
<aop:config proxy-target-class="true"/>
<!-- 或者你使用了 <aop:aspectj-autoproxy proxy-target-class="true" /> 也可以。 -->

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

更有效的协同程序【插件:More Effective Coroutines】

插件地址&#xff1a;传送门 1、命名空间 using System.Collections.Generic; using MEC; 2、与传统的协程相比 传统&#xff1a;StartCoroutine(_CheckForWin()); 被RunCoroutine取代。必须选择执行循环进程&#xff0c;默认为“Segment.Update”。 using System.Coll…

华为OD机试真题 JavaScript 实现【最优资源分配】【2023Q1 200分】

一、题目描述 某块业务芯片最小容量单位为 1.25G&#xff0c;总容量为 M*1.25G&#xff0c;对该芯片资源编号为 1&#xff0c;2&#xff0c;… M。 该芯片支持 3 种不同的配置&#xff0c;分别为 A、B、C. 配置 A: 占用容量为 1.25 * 1 1.25G配置 B: 占用容量为 1.25* 2 2…

一文了解智能驾驶架构平台ROS2和自适应AUTOSAR之间的区别

公众号致力于点云处理&#xff0c;SLAM&#xff0c;三维视觉&#xff0c;高精地图等领域相关内容的干货分享&#xff0c;欢迎各位加入&#xff0c;有兴趣的可联系dianyunpcl163.com。未经作者允许请勿转载&#xff0c;欢迎各位同学积极分享和交流。 背景介绍 在汽车行业&#x…

有人问高考能改变命运不,老大爷回答是不能

昨天看到一段小视频&#xff0c;问高考能改变命运不&#xff0c;老大爷回答是不能。 我遂想起写写这个话题。 &#xff08;1&#xff09; 成功者的要素到底是什么&#xff1f; 我们中国人经常用&#xff1a;成者为王败者为寇&#xff0c;这句话来形容成功者。 我们见过很多成功…

警惕超声波工艺对晶振造成损伤

超声波技术被广泛应用于工业生产中&#xff0c;常见的超声波工艺有&#xff1a;超声波清洗工艺、焊接工艺。 使用该两种工艺时&#xff0c;超声波仪器通常以20KHz至60KHz的频率运行。 清洗工艺是指清除工件表面上液体或固体的污染物&#xff1b;而焊接工艺中&#xff0c;高频机…

论不同阶段的教育”焦虑“

有了孩子&#xff0c;父母们大体对其教育都会有点焦虑感。焦虑是一种情绪&#xff0c;但这里不分析情绪&#xff0c;而是在文题中加了引号&#xff0c;拆开来讨论下不同阶段教育的焦点与考虑。 学前 教育的焦虑在上小学前就开始了&#xff0c;担心起跑线落后了。德国和美国都做…

直播回顾|走进元服务,携手小强停车探索鸿蒙新流量阵地

本期直播《“元”来如此&#xff0c;“服务”直达——揭秘鸿蒙新流量阵地》聚焦元服务的商业流量价值&#xff0c;介绍元服务提供的服务直达和卡片动态变化等轻量化服务。网约停车旗舰平台小强停车做客直播间&#xff0c;分享小强停车在HarmonyOS生态中&#xff0c;如何通过元服…

数据库信息速递 SQL Server新版本许可证价格上涨了10%

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

STM32单片机蓝牙APP GSM短信安全监控系统湿度烟雾入侵报警系统

实践制作DIY- GC0147---蓝牙APP GSM短信安全监控系统 基于STM32单片机设计---蓝牙APP GSM短信安全监控系统 二、功能介绍&#xff1a; 硬件组成&#xff1a;STM32F103C系列最小系统板 LCD1602显示器DHT11空气湿度传感器MQ2烟雾传感器红外热释电人体检测模块SIM800短信发送模块…

记录--详解 XSS(跨站脚本攻击)

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 前言&#xff1a;我们知道同源策略可以隔离各个站点之间的 DOM 交互、页面数据和网络通信&#xff0c;虽然严格的同源策略会带来更多的安全&#xff0c;但是也束缚了 Web。这就需要在安全和自由之间找…

基于短期替代变量的因果效应估计

本篇文章分为&#xff1a; 1.前言 2.模型方案 2.1 替代指数背景 2.2 替代指数的基本设定 2.3 模型结构 3.评估 4.总结与展望 1. 前言 在网约车双边交易市场中&#xff0c;量化策略对平衡市场供需的影响起着重要的作用。对市场供需的影响分为两种情况&#xff1a; 短期价值…

【Linux】Linux环境与历史

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 1.Linux发展史 a.是什么&#xff1f; b.计算机发展(硬件) 硅谷模式: c.操作系统的故事(软件) 计算机软硬件发展史: d.Linux操作系统 2.Linux提炼特征 a.开源 b.更多…

java设计模式之:组合模式

文章目录 1、什么是组合模式&#xff1f;2、组合模式定义3、组合模式通用代码实现4、组合模式优点5、组合模式应用场景 相信树形结构大家都知道&#xff0c;但是你是否知道用到了什么设计模式吗&#xff1f; 1、什么是组合模式&#xff1f; Compose objects into tree structu…

第二章 翻译

第二章 翻译 2010年真题&#xff08;主旨词&#xff1a;sustainability&#xff09; Section Ⅲ Translation Directions: In this section, there is a text in English. Translate it into Chinese. Write your translation on ANSWER SHEET 2. (15points) “Sustainabili…

转转前端周刊第六十九期

转转前端周刊 本刊意在将整理业界精华文章给大家&#xff0c;期望大家一起打开视野 如果你有发现一些精华文章想和更多人分享&#xff0c;可以点击我们的公众号名称&#xff0c;将文章链接和你的解读文案发给我们&#xff01;我们会对内容进行筛选和审核&#xff0c;保留你的推…

【Rust日报】2023-06-11 Rust 中的 Telegram Bot 框架

MOBOT 的第一个版本&#xff0c;这是一个 Rust 中的 Telegram Bot 框架 这是我一直在研究的 Rust 中的 Telegram Bot 框架。我尝试了其他 Rust 库&#xff0c;但我发现它们对于我正在构建的机器人类型来说过于原始或过于具体。 它带有 Telegram Bot API 的完全原生实现&#xf…

最新前端技术趋势

本文作者系360奇舞团前端开发工程师 前端的车轮滚滚向前&#xff0c;轮子造的越来越圆&#xff0c;速度造的越来越快&#xff0c;每个人都在适应这个轮子的节奏&#xff0c;稍微不注意就会被甩出车轮之外。狼狈不堪之外还会发自心底的大喊一声&#xff1a;别卷了&#xff01;&a…

数据空间基础设施的技术挑战及数联网解决方案

数据空间基础设施的技术挑战及数联网解决方案 罗超然1,2, 马郓1,2,3, 景翔1,2,4&#xff0c;黄罡1,2,5 1 数据空间技术与系统全国重点实验室&#xff0c;北京 100091 2 北京大数据先进技术研究院&#xff0c;北京 100091 3 北京大学人工智能研究院&#xff0c;北京 100871 4 北…

学了三年,一文告诉你物联网到底如何实现万物互联?

前言&#xff1a;作为计算机相关专业的你&#xff0c;绝对听说过物联网这个词&#xff0c;它的解释相比你也听过&#xff0c;叫万物互联&#xff0c;也就是所谓的IOT&#xff0c;但是说实话它到底如何实现的万物互联的你可能还真不知道。不是每个物体都有一个网络接口或者实体接…

线程同步(二)

目录 死锁 加锁之后忘记解锁&#xff1a; 重复加锁&#xff0c;造成死锁 在程序中有多个共享资源&#xff0c;因此有很多把锁&#xff0c;随意加锁&#xff0c;导致相互被阻塞 在使用多线程编程的时候&#xff0c;如何避免死锁呢&#xff1f; 读写锁 在程序中对读写锁加…