专栏电商日志财经减肥爱情
投稿投诉
爱情常识
搭配分娩
减肥两性
孕期塑形
财经教案
论文美文
日志体育
养生学堂
电商科学
头戴业界
专栏星座
用品音乐

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

  背景
  在高并发的项目中,单数据库已无法承载大数据量的访问,因此需要使用多个数据库进行对数据的读写分离,此外就是在微服化的今天,我们在项目中可能采用各种不同存储,因此也需要连接不同的数据库,居于这样的背景,这里简单分享实现的思路以及实现方案。如何实现
  多数据源实现思路有两种,一种是通过配置多个SqlSessionFactory实现多数据源;
  另外一种是通过Spring提供的AbstractRoutingDataSource抽象了一个DynamicDataSource实现动态切换数据源;
  实现方案准备
  采用SpringBoot2。7。8框架,数据库Mysql,ORM框架采用Mybatis,整个Maven依赖如下:propertiesmaven。compiler。source8maven。compiler。sourcemaven。compiler。target8maven。compiler。targetspringboot。version2。7。8springboot。versionmysqlconnectorjava。version5。1。46mysqlconnectorjava。versionmybatisspringbootstarter。version2。0。0mybatisspringbootstarter。versionmybatis。version3。5。1mybatis。versionpropertiesdependencyManagementdependenciesdependencygroupIdorg。springframework。bootgroupIdspringbootdependenciesartifactIdversion{springboot。version}versiontypepomtypescopeimportscopedependencydependencygroupIdmysqlgroupIdmysqlconnectorjavaartifactIdversion{mysqlconnectorjava。version}versiondependencydependencygroupIdorg。mybatisgroupIdmybatisartifactIdversion{mybatis。version}versiondependencydependencygroupIdorg。mybatis。spring。bootgroupIdmybatisspringbootstarterartifactIdversion{mybatisspringbootstarter。version}versiondependencydependenciesdependencyManagement指定数据源操作指定目录XML文件
  该种方式需要操作的数据库的Mapper层和Dao层分别建立一个文件夹,分包放置,整体项目结构如下图:
  Maven依赖如下:dependenciesdependencygroupIdorg。springframework。bootgroupIdspringbootstarterwebartifactIddependencydependencygroupIdorg。springframework。bootgroupIdspringbootstartertestartifactIddependencydependencygroupIdcom。zaxxergroupIdHikariCPartifactIdversion4。0。3versiondependencydependencygroupIdmysqlgroupIdmysqlconnectorjavaartifactIddependencydependencygroupIdmysqlgroupIdmysqlconnectorjavaartifactIddependencydependencygroupIdorg。mybatisgroupIdmybatisartifactIddependencydependencygroupIdorg。mybatis。spring。bootgroupIdmybatisspringbootstarterartifactIddependencydependencygroupIdjunitgroupIdjunitartifactIdscopetestscopedependencydependenciesYaml文件spring:datasource:user:jdbcurl:jdbc:mysql:127。0。0。1:3306studyuser?useSSLfalseuseUnicodetruecharacterEncodingUTF8username:rootpassword:123456driverclassname:com。mysql。jdbc。Drivertype:com。zaxxer。hikari。HikariDataSourcehikari连接池配置hikari:poolnamepoolname:user最小空闲连接数minimumidle:5最大连接池maximumpoolsize:20链接超时时间3秒connectiontimeout:3000连接测试queryconnectiontestquery:SELECT1soul:jdbcurl:jdbc:mysql:127。0。0。1:3306soul?useSSLfalseuseUnicodetruecharacterEncodingUTF8username:rootpassword:123456driverclassname:com。mysql。jdbc。Drivertype:com。zaxxer。hikari。HikariDataSourcehikari连接池配置hikari:poolnamepoolname:soul最小空闲连接数minimumidle:5最大连接池maximumpoolsize:20链接超时时间3秒connectiontimeout:3000连接测试queryconnectiontestquery:SELECT1不同库的Mapper指定不同的SqlSessionFactory
  针对不同的库分别放置对用不同的SqlSessionFactoryConfigurationMapperScan(basePackagesorg。datasource。demo1。usermapper,sqlSessionFactoryRefuserSqlSessionFactory)publicclassUserDataSourceConfiguration{publicstaticfinalStringMAPPERLOCATIONclasspath:usermapper。xml;PrimaryBean(userDataSource)ConfigurationProperties(prefixspring。datasource。user)publicDataSourceuserDataSource(){returnDataSourceBuilder。create()。build();}Bean(nameuserTransactionManager)PrimarypublicPlatformTransactionManageruserTransactionManager(Qualifier(userDataSource)DataSourcedataSource){returnnewDataSourceTransactionManager(dataSource);}PrimaryBean(nameuserSqlSessionFactory)publicSqlSessionFactoryuserSqlSessionFactory(Qualifier(userDataSource)DataSourcedataSource)throwsException{finalSqlSessionFactoryBeansessionFactoryBeannewSqlSessionFactoryBean();sessionFactoryBean。setDataSource(dataSource);sessionFactoryBean。setMapperLocations(newPathMatchingResourcePatternResolver()。getResources(UserDataSourceConfiguration。MAPPERLOCATION));returnsessionFactoryBean。getObject();}}ConfigurationMapperScan(basePackagesorg。datasource。demo1。soulmapper,sqlSessionFactoryRefsoulSqlSessionFactory)publicclassSoulDataSourceConfiguration{publicstaticfinalStringMAPPERLOCATIONclasspath:soulmapper。xml;Bean(soulDataSource)ConfigurationProperties(prefixspring。datasource。soul)publicDataSourcesoulDataSource(){returnDataSourceBuilder。create()。build();}Bean(namesoulTransactionManager)publicPlatformTransactionManagersoulTransactionManager(Qualifier(soulDataSource)DataSourcedataSource){returnnewDataSourceTransactionManager(dataSource);}Bean(namesoulSqlSessionFactory)publicSqlSessionFactorysoulSqlSessionFactory(Qualifier(soulDataSource)DataSourcedataSource)throwsException{finalSqlSessionFactoryBeansessionFactoryBeannewSqlSessionFactoryBean();sessionFactoryBean。setDataSource(dataSource);sessionFactoryBean。setMapperLocations(newPathMatchingResourcePatternResolver()。getResources(SoulDataSourceConfiguration。MAPPERLOCATION));returnsessionFactoryBean。getObject();}}使用ServicepublicclassAppAuthService{AutowiredprivateAppAuthMapperappAuthMapper;Transactional(rollbackForException。class)publicintgetCount(){intaappAuthMapper。listCount();intb10;returna;}}SpringBootTestRunWith(SpringRunner。class)publicclassTestDataSource{AutowiredprivateAppAuthServiceappAuthService;AutowiredprivateSysUserServicesysUserService;TestpublicvoidtestdataSource1(){intbsysUserService。getCount();intaappAuthService。getCount();}}总结
  此种方式使用起来分层明确,不存在任何冗余代码,不足地方就是每个库都需要对应一个配置类,该配置类中实现方式都基本类似,该种解决方案每个配置类中都存在事务管理器,因此不需要单独再去额外的关注。AOP自定义注解
  关于采用SpringAOP方式实现原理就是把多个数据源存储在一个Map中,当需要使用某个数据源时,从Map中获取此数据源进行处理。
  AbstractRoutingDataSource
  在Spring中提供了AbstractRoutingDataSource来实现此功能,继承AbstractRoutingDataSource类并覆写其determineCurrentLookupKey()方法就可以完成数据源切换,该方法只需要返回数据源key即可,也就是存放数据源的Map的key,接下来我们来看一下AbstractRoutingDataSource整体的继承结构,看他是如何做到的。
  在整体的继承结构上我们会发现AbstractRoutingDataSource最终是继承于DataSource,因此当我们继承AbstractRoutingDataSource是我们自身也是一个数据源,对于数据源必然有连接数据库的动作,如下代码:publicConnectiongetConnection()throwsSQLException{returnthis。determineTargetDataSource()。getConnection();}publicConnectiongetConnection(Stringusername,Stringpassword)throwsSQLException{returnthis。determineTargetDataSource()。getConnection(username,password);}
  只是AbstractRoutingDataSource的getConnection()方法里实际是调用determineTargetDataSource()返回的数据源的getConnection()方法。protectedDataSourcedetermineTargetDataSource(){Assert。notNull(this。resolvedDataSources,DataSourcerouternotinitialized);ObjectlookupKeythis。determineCurrentLookupKey();DataSourcedataSource(DataSource)this。resolvedDataSources。get(lookupKey);if(dataSourcenull(this。lenientFallbacklookupKeynull)){dataSourcethis。resolvedDefaultDataSource;}if(dataSourcenull){thrownewIllegalStateException(CannotdeterminetargetDataSourceforlookupkey〔lookupKey〕);}else{returndataSource;}}
  该方法通过determineCurrentLookupKey()方法获取一个key,通过key从resolvedDataSources中获取数据源DataSource对象。determineCurrentLookupKey()是个抽象方法,需要继承AbstractRoutingDataSource的类实现;而resolvedDataSources是一个MapObject,DataSource,里面应该保存当前所有可切换的数据源,接下来我们来聊聊实现,我们首先来看下目录,与分包的不同的是将所有的Mapper文件都放到一起,其他Maven依赖以及配置文件都保持一致。
  DataSourceType
  该枚举用来存放数据源的名称,publicenumDataSourceType{USERDATASOURCE(userDataSource),SOULDATASOURCE(soulDataSource);privateStringname;DataSourceType(Stringname){this。namename;}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this。namename;}}DynamicDataSourceConfiguration
  通过读取配置文件中的数据源配置信息,创建数据连接,将多个数据源放入Map中,注入到容器中:ConfigurationMapperScan(basePackagesorg。datasource。demo2。mapper)publicclassDynamicDataSourceConfiguration{PrimaryBean(nameuserDataSource)ConfigurationProperties(prefixspring。datasource。user)publicDataSourceuserDataSource(){returnDataSourceBuilder。create()。build();}Bean(namesoulDataSource)ConfigurationProperties(prefixspring。datasource。soul)publicDataSourcesoulDataSource(){returnDataSourceBuilder。create()。build();}Bean(namedynamicDataSource)publicDynamicDataSourceDataSource(Qualifier(userDataSource)DataSourceuserDataSource,Qualifier(soulDataSource)DataSourcesoulDataSource){targetDataSource集合是我们数据库和名字之间的映射MapObject,ObjecttargetDataSourcenewHashMap();targetDataSource。put(DataSourceType。USERDATASOURCE。getName(),userDataSource);targetDataSource。put(DataSourceType。SOULDATASOURCE。getName(),soulDataSource);DynamicDataSourcedataSourcenewDynamicDataSource();dataSource。setTargetDataSources(targetDataSource);设置默认对象dataSource。setDefaultTargetDataSource(userDataSource);returndataSource;}Bean(namesqlSessionFactory)publicSqlSessionFactorysqlSessionFactory(Qualifier(dynamicDataSource)DataSourcedynamicDataSource)throwsException{SqlSessionFactoryBeanbeannewSqlSessionFactoryBean();bean。setTransactionFactory(newMultiDataSourceTransactionFactory());bean。setDataSource(dynamicDataSource);设置我们的xml文件路径bean。setMapperLocations(newPathMatchingResourcePatternResolver()。getResources(classpath:mapper。xml));returnbean。getObject();}}DataSourceContext
  DataSourceContext使用ThreadLocal存放当前线程使用的数据源类型信息;publicclassDataSourceContext{privatefinalstaticThreadLocalStringLOCALDATASOURCEnewThreadLocal();publicstaticvoidset(Stringname){LOCALDATASOURCE。set(name);}publicstaticStringget(){returnLOCALDATASOURCE。get();}publicstaticvoidremove(){LOCALDATASOURCE。remove();}}DynamicDataSource
  DynamicDataSource继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法,可以选择对应Key;publicclassDynamicDataSourceextendsAbstractRoutingDataSource{OverrideprotectedObjectdetermineCurrentLookupKey(){returnDataSourceContext。get();}}CurrentDataSource
  定义数据源的注解;Retention(RetentionPolicy。RUNTIME)Target(ElementType。METHOD)publicinterfaceCurrentDataSource{DataSourceTypevalue()defaultDataSourceType。USERDATASOURCE;}DataSourceAspect
  定义切面切点,用来切换数据源,AspectOrder(1)ComponentpublicclassDataSourceAspect{Pointcut(annotation(org。datasource。demo2。constant。CurrentDataSource))publicvoiddsPointCut(){}Around(dsPointCut())publicObjectaround(ProceedingJoinPointpoint)throwsThrowable{MethodSignaturesignature(MethodSignature)point。getSignature();Methodmethodsignature。getMethod();CurrentDataSourcedataSourcemethod。getAnnotation(CurrentDataSource。class);if(Objects。nonNull(dataSource)){System。out。println(切换数据源为dataSource。value()。getName());DataSourceContext。set(dataSource。value()。getName());}try{returnpoint。proceed();}finally{销毁数据源在执行方法之后System。out。println(销毁数据源dataSource。value()。getName());DataSourceContext。remove();}}}多数据源切换以后事务问题
  Spring使用事务的方式有两种,一种是声明式事务,一种是编程式事务,我们讨论的都是关于声明式事务,这种方式很方便,也是大家常用的,这里为什么讨论这个问题,当我们想将不同库的表放在同一个事务使用的时候,这个是时候我们会报错,如下图:
  这部分也就是其他技术贴没讲解的部分,因此这里我们来补充一下这个话题,背过八股们的小伙伴都知道Spring事务是居于AOP实现,从这个角度很容易会理解到这个问题,当我们将两个Service方法放在同一个Transactional下的时候,这个代理对象就是当前类,因此导致数据源对象也是当前类下的DataSource,导致就出现表不存在问题,当Transactional分别放在不同Service的时候没有这种情况。Transactional(rollbackForException。class)publicvoidupdate(){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处理。publicclassMultiDataSourceTransactionimplementsTransaction{privatefinalDataSourcedataSource;privateConcurrentMapString,ConnectionconcurrentMap;privatebooleanautoCommit;publicMultiDataSourceTransaction(DataSourcedataSource){this。dataSourcedataSource;concurrentMapnewConcurrentHashMap();}OverridepublicConnectiongetConnection()throwsSQLException{StringdatabaseIdentificationDataSourceContext。get();if(StringUtils。isEmpty(databaseIdentification)){databaseIdentificationDataSourceType。USERDATASOURCE。getName();}获取数据源if(!this。concurrentMap。containsKey(databaseIdentification)){try{Connectionconnthis。dataSource。getConnection();autoCommitfalse;conn。setAutoCommit(false);this。concurrentMap。put(databaseIdentification,conn);}catch(SQLExceptionex){thrownewCannotGetJdbcConnectionException(CouldbotgetJDBCotherConnection,ex);}}returnthis。concurrentMap。get(databaseIdentification);}Overridepublicvoidcommit()throwsSQLException{for(Connectionconnection:concurrentMap。values()){if(!autoCommit){connection。commit();}}}Overridepublicvoidrollback()throwsSQLException{for(Connectionconnection:concurrentMap。values()){connection。rollback();}}Overridepublicvoidclose()throwsSQLException{for(Connectionconnection:concurrentMap。values()){DataSourceUtils。releaseConnection(connection,this。dataSource);}}OverridepublicIntegergetTimeout()throwsSQLException{returnnull;}}publicclassMultiDataSourceTransactionFactoryextendsSpringManagedTransactionFactory{OverridepublicTransactionnewTransaction(DataSourcedataSource,TransactionIsolationLevellevel,booleanautoCommit){returnnewMultiDataSourceTransaction(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对象。然后将执行TransactionFactorynewTransaction()方法,初始化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开启事务时候创建的对象,这样就保证了SpringTransaction中的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的方式,整体代码如下:publicclassDynamicDataSourceBeanDefinitionRegistrarimplementsImportBeanDefinitionRegistrar,EnvironmentAware{默认dataSourceprivateDataSourcedefaultDataSource;数据源mapprivateMapString,DataSourcedataSourcesMapnewHashMap();OverridepublicvoidsetEnvironment(Environmentenvironment){initConfig(environment);}privatevoidinitConfig(Environmentenv){读取配置文件获取更多数据源StringdsNamesenv。getProperty(spring。datasource。names);for(StringdsName:dsNames。split(,)){HikariConfighikariConfignewHikariConfig();hikariConfig。setPoolName(dsName);hikariConfig。setDriverClassName(env。getProperty(spring。datasource。dsName。trim()。driverclassname));hikariConfig。setJdbcUrl(env。getProperty(spring。datasource。dsName。trim()。jdbcurl));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。connectiontimeout))));hikariConfig。setMinimumIdle(Integer。parseInt(Objects。requireNonNull(env。getProperty(spring。datasource。dsName。trim()。hikari。minimumidle))));hikariConfig。setMaximumPoolSize(Integer。parseInt(Objects。requireNonNull(env。getProperty(spring。datasource。dsName。trim()。hikari。maximumpoolsize))));hikariConfig。setConnectionInitSql(SELECT1);HikariDataSourcedataSourcenewHikariDataSource(hikariConfig);if(dataSourcesMap。size()0){defaultDataSourcedataSource;}dataSourcesMap。put(dsName,dataSource);}}OverridepublicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){MapObject,ObjecttargetDataSourcesnewHashMapObject,Object();添加其他数据源targetDataSources。putAll(dataSourcesMap);创建DynamicDataSourceGenericBeanDefinitionbeanDefinitionnewGenericBeanDefinition();beanDefinition。setBeanClass(DynamicDataSource。class);beanDefinition。setSynthetic(true);MutablePropertyValuesmpvbeanDefinition。getPropertyValues();defaultTargetDataSource和targetDataSources属性是AbstractRoutingDataSource的两个属性Mapmpv。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})publicinterfaceEnableDynamicDataSource{}SpringBootApplicationEnableAspectJAutoProxyEnableDynamicDataSourcepublicclassDataSourceApplication{publicstaticvoidmain(String〔〕args){SpringApplication。run(DataSourceApplication。class,args);}}DynamicDataSourceConfig
  该类负责将Mapper扫描以及SpringFactory定义;ConfigurationMapperScan(basePackagesorg。datasource。demo3。mapper)publicclassDynamicDataSourceConfig{AutowiredprivateDataSourcedynamicDataSource;Bean(namesqlSessionFactory)publicSqlSessionFactorysqlSessionFactory()throwsException{SqlSessionFactoryBeanbeannewSqlSessionFactoryBean();bean。setTransactionFactory(newMultiDataSourceTransactionFactory());bean。setDataSource(dynamicDataSource);设置我们的xml文件路径bean。setMapperLocations(newPathMatchingResourcePatternResolver()。getResources(classpath:mapper。xml));returnbean。getObject();}}yaml
  关于yaml部分我们增加了names定义,方便识别出来配置了几个DataSource,剩下的部分与AOP保持一致。spring:datasource:names:user,souluser:jdbcurl:jdbc:mysql:127。0。0。1:3306studyuser?useSSLfalseuseUnicodetruecharacterEncodingUTF8username:rootpassword:123456driverclassname:com。mysql。jdbc。Drivertype:com。zaxxer。hikari。HikariDataSourcehikari连接池配置hikari:最小空闲连接数minimumidle:5最大连接池maximumpoolsize:20链接超时时间3秒connectiontimeout:3000soul:jdbcurl:jdbc:mysql:127。0。0。1:3306soul?useSSLfalseuseUnicodetruecharacterEncodingUTF8username:rootpassword:123456driverclassname:com。mysql。jdbc。Drivertype:com。zaxxer。hikari。HikariDataSourcehikari连接池配置hikari:最小空闲连接数minimumidle:5最大连接池maximumpoolsize:20链接超时时间3秒connectiontimeout:3000结束

广西南宁农副产品丰富多样市场供应充足央视网消息随着兔年春节的临近,广西南宁的菜市场和超市显得格外热闹,市民们忙着采购米面油肉蛋奶果蔬等农副产品,市场货源供应充足。在南宁市的一家菜市场里,记者看到,北方的萝卜白菜,云南桂阳县市场监管局五抓守护群众安全红网时刻新闻1月4日讯(通讯员王成家李歌宏张卫平)老板,千万要确保产品质量!1月4日,记者见到桂阳县市场监督管理局工作人员在检查某药店时一再叮嘱。该县自2022年12月26日启动年九成感染者占比到底是无症状还是有症状?今天一口气讲明白今天要和大家聊的话题是,在疫情放开初期,一些城市官方发布的疫情感染报告中明确指出,无症状感染者占据九成比例。然而放开后的真实情况却恰恰相反,只有极少数的人是无症状表现,有位北京市东白宫喊话中方不要对各国针对中国的措施加以报复,中方回应白宫发言人皮埃尔据悉,上个月中方宣布对疫情防控政策做出调整,并同步做出取消入境全员核酸检测和集中隔离措施。但接下来多个国家开始轮番对华实行疫情加码防控措施,美国日本意大利印度等国出民调65的英国人支持举行重入欧盟公投英国独立报网站近日报道称,一份民调显示,在英国退出欧盟两年后,近三分之二的英国人支持就重新加入欧盟举行公投。民调显示,认为应该就再次加入欧盟举行公投的英国人占65,高于一年前的55探寻文明源流坚定文化自信山东岗上遗址南区墓地出土文物。图由国家文物局提供考古工作者在湖北学堂梁子遗址工作。图由国家文物局提供上图考古工作者在四川皮洛遗址进行考古挖掘。郑喆轩摄下图陕西石峁遗址外景。图由国家我川妹子,32岁不结婚不生孩,骑摩托环游全国,一年行36000公里这是我们讲述的第1362位真人故事我叫安娜长发摩侣奇遇记,90后川妹子。我的家乡在四川省德阳市中江县,和汶川一山相隔。2008年正值我高三,那一年的地震,不仅毁了大半个汶川,也直接2023新风向数字经济势不可挡,看运营商弄潮新蓝海(图片来源摄图网)新动能茁壮成长,新经济方兴未艾。近日发布的2022中国数字经济主题报告显示,中国数字经济规模已经达到了45。5万亿元,位居世界第二,成为经济增长新引擎。在2022元旦假期客流恢复深圳旅游市场稳步升温央视网消息连日来,广东深圳各大景区客流恢复。据统计,元旦假期三天,深圳全市共接待游客超过123万人次,旅游总收入8。22亿元,旅游市场稳步升温。1月2日晚,深圳世界之窗新年第一场烟一起来讨论今年过年你们当地解禁燃放烟花爆竹了吗?为什么过年要放鞭炮?首先要从民间世代相传的年兽的故事开始说起。年兽,又称为夕。是古代民间神话传说当中的恶兽,相传古时候每到一年年末的午夜,年兽就会出来进攻村子,凡是被年兽进攻占领的台军导弹疑似泄密,核心部件运至大陆维修,若两岸开战即终战在美国的挑唆下,民进党当局在两岸问题上不断挑衅,并逐渐放弃其所宣称的不挑衅,不冒进,不激化,维持现状的缓独政策,并正在滑向急独的深渊,台海形势持续高烧不退。台海局势紧张的根源,从来
以岭药业市值暴跌411亿,莲花清瘟掉价近八成,网友说活该最近的以岭药业市值暴跌,冲上了热搜。据媒体报道,以岭药业莲花清瘟,每盒价格102元降至21。8元,跌幅将近8成。市值两个月蒸发了411亿,公司实际控制人,吴以岭身家缩水130亿。这一加Ace2来了,荣耀80GT更难了,网友百亿补贴真的猛荣耀从华为独立后,很长一段时间都没有发布面向线上市场的产品,不过去年荣耀X40GT的发布算是表明荣耀有重返线上市场的想法,随后发布的荣耀80GT也可以说是面对线上市场的产品,不过面小米12SU价格暴跌,12GB256GB突降1170元,网友两千内可以考虑小米冲击高端的第四年,关于小米小米高端成了吗?这个话题仍旧众所纷纭,从消费者的印象来看,这说明小米高端尚未成功。性价比起家的小米,也很难摆脱这个赖以生存的包袱。但应该承认小米冲击高怪不得!最近那么多人在使用QQ,原来里面暗藏这么多实用的功能QQ和微信现在被我们很多人相比较,每次都是微信略胜一筹,但这次QQ终于扬眉吐气了一番,只因里面拥有这些,连微信都自愧不如的玩法,让我们一起来看看吧!1。体验模式如果你觉得QQ的界面非常隐蔽的聊天软件,双向删除,思语无痕密聊app思语加密聊天app是一款可以兼容安卓ios的聊天软件,也支持手机端网页端聊天,简单方便,安全隐秘。软件采用的加密技术是端对端加密,这种加密技术意味着,你的信息只供你和你的接收者观看成理地灾国重实验室土耳其地震群诱发滑坡预测次生灾害影响严峻2月8日,记者从成都理工大学地质灾害防治与地质环境保护国家重点实验室了解到,土耳其地震发生后,实验室强震地质灾害研究团队第一时间利用前期研发的地震诱发滑坡预测模型,对土耳其地震群诱与人工智能机器人ai谈恋爱这件事,你会同意吗?头条创作挑战赛与机器人谈恋爱,看似不可能且又浪漫的事情,在现如今的世界里,真的有可能像电影和动漫里一样成为现实。在小日子的国家里,几年前就已经出现了仿真机器人,那些机器人外观与普通1500元左右被严重低估的三款手机买到就是赚到您在阅读前请点击上面的关注二字,后续会第一时间为您提供更多有价值的相关内容,感谢您的支持。花个一千元多购买一款性能不错,续航还长的手机对于我们大多数人来说是一个非常不错的选择。在这卖掉iPhone13,入手了iPhone14ProMax,从六个方面说说体验iPhone13是苹果在2021年9月份发布的机型,截止目前这款手机仍有在售卖。当然,这跟iPhone13的配置情况和操作体验有关系。换句话说,iPhone13一度被外界称为十三香三星GalaxyZFlip4高级感十足,前辈机皇加速沦为百元神机三星GalaxyZFlip3搭载骁龙888处理器LPDDR5和UFS3。1存储配置。在骁龙888被称为喷火龙的时代,三星GalaxyZFlip3在游戏运行过程中有了惊喜发现。三星GiPhone8P电池健康度78,已更新iOS16。3,体验后我做个决定iOS16。3正式版推送后,不少人的iPhone手机都已经升级系统了。我手里的iPhone14ProMax和iPhone11ProMax均更新完毕。昨天我才给退休的iPhone8P
友情链接:快好找快生活快百科快传网中准网文好找聚热点快软网