范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

万字详谈SpringBoot多数据源以及事务处理

  背景
  在高并发的项目中,单数据库已无法承载大数据量的访问,因此需要使用多个数据库进行对数据的读写分离,此外就是在微服化的今天,我们在项目中可能采用各种不同存储,因此也需要连接不同的数据库,居于这样的背景,这里简单分享实现的思路以及实现方案。 如何实现
  多数据源实现思路有两种,一种是通过配置多个SqlSessionFactory实现多数据源;
  另外一种是通过Spring提供的AbstractRoutingDataSource抽象了一个DynamicDataSource实现动态切换数据源;
  实现方案准备
  采用Spring Boot2.7.8框架,数据库Mysql,ORM框架采用Mybatis,整个Maven依赖如下:              8         8         2.7.8         5.1.46         2.0.0         3.5.1                                                  org.springframework.boot                 spring-boot-dependencies                 ${spring-boot.version}                 pom                 import                                           mysql                 mysql-connector-java                 ${mysql-connector-java.version}                                           org.mybatis                 mybatis                 ${mybatis.version}                                           org.mybatis.spring.boot                 mybatis-spring-boot-starter                 ${mybatis-spring-boot-starter.version}                             指定数据源操作指定目录XML文件
  该种方式需要操作的数据库的Mapper层和Dao层分别建立一个文件夹,分包放置,整体项目结构如下图:
  Maven依赖如下:                          org.springframework.boot             spring-boot-starter-web                               org.springframework.boot             spring-boot-starter-test                               com.zaxxer             HikariCP             4.0.3                               mysql             mysql-connector-java                               mysql             mysql-connector-java                               org.mybatis             mybatis                               org.mybatis.spring.boot             mybatis-spring-boot-starter                               junit             junit             test               Yaml文件spring:   datasource:     user:       jdbc-url: jdbc:mysql://127.0.0.1:3306/study_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8       username: root       password: 123456       driver-class-name: com.mysql.jdbc.Driver       type: com.zaxxer.hikari.HikariDataSource       #hikari连接池配置       hikari:         #pool name         pool-name: user         #最小空闲连接数         minimum-idle: 5         #最大连接池         maximum-pool-size: 20         #链接超时时间  3秒         connection-timeout: 3000         # 连接测试query         connection-test-query: SELECT 1     soul:       jdbc-url: jdbc:mysql://127.0.0.1:3306/soul?useSSL=false&useUnicode=true&characterEncoding=UTF-8       username: root       password: 123456       driver-class-name: com.mysql.jdbc.Driver       type: com.zaxxer.hikari.HikariDataSource       #hikari连接池配置       hikari:         #pool name         pool-name: soul         #最小空闲连接数         minimum-idle: 5         #最大连接池         maximum-pool-size: 20         #链接超时时间  3秒         connection-timeout: 3000         # 连接测试query         connection-test-query: SELECT 1 不同库的Mapper指定不同的SqlSessionFactory
  针对不同的库分别放置对用不同的SqlSessionFactory @Configuration @MapperScan(basePackages = "org.datasource.demo1.usermapper",         sqlSessionFactoryRef = "userSqlSessionFactory") public class UserDataSourceConfiguration {      public static final String MAPPER_LOCATION = "classpath:usermapper/*.xml";      @Primary     @Bean("userDataSource")     @ConfigurationProperties(prefix = "spring.datasource.user")     public DataSource userDataSource() {         return DataSourceBuilder.create().build();     }       @Bean(name = "userTransactionManager")     @Primary     public PlatformTransactionManager userTransactionManager(@Qualifier("userDataSource") DataSource dataSource) {         return new DataSourceTransactionManager(dataSource);     }       @Primary     @Bean(name = "userSqlSessionFactory")     public SqlSessionFactory userSqlSessionFactory(@Qualifier("userDataSource") DataSource dataSource) throws Exception {         final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();         sessionFactoryBean.setDataSource(dataSource);         sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(UserDataSourceConfiguration.MAPPER_LOCATION));         return sessionFactoryBean.getObject();     }  } @Configuration @MapperScan(basePackages = "org.datasource.demo1.soulmapper",         sqlSessionFactoryRef = "soulSqlSessionFactory") public class SoulDataSourceConfiguration {      public static final String MAPPER_LOCATION = "classpath:soulmapper/*.xml";       @Bean("soulDataSource")     @ConfigurationProperties(prefix = "spring.datasource.soul")     public DataSource soulDataSource() {         return DataSourceBuilder.create().build();     }       @Bean(name = "soulTransactionManager")     public PlatformTransactionManager soulTransactionManager(@Qualifier("soulDataSource") DataSource dataSource) {         return new DataSourceTransactionManager(dataSource);     }       @Bean(name = "soulSqlSessionFactory")     public SqlSessionFactory soulSqlSessionFactory(@Qualifier("soulDataSource") DataSource dataSource) throws Exception {         final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();         sessionFactoryBean.setDataSource(dataSource);         sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(SoulDataSourceConfiguration.MAPPER_LOCATION));         return sessionFactoryBean.getObject();     }  } 使用@Service public class AppAuthService {      @Autowired     private AppAuthMapper appAuthMapper;      @Transactional(rollbackFor = Exception.class)     public int getCount() {         int a = appAuthMapper.listCount();         int b = 1 / 0;         return a;     }  }  @SpringBootTest @RunWith(SpringRunner.class) public class TestDataSource {      @Autowired     private AppAuthService appAuthService;      @Autowired     private SysUserService sysUserService;      @Test     public void test_dataSource1(){         int b=sysUserService.getCount();         int a=appAuthService.getCount();     } } 总结
  此种方式使用起来分层明确,不存在任何冗余代码,不足地方就是每个库都需要对应一个配置类,该配置类中实现方式都基本类似,该种解决方案每个配置类中都存在事务管理器,因此不需要单独再去额外的关注。 AOP+自定义注解
  关于采用Spring AOP方式实现原理就是把多个数据源存储在一个 Map中,当需要使用某个数据源时,从 Map中获取此数据源进行处理。
  AbstractRoutingDataSource
  在Spring中提供了AbstractRoutingDataSource来实现此功能,继承AbstractRoutingDataSource类并覆写其determineCurrentLookupKey()方法就可以完成数据源切换,该方法只需要返回数据源key即可,也就是存放数据源的Map的key,接下来我们来看一下AbstractRoutingDataSource整体的继承结构,看他是如何做到的。
  在整体的继承结构上我们会发现AbstractRoutingDataSource最终是继承于DataSource,因此当我们继承AbstractRoutingDataSource是我们自身也是一个数据源,对于数据源必然有连接数据库的动作,如下代码: public Connection getConnection() throws SQLException {   return this.determineTargetDataSource().getConnection(); }  public Connection getConnection(String username, String password) throws SQLException {   return this.determineTargetDataSource().getConnection(username, password); }
  只是AbstractRoutingDataSource的getConnection()方法里实际是调用determineTargetDataSource()返回的数据源的getConnection()方法。 protected DataSource determineTargetDataSource() {   Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");   Object lookupKey = this.determineCurrentLookupKey();   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;   } }
  该方法通过determineCurrentLookupKey()方法获取一个key,通过key从resolvedDataSources中获取数据源DataSource对象。determineCurrentLookupKey()是个抽象方法,需要继承AbstractRoutingDataSource的类实现;而resolvedDataSources是一个Map,里面应该保存当前所有可切换的数据源,接下来我们来聊聊实现,我们首先来看下目录,与分包的不同的是将所有的Mapper文件都放到一起,其他Maven依赖以及配置文件都保持一致。
  DataSourceType
  该枚举用来存放数据源的名称, public enum DataSourceType {      USERDATASOURCE("userDataSource"),      SOULDATASOURCE("soulDataSource");      private String name;       DataSourceType(String name) {         this.name=name;     }       public String getName() {         return name;     }      public void setName(String name) {         this.name = name;     } } DynamicDataSourceConfiguration
  通过读取配置文件中的数据源配置信息,创建数据连接,将多个数据源放入Map中,注入到容器中: @Configuration @MapperScan(basePackages = "org.datasource.demo2.mapper") public class DynamicDataSourceConfiguration {      @Primary     @Bean(name = "userDataSource")     @ConfigurationProperties(prefix = "spring.datasource.user")     public DataSource userDataSource() {         return DataSourceBuilder.create().build();     }      @Bean(name = "soulDataSource")     @ConfigurationProperties(prefix = "spring.datasource.soul")     public DataSource soulDataSource() {         return DataSourceBuilder.create().build();     }       @Bean(name = "dynamicDataSource")     public DynamicDataSource DataSource(@Qualifier("userDataSource") DataSource userDataSource,                                         @Qualifier("soulDataSource") DataSource soulDataSource) {         //targetDataSource 集合是我们数据库和名字之间的映射         Map targetDataSource = new HashMap<>();         targetDataSource.put(DataSourceType.USERDATASOURCE.getName(), userDataSource);         targetDataSource.put(DataSourceType.SOULDATASOURCE.getName(), soulDataSource);         DynamicDataSource dataSource = new DynamicDataSource();         dataSource.setTargetDataSources(targetDataSource);         //设置默认对象         dataSource.setDefaultTargetDataSource(userDataSource);         return dataSource;     }       @Bean(name = "sqlSessionFactory")     public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)             throws Exception {         SqlSessionFactoryBean bean = new SqlSessionFactoryBean();         bean.setTransactionFactory(new MultiDataSourceTransactionFactory());         bean.setDataSource(dynamicDataSource);         //设置我们的xml文件路径         bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(                         "classpath*:mapper/*.xml"));         return bean.getObject();     } } DataSourceContext
  DataSourceContext使用ThreadLocal存放当前线程使用的数据源类型信息; public class DataSourceContext {      private final static ThreadLocal LOCAL_DATASOURCE =             new ThreadLocal<>();      public static void set(String name) {         LOCAL_DATASOURCE.set(name);     }      public static String get() {         return LOCAL_DATASOURCE.get();     }      public static void remove() {         LOCAL_DATASOURCE.remove();     } } DynamicDataSource
  DynamicDataSource继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法,可以选择对应Key; public class DynamicDataSource extends AbstractRoutingDataSource {      @Override     protected Object determineCurrentLookupKey() {         return DataSourceContext.get();     }  } CurrentDataSource
  定义数据源的注解; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface CurrentDataSource {     DataSourceType value() default DataSourceType.USERDATASOURCE; } DataSourceAspect
  定义切面切点,用来切换数据源, @Aspect @Order(-1)  @Component public class DataSourceAspect {      @Pointcut("@annotation(org.datasource.demo2.constant.CurrentDataSource)")     public void dsPointCut() {      }      @Around("dsPointCut()")     public Object around(ProceedingJoinPoint point) throws Throwable {         MethodSignature signature = (MethodSignature) point.getSignature();          Method method = signature.getMethod();          CurrentDataSource dataSource = method.getAnnotation(CurrentDataSource.class);          if (Objects.nonNull(dataSource)) {             System.out.println("切换数据源为" + dataSource.value().getName());             DataSourceContext.set(dataSource.value().getName());         }          try {             return point.proceed();         } finally {             // 销毁数据源 在执行方法之后             System.out.println("销毁数据源" + dataSource.value().getName());             DataSourceContext.remove();         }     }  } 多数据源切换以后事务问题
  Spring使用事务的方式有两种,一种是声明式事务,一种是编程式事务,我们讨论的都是关于声明式事务,这种方式很方便,也是大家常用的,这里为什么讨论这个问题,当我们想将不同库的表放在同一个事务使用的时候,这个是时候我们会报错,如下图:
  这部分也就是其他技术贴没讲解的部分,因此这里我们来补充一下这个话题,背过八股们的小伙伴都知道Spring事务是居于AOP实现,从这个角度很容易会理解到这个问题,当我们将两个Service方法放在同一个Transactional下的时候,这个代理对象就是当前类,因此导致数据源对象也是当前类下的DataSource,导致就出现表不存在问题,当Transactional分别放在不同Service的时候没有这种情况。     @Transactional(rollbackFor = Exception.class)     public void update(){         sysUserMapper.updateSysUser("111");         appAuthService.update("111");     }
  有没有办法解决这个问题呢,当然是有的,这里我就不一步一步去探讨源码问题,我就直接直捣黄龙,把问题本质说一下,在Spring事务管理中有一个核心类DataSourceTransactionManager,该类是Spring事务核心的默认实现,AbstractPlatformTransactionManager是整体的Spring事务实现模板类,整体的继承结构如下图,
  在方案一中,我们针对每个DataSourece都创建对应的DataSourceTransactionManager实现,也可以看出DataSourceTransactionManager就是管理我们整体的事务的,当我们配置了事物管理器以及拦截Service中的方法后,每次执行Service中方法前会开启一个事务,并且同时会缓存DataSource、SqlSessionFactory、Connection,因为DataSource、Conneciton都是从缓存中拿的,因此我们怎么切换数据源也没用,因此就出现表不存在的报错,具体源码可参考下面截图部分:
  看到这里我们大致明白了为什么会报错,那么我们该如何做才能实现这种情况呢?其实我们要做的事就是动态的根据DataSourceType获取不同的Connection,不从缓存中获取Connection。 解决方案
  我们来自定义一个MultiDataSourceTransaction实现Mybatis的事务接口,使用Map存储Connection相关连接,所有事务都采用手动提交,之后将MultiDataSourceTransaction交给SpringManagedTransactionFactory处理。 public class MultiDataSourceTransaction implements Transaction {      private final DataSource dataSource;      private ConcurrentMap concurrentMap;      private boolean autoCommit;       public MultiDataSourceTransaction(DataSource dataSource) {         this.dataSource = dataSource;         concurrentMap = new ConcurrentHashMap<>();     }       @Override     public Connection getConnection() throws SQLException {         String databaseIdentification = DataSourceContext.get();         if (StringUtils.isEmpty(databaseIdentification)) {             databaseIdentification = DataSourceType.USERDATASOURCE.getName();         }         //获取数据源         if (!this.concurrentMap.containsKey(databaseIdentification)) {             try {                 Connection conn = this.dataSource.getConnection();                 autoCommit=false;                 conn.setAutoCommit(false);                 this.concurrentMap.put(databaseIdentification, conn);             } catch (SQLException ex) {                 throw new CannotGetJdbcConnectionException("Could bot get JDBC otherConnection", ex);             }         }         return this.concurrentMap.get(databaseIdentification);     }       @Override     public void commit() throws SQLException {         for (Connection connection : concurrentMap.values()) {             if (!autoCommit) {                 connection.commit();             }         }     }      @Override     public void rollback() throws SQLException {         for (Connection connection : concurrentMap.values()) {             connection.rollback();         }     }      @Override     public void close() throws SQLException {         for (Connection connection : concurrentMap.values()) {             DataSourceUtils.releaseConnection(connection, this.dataSource);         }     }      @Override     public Integer getTimeout() throws SQLException {         return null;     } }  public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory {     @Override     public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {         return new MultiDataSourceTransaction(dataSource);     } } 为什么可以这么做
  在Mybatis自动装配式会将配置文件装配为Configuration对象,也就是在方案一种SqlSessionFactory配置的过程,其中SqlSessionFactoryBean类实现了InitializingBean接口,初始化后执行afterPropertiesSet()方法,在afterPropertiesSet()方法中会执行 BuildSqlSessionFactory() 方法生成一个SqlSessionFactory对象。在BuildSqlSessionFactory中,会创建SpringManagedTransactionFactory对象,该对象就是MyBatis跟 Spring的桥梁。
  在MapperScan自动扫描Mapper过程中,会通过ClassPathMapperScanner扫描器找到Mapper接口,封装成各自的BeanDefinition,然后循环遍历对Mapper的BeanDefinition修改beanClass为MapperFactoryBean。
  由于MapperFactoryBean实现了FactoryBean,在Bean生命周期管理时会调用getObject方法,通过JDK动态代理生成代理对象MapperProxy,Mapper接口请求的时候,执行MapperProxy代理类的invoke方法,执行的过程中通过SqlSessionFactory创建的SqlSession去调用Executor执行器,进行数据库操作。下图是SqlSession创建的整个过程:
  openSession方法是将Spring事务管理关联起来的核心代码,首先这里将通过 getTransactionFactoryFromEnvironment()方法获取TransactionFactory。这个操作会得到初始化时候注入的 SpringManagedTransactionFactory对象。然后将执行TransactionFactory#newTransaction() 方法,初始化 MyBatis的Transaction。
  这里通过Configuration.newExecutor()创建一个Executor,Configuration指定在Executor默认为Simple,因此这里会创建一个SimpleExecutor,并初始化Transaction属性。接下来我们来看下SimpleExecutor执行执行update方法时候执行prepareStatement方法,在prepareStatement方法中执行了getConnection方法,
  这里我们可以看到Connection获取过程,是通过Transaction获取的getConnection(),也就是通过之前注入的Transaction来获取Connection,这个Transaction就是SpringManagedTransaction,整体的时序图如下:
  在整个调用链过程中,我们看到在DataSourceUtils有我们熟悉的TransactionSynchronizationManager,在上面Spring事务的时候我们也提到这个类,在开始Spring事务以后就会把Connetion绑定到当前线程,在DataSourceUtils获取到的Connection对象就是Srping开启事务时候创建的对象,这样就保证了Spring Transaction中的Connection跟MyBatis中执行SQL语句用的Connection为同一个 Connection,也就可以通过Spring事务管理机制进行事务管理了。
  明白了整个流程,我们要做的事也就很简单,也就是每次切换DataSoure的同时获取最新的Connection,然后用一个Map对象来记录整个过程中的Connection,出现回滚这个Map对象里面Connection对象都回滚就可以了,然后将我们自定义的Transaction,委托给Spring在进行管理。 总结
  采用AOP的方式是切换数据源已经非常好了,唯一不太好的地方就在于依然要手动去创建DataSource,每次增加都需要增加一个Bean,那有没有办法解决呢?当然是有的,让我们来更上一层楼,解放双手。 更上一层楼ImportBeanDefinitionRegistrar
  ImportBeanDefinitionRegistrar接口是Spring提供一个扩展点,主要用来注册BeanDefinition,常见的第三方框架在集成Spring的时候,都会通过该接口,实现扫描指定的类,然后注册到Spring容器中。比如 Mybatis中的Mapper接口,SpringCloud中的Feignlient接口,都是通过该接口实现的自定义注册逻辑。 我们要做的事情就是通过ImportBeanDefinitionRegistrar帮助我们动态的将DataSource扫描的到容器中去,不在采用增加Bean的方式,整体代码如下: public class DynamicDataSourceBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {       /**      * 默认dataSource      */     private DataSource defaultDataSource;      /**      * 数据源map      */     private Map dataSourcesMap = new HashMap<>();       @Override     public void setEnvironment(Environment environment) {         initConfig(environment);     }      private void initConfig(Environment env) {         //读取配置文件获取更多数据源         String dsNames = env.getProperty("spring.datasource.names");         for (String dsName : dsNames.split(",")) {             HikariConfig hikariConfig = new HikariConfig();             hikariConfig.setPoolName(dsName);             hikariConfig.setDriverClassName(env.getProperty("spring.datasource." + dsName.trim() + ".driver-class-name"));             hikariConfig.setJdbcUrl(env.getProperty("spring.datasource." + dsName.trim() + ".jdbc-url"));             hikariConfig.setUsername(env.getProperty("spring.datasource." + dsName.trim() + ".username"));             hikariConfig.setPassword(env.getProperty("spring.datasource." + dsName.trim() + ".password"));             hikariConfig.setConnectionTimeout(Long.parseLong(Objects.requireNonNull(env.getProperty("spring.datasource." + dsName.trim() + ".hikari.connection-timeout"))));             hikariConfig.setMinimumIdle(Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource." + dsName.trim() + ".hikari.minimum-idle"))));             hikariConfig.setMaximumPoolSize(Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource." + dsName.trim() + ".hikari.maximum-pool-size"))));             hikariConfig.setConnectionInitSql("SELECT 1");             HikariDataSource dataSource = new HikariDataSource(hikariConfig);             if (dataSourcesMap.size() == 0) {                 defaultDataSource = dataSource;             }             dataSourcesMap.put(dsName, dataSource);         }     }      @Override     public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {          Map targetDataSources = new HashMap();         //添加其他数据源         targetDataSources.putAll(dataSourcesMap);         //创建DynamicDataSource         GenericBeanDefinition beanDefinition = new GenericBeanDefinition();         beanDefinition.setBeanClass(DynamicDataSource.class);         beanDefinition.setSynthetic(true);         MutablePropertyValues mpv = beanDefinition.getPropertyValues();         //defaultTargetDataSource 和 targetDataSources属性是 AbstractRoutingDataSource的两个属性Map         mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);         mpv.addPropertyValue("targetDataSources", targetDataSources);         //注册         registry.registerBeanDefinition("dataSource", beanDefinition);     }  } @Import
  @Import模式是向容器导入Bean是一种非常重要的方式,在注解驱动的Spring项目中,@Enablexxx的设计模式中有大量的使用,我们通过ImportBeanDefinitionRegistrar完成Bean的扫描,通过@Import导入到容器中,然后将EnableDynamicDataSource放入SpringBoot的启动项之上,到这里有没有感觉到茅塞顿开的感觉。 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Import({DynamicDataSourceBeanDefinitionRegistrar.class}) public @interface EnableDynamicDataSource { } @SpringBootApplication @EnableAspectJAutoProxy @EnableDynamicDataSource public class DataSourceApplication {      public static void main(String[] args) {         SpringApplication.run(DataSourceApplication.class, args);     } } DynamicDataSourceConfig
  该类负责将Mapper扫描以及SpringFactory定义; @Configuration @MapperScan(basePackages = "org.datasource.demo3.mapper") public class DynamicDataSourceConfig {       @Autowired     private DataSource dynamicDataSource;      @Bean(name = "sqlSessionFactory")     public SqlSessionFactory sqlSessionFactory()             throws Exception {         SqlSessionFactoryBean bean = new SqlSessionFactoryBean();         bean.setTransactionFactory(new MultiDataSourceTransactionFactory());         bean.setDataSource(dynamicDataSource);         //设置我们的xml文件路径         bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(                 "classpath*:mapper/*.xml"));         return bean.getObject();     } } yaml
  关于yaml部分我们增加了names定义,方便识别出来配置了几个DataSource,剩下的部分与AOP保持一致。 spring:   datasource:     names: user,soul     user:       jdbc-url: jdbc:mysql://127.0.0.1:3306/study_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8       username: root       password: 123456       driver-class-name: com.mysql.jdbc.Driver       type: com.zaxxer.hikari.HikariDataSource       #hikari连接池配置       hikari:         #最小空闲连接数         minimum-idle: 5         #最大连接池         maximum-pool-size: 20         #链接超时时间  3秒         connection-timeout: 3000     soul:       jdbc-url: jdbc:mysql://127.0.0.1:3306/soul?useSSL=false&useUnicode=true&characterEncoding=UTF-8       username: root       password: 123456       driver-class-name: com.mysql.jdbc.Driver       type: com.zaxxer.hikari.HikariDataSource       #hikari连接池配置       hikari:         #最小空闲连接数         minimum-idle: 5         #最大连接池         maximum-pool-size: 20         #链接超时时间  3秒         connection-timeout: 3000 结束

康有为主张女子20岁前不结婚,可是他却娶了5个不满20岁的老婆康有为是横跨晚清和民国的政治家,教育家,文学家,书法家。他的一生颇为传奇,主张维新变法又推崇溥仪复辟,推崇进步思想又倒行逆流,最后落得弟子与他反目,时人骂他是贼和伪君子的下场。最让2023人才迁徙报告出炉互联网岗位量减半,碳中和岗位量翻倍如果说2022年职场和就业有什么关键词,那么逃离互联网大厂一定有一席之地。近日,脉脉发布人才迁徙报告2023,报告内容显示,2022年脉脉平台招聘岗位总量同比减少21。6,尤其是纯一个消息传来,不用等iPhone15了,人民网报道值得细品在iPhone14系列翻车后,不少消费者就押注在iPhone15系列身上。但有一句话叫做,希望越大,失望就越大,一个消息传来,或许真的不用等iPhone15了!据外媒透露,iPho预计iPhone15上将会出现这5大功能!虽然iPhone14系列才刚发布没多久,但是大家对于这款产品的认可度似乎并不大,于是就开始期待新一款iPhone15了。从众多爆料上显示,新款iPhone上市时间将会在今年9月份,同样是性能赛道玩家!一加11iQOO11除了价格还有哪些区别?日前,安卓圈又一款骁龙8Gen2旗舰机王诞生,专注于性能赛道的一加11,但似乎不止性能硬核,其它方面也是优势点拉满,瞬间成为不少人心目中理想机型。那么同为第二代骁龙82K屏幕等高端韦布望远镜背后的守护者,是这样一位女科学家记者王晓莹2022年,人类继续在探索宇宙的路上稳步行进,这一年升空并开始运行的詹姆斯韦布空间望远镜备受关注。不过,它背后的守护者或许不是那么为人所知,英国自然周刊网站在2022年1华为要让老外开眼?确认参加MWC展会,华为P60系列有望亮相!今年MWC世界移动通信大会正式定档,将在2月27日至3月2日期间召开,全球通信厂商将再次争奇斗艳,展示最新科技产品。华为确认,将参加今年展会并大展拳脚,预计规模创造历史之最,新一代特斯拉全新两厢车型被爆,或定名Model2,售价或低于18万大鑫获悉,有海外媒体拍到了特斯拉的全新车型的谍照,从谍照来看,新车采用两厢的设计,溜背的车顶,定位比Model3低,或定名为Model2。据悉,新车的售价预计不会超过18万。而海外Bert模型的参数大小计算BERTPretrainingofDeepBidirectionalTransformersforLanguageUnderstandingAttentionisallyounee换新电池的特斯拉ModelY在美国上市,真比较好?根据外媒报道,日前美国特斯拉官网被发现有着疑似被称为StandardRangeAWD的ModelY车型代码,而且外媒指出该车型会采用4680电池组,等于搭载4680电池车型不再仅限广东灯塔工厂藏着哪些数字化密码?广东科创产业调研行日前,总投资100亿元的美的数字科技产业园项目首期用地落子广东顺德北滘,与美的厨热洗碗机顺德工厂相距仅十余公里,去年10月该工厂成功入选世界经济论坛新一批的灯塔工厂名单。刚刚召开的
男人的爱,全部体现在他的护短行为里头条创作挑战赛他气红了双眼,眼里噙着泪水,捏紧了拳头,恨得咬牙切齿,一句话都不说,就在那里僵站着,愤怒的身体随时都有可能破门而出去打那个对我破口大骂,叫嚣着要我快点给她滚的女人。我人,一定要大气!大气才有大财一切事业的成功,其实都源自于做人的成功。一个人的成功,包含了非常多的因素,可以说是他的综合素质,综合能力,杰出的人格魅力所导致的自然结果。因此,归根结底,我们每个人想要改变自己的命有福之人必有天相,一个人有没有福气,看4个地方就知道了图源自网络侵权请联系删除一个人是不是有福之人,从他日常的行为处事中就能看出来。真正的有福之人,一定不会活得拧巴又小家子气。相反,他们比谁都通透,比谁都明白,他们早已看透了人情世故,刻意练习这14种心态后,生活会变的很爽欢迎来到林小北的屋子,点击右上方关注,每天为你分享自我提升成长干货自媒体写作内容。写在前面一个人的心态有多重要?越长大越是觉得,一个人的心态就像一个牢笼一样,总是束缚了我们的行动,男篮世预赛迎战伊朗,应该以年轻球员为主,利用体能优势冲击对方中国男篮将于11月份迎来世预赛下一个窗口期,对手有伊朗队,目前男篮的队员正在备战CBA第一阶段的比赛,第一阶段结束之后,将开启新一期的集训,伊朗队对于中国队来说并不陌生,在姚明退役不差钱!女篮球员顶薪已超CBA,有人年薪400万,女篮投入世界前列结束了女篮世界杯的比赛,中国女篮的所有运动员和教练组工作人员也将于今天晚些时候返回国内,时隔28年再次夺得世界杯的亚军,中国女篮也是为中国的篮球涨了脸面,尽管决赛中李梦没有登场,武本赛季C罗8场1球,5次替补,那全场首发的梅西表现怎样?新赛季已经踢了将近两个月了,五大联赛的赛程也已经进入了中期阶段,各支球队也基本上稳定了下来,开始不断地在比赛中摸索前进。我们都知道,自从这个赛季开始之后,37岁的C罗在曼联就逐渐沦在袖珍国的俱乐部,你一样能找到,8名欧非现役国脚球员尼科西亚阿波尔是袖珍国塞浦路斯的豪门球队。这支球队的评估身价不过1600万欧元稍多。这支队伍中有8位现役的欧洲非洲国脚,还有多名前国脚。总之,这支队伍的看点不少,国脚队员也不少。首季前赛双塔缺阵爱德华兹24分森林狼胜热火,希罗阿德巴约各22分森林狼季前赛121111击败热火。唐斯戈贝尔拉塞尔缺阵,热火这边吉米巴特勒文森特奥拉迪波休战。首节卡勒布马丁阿德巴约希罗发挥抢眼,三人包办了全队24分中的18分。森林狼这边爱德华兹法国队黑人球员多且女朋友很漂亮!欧冠切尔西vsAC米兰10欧冠切尔西vsAC米兰202210060300思路打主胜比分21,10和20AC米兰在新赛季的状况是相当糟糕,球队目前伤病是相当之多,三名AC米兰球员因伤被迫离开对阵恩波利的比赛,东北虎豹国家公园一周岁啦!这里人与自然和谐共生,经济与生态协同发展巍巍长白,茫茫林海虎啸山谷,王者归来。2021年10月12日,东北虎豹国家公园正式设立。一年来,东北虎豹国家公园自然生态系统得到整体保护和修复,支撑保障体系逐步建立,野生东北虎东北