-
SpringBoot+MyBatis+AOP 实现多数据源动态自主切换
在实际开发中,我们一个项目可能会用到多个数据库,通常一个数据库对应一种数据,而且在大数据量的业务下通常都会有多个数据源的。最近恰好工作上遇到一个SpringBoot+Mybatis框架的多数据源配置切换的业务,所以就研究了一下关于Springboot+Mybatis多数据源切换。1.多数据源常见解决方案正常情况下,我们操作数据是通过配置一个DataSource数据源来连接数据库,然后绑定给SqlSessionFactory,然后通过Dao或Mapper指定SqlSessionFactory来操作数据库的。而操作多数据源则更要复杂一点,可以通过如下两种方式来实现:普通的多数据源多个DataSource数据源绑定多个SqlSessionFactory,每个数据源绑定一个SqlSessionFactory,然后通过Dao或Mapper指定SqlSessionFactory来操作数据库。操作不同的数据源是通过在业务层调用对应的实现了不同数据源的方法来同时操作不同的数据源的。动态切换的数据源方式一中,必须要使多个数据源之间完全的物理分离,如果存在一个用户表,几个数据库都有的情况,并且业务也类似,那写多套代码是冗余的,并且代码维护起来也更加困难,有没有更便捷的方式呢?其实可以通过配置多个DataSource数据源到一个DynamicDataSource动态数据源上,动态数据源绑定一个SqlSessionFactory,除了中间多出一个动态数据源外,其他部分都是相同的。Q:那么这种方式是怎么实现数据源的切换的呢?A:通过在业务类或方法上添加一个数据源标识(注解),使用切面来监听这个标志,进而切换数据源,通过一个注解就可以更加灵活切换数据源。2.实现动态数据源切换2.1禁用SpringBoot的自动配置数据源类在SpringBoot中,程序会自动读取src/main/resources/application.yml或application.properties配置文件中的spring.datasource.xxx的数据源配置信息,如果我们需要配置多数据源的话,要先把这个自动读取数据源配置信息的类禁掉。禁掉该类的方法是:在SpringBoot的启动类中,使用@SpringBootApplication注解时把类DataSourceAutoConfiguration.class排除。2.2重写数据源配置读取类禁掉自动读取数据源配置类之后,需要自己写读取不同数据源的配置信息代码了,要配置多少个数据源,就有多少个配置方法,并将数据源实例交给Spring容器管理,如下(这里以MySQL和TiDB为例,数据库连接池使用Hikari):2.3自定义DynamicDataSource实现数据源可切换有了多个数据源之后我们需要一个地方来管理系统的数据源,可以使用枚举,也可以使用常量类,如下定义了MYSQL和TiDB两种数据源枚举:SpringBoot中提供了一个类AbstractRoutingDataSource可以让用户实现数据源的动态切换,我们需要重写该类的determineCurrentLookupKey()方法,从数据源类型容器中获取当前线程的数据源类型。如下:其中DataSourceSwith是自定义的数据源切换类,它保存了当前线程下的应该使用的数据源名称,构建一个数据源类型容器,并提供了向其中设置、获取和清空数据源类型的方法,具体代码如下:2.4使用自定义DynamicDataSource重写SpringBoot的数据源配置在DataSourceConfig类中增加方法,从DynamicDataSource中获取当前线程的Datasoure,实现数据源的切换:根据动态数据源配置,创建sqlSessionFactory,具体代码如下:根据动态数据源配置,创建transactionManager,具体代码如下:根据动态数据源配置,创建mybatissessionConfiguration,具体代码如下:3.AOP+自定义注解实现数据源动态切换通过上面第2步我们配置了MySQL和TiDB两种可动态切换的数据源,我们只需要在业务代码中使用自定义的DataSourceSwitch.setDataSourceType()方法设置业务员方法应该使用的数据源即可实现数据源的动态切换,可是这种方式略显麻烦且不够优雅,在AOP加持下,我们可以实现通过自定义注解实现数据源动态切换。实现的基本原理也很简单:通过在业务类上标注自定义注解并指定数据源,然后AOP切面监听注解,如果被注解标注的类或方法被执行那么就按照注解中指定的数据源调用DataSourceSwitch的setDataSourceType方法实现数据源切换。3.1自定义数据@DataSource3.2在业务类上标注自定义数据源注解在使用MySQL数据源的业务类上标注注解@DataSource(value=DataSourceDialect.MYSQL_MASTER),由于注解默认值使用的就是MySQL数据源,所以可以缺省写为@DataSource在使用TiDB数据源的业务类上标注注解@DataSource(value=DataSourceDialect.TIDB_MASTER)3.3通过切面实现动态切换数据源4.验证结果访问依赖MySQL数据源的接口/v1/user接口正常返回&日志打印了切换过程:访问依赖TiDB数据源的接口/v1/order/list接口正常返回&日志打印了切换过程:
LoveIT 2023-01-30Spring -
Spring教程:事物详解(三)声明式事务源码详解
引言上篇我们着重对Spring编程式事物进行了源码阅读以及原理分析,了解了Spring事物的运行原理,但这种管理事务的方式的代码侵入行非常高,现在开发基本不会使用这个,而且现在Java开发基本都会使用SpringBoot,配合SpringBoot的自动配置,声明式事物简直不要太好用!本篇就深入SpringBoot源码,看看@EnableTransactionManagement配合@Transactional两个简单的注解(写法简单,但内在其实不简单)是如何帮助我们管理事务的。一、AOP有关概念回顾声明式事务是依赖SpringAOP实现的,即面向切面编程。所谓AOP...一句话概括就是:把业务代码中重复代码做成一个切面,提取出来,并定义哪些方法需要执行这个切面。其它的自行百度吧...AOP核心概念如下:通知(Advice):定义了切面(各处业务代码中都需要的逻辑提炼成的一个切面)做什么what+when何时使用。例如:前置通知Before、后置通知After、返回通知After-returning、异常通知After-throwing、环绕通知Around.连接点(Jointpoint):程序执行过程中能够插入切面的点,一般有多个。比如调用方式时、抛出异常时。切点(Pointcut):切点定义了连接点,切点包含多个连接点,即where哪里使用通知.通常指定类+方法或者正则表达式来匹配类和方法名称。切面(Aspect):切面=通知+切点,即when+where+what何时何地做什么。引入(Introduction):允许我们向现有的类添加新方法或属性。织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。二、声明式事物源码解读由于采用声明式@Transactional这种注解的方式,那么我们从SpringBoot启动时的自动配置载入开始看。在SpringBootautoconfigure包下的/META-INF/spring.factories中配置文件中查找到有关声明式事物的自动配置,如下图:发现,SpringBoot载入2个关于事务的自动配置类:org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,重点看一下TransactionAutoConfiguration这个自动配置类。2.1TransactionAutoConfigurationTransactionAutoConfiguration这个类主要看:两个类注解和两个内部类2.1.1两个类注解@ConditionalOnClass(PlatformTransactionManager.class)会在类路径下包含PlatformTransactionManager这个类时这个自动配置生效,这个类是Spring事务的核心包,肯定引入了。@AutoConfigureAfter({JtaAutoConfiguration.class,HibernateJpaAutoConfiguration.class,DataSourceTransactionManagerAutoConfiguration.class,Neo4jDataAutoConfiguration.class}),这个配置在括号中的4个配置类后才生效。2.1.2两个类内部类TransactionTemplateConfiguration事务模板配置类有两个注解,含义分别如下:@ConditionalOnSingleCandidate(PlatformTransactionManager.class)当能够唯一确定一个PlatformTransactionManagerBean实例时才生效。@ConditionalOnMissingBean如果没有定义TransactionTemplateBean生成一个。EnableTransactionManagementConfiguration开启事务管理器配置类在源码中我们还可以看到,EnableTransactionManagementConfiguration支持2种代理方式:JdkDynamicAutoProxyConfiguration和CglibAutoProxyConfigurationJdkDynamicAutoProxyConfiguration@EnableTransactionManagement(proxyTargetClass=false),表示是JDK动态代理,支持面向接口的代理。@ConditionalOnProperty(prefix="spring.aop",name="proxy-target-class",havingValue="false",matchIfMissing=false),即spring.aop.proxy-target-class=false时生效,且没有这个配置不生效。CglibAutoProxyConfiguration@EnableTransactionManagement(proxyTargetClass=true),表示使用Cglib代理,支持的是子类继承代理。@ConditionalOnProperty(prefix="spring.aop",name="proxy-target-class",havingValue="true",matchIfMissing=true),即spring.aop.proxy-target-class=true时生效,且没有这个配置默认生效。也就是说,如果开始事物后,没有做任何配置,那么SpringBoot会默认使用Cglib代理,那么也就是说,@Transactional注解默认情况下支持直接加在类上。看到这里,我们需要对@EnableTransactionManagement注解做一个更深的认识,下图所示是其源码。proxyTargetClass:值为false表示是JDK动态代理支持接口代理。true表示是Cglib代理支持子类继承代理。mode:事务通知模式(切面织入方式),默认代理模式(同一个类中方法互相调用拦截器不会生效),可以选择增强型AspectJorder:连接点上有多个通知时用于优先级排序,默认最低。值越大优先级越低。属性先说这么多,这个注解重点还是看它头上的@Import(TransactionManagementConfigurationSelector.class)这个注解。@Import注解的功能不用多说,它就是用来导入配置的,重点关注配置类TransactionManagementConfigurationSelector,类图如下:如上图所示,TransactionManagementConfigurationSelector继承自AdviceModeImportSelector实现了ImportSelector接口。如上图,最终会执行selectImports方法导入需要加载的类,我们只看PROXY模式下,载入了AutoProxyRegistrar、ProxyTransactionManagementConfiguration2个类。AutoProxyRegistrar:给容器中注册一个InfrastructureAdvisorAutoProxyCreator组件;利用后置处理器机制在对象创建以后,包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用;ProxyTransactionManagementConfiguration:一个配置类,定义了事务增强器。三、声明式事物工作时序图最后,整理了一下声明式事物工作时序图,建议收藏!
LoveIT 2021-10-26Spring -
Spring教程:事物详解(二)编程式事务源码详解
引言Spring提供了两种事物管理实现方式:编程式事务管理:编程式事务管理使用TransactionTemplate可实现更细粒度的事务控制。声明式事务管理:基于SpringAOP实现。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务管理没有入侵代码,通过**@Transactional**就可以进行事务操作,更快捷而且简单(尤其是配合SpringBoot自动配置,可以说是精简至极!)且大部分业务都可以满足,推荐使用。其实不管是编程式事务还是声明式事务,最终调用的底层核心代码是一致的。本章分上下两篇,分别从编程式、声明式入手,再进入核心源码贯穿式讲解。本片重点分析编程式事物的实现方式。Let'sgo~~一、编程式事务TransactionTemplate全路径名是:org.springframework.transaction.support.TransactionTemplate。看包名也知道了,这是Spring对事务的模板类。在Spring中模版模式使用非常广泛的~点进TransactionTemplate源码它是实现了TransactionOperations、InitializingBean这2个接口(熟悉Spring源码的小伙伴知道这个InitializingBean又是老套路),我们来看下接口源码如下:TransactionOperations接口InitializingBeanTransactionOperations这个接口用来执行事务的回调方法,InitializingBean这个是典型的SpringBean初始化流程中的预留接口,专用用来在bean属性加载完毕时执行的方法(不熟悉的小伙伴去看看SpringBean生命周期的流程)。看到这里,有的小伙伴自然而然就有疑问了:TransactionTemplate的2个接口的方法做了什么?接着往下说!如上图所示,实际上afterPropertiesSet方法只是校验了事务管理器不为空,execute()才是核心方法,execute主要步骤:总结一下,核心步骤如下:getTransaction()获取事务,源码见见下篇3.3.1节doInTransaction()执行业务逻辑,这里就是用户自定义的业务代码。如果是没有返回值的,就是doInTransactionWithoutResult()。commit()事务提交,根据事物执行状态,调用AbstractPlatformTransactionManager类的commit()提交事物、rollbackOnException()异常回滚或者rollback()事务提交回滚,源码见下篇3.3.3节
LoveIT 2021-10-26Spring -
Spring教程:事物详解(一)初探事物
引言很多coder在不理解事务的原理甚至连基本概念都不清楚的情况下,就去使用数据库事务,是极容易出错,写出一些自己不能掌控的代码。网上很多文章要不就是概念,或者一点源码,或者一点测试验证,都不足以全面了解事务,所以本文出现了,本系列Spring事务详解包含四部分:第一章讲概念,对事务的整体有一个了解。第二章从源码来看底层实现机制。第三章实例测试验证。第四章总结提高。一、背景1.1拜神Spring事务领头人叫JuergenHoeller,先混个脸熟哈,他写了几乎全部的Spring事务代码。读源码先拜神,掌握他的源码的风格,读起来会通畅很多。最后一节咱们总结下这个大神的代码风格。1.2事务的定义事务(Transaction)是数据库区别于文件系统的重要特性之一。目前国际认可的数据库设计原则是ACID特性,用以保证数据库事务的正确执行。Mysql的innodb引擎中的事务就完全符合ACID特性。Spring对于事务的支持,分层概览图如下:二、事务的ACID特性原子性(Atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。主要涉及InnoDB事务。相关特性:事务的提交,回滚,信息表。一致性(consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态。在事务开始前后,数据库的完整性约束没有被破坏。例如违反了唯一性,必须撤销事务,返回初始状态。隔离性(isolation):每个读写事务的对象对其他事务的操作对象能相互分离,即:事务提交前对其他事务是不可见的,通常内部加锁实现。主要涉及事务,尤其是事务隔离级别,相关特性:隔离级别、innodb锁的底层实现细节。持久性(durability):一旦事务提交,则其所做的修改会永久保存到数据库。涉及到MySQL软件特性与特定硬件配置的相互影响,相关特性:4个配置项:双写缓冲开关、事务提交刷新log的级别、binlog同步频率、表文件;写缓存、操作系统对于fsync()的支持、备份策略等。三、事务的属性定义要保证事务的ACID特性,Spring给事务定义了6个属性,对应于声明式事务注解@Transational(org.springframework.transaction.annotation.Transactional)中6个成员属性。事务名称:用户可手动指定事务的名称,当多个事务的时候,可区分使用哪个事务。对应注解中的属性value、transactionManager隔离级别:为了解决数据库并发事物带来的问题,采用分级加锁处理策略。对应注解中的属性isolation超时时间:定义一个事务执行过程多久算超时,以便超时后回滚。可以防止长期运行的事务占用资源.对应注解中的属性timeout是否只读:表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务.对应注解中的属性readOnly传播机制:对事务的传播特性进行定义,共有7种类型。对应注解中的属性propagation回滚机制:定义遇到异常时回滚策略。对应注解中的属性rollbackFor、noRollbackFor、rollbackForClassName、noRollbackForClassName其中==隔离级别==和==传播机制==比较复杂,我们来细细的品一品。3.1隔离级别3.1.1现象事物的隔离级别(isolationlevel)定义了一个事务可能受其他并发事务影响的程度。在并发事务中,经常会引起以下问题:脏读(Dirtyreads)——脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。不可重复读(Nonrepeatableread)——不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。幻读(Phantomread)——幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。3.1.2MySQL底层支持(InnoDB事物模型)当多个事务同时执行sql操作时,隔离级别用于平衡InnoDB的性能、可靠性、并发性、结果的可再现性。可以通过set_transaction进行单个用户连接的隔离级别设置。通过showvariableslike‘tx_isolation’查看当前使用的隔离级别。加上server启动参数--transaction-isolation或者在配置文件中设定serverlevel的隔离级别。InnoDB使用不同的锁策略来实现对不同事务隔离级别的支持。具体如下:隔离级别含义ISOLATION_DEFAULT使用后端数据库默认的隔离级别(Spring独有的)ISOLATION_READ_UNCOMMITTED最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读ISOLATION_READ_COMMITTED允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生ISOLATION_SERIALIZABLE最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的3.2传播行为**事务的传播行为(propagationbehavior)**是指一个事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring在org.springframework.transaction包下的TransactionDefinition接口定义了七种传播行为:传播行为含义PROPAGATION_REQUIRED表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务PROPAGATION_SUPPORTS表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行PROPAGATION_MANDATORY表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常PROPAGATION_REQUIRED_NEW表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManagerPROPAGATION_NOT_SUPPORTED表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManagerPROPAGATION_NEVER表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常PROPAGATION_NESTED表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务参考资料spring事务详解(一)初探事务.https://www.likecs.com/default/index/url?u=aHR0cHM6Ly9pLmNuYmxvZ3MuY29tL0VkaXRQb3N0cy5hc3B4P3Bvc3RpZD05NTQ5NTM1Mysql技术内幕-InnoDB存储引擎InnoDBLockingandTransactionModel.MySQL官网
LoveIT 2021-10-26Spring -
秒懂,Redis缓存穿透、缓存击穿、缓存雪崩概念以及应对策
如上图所示,我们在应用程序和Mysql数据库中建立一个中间层,即Redis缓存。通过Redis缓存可以有效减少查询数据库的时间消耗,这极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求极高,那么就不能使用缓存。另外的一些典型问题就是,缓存穿透、缓存击穿和缓存雪崩。本文将简单介绍缓存穿透、缓存雪崩和缓存击穿这三者之间的区别以及这三类问题的解决方法。一、缓存穿透缓存穿透是指用户访问了不存在的数据,导致缓存无法命中,大量的请求都要穿透到数据库进行查询,从而使得数据库压力过大,甚至挂掉。比如:数据库使用了id为正整数作为键,但是黑客使用负整数向服务器发起请求,这时所有的请求都没有在缓存中命中,从而导致大量请求数据库,如果超过了数据库的承载能力,会导致数据库服务器宏机。一般解决缓存穿透的方法有:(1)缓存空对象这是一个简单粗暴的方法,方法是如果一个查询返回的结果是空的,仍然把这个空结果进行缓存,这样的话缓存层就存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个比较短的过期时间,让其自动过期。(2)使用布隆过滤器拦截这是一种常见而且有效的策略,它将所有可能存在的数据哈希到一个足够大的bitmap中,当查询一个不存在的key时会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。Redis实现了布隆过滤器,我们可以直接使用来达到过滤的目的。具体的原理和使用方法可以参考我的另一篇博文布隆过滤器(BloomFilter)的原理和实现二、缓存击穿(缓存并发)缓存击穿,是指当某个key在过期的瞬间,有大量的请求并发访问过期的键,这类数据一般是热点数据,由于缓存过期了,会同时访问数据库来查询数据,并写回缓存,从而导致数据库瞬间压力过大。缓存击穿的解决方法也有两个:(1)设置热点数据永不过期当遇到这种情况的时候,数据库很难扛下来这么大的并发。最简单的方法就是将热点数据缓存永不过期就好了。(2)使用互斥锁业界比较常用的方法,使用锁控制访问后盾服务的并发量。分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其线程没有获取到分布式锁等待就好了。这种方式将高并发的压力转移到了分布式锁,因此对系统的分布式锁是否合格考验很大。关于分布式锁内容可以参考我的另一篇博文分布式锁的三种实现方案本地锁:与分布式锁的作用类似,我们通过本地锁的方式来限制只有一个线程去数据库中查询数据,而其他线程只需等待,等前面的线程查询到数据后再访问缓存。但是,这种方法只能限制一个服务节点只有一个线程去数据库中查询,如果一个服务有多个节点,则还会有多个数据库查询操作,也就是说在节点数量较多的情况下并没有完全解决缓存并发的问题。基于Redis实现的分布式锁伪代码:三、缓存雪崩缓存雪崩是指设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致所有的查询同一时刻都落到了数据库上,造成了数据库压力过大。缓存雪崩与缓存击穿的区别在于这里针对很多key同时失效,前者则是针对某一个热点key失效解决方案:(1)不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀(2)在缓失效后,通过分布式锁或者分布式队列的方式控制数据库写缓存的线程数。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(和缓存击穿中使用互斥锁类似)(3)如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。随机过期时间伪代码:解释:在同一分类中的key,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。这种情况就应该考虑使用主备缓存策略了。最后再介绍几个有关缓存的概念缓存预热缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!解决思路:1、直接写个缓存刷新页面,上线时手工操作下;2、数据量不大,可以在项目启动的时候自动进行加载;3、定时刷新缓存;缓存更新除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:(1)定时去清理过期的缓存;(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。缓存降级当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。总结本文介绍了缓存应用中比较典型的是三个问题:缓存穿透,缓存击穿和缓存雪崩的概念和基本的解决方法。最后还介绍了缓存预热、缓存更新以及缓存降级等概念。在缓存的应用中还有一个比较典型的问题就是如何保证数据的一致性问题,也欢迎大家参考我的另一篇博文Redis缓存和MySQL数据一致性方案详解参考[1]https://www.pieruo.com/13549.html[2]https://www.cnblogs.com/midoujava/p/11277096.html
LoveIT 2021-01-18Redis -
充分理解跨域问题并解决跨域问题
现在越来越多的项目就算是一个管理后端也偏向于使用前后端分离的部署方式去做,为了顺应时代的潮流,前后端分离就产生了跨域问题,所以许多同学把跨域和前后端分离项目联系在了一起,其实跨域产生的原因并不是前后端分离导致的,那我们一起来看一下,希望可以靠这一篇文章解答大家所有的跨域问题。跨域产生的条件使用xmlHttpRequest,即我们通常说的ajax请求浏览器做了这个事访问的域名不同,即访问的html页面是a域名下的,但内部js发送的ajax请求的目标地址却是b域名以上三个条件缺一不可,尤其是第三个条件许多做移动端的同学可能都没有听过,因为移动端爽爽的用各种http请求狂发不同的域名,但是浏览器不允许我们这么做,为了一个词如何解决跨域问题解决跨域问题的根本就是要打破上述的三个限制中的任何一个,我们来看一下逐个击破的方式JSONP方式jsonp是打破第一重限制,用了XMLHttpRequest就跨域,那我不用这种方式了,我们怎么做的,来看一段jquery的带jsonp的ajax请求看似用了ajax请求,其实内部完全不是那么回事,多了jsonp和jsonpCallback选项,它内部将代码翻译并把页面上的dom操作成这样这个时候,html页面的scriptsrc标签回去访问api.map.baidu.com的服务端,由于script,img这种标签浏览器是不受xmlhttprequest限制的,可以随意访问,这个时候对应的后端代码取得address参数,最后根据双方约定好的callback参数,返回一个被包装后的json,即然后浏览器直接执行了对应的这个showLocation()…等等,这个不就相当于执行了我们上面定义的window.showLocation方法并且传入了我们需要的json返回吗,那我们的ajaxsuccess方法里就可以得到这个返回类型了,并且没有跨域,是不是很精妙。CORSCORS是一个W3C标准,全称是"跨域资源共享"(Cross-originresourcesharing)跨域资源共享CORS详解。这个玩样用于“破解”掉浏览器的限制,说是破解其实也是浏览器认识到了一些头部就放行了的意思,需要在http的response内多设置几个头部Access-Control-Allow-Origin:*表明允许所有的origin(浏览器的html页面路径)访问,而并非是同源的originAccess-Control-Request-Method:*表明允许所有的httprequest头,访问,因为浏览器在触发如下几个场景会在发送真正的数据前发送options这样的预检请求检测,一旦预检通过后才会发送真正的get或post数据请求,这个时候我们按照cors的设置就需要允许对应的method访问,触发的几种情况包括1:请求的方法不是GET/HEAD/POST2:POST请求的Content-Type并非application/x-www-form-urlencoded,multipart/form-data,或text/plain3:请求设置了自定义的header字段等Access-Control-Allow-Headers:*设置所有header均可以被允许,这个配置联通上述的requestmethodoptions检测一起使用,可以在需要自定义header的场景下使用Access-Control-Allow-Credentials:true这个参数只有当需要跨域使用cookie传递时才需要设置为true,并且需要前端ajax配置使用xhrField:{withCredential:true}时才能传递cookie,另外safari和最新版本的chrome浏览器还需要在设置内放开对应限制,当这个参数被设置成true时候Access-Control-Allow-Origin就不能设置为*,否则就变成任何origin域都能允许传递cookie了,可将其调整为前端origin字段传什么我就用什么若你使用的是nginx反向代理,则可以直接在nginx反向代理上配置代理法打破不同源的限制,我只要让它同源就可以了,比如要我的静态页面是http://a.com/index.html动态ajax请求访问的是http://b.com/api/***我只需要将对应的服务部署在不同的机器上,然后使用一个公共的c.com的域名作为nginx反向代理的入口域名,在将静态服务和动态服务分别挂在后面的被代理局域网服务器内,修改配置
LoveIT 2020-12-15Nginx -
管理分布式会话的四种方式以及基于Redis的分布式会话实现方案
应用服务器的高可用架构设计最为理想的是服务无状态,但实际上业务总会有状态的,以session记录用户信息的例子来讲,未登入时,服务器没有记入用户信息的session访问网站都是以游客方式访问的,账号密码登入网站后服务器必须要记录你的用户信息记住你是登入后的状态,以该状态分配给你更多的权限。那么管理session有哪些方法呢?一、四种分布式Session管理方案1、Session复制session复制是早期企业应用系统使用比较多的一种服务器集群Session管理机制。应用服务器开启Web容器的的Session复制功能,在集群中的几台服务器之间同步Session对象,是的每台服务器上都保存所有用户的Session信息,这样任何一台机器宕机都不会导致Session数据的丢失,而服务器使用Session时候,也只需要在本机获取即可。如图1所示。这种方案简单,且从本机读取session也相当快捷,但有非常明显的缺陷:只能使用在集群规模比较小的情况下(企业应用系统,使用人数少,相对比较常见这种模式),当集群规模比较大的时候,集群服务器之间需要大量的通信进行Session的复制,占用服务器和网络的大量资源,系统负担较大。而且由于用户的session信息在每台服务器上都有备份,在大量用户访问下,可能会出现服务器内存都还不够session使用的情况。2.session会话保持(黏滞会话)会话保持是利用负载均衡的原地址Hash算法实现,负载均衡服务器总是将来源于同一IP的请求分发到同一台服务器上,,也可以根据cookie信息将同一个用户的请求每次都分发到同一台服务器上,不过这时的负载均衡服务器必须工作在HTTP协议层上。这种会话保持也叫黏滞会话(StickySessions)在Nginx中配置的会话保持:这种方案虽然保证了每个用户都能准确的拿到自己的session,而且大量用户访问也不怕,但是这种会话保持不符合系统高可用的需求。这种方案有着致命的缺陷:一旦某台服务器发生宕机,则该服务器上的所有session信息就会不存在,用户请求就会切换到其他服务器,而其他服务器因为没有其对应的session信息导致无法完成相关业务。所以这种方法基本上不会被采纳。3.利用cookie记录session 早期的企业应用系统使用C/S架构,管理session的方法就是将session记录在客户端,每次请求服务器的时候将session放在请求中发送给服务器,服务器处理过请求后再将修改过的session返回给客户端。网站虽然没有客户端,但是可以利用浏览器支持的cookie记录session。 利用cookie记录session是存在很多缺点:比如cookie的大小存在限制能记录的信息不能超过限制;比如每次请求都要传输cookie影响性能;比如cookie可被修改或者存在破解的可能,导致cookie不能存重要信息,安全系数不够。但是由于cookie简单易用,支持服务器的线性伸缩,而且大部分的session信息相对较小,所以其实很多网站或多或少的都会使用cookie来记录部分不重要的session信息。4.session服务器(集群)目前最理想的服务器集群的session管理应该是session服务器,集成了高可用、伸缩性好、对保存信息大小没有限制、性能也相对很好。这种统一管理session的方式将应用服务器分离,分为无状态的应用服务器和有状态的session服务器。如下图所示:二、SpringBoot+Redis+Nginx实现分布式Session1、环境准备(1)SpringBoot2.1.8.RELEASE(2)Redis5(3)Nginx1.17.8(4)Tomcat92、实现基本原理使用redis实现session共享是基于session集中存储的实现方案,即把session放在一个公共的redis服务器里,所有Web服务器节点都连接着这个公用redis服务器,从而在请求时从公用的redis里查询存放的session值。这就是实现了session共享。思路在用户登录成功时,把用户的信息设置到redis服务器里,然后每次请求时都在过滤器(或拦截器)里获取该值,若有值继续操作,没值跳转到登录页面重新登录。关于Redis的配置这里就不赘述了,不清楚的小伙伴自行百度或Google~~这里重点说一下登录逻辑中如何处理Session问题以及如果在之后如何验证用户身份第一步、写一个登录拦截器(过滤器),检查用户是否登录,如果没有登录就重定向用户到登录也爱你登录去,如果登录了返回true,放行拦截器编写完成后注意在WebConfiguration的addInterceptor中注册一下:第二步、实现正常登录逻辑,主要就是检查用户密码是否匹配,如果检查没问题的话,就继续下一步第三步、用户名密码检查无误之后,设置cookie值,把作为保存Session信息在redis中的key值存入cookie,刷新浏览器的时候,过滤器可以从cookie中取到key值,进而去redis取对应的value值,即Session完成这一操作,用户的session信息已经存入到redis中,可在redis中查看是否存入参考【1】程序猿中的小白.管理分布式session的四种方式.博客园【2】零度编程.详解Session分布式共享.搜狐【3】任枫丶.Java中使用Redis实现Session共享.CSDN
LoveIT 2020-11-16Redis -
向 Dockerfile 传参 :--build-arg
ARGARG是一个构建时有效的变量。它们只能从在Dockerfile中使用ARG指令开始到生成dockerimage的那一端时间内使用。正在运行的容器无法访问ARG变量的值。这也适用于CMD和Enrypoint指令,它们只是告诉容器在默认情况下应该运行什么。如果您告诉Dockerfile需要各种ARG变量(即从外部传递参数),需要配合--build-arg参数来指定参数值,但是如果在运行dockerbuild命令时没有提供任何变量,那么就会出现错误消息。虽然其在container启动后不再生效,但是使用‘dockerhistory’可以查看到。所以,敏感数据不建议使用ARG.设置ARG和使用ARG编译image,实例如下:之后在shell命令行中我们可以执行如下命令:ENV在Dockerfile中有一个和ARG功能类似的指令ENV,它在构建dockerimag的过程中有效,在image被创建和container启动后作为环境变量依旧也有效,并且可以重写覆盖。printenv可查看其值。下图是ARG和ENV的作用域示意图:设置ENV和使用env,实例如下对于没有设定初始值的ENV变量,我们可以在docker容器启动的时候指定变量的参数:参考【1】Vladislav.DockerARG,ENVand.env-aCompleteGuide.Vsupalov
LoveIT 2020-10-30Docker -
Redis的过期键删除策略以及内存淘汰策略
Redis的数据已经设置了TTL,不是过期就已经删除了吗?为什么还存在所谓的淘汰策略呢?这个原因我们需要从redis的过期策略聊起。一、Redis过期键策略redis键的过期时间都保存在过期字典中,过期键的删除策略有3种:定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即删除对键的删除操作。惰性删除:放任键过期不管,但每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键。如果没有过期,就返回该键。定期删除:每隔一段时间,程序对数据库进行一次检查,删除里面的过期键。至于删除多少过期键,以及检查多少数据库,有算法决定。1、定时删除特点优点:对内存友好,通过定时器可以保证过期键过期键会尽可能快的删除,并释放过期键占用的空间。缺点:1)cpu不友好,在过期键比较多的情况下,删除过期键可能会占用相当一部分cpu时间;在内存不紧张cpu紧张的情况下,将cpu时间用在删除和当前任务无关的过期键上,无疑会对服务器响应时间和吞吐量造成影响。2)创建定时器需要Redis服务器中的时间事件,而现在时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N),并不能高效的处理大量时间事件。2、惰性删除优点:1)对cpu友好,程序只在取出键时才对建进行过期检查,删除的目标仅限于当前处理的键。缺点:1)对内存不友好,当数据库中有大量的过期键,而这些键又没有被访问到,那么他们也许会永远不会被删除。3、定期删除定期删除是前两种删除策略的一种折中。会每隔一段时间执行一次删除过期键操作,并通过限制操作执行的时长和频率来减少删除操作对cpu时间的影响。定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多地消耗在删除过期键上面如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况二、Redis内存淘汰策略Redis内存淘汰策略有了以上过期策略的说明后,就很容易理解为什么需要淘汰策略了,因为不管是定期采样删除还是惰性删除都不是一种完全精准的删除,就还是会存在key没有被删除掉的场景,如果大量过期key堆积在内存里,导致redis内存块耗尽了,怎么办?所以就需要内存淘汰策略进行补充,内存淘汰策略就是当redis使用的内存到达了最大内存阈值之后移除键的一种策略。Redis有如下几种内存淘汰策略:volatile-lru:从所有设置了过期时间的key中,选择删除最近最少使用的keyallkeys-lru:从所有的key中选择删除最近最少使用的keyvolatile-lfu:从所有设置了过期时间的key中,选择删除使用频率最低的keyallkeys-lfu:从所有key中选择删除使用频率最低的keyvolatile-random:从所有设置了过期时间的key中,随机选择删除keyallkeys-random:从所有key中随机选择删除keyvolatile-ttl:从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰。noeviction:当内存达到阈值的时候,新的写入操作报错总结Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。参考【1】幂次方.彻底弄懂Redis的内存淘汰策略.博客园【2】小小码农甲.Redis的过期策略和内存淘汰策略.简书
LoveIT 2020-10-10Redis -
Redis五种常见数据结构的实现及使用场景
一、Redis对象底层数据结构Redis的八种编码类型,如下表所示:编码类型编码所对应的底层数据结构REDIS_ENCODING_INT8字节的long长整型REDIS_ENCODING_EMBSTRembstr编码的简单动态字符串REDIS_ENCODING_RAW简单动态字符串REDIS_ENCODING_HT字典REDIS_ENCODING_LINKEDLIST双端链表REDIS_ENCODING_ZIPLIST压缩列表REDIS_ENCODING_INTSET整数集合REDIS_ENCODING_SKIPLIST跳跃表和字典1、SDS(简单动态字符串)字符串对象的编码可以是int、raw或者embstr(专门保存段字符串的优化编码方式)1.1、raw重点说一下SDS,当字符串长度大于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(39)(39字节)的时候,底层实现为SDS,encoding编码设置为rawRedis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simpledynamicstring,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示.在Redis里面,C字符串只会作为字面量(stringliteral)用在一些无须对字符串值进行修改的地方。在一个可以被修改的字符串值里面,Redis就会使用SDS来表示字符串值,比如:Redis数据库里面,包含字符串值得健值对在底层得实现都是由SDS实现的注意:更正一下,buf在Redis中实质是一个字节数组,它里面保存的是二进制数据。SDS与C字符串的区别(1)常数O(1)复杂度获取字符串长度C字符串不记录自身的长度信息,获取字符串长度时会遍历字节数组,直到遇到空字符为止.复杂度为O(N)SDS直接通过len属性获取字符串长度.复杂度为O(1)(2)杜绝缓冲区溢出C字符串不记录自身长度,修改字符串时不会判断本身是否拥有足够的内存空间,当内存空间不足时,则会造成缓冲区的溢出.SDS对字符串进行修改时,先检查内存空间是否满足修改的需要,若不满足,则自动扩展SDS的内存空间.所以使用SDS既不需要手动修改内存空间的大小,也不会出现缓冲区溢出的情况.(3)空间预分配第一次创建字符串对象时,SDS不会分配冗余空间,即len=0,当SDS的API修改SDS时,则会为其分配冗余空间.当修改后的SDS的len属性小于1MB时,则为其分配和len同样大小的冗余空间,即free=len,此时buf[]的实际长度=len(实际长度)+free(冗余空间)+1(空字符)当修改后的SDS的len属性大于等于1MB时,则为其分配1MB的冗余空间.buf[]的实际长度=len(实际长度)+free(1MB)+1(空字符)(4)惰性空间释放SDS的API缩短SDS的字符串时,不会立即使用内存分配回收缩短后多出来的字节,而是记录在free属性中,并等待将来使用.(5)二进制安全C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。SDS的API都是二进制安全的。所有SDSAPI都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。这也是我们将SDS的buf属性称为字节数组的原因——Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。1.2、embstr从Redis3.0版本开始字符串引入了EMBSTR编码方式,长度小于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(3.2以前是39字节、3.2以后是44字节)的字符串将以EMBSTR方式存储。EMBSTR方式的意思是embeddedstring,字符串的空间将会和redisObject对象的空间一起分配,两者在同一个内存块中。Redis中内存分配使用的是jemalloc(内存分配器),jemalloc分配内存的时候是按照8,16,32,64作为chunk的单位进行分配的。为了保证采用这种编码方式的字符串能被jemalloc分配在同一个chunk中,该字符串长度不能超过64,故字符串长度限制OBJ_ENCODING_EMBSTR_SIZE_LIMIT我们就可以来计算一下在redis3.2版本以前structSDS{unsignedintcapacity;//4byteunsignedintlen;//4bytebyte[]content;//内联数组,长度为capacity}这里的unsignedint一个4字节,加起来是8字节.内存分配器jemalloc分配的内存如果超出了64个字节就认为是一个大字符串,就会用到raw编码。前面提到SDS结构体中的content的字符串是以字节\0结尾的字符串,之所以多出这样一个字节,是为了便于直接使用glibc的字符串处理函数,以及为了便于字符串的调试打印输出。所以我们还要减去1字节64byte-16byte-8byte-1byte=39byte在redis3.2版本之后这里unsignedint变成了uint8_t、uint16_t的形式,还加了一个int8flags标识,总共只用了3个字节的大小。相当于优化了sds的内存使用,相应的用于存储字符串的内存就会变大。然后进行计算:64byte-16byte-3byte-1byte=44byte。所以,redis3.2版本之后embstr最大能容纳的字符串长度是44,之前是39。长度变化的原因是SDS中内存的优化2、intint编码以整数的方式保存字符串数据,即使在写的时候加上了引号,Redis也会把这些数字当作整数类型来保存。0-10000之间的OBJ_ENCODING_INT编码的字符串对象将进行共享。注意:当整数的范围超过long了,还是会使用embstr或raw来保存。3、双向链表Redis中的双链表是这样式的:每个节点都是一个listNode,拥有前驱节点,后继节点和值。这就是C语言中的双向链表只要有多个节点就可以组成一个链表了,但是redis再在外面封装了一层,也就是使用adlist.h/list来实现,这样操作起来更加方便。链表结构比较简单,这里不多说了。4、ziplist(压缩列表)压缩列表。redis的列表键和哈希键的底层实现之一。此数据结构是为了节约内存而开发的。和各种语言的数组类似,它是由连续的内存块组成的,这样一来,由于内存是连续的,就减少了很多内存碎片和指针的内存占用,进而节约了内存。Emtry结点的内部结构是这样的:元素的遍历先找到列表尾部元素:然后再根据ziplist节点元素中的previous_entry_length属性,来逐个遍历:连锁更新再次看看entry元素的结构,有一个previous_entry_length字段,他的长度要么都是1个字节,要么都是5个字节:前一节点的长度小于254字节,则previous_entry_length长度为1字节前一节点的长度大于254字节,则previous_entry_length长度为5字节5、哈希表哈希表的结构是这样的:蓝色部分很好理解,数组就是bucket,一般不同的key首先会定位到不同的bucket,若key重复,就用链表把冲突的key串起来。熟悉HashMap的同学应该不陌生,这个结构和HashMap的结构几乎一样,就连处理冲突的的方式都是采用一样的方式:拉链法。rehash再来看看哈希表总体图中左边橘黄色的部分,其中有两个关键的属性:ht和rehashidx。ht是一个数组,有且只有俩元素ht[0]和ht[1];其中,ht[0]存放的是redis中使用的哈希表,而ht[1]和rehashidx与哈希表的扩容有关,具体来说,ht[1]自相的是扩容后的hash表。rehash指的是重新计算键的哈希值和索引值,然后将键值对重排的过程。加载因子(loadfactor)=ht[0].used/ht[0].size。扩容和收缩标准扩容:没有执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于等于1。正在执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于等于5。收缩:加载因子小于0.1时,程序自动开始对哈希表进行收缩操作。扩容和收缩的数量扩容:第一个大于等于ht[0].used*2的2^n(2的n次方幂)。收缩:第一个大于等于ht[0].used的2^n(2的n次方幂)。扩容过程收缩过程渐进式rehash上面说到,扩容或者收缩哈希表时需要将ht[0]中的所有键值对迁移到ht[1]中,如果ht[0]中的数据量不是很大,几十几百甚至几万个,对于redis来说都不是大问题;但是,如果hash表中存储的键值对数量是几百万、几千万甚至上亿的数据,那么要一次性将这些键值对迁移到ht[1]中,庞大的数据可能会使redis服务器在一段时间内停止服务。因此为了避免rehash对服务器性能造成影响,服务器不会一次将ht[0]中的数据迁移到ht[1]中,而是分多次,渐进式的完成。以下是哈希表渐进式rehash的详细步骤:1)为ht[1]分配空间,让字段同事持有ht[0]和ht[1]两个hash表。2)在字典中维持一个索引计数器变量rehashidx,并将他的值设置为0,表示rehash开始。3)在rehash期间,每次对ht[0]字典执行添加、删除、查找或者更新操作时,程序除了执行指定操作外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对迁移到ht[1]上,完成后,rehashidx值+1。4)随着字典操作不断进行,最总在某个时间点上,ht[0]上的所有键值对都将会被rehash到ht[1],这是程序设置rehashidx为-1,表示rehash操作完成。渐进式rehash的好处在于它采用分而治之的方式,将rehash键值对所需的计算工作量均摊到了对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash带来的性能影响。渐进式refresh和下图中左边黄色的部分中的rehashidx密切相关:rehashidx的数值就是现在rehash的元素位置rehashidx等于-1的时候说明没有在进行refresh甚至在进行期间,每次对哈希表的增删改查操作,除了正常执行之外,还会顺带将ht[0]哈希表相关键值对rehash到ht[1]。已扩容过程为例:6、intset整数集合是集合键的底层实现方式之一。7、skiplist(跳表)skiplist是一种基于有序列表发展而来的数据数据结构,他可以支持平均O(logN),最坏O(N)的时间复杂度。大部分情况下,skiplist的效率可以和平衡树相媲美,而且skiplist的实现更加简单,只要你能熟练操作链表,就能轻松实现一个skiplist。有序表的搜索考虑一个有序表:从该有序表中搜索元素<23,43,59>,需要比较的次数分别为<2,4,6>,总共比较的次数为2+4+6=12次。有没有优化的算法吗?链表是有序的,但不能使用二分查找。类似二叉搜索树,我们把一些节点提取出来,作为索引。得到如下结构:这里我们把<14,34,50,72>提取出来作为一级索引,这样搜索的时候就可以减少比较次数了。我们还可以再从一级索引提取一些元素出来,作为二级索引,变成如下结构:元素越多skiplist的优势就越明显skiplist跳表具有如下性质:(1)由很多层结构组成,支持平均O(logN),最坏O(N)的时间复杂度(2)每一层都是一个有序的链表(3)最底层(Level1)的链表包含所有元素(4)如果一个元素出现在Leveli的链表中,则它在Leveli之下的链表也都会出现。(5)每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。插入操作上图所示的skiplist有9个结点,一共4层,可以说是理想的跳跃表了,不过随着我们对跳跃表进行插入/删除结点的操作,那么跳跃表结点数就会改变,意味着跳跃表的层数也会动态改变。这里我们面临一个问题,就是新插入的结点应该跨越多少层?这个问题已经有大牛替我们解决好了,采取的策略是通过抛硬币来决定新插入结点跨越的层数:每次我们要插入一个结点的时候,就来抛硬币,如果抛出来的是正面,则继续抛,直到出现负面为止,统计这个过程中出现正面的次数,这个次数作为结点跨越的层数。例如,我们要插入结点3,4,通过抛硬币知道3,4跨越的层数分别为0,2(层数从0开始算),则插入后skiplist如下:删除操作解决了插入之后,我们来看看删除,删除就比较简单了,例如我们要删除4,那我们直接把4及其所跨越的层数删除就行了redis中把跳表抽象成如下所示:看这个图,左边黄色部分:header:跳表表头tail:跳表表尾level:层数最大的那个节点的层数length:跳表的长度右边蓝色部分:表头:是链表的哨兵节点,不记录主体数据。是个双向链表分值是有顺序的o1、o2、o3是节点所保存的成员,是一个指针,可以指向一个SDS值。层级高度最高是32。每次创建一个新的节点的时候,程序都会随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是“高度”二、Redis五种数据结构的实现Redis对象头结构体redis中并没有直接使用以上所说的各种数据结构来实现键值数据库,这里我们首先来了解一下Redis对象头结构体,所有的Redis对象都有下面的这个结构头:不同的对象具有不同的类型**type(4bit)**,同一个类型的type会有不同的存储形式encoding(4bit),为了记录对象的LRU信息,使用了24个bit来记录LRU信息。每个对象都有个引用计数refcount(4字节),当引用计数为零时,对象就会被销毁,内存被回收。*ptr指针将指向对象内容(body)的具体存储位置。这样一个RedisObject对象头就占用16字节的空间。1、字符串(string)字符串对象的编码可以是int、raw或者embstr。如果一个字符串的内容可以转换为int,那么该字符串就会被转换成为int类型,对象的ptr就会指向该int,并且对象类型也用int类型表示。普通的字符串有两种,embstr和raw。embstr应该是Redis3.0新增的数据结构,在2.8中是没有的。如果字符串对象的长度小于39字节,就用embstr对象。使用场景:缓存功能:字符串最经典的使用场景,redis做为缓存层,Mysql作为储存层,绝大部分请求数据都是redis中获取,由于redis具有支撑高并发特性,所以缓存通常能起到加速读写和降低后端压力的作用。计数器:许多运用都会使用redis作为计数的基础工具,他可以实现快速计数、查询缓存的功能,同时数据可以一步落地到其他的数据源。如:视频播放数系统就是使用redis作为视频播放数计数的基础组件。共享session:出于负载均衡的考虑,分布式服务会将用户信息的访问均衡到不同服务器上,用户刷新一次访问可能会需要重新登录,为避免这个问题可以用redis将用户session集中管理,在这种模式下只要保证redis的高可用和扩展性的,每次获取用户更新或查询登录信息都直接从redis中集中获取。限速:处于安全考虑,每次进行登录时让用户输入手机验证码,为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率。2、listlist对象的编码可以是ziplist或者linkedlist。ziplist是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。但当数据量过大时就ziplist就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。当list对象同时满足以下两个条件时,列表对象使用ziplist编码:(1)列表对象保存的所有字符串元素的长度都小于64字节(2)列表对象保存的元素数量小于512个当有任一条件不满足时将会进行一次转码,使用linkedlist。使用场景:消息队列:redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端是用lupsh从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞时的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性3、hash哈希对象的底层实现可以是ziplist或者hashtable。zipList编码的哈希对象使用压缩列表作为底层实现,每当有新的健值对要加入到哈希对象时,程序会先将保存了key的压缩列表节点推入到压缩列表尾,然后再将保存了value的压缩节点推入到压缩列表尾,因此:保存了同一key:vlaue对的两个节点总是紧挨在一起,保存key的节点在前,保存value的节点在后,如下图所示:当对象数目不多且内容不大时,这种方式效率是很高的。此时redis会使用哈希表(hashtable)作为底层数据结构hashtable编码的哈希独享使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典健值对来保存。字典的每个键(key)和每个值(value)都是一个字符串对象,对象中保存了键或值。这种结构的时间复杂度为O(1),但是会消耗比较多的内存空间。当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:当键的个数小于hash-max-ziplist-entries(默认512)当所有值都小于hash-max-ziplist-value(默认64)使用场景:哈希结构相对于字符串序列化缓存信息更加直观,并且在更新操作上更加便捷。所以常常用于用户信息等管理,但是哈希类型和关系型数据库有所不同,哈希类型是稀疏的,而关系型数据库是完全结构化的,关系型数据库可以做复杂的关系查询,而redis去模拟关系型复杂查询,开发困难,维护成本高。4、setset对象的编码可以是intset或者hashtable。intset是一个整数集合,里面存的为某种同一类型的整数,支持如下三种长度的整数:intset编码使用的条件:(1)集合对象保存的元素全部都是整数(2)集合对象保存的元素不超过512个不满足以上条件,集合对象需要使用hashtable注:第二个条件的上限可以修改,set-max-intset-entries默认值为512。表示如果entry的个数小于此值,则可以编码成REDIS_ENCODING_INTSET类型存储,节约内存。否则采用dict的形式存储。使用场景:标签(tag):集合类型比较典型的使用场景,如一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴趣,这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签,这些数据对于用户体验以及曾强用户粘度比较重要。(用户和标签的关系维护应该放在一个事物内执行,防止部分命令失败造成数据不一致)sadd=tagging(标签)生成随机数,比如抽奖:spop/srandmember=randomitemsadd+sinter=socialGraph(社交需求)5、zsetzset的编码有两种,一种是ziplist,另一种是skiplist与dict的结合。ziplist编码的压缩对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个原属则保存元素的分值(score)压缩列表内的集合原属按分值从小到大进行排序,分值较小的元素放置在靠近表头的方向,而分值较大的元素则放置在靠近表位的方向ziplist编码的使用条件:(1)有序集合保存的元素数量小于128个(2)有序集合保存的所有元素成员长度小于64字节上限值可以根据配置文件中的配置进行调整skiplist是一种跳跃表,它实现了有序集合中的快速查找,在大多数情况下它的速度都可以和平衡树差不多。但它的实现比较简单,可以作为平衡树的替代品。它的结构比较特殊。使用场景:排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。参考【1】妖四灵.Shuen.Redis5种数据结构(底层原理,性能分析,使用场景.CSDN)【2】RyuGou.图解redis五种数据结构底层实现(动图版).今日头条【3】唐宋缘明卿_cris.redis五种数据结构的实现及使用场景.CSDN【4】Redis---String.码迷
LoveIT 2020-10-06Redis -
docker网络模型
一、docker网络基础知识Docker在启动时会创建一个虚拟网桥docker0,默认地址为172.17.0.1/16,容器启动后都会被桥接到docker0上,并自动分配到一个IP地址。使用ipaddr或ifconfig命令可以看到:Docker容器在创建的时候有四种网络模型,如果没有指定,默认使用是birdge模式,其余三种需要使用-net显式指定。bridge模式:使用–net=bridge指定,默认设置。docker网络隔离基于网络命名空间,在物理机上创建docker容器时会为每一个docker容器分配网络命名空间,并且把容器IP桥接到物理机的虚拟网桥上。none模式:使用–net=none指定。此模式下创建容器是不会为容器配置任何网络参数的,如:容器网卡、IP、通信路由等,全部需要自己去配置。host模式:使用–net=host指定。此模式创建的容器没有自己独立的网络命名空间,是和物理机共享一个NetworkNamespace,并且共享物理机的所有端口与IP,并且这个模式认为是不安全的。container模式:用–net=container:容器名称或ID指定。此模式和host模式很类似,只是此模式创建容器共享的是其他容器的IP和端口而不是物理机,此模式容器自身是不会配置网络和端口,创建此模式容器进去后,你会发现里边的IP是你所指定的那个容器IP并且端口也是共享的,而且其它还是互相隔离的,如进程等。以下是docker网络初始化的过程:详细过程:二、docker网络模型详解1、桥接模式在bridge模式下,连在同一网桥上的容器可以相互通信(如果为了安全考虑,也可以设置禁止它们通信),容器也可以与外部通信,数据包从容器出来,由于容器是桥接到docker0上,所以数据包会发到docker0上,查看iptables的策略,会发现有一个策略,将源地址从172.17.0.1的包进行源地址转换,转化成宿主机的网卡地址,(注意ipfoward功能要开启)因此只要宿主机可以与外部通信,那么容器就可以与外部通信。Bridge桥接模式的实现步骤如下:DockerDaemon利用vethpair技术,在宿主机上创建两个虚拟网络接口设备,假设为veth0和veth1。而vethpair技术的特性可以保证无论哪一个veth接收到网络报文,都会将报文传输给另一方。DockerDaemon将veth0附加到DockerDaemon创建的docker0网桥上。保证宿主机的网络报文可以发往veth0DockerDaemon将veth1添加到DockerContainer所属的namespace下,并被改名为eth0。如此一来,保证宿主机的网络报文若发往veth0,则立即会被eth0接收,实现宿主机到DockerContainer网络的联通性;同时,也保证DockerContainer单独使用eth0,实现容器网络环境的隔离性。桥接模式下的DockerContainer在使用时,并非为开发者包办了一切。最明显的是,该模式,DockerContainer不具有一个公有IP,即和宿主机的eth0不处于同一个网段。导致的结果是宿主机以外的世界不能直接和容器进行通信。虽然NAT模式经过中间处理实现了这一点,但是NAT模式仍然存在问题与不便,如:容器均需要在宿主机上竞争端口,容器内部服务的访问者需要使用服务发现获知服务的外部端口等。另外NAT模式由于是在三层网络上的实现手段,故肯定会影响网络的传输效率。2、Host网络模式:使用与宿主机相同的命名空间host模式是bridge桥接模式很好的补充。采用host模式的DockerContainer,可以直接使用宿主机的IP地址与外界进行通信,若宿主机的eth0是一个公有IP,那么容器也拥有这个公有IP。同时容器内服务的端口也可以使用宿主机的端口,无需额外进行NAT转换。当然,有这样的方便,肯定会损失部分其他的特性,最明显的是DockerContainer网络环境隔离性的弱化,即容器不再拥有隔离、独立的网络栈。另外,使用host模式的DockerContainer虽然可以让容器内部的服务和传统情况无差别、无改造的使用,但是由于网络隔离性的弱化,该容器会与宿主机共享竞争网络栈的使用;另外,容器内部将不再拥有所有的端口资源,原因是部分端口资源已经被宿主机本身的服务占用,还有部分端口已经用以bridge网络模式容器的端口映射。3、Container网络模式(1)查找othercontainer(即需要被共享网络环境的容器)的网络namespace;(2)将新创建的DockerContainer(也是需要共享其他网络的容器)的namespace,使用othercontainer的namespace。DockerContainer的othercontainer网络模式,可以用来更好的服务于容器间的通信。在这种模式下的DockerContainer可以通过localhost来访问namespace下的其他容器,传输效率较高。虽然多个容器共享网络环境,但是多个容器形成的整体依然与宿主机以及其他容器形成网络隔离。另外,这种模式还节约了一定数量的网络资源。但是需要注意的是,它并没有改善容器与宿主机以外世界通信的情况。4、None网络模式网络环境为none,即不为DockerContainer任何的网络环境。一旦DockerContainer采用了none网络模式,那么容器内部就只能使用loopback网络设备,不会再有其他的网络资源。可以说none模式为DockerContainer做了极少的网络设定,在没有网络配置的情况下,作为Docker开发者,才能在这基础做其他无限多可能的网络定制开发。这也恰巧体现了Docker设计理念的开放。在none网络模式下分配固定ip:netns是在linux中提供网络虚拟化的一个项目,使用netns网络空间虚拟化可以在本地虚拟化出多个网络环境,目前netns在lxc容器中被用来为容器提供网络。使用netns创建的网络空间独立于当前系统的网络空间,其中的网络设备以及iptables规则等都是独立的,就好像进入了另外一个网络一样。容器间互联:--link参数可以在不映射端口的前提下为两个容器间建立安全连接,--link参数可以连接一个或多个容器到将要创建的容器,–link表示添加连接到另一个容器。--link参数的格式为--linkname:alias,其中name是要链接的容器的名称,alias是这个连接的别名。
LoveIT 2020-09-22Docker -
提升10倍生产力: IDEA集成Docker一键部署Spring Boot项目(war包部署)
1、准备工作1.1在远程CentOS服务器上已经安装好了docker。可以参考:1.官方手册:https://docs.docker.com/install/2.我的笔记:手把手教你在Centos7上安装和使用Docker1.2配置docker远程连接端口图1.2.12375端口正在监听注意:在实际线上环境中千万不能这么干,这么干分分钟你的机器就会被肉鸡~~~1.3Idea安装docker插件,重启在比较新的IDEA中是默认安装Docker插件的,只需要点击File->Settings->Build,Execution,Deployment->Docker,如果发现有Docker,那就可以直接进入下一步;如果是比较老的IDEA版本可以在IDEA的插件安装中搜索docker,安装并重启装即可使用!图1.3.1安装Docker插件1.4idea连接远程Docker上面的一切操作都没毛病后,接下来还是依次点击File->Settings->Build,Execution,Deployment->Docker打开docker,配置远程主机的连接信息:填写Name,选择tcpsocket填写EngineAPIURL,填好后会自动连接,连接成功会提示Connectionsuccessful,如图:图1.4.1配置docker连接信息注意:这一步如果是在远程云服务器上操作时,请注意还要在安全组中开启2375端口连接成功后点击Apply->OK,之后IDEA就会在界面的下方菜单栏把docker服务列出来,我们双击就可以连接上docker,并操作管理docker。图1.4.2双击连接docker2、准备项目这个没啥好说的,并且也不是本文的重点,此处不再赘述。3、配置Dockerfile文件3.1Dockerfile文件的配置接下来在我们项目的根目录下新建一个取名为Dockerfile的文件,用于构建镜像:图3.1新建Dockerfile文件**文件内容如下**:3.2执行Dockerfile文件生成镜像在IDEA的右上方的Run/DebugConfigurations中做如下配置:图3.2.1配置执行Dockerfile文件配置好后使用mvncleanpackage-Dmaven.test.skip=true命令打包项目,然后再点击运行刚才配置的Dockerfile文件:如图所示:图3.2.2运行按钮运行日志:图3.2.3运行日志截图(部分)运行成功后就会生成目标镜像并上传到目标服务器中:图3.2.3成功构建镜像4、启动应用图4.1创建镜像之后配置镜像启的参数:镜像ID、容器名、端口映射、是否后台启动......。这里由于我的项目中使用到了redis和mysql,因此需要使用--link来实现docker容器内通信图4.2配置启动参数点击Run之后就会看到下面的画面:图4.3启动成功浏览器中访问121.36.49.86/user/loginPage,来到我项目的登录页面,docker和IDEA集成使用成功!此后发布项目将会变得非常简单快捷,而且借助于强大的Docker可以做到持续集成和弹性扩容!图4.4浏览器访问项目登录页面
LoveIT 2020-07-16Docker -
MyBatis源码学习—MyBatis结果处理器ResultSetHandler详解
上一篇博文MyBatis源码学习—MyBatis数据仓库会话器StatementHandler详解我们通过Statement将SQL发送到了数据库,并返回了ResultSet,接下来就是将结果集ResultSet自动映射成实体类对象。这样使用者就无需再手动操作结果集,并将数据填充到实体类对象中。这可大大降低开发的工作量,提高工作效率。1、ResultSetHandler简介回想一下,一条SQL的请求在MyBatis中会经过哪几个步骤?(1)首先会经过Executor,一般默认的配置是SimpleExecutor,Eexcutor负责创建并维护一个StatementHandler对象,之后直接调用StatementHandler中的增删改查方法即可;(2)StatementHandler有三种类型的:简单类型的SimpleStatementHandler、预编译SQL类型的PrepraedStatementHandler以及执行存储过程的CallabletatenmentHandler,他们内部分别维护了一个Statement对象、PrepraedStatementHandler对象以及CallableStatementHandler对象,另外在ParameterStatementHandler和CallabletatenmentHandler还维护一个ParameterHandler对象用于解析参数。说白了,MyBatis最终还是通过JDBC中的Statement对象、PrepraedStatement对象以及CallableStatement对象和数据库交互的。(3)查询数据库返回结果之后就需要把查询的结果通过某个东东映射成为实体类对象并且以Mapper方法指定的类型返回结果集,这个东东就是今天我们要了解的神器**ResultSetHandler**下图所示就是ResultSetHnalder接口的方法。ResultSetHandler接口,和ParameterHandler一样,ResultSetHandler也只有一个默认实现类DefaultResultSetHandler2、ResultSetHandler对象的创建ResultSetHandler对象是在创建StatementHandler对象的同时被创建,由Configuration对象负责创建,以BaseStatementHandler为例具体创建逻辑在Configuration类中的newResiltSetHandler方法中,如下图所示。可以看到,关键过程非常简单,就是直接new了一个默认ResultSetHandler实现类DefaultResultSetHandler3、ResultSetHandler处理结果映射(1)映射结果处理过程我们来看看上次看源码的位置,如下图所示源码就是SimpleStatementHandler中query方法源码。在StatementHanlder借助Statement查询到结果后,直接调用了ResultSetHandler的handleResultSets方法来处理返回的结果集。从上面的源码可以看到,在StatementHandler中还维护了一个ResultSetHandler对象。上面我说到过,ResultSetHandler只有一个默认实现类DefaultResultSetHandler,ResultSetHandler主要负责处理两件事:处理Statement执行后产生的结果集,生成结果列表处理存储过程执行后的输出参数按照Mapper文件中配置的ResultType或ResultMap来封装成对应的对象,最后将封装的对象返回即可。来看一下主要的源码:在实际运行过程中,通常情况下一个Sql语句只返回一个结果集,对多个结果集的情况不做分析。实际很少用到。继续看handleResultSet方法通过handleRowValues映射ResultSet结果,最后映射的结果会在defaultResultHandler的ResultList集合中,最后将结果加入到multipleResults中就可以返回了,我们继续跟进handleRowValues这个核心方法我们可以通过resultMap.hasNestedResultMaps()判断查询语句是否是嵌套查询,如果resultMap中包含和且其select属性不为空,则为嵌套查询。本文先分析简单的映射:(2)ResultListMyBatis默认提供了RowBounds用于分页,从上面的代码中可以看出,这并非是一个高效的分页方式,是查出所有的数据,进行内存分页。除了使用RowBounds,我们可以使用一些第三方分页插件进行分页,比如PageHelper。重要的逻辑已经注释出来了。分别如下:创建实体类对象自动映射结果集中有的column,但resultMap中并没有配置根据节点中配置的映射关系进行映射创建实体类对象说明:创建代理类,默认使用Javassist框架生成代理类,但是深入到createProxy方法中最终会发现是通过CGLib实现创建代理对象的。createResultObject重载方法的逻辑:一般情况下,MyBatis会通过ObjectFactory调用默认构造方法创建实体类对象。看看是如何创建的很简单,就是通过反射创建对象(3)结果集映射映射结果集分为两种情况:一种是自动映射(结果集有但在resultMap里没有配置的字段),在实际应用中,都会使用自动映射,减少配置的工作。自动映射在Mybatis中也是默认开启的。第二种是映射ResultMap中配置的,这两中映射我们分开来看自动映射自动映射就是MyBatis根据实体类中的字段,把查询的结果自动的映射到字段属性上,然后封装成一个对象返回数据。首先是获取**UnMappedColumnAutoMapping**集合,然后遍历该集合,并通过TypeHandler从结果集中获取数据,最后再将获取到的数据设置到实体类对象中。UnMappedColumnAutoMapping是DefaultResultSetHandler的内部类,用于记录没有在配置在节点中的映射关系。它的代码如下:UnMappedColumnAutoMapping仅仅就是用于记录一些未在resultMap标签中配置的自动映射行(即实体类中的属性)。下面看一下获取UnMappedColumnAutoMapping集合的过程,如下:先来看看从**ResultSetWrapper**中获取未配置在中的列名来看看那loadMappedAndUnmappedColumnNames方法是如何加载列名的。首先是从当前数据集中获取列名集合,然后获取中配置的列名集合。之后遍历数据集中的列名集合,并判断列名是否被配置在了节点中。若配置了,则表明该列名已有映射关系,此时该列名存入mappedColumnNames中。若未配置,则表明列名未与实体类的某个字段形成映射关系,此时该列名存入unmappedColumnNames中。映射result节点接下来分析一下MyBatis是如何将结果集中的数据填充到已配置ResultMap映射的实体类字段中的。从ResultMap获取映射对象ResultMapping集合。然后遍历ResultMapping集合,再此过程中调用getPropertyMappingValue获取指定指定列的数据,最后将获取到的数据设置到实体类对象中。这里和自动映射有一点不同,自动映射是从直接从ResultSet中获取指定列的值,但是通过ResultMap多了一种情况,那就是关联查询,也可以说是延迟查询,此关联查询如果没有配置延迟加载,那么就要获取关联查询的值,如果配置了延迟加载,则返回DEFERED参考资料[1]https://www.cnblogs.com/java-chen-hao/p/11760777.html[2]https://www.cnblogs.com/cxuanBlog/p/11318861.html
LoveIT 2020-07-13MyBatis -
MyBatis源码学习—MyBatis参数处理器ParameterHandler详解
MyBatis的四大组件我们已经了解过两种了:一个是Executor,它在创建SqlSession的时候会被初始化,它是MyBatis解析SQL请求首先会经过的第一道关卡,它的主要作用在于创建缓存,管理StatementHandler的调用,为StatementHandler提供Configuration环境等。StatementHandler组件最主要的作用在于创建Statement对象与数据库进行交流,还会使用ParameterHandler进行参数配置,使用ResultSetHandler把查询结果与实体类进行绑定。这篇文章我们就来学习一下ParameterHandler。1、ParameterHandler简介ParameterHandler相比于其他的组件就简单很多了,ParameterHandler>ParameterHandler相比于其他的组件就简单很多了,ParameterHandler译为参数处理器,负责为PreparedStatement的sql语句参数动态赋值,这个接口很简单只有两个方法ParameterHandler只有一个实现类DefaultParameterHandler,它实现了这两个方法:getParamenterObject():用于读取参数setParamenters():用于对PreparedStatement参数赋值2、ParameterHandler对象的创建参数处理器对象是在创建StatementHandler对象的同时被创建的,由Configuration对象负责创建,以BaseStatementHandler为例在创建ParameterHandler时,需要传入mappedStatement对象,用于读取参数和SQL语句注意:一个BoundSql对象,就代表了一次sql语句的实际执行,而SqlSource对象的责任,就是根据传入的参数对象,动态计算这个BoundSql,也就是Mapper文件中节点的计算,是由SqlSource完成的,SqlSource最常用的实现类是DynamicSqlSourceConfiguration.java上面是Configuration创建ParameterHandler的过程,它实际上是交由LanguageDriver来创建具体的参数处理器,LanguageDriver默认的实现类是XMLLanguageDriver,由它调用DefaultParameterHandler中的构造方法完成ParameterHandler的创建工作3、ParameterHandler解析绑定参数的流程上面我们了解了参数处理器的创建过程,创建完成之后,该进行具体的解析工作,那么ParameterHandler如何解析SQL中的参数呢?SQL中的参数从哪里来的?在PraparedStatementHandler的parameterize方法调用参数解析器的setParameters给PreparedStatement的SQL语句设置参数DefaultParameterHandler.java至此我们可以知道,每一次的调用查询(数据库查询,不走Mybatis缓存),Executor在执行doQuery的时候都会创建一个StatementHandler实例,每个StatementHandler在实例化的时候,都会创建并持有两个处理器即ParameterHandler和ResultSetHandler这样StatementHandler就可以利用ParameterHandler完成预处理语句的参数化设置,以及结果查询出来以后再利用ResultSetHandler处理结果集下图展示的就是一条SQL语句从Executor到ParameterHandler的关键流程:
LoveIT 2020-07-12MyBatis -
MyBatis源码学习—MyBatis数据仓库会话器StatementHandler详解
1、StatementHandler对象的创建过程在上一节MyBatis源码学习—MyBatis执行器Executor详解中我们了解MyBatis执行器的产生以及执行SQL的大致过程。StatementHandler对象是在SqlSession对象接收到SQL执行命令时,由Configuration对象中的newStatementHandler负责调用的,也就是说Configuration中的newStatementHandler是由执行器中的查询、更新(插入、更新、删除)方法来提供的,StatementHandler其实就是由Executor负责管理和创建的。以查询为例,在SimpleExecutor中最终落到了doQuery()这个方法,如下图所示。在执行查询逻辑中比较重要的异步操作就是StatementHandlerhandler=configuration.newStatementHandler(......)这一句代码,从字面意思看,它就是创建了一个StatementHandler实例对象,一个用来执行SQL查询的处理器。追进去看看newStatementHandler方法干了什么,如下图所示Configuration类的newStatementHandler方法。可以看到他直接为我们new了一个RoutingStatementHandler对象,然后返回了这个对象。RoutingStatementHandler在下面会详细介绍,别着急!!!2、StatementHandler继承结构和Executor体系的继承体系类似,最顶层的接口是StatementHandler,往下有2个直接实现类:BaseStatementHandler和RoutingStatementHandler,BaseStatementHandler又有3个子类:PreparedStatementHandler、SimpleStatementHandler以及CallableStatementHandlerStatementHandler:MyBatis四大组件中最重要的一个对象,负责操作Statement对象(JDBC)与数据库进行交流,在工作时还会使用ParameterHandler和ResultSetHandler对参数进行映射,对结果进行实体类的绑定。StatementHandler其实就是对JDBC中Statement的包装,为了一探究竟我们直接打开MyBatis源码来看一下。下图所示是StatementHandler接口的方法。prepare():用于创建一个具体的Statement对象的实现类实例parameterize():用于初始化Statement实例对象以及对SQL语句中的占位符进行数据填充update():用于通知Statement对象将insert、update、delete操作发送给数据库query():用于通知Statement对象将select操作发送到数据库RoutingStatementHandler:从它的名字就可以窥见它的作用,它的作用就是选择合适的StatementHandler实现类的,具体就是根据StatementType参数来创建一个代理,代理的就是对应Handler的三种实现类看到这里,是不是对RoutingStatementHandler这个类的作用恍然大悟~~他就类似与一个路由器,把一条SQL请求路由到一个具体的StatementHandler实现类去BaseStatementHandler:是StatementHandler接口的另一个实现类,本身是一个抽象类,用于简化StatementHandler接口实现的难度,属于适配器设计模式体现,它主要有三个实现类:SimpleStatementHandler:用于处理不需要预编译的SQL语句,类似于JDBC中普通的StatementPraparedStatementHandler:用于处理不需要预编译的SQL语句,类似于JDBC中的PraparedStatementCallableStatementHandler:用于处理存储过程,类似JDBC中的CallableStatement3、StatementHandler核心方法源码分析prepare方法调用流程分析执行器Executor在执行SQL语句的时候会创建StatementHandler对象,进而经过一系列的StatementHandler类型的判断并初始化。再拿到StatementHandler返回的statementhandler对象的时候,会调用其prepareStatement()方法,下面就来一起看一下preparedStatement()方法从Executor的doQuery方法开始,首先实例化一个Statement对象,其实就是RoutingStatementHandler,在RoutingStatementHandler内部维护了一个真实干活的StatementHandler对象。实例化完成后,调用了执行器的prepareStatement方法获取数据库连接Connetion,紧接着调用了StatementHandler的prepare方法,这个调用首先会到RoutingStatementHandler中的同名方法,之后会调到BaseStatementHandler中,这个方法只在BaseStatementHandler有具体实现,在这个实现中,它又调用了**instantiateStatement**,这个方法在BaseStatementHandler没有具体实现,实际执行的是三种StatementHandler中的一种,我们还以SimpleStatementHandler为例从上面代码我们可以看到,instantiateStatement()最终返回的是Statement对象,经过一系列的调用会把Statement对象返回到SimpleExecutor执行器中,为parametersize方法所用。也就是说,prepare方法负责生成Statement实例对象,而parameterize方法用于处理Statement实例对应的参数。parametersize方法调用流程分析在上面的prepareStatement方法中执行器拿到具体的Statement对象实例之后就会调用parametersize方法用处理Statement实例的参数对于这个SimpleStatementHanlder在前面我们说过他就是用来处理简单SQL语句的,因此它的实现方法是空的,如下图所示。因此具体的原理我们着重看PraparedStatementHandler的同名方法的实现,如下图所示。可以看到,具体给SQL语句参数赋值的操作MyBatis有把它交给了下一个组件ParameterHandler(参数处理器),具体内容请参考我的下一篇博文:MyBatis源码学习—MyBatis参数处理器ParameterHandler详解query方法调用流程分析StatementHandler中query方法是用于执行SQL查询语句的方法,它的具体实现在BaseStatementHandler中没有实现,我们以SimpleStatementHandler为例来分析一下。下图所示是SimpleStatementHandler的query方法。流程非常简单暴力:获取到sql语句=>调用Statement的execute方法执行sql=>调用结果处理器封装查询结果。具体查询结果集封装返回值的类容请参考我的博文:MyBatis源码学习—MyBatis结果处理器ResultHandler详解update方法调用流程分析StatementHandler的update方法是用于SQL语句的insert、update、delete语句的执行,我们以SimpleStatementHandler为例来分析一下。下图所示是SimpleStatementHandler的update方法。简单描述一下update方法的执行过程:MyBatis接收到update请求后会先找到CachingExecutor缓存执行器查询是否需要刷新缓存,然后找到BaseExecutor执行update方法;BaseExecutor基础执行器会清空一级缓存,然后交给再根据执行器的类型找到对应的执行器,继续执行update方法;具体的执行器会先创建Configuration对象,根据Configuration对象调用newStatementHandler方法,返回statementHandler的句柄;具体的执行器会调用prepareStatement方法,找到本类的prepareStatement方法后,再有prepareStatement方法调用StatementHandler的子类BaseStatementHandler中的prepare方法BaseStatementHandler中的prepare方法会调用instantiateStatement实例化具体的Statement对象并返回给具体的执行器对象由具体的执行器对象调用parameterize方法给参数进行赋值。用一幅图来表示一下这个调用过程:参考资料[1]https://www.cnblogs.com/cxuanBlog/p/11295488.html
LoveIT 2020-07-09MyBatis -
MyBatis源码学习—MyBatis 执行器Executor详解
从前面分析我们知道了sql的具体执行是通过调用SqlSession接口的对应的方法去执行的,而SqlSession最终都是通过调用了自己的Executor对象的query和update去执行的。本文就分析下sql的执行器-----Executor。1、Executor继承体系下图展示的是MyBatis的执行器的核心类的继承体系图。Executor是执行器的顶层接口,它定义了查询、更新事务提交、回滚等一系类和执行SQL有关的方法。他有两个直接实现类:BaseExecutor和CachingExecutorBaseExecutor:是一个抽象类,这种通过抽象的实现接口的方式是适配器设计模式的体现,是Executor的默认实现,实现了大部分Executor接口定义的功能,降低了接口实现的难度。BaseExecutor有三个子类,分别对应三种执行器类型,在ExecutorType这个枚举类中有定义,如下图所示。SIMPLE对应SimpleExecutor,是一种常规执行器,每次执行都会创建一个statement,用完后关闭,默认值。REUSE对应ReuseExecutor,它是一个可重用Statement对象的执行器,不会关闭statement,而是把statement放到缓存中(Map中)。缓存的key为sql语句,value即为对应的statement。也就是说不会每一次调用都去创建一个Statement对象,而是会重复利用以前创建好的(如果SQL相同的话),这也就是在很多数据连接池库中常见的PSCache概念。BATCH对应BatchExecutor,一个用于执行存储过程的和批量操作的执行器,他也会重用Statement对象。在MyBatis的三种执行器,我们可以在配置文件中通过设定settings中的defaultExecutorType属性的值来选择,通常来说:SimpleExecutor比ReuseExecutor的性能要差,因为SimpleExecutor没有做PSCache。为什么做了PSCache性能就会高呢,因为当SQL越复杂占位符越多的时候预编译的时间也就越长,创建一个PreparedStatement对象的时间也就越长。猜想中BatchExecutor比ReuseExecutor功能强大性能高,实际上并非如此,BatchExecutor是没有做PSCache的。BatchExecutor与SimpleExecutor和ReuseExecutor还有一个区别就是,BatchExecutor的事务是没法自动提交的。因为BatchExecutor只有在调用了SqlSession的commit方法的时候,它才会去执行executeBatch方法。CachingExecutor:缓存执行器,MyBatis二级缓存的实现依赖他实现,先从缓存中查询结果,如果存在,就返回结果;如果不存在,再委托给Executordelegate去数据库中取,delegate可以是上面任何一个执行器,默认是SimpleExecutor。2、Executor创建过程工作原理上面说到过,在配置文件中可以通过设定settings中的defaultExecutorType属性的值来选择执行器类型,如果没有设置,则默认会选择SimpleExecutor。其实创建Executor只是创建SqlSession的一个子步骤,在我的另一篇博文:MyBatis源码学习—SqlSession的运行过程中有过分析,这里再来分析一下。下图所示就是创建SqlSession的核心代码,在try块倒数2行就是创建执行器的逻辑,我们看到它调用了Configuration对象的newExecutor方法追踪到newExecutor方法内部看看执行器创建具体创建流程,如下图所示。上面代码的逻辑大致如下:根据形参executorType是否null获得即将要创建的执行器的类型,如果executorType==null,那就创建SimpleExecutor,否则创建指定类型的执行器;根据执行器类型executorType直接new指定类型的执行器;判断是否配置开启了二级缓存(cacheEnable==true?),如果开启了的话,那就在new一个CachingExecutor包装一下上面的简单执行器executor;在创建Executor成功之后,MyBatis会执行下面一行代码:这里用到了责任链模式,主要就是MyBatis配置插件,这里它将为我们构建一层层的动态代理对象。在调度真实的Executor方法之前执行配置插件的代码可以修改。到这里,执行器Executor实例就被创建出来了,伴随着SqlSession对象实例也即将创建成功。接下来我们分析一个Executor中主要方法的执行流程。3、Executor接口的主要方法Executor接口的方法还是比较多的,这里我们就几个主要的方法和调用流程来做一个简单的描述。Executor中的大部分方法的调用链其实是差不多的,下面都是深入源码分析执行过程,下图所示是MyBatis的一般执行过程。query()方法query方法有两种形式,一种是直接查询;一种是从缓存中查询,下面来看一下源码当有一个查询请求访问的时候,首先会经过Executor的实现类CachingExecutor,先从缓存中查询SQL是否是第一次执行,如果是第一次执行的话,那么就直接执行SQL语句,并创建缓存,如果第二次访问相同的SQL语句的话,那么就会直接从缓存中提取CachingExecutor类中对query()方法实现首先会由第一个query()方法执行,他会创建缓存的key以及从MappedStetament中获取到SQL基本信息,然后调用第二个query()方法,这个方法中首先会从缓存中查询,没有数据调用delegate执行器(BaseExecutor下的三个子执行器实例)的query()方法。SimpleExecutor的doQuery()方法经过一系列的调用,最终来到了实例类的doQuery()方法,这里首先以SimpleExecutor为例分析,如下图所示。显然MyBatis根据Configuration对象构建了一个StatementHandler对象,然后使用prepareStatement方法,对SQL编译并对参数进行初始化,我们在看它的实现过程,它调用了StatementHandler的prepare()进行了预编译和基础设置,然后通过StatementHandler的parameterize(来设置参数并执行,resultHandler再组装查询结果返回给调用者来完成一次查询。ReuseExecutor的doQuery()方法几乎和SimpleExecutor完成的工作一样,其内部不过是使用一个Map来存储每次执行的查询语句,为后面的SQL重用作准备(Map中存缓存的其实是Statement)。上面的源码可以看出,Executor执行器所起的作用相当于是管理StatementHandler的整个生命周期的工作,包括创建、初始化、解析、关闭。BatchExecutor的doQuery()方法工作原理和SimpleExecutor基本类似,不过要注意的是BatchExecutor支持一次性将多条SQL语句送到数据库执行。update()方法还是一样,在开启缓存的情况下会首先执行CachingExecutor的的update()方法,如下图所示。首先还是会调用到BaseExcutor的doUpdate()方法,之后调用到具体实例子类执行器的doUpdate()SimpleExecutor的doUpdate()方法逻辑大致和query()方法的逻辑类似,图中有注释这里不再解释。queryCursor()方法我们查阅其源码的时候,在执行器的执行过程中并没有发现其与query方法有任何不同之处,但是在doQueryCursor()方法中我们可以看到它返回了一个cursor对象,网上搜索cursor的相关资料并查阅其基本结构,得出来的结论是:用于逐条读取SQL语句,应对数据量flushStatements()方法flushStatement()的主要执行流程和query,update的执行流程差不多,flushStatement()的主要作用,flushStatement()主要用来释放statement,或者用于ReuseExecutor和BatchExecutor来刷新缓存在SimpleExecutor中它就是返回了一个空集合对象在ReuseExecutor中它用于释放缓存的Statement,同样也是返回了空集合createCacheKey()方法createCacheKey()方法用于创建缓存数据的key的,要想了解这个方法的原理,我们需要先研究一下CacheKey这个类,下图是Cachekey的属性和构造方法在cache中唯一确定一个缓存项需要使用缓存项的key,Mybatis中因为涉及到动态SQL等多方面因素,其缓存项的key不能仅仅通过一个String表示,所以MyBatis提供了CacheKey类来表示缓存项的key,在一个CacheKey对象中可以封装多个影响缓存项的因素。key的hashCode17是质子数中一个“不大不小”的存在,如果你使用的是一个如2这样较小的质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。而如果对超过50,000个英文单词(由两个不同版本的Unix字典合并而成)进行hashcode运算,并使用常数31,33,37,39和41作为乘子(cachekey使用37),每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了最终一个CacheKey中的key的hashcode=乘子{31,33,37,39或41}*oldHashCode+objectHashCodeExecutor的现实抽象在上面的分析过程中我们了解到,Executor执行器是MyBatis中很重要的一个组件,Executor相当于是外包的boss,它定义了甲方(SQL)需要干的活(Executor的主要方法),这个外包公司是个小公司,没多少人,每个人都需要干很多工作,boss接到开发任务的话,一般都找项目经理(CachingExecutor),项目经理几乎不懂技术,它主要和技术leader(BaseExecutor)打交道,技术leader主要负责框架的搭建,具体的工作都会交给下面的程序员来做,程序员的技术也有优劣,高级程序员(BatchExecutor)、中级程序员(ReuseExecutor)、初级程序员(SimpleExecutor),它们干的活也不一样。一般有新的项目需求传达到项目经理这里,项目经理先判断自己手里有没有现成的类库或者项目直接套用(Cache),有的话就直接让技术leader拿来直接套用就好,没有的话就需要搭建框架,再把框架存入本地类库中,再进行解析。参考资料[1]https://www.cnblogs.com/cxuanBlog/p/11178489.html#executor%E6%8E%A5%E5%8F%A3%E7%9A%84%E4%B8%BB%E8%A6%81%E6%96%B9%E6%B3%95[2]https://blog.csdn.net/xl3379307903/article/details/80517841
LoveIT 2020-07-08MyBatis -
MyBatis源码学习—SqlSession的运行过程
SqlSession的运行过程是整个MyBatis最难以理解的部分。SqlSession是一个接口,使用它并不复杂。我们构建SqlSessionFactory之后就可以轻易地拿到SqlSession了。SqlSession接口给出了查询、插入、更新、删除的方法,在旧版本的MyBatis或iBatis中常常使用这些接口方法,而在新版的MyBatis-3中我们建议使用Mapper,SqlSession作为MyBatis最为常用和重要的接口之一,我们有必要学习一下其中的设计思路。1、构建SqlSession对象首先我们先来看一段代码:在我的上一篇博文:MyBatis源码学习—SqlSessionFactory的构建过程中讨论了MyBatis的SqSessionFactory的构建过程,有了SqlSeesionFactory我们就可以很轻松的使用sqlSessionFactory.openSession()一句代码构建一个SqlSession对象了,那么SqlSession的构建过程中有发生了那些事情呢?我们继续追踪源码~~跟随源码,首先我们来到SqlSeesionFactory接口的默认实现类DefaultSqlSessionFactory内部。如下图所示。openSession()方法调用了另一个方法openSessionFromDataSource,也即使用过数据库数据源配置获取SqlSession对象;除此之外,DefaultSessionFactory还提供了另一种获取SqlSession对象的方法:openSessionFromConnection,即我们还可以给MyBatis一个和数据库建立好的连接Connection对象来创建SqlSession对象,如下图所示。好的,回到主题来。我继续追到openSessionFromDataSource方法中看看MyBatis为我们做了哪些工作,如下图所示。方法传入三个参数:ExecutorType、TransactionIsolationLevel和一个boolean类型的表示autoCommitExecutorType:执行器的类型,是个枚举类,定义了三种类型的执行器:SIMPLE、REUSE、BATCH,分别对应MyBatis中三种不同类型的执行器。详细内容请参考我的另一篇博文:MyBatis源码学习—MyBatis中的ExecutorType。TransactionIsolationLevel:事务隔离级别。MyBatis中定义的事物隔离级别和MySQL、Spring的事物隔离级别是一致的,详细内容请参考我的另一篇博文:Spring从入门到精通—Spring事务详解。参数autoCommit:指示是否开启自动提交事务,默认是关闭的,也即不自动提交事务。方法的执行流程大致如下:通过configuration对象获取到配置信息Enviroment使用Enviromenth获取创建事务管理工厂,并通过事务工厂获取到事物管理器JdbcTransaction根据执行器类型ExecutorType创建指定执行器,默认的执行器是SimpleExecutor获得执行器对象之后,MyBatis直接为我们new了一个SqlSeesion的默认实现类DefaultSqlSessionExecutor创建细节简单分析MyBatis通过调用configuration对象的newExecutor方法获取到执行器实例,如下图所示。方法开始,检查如果没有指定执行器类型,如果没有指定执行器类型,MyBatis就会选择使用默认的执行器SimpleExecutor,之后MyBatis就会通过一个参数cacheEnabled来判断是否开启二级缓存(不清楚的老铁才可以参考我的另一篇博文:深入理解MyBatis缓存机制),因此new了一个CacheingExecutor包装了一下executor。顺便提一嘴,对于一级缓存/session级缓存MyBatis默认是开启的,并且不可控制,它是在BaseExecutor中使用PerpetualCache类型的localCache字段控制的,如下图所示。2、映射器的动态代理到这里,我们就获得了一个SqlSession对象,通过SqlSession对象我们就可以调用getMapper()获取到Mapper接口的“实例”,进而执行sql查询了,如下图所示。正如我们看到的那样,在使用MyBatis的时候我们只需要一个Mapper接口,然后使用SqlSession的getMapper()方法就可以获得这个Mapper接口的实例,进而可以实现对数据库的CRUD,但Mapper仅仅是一个接口,而不是一个包含逻辑的实现类。我们知道一个接口是没有办法去执行的,那么它是怎么运行的呢?这不是违反了教科书所说的接口不能运行的道理吗?相信有很多同学对此有疑惑,并且在面试中被面试官这样问过。答案就是动态代理。我们不妨先来看看Mapper是个什么东西。如下图所示。原来是MyBatis为Mapper接口自动生成了一个代理对象。原理就是使用jdk动态代理通过反射为我们创建了代理对象,具体看源码吧。从我们代码的getMapper进去,这里封装了一下,直接调用了configuration对象的getMapper方法configuration对象的getMapper方法也是进行了封装,直接调用了MapperRegistry类的getMapper方法。注:(MapperRegistry是一个专门管Mapper注册的一个类,这一步在构建SqlSessionFactory的时候就初始化了,详情参考MyBatisXMLConfigBuilder类的mapperElement方法)MapperRegistry中核心的就是那句mapperProxyFactory.newInstance(sqlSession),它调用了Mapper代理工厂的newInstance方法如下图所示,我们可以看到MapperProxyFactory中使用了jdk动态代理为Mapper接口创建了代理对象。关于jdk动态代理的原理可以参考我的另一篇博文:透过代理模式探究JAVA的静态代理和动态代理。我们看到,在调用jdk动态代理之前,MyBatis先new了一个MyBatisProxy对象,这是啥呢?其实MapperProxy就是jdk动态代理的重要接口InvocationHandler的一个实现类,被代理的方法全部都在MapperProxy这个类中MapperProxy细节MyBatis在MapperProxyFctory中调用了Proxy.newProxyInstance方法产生了Mapper接口的代理对象,代理对象的全部方都在MapperProxy这个类中,我们来看看。上面运用了invoke方法。invoke方法会拦截Mapper中的所有方法。拦截之后invoke首先判断method如果是Object中定义的方法,直接执行。如toString(),hashCode()等,其他Mapper接口定义的方法交由MapperMethodInvoker来执行。MapperMethodInvoker是一个接口,它在MapperProxy这个类中有两个实现类DefaultMethodInvoker和PlainMethodInvoker。这两个类都实现了MapperMethodInvoker的invoke方法,那么如何调用呢?代码写的很清楚了,如果方法是默认方法,那就调用DefaultMethodInvoker的invoke方法,否则如果不是默认默认方法,那就调用PlainMethodInvoker实现的invoke方法。读到这里有些同学就有疑问了,上面是默认方法呢?这个是jdk中的一个方法,我们来看一下jdk的解释,如下图所示。我用谷歌翻译翻了一下,意思是:默认方法是公共非抽象实例方法,即是在接口中声明的带有主体的非静态方法类型。也就是说,默认方法就是我们说的成员方法,在Mapper中定义的抽象方法不属于默认方法,那自然就会调用PlainMethodInvoker实现的invoke方法然后在PlainMethodInvoker实现的invoke方法中调用了MapperMethod的execute方法执行SQL语句.如下图所示,我们可以看到它首先判断了一下SQL语句的类型,让后按照不同类型的sql语句执行不同的代码。方法还是很多的,我们不需要全看,看一个常用的查询返回多条记录的方法executorForMany,如下图所示。MapperMethod采用命令模式运行,根据上下文跳转,它可能跳转到许多方法中,我们不需要全部明白。我们可以看到里面的executeForMany方法,再看看它的实现,实际上它最后就是通过sqlSession对象去运行对象SQL。至此,相信大家已经了解了MyBatis为什么只用Mappper接口便能够运行SQL,因为映射器的XML文件的命名空间对应的便是这个接口的全路径,那么它根据全路径和方法名便能够绑定起来,通过动态代理技术,让这个接口跑起来。而后采用命令模式,最后还是使用SqlSession接口的方法使得它能够执行查询,有了这层封装我们便可以使用接口编程,这样编程就更简单了。3、SqlSession四大对象现在我们已经知道了映射器其实就是一个动态代理对象,进入到了MapperMethod的execute方法。它经过简单判断就进入了SqlSession的删除、更新、插入、查询等方法,那么这些方法如何执行呢?这是我们需要关心的问题,也是正确编写插件的根本。显然通过类名和方法名字就可以匹配到我们配置的SQL,我们不需要去关心这些细节,我们关心的是设计架构。Mapper执行的过程是通过Excutor、StatementHandler、ParameterHandler和ResultHandler来完成数据库操作和结果返回的。Executor表示执行器,由他调度StatementHandler、ParameterHandler和ResultHandler等来执行SQL语句StatementHandler使用JDBC的Statement或PreparedStatement来执行操作,他是四大对象的核心,器承上启下的作用。ParameterHandler用于SQL参数的处理。ResultHandler对JDBC的ResultSet进行封装处理并返回。了解到这里我们在看一下MyBatis处理流程图,如下图所示。构建SqlSession的过程在MyBatis源码学习—SqlSessionFactory的构建过程我们已经学习了,创建SqlSession的流程在本节也进行了学习。下面我们将逐一分析学习剩下的四个对象的生成和运作原理。到这里我们已经来到了MyBatis的底层设计,对Java语言基础不牢的同学来说,这将是一次挑战。详细的内容可以参考我的博文:MyBatis源码学习—MyBatis执行器Executor详解MyBatis源码学习—MyBatis数据仓库会话器StatementHandler详解MyBatis源码学习—MyBatis参数处理器ParameterHandler详解MyBatis源码学习—MyBatis结果处理器ResultHandler详解
LoveIT 2020-07-04MyBatis -
MyBatis源码学习—SqlSessionFactory的构建过程
MyBatis的运行分为两大部分,第一部分是读取配置文件缓存到Configuration对象,用以创建SqlSessionFactory,第二部分是SqlSession的执行过程。相对而言,SqlSessionFactory的创建比较容易理解,而SqlSession的执行过程远远不是那么简单了,本节我们解先来学习一下MyBatis是如何读取配置文件生成Configuration,并最终生成SqlSession的。 SqISessionFactory是MyBatis的核心类之一,其最重要的功能就是提供创建MyBatis的核心接口SqlSession,所以我们需要先创建SqlSessionFactory,为此我们需要提供配置文件和相关的参数。而MyBatis是一一个复杂的系统,采用建造者模式去创建SqlSessionFactory,我们可以通过SqlSessionFactoryBuilder去构建。构建分为两步。第一步,通过org.apache.ibatis.builder.xml.XMLConfigBuilder解析配置的XML文件,读出配置参数,并将读取的数据存入这个orgapache.ibatis.session.Configuration类中。注意,MyBatis几乎所有的配置都是存在Configuration里的。第二步,使用Confinguration对象去创建SqlSessionFactory。MyBatis中的SqlSessionFactory是一个接口,而不是实现类,为此MyBatis提供了一个默认的SqlSessionFactory实现类,我们一般都会使用它org.apache.iatis.session.defaults.DefaultSqlSessionFactory。注意,在大部分情况下我们都没有必要自己去创建新的SqlSessionFactory的实现类。 特别需要注意的是,在构建SqlSessionFctory的过程中,MyBatis使用Builder设计模式,采用这种方式可以构建复杂的对象而且条理还很清晰,这一点是我们需要在源码中首先学习到的一点知识。接下来,详细的说明一下构建过程的详细过程。1、构建Configuration 在SqlSessionFactory的构建过程中,Configuration的生成是接力比赛的“第一棒”,他能否成功生成直接决定了接下来流程是否可以继续执行。我们来看一段代码: 程序启动执行,首先会由MyBatis的io包下的Resources类到指定的位置加载全局配置文件,方法最终调到了JDK的ClassLoader这个类的同名方法,方法内部核心的实现就一句话URLurl=getResource(name);。最终返回的类型是BufferedInputStream,这个也是Java的io包下的InputStream实现。 成功读取配置文件后就可以拿着输入流对象到SqlSessionFactoryBuilder中构建SqlSessionFactory了,继续跟踪源码,我们看看MyBatis是如何做的~~ 跟随着DEBUG,我们来到了SqlSessionFactoryBuilder内部,来到源码的第68行,可以看到MyBatis使用了一个叫XMLConfigBuilder的类来解析我们输入流中的东西。 继续追进去,我们看到它在构造方法中使用XMLPathParser来最终处理了我们的输入流,这个XMLPathParser是MyBatis的parsing包下的一个用于解析XML文件的一个类,它内部还是使用了Jdk提供的一个类XPath来解析XML文档,经过他解析XML成为一个DOM树(Document对象,也是JDK提供的)。拿到解析出来的对象,XMLConfigParser就会进入下图所示的另一个构造方法中执行Configuration对象的生成。 此时我们已经得到了XMLConfigBuilder对象,再看SqlSessionFactoryBuilder的build方法,将XMLConfigBuilder实例对象parser调用parser()方法得到的Configuration实例对象config作为参数,调用SqlSessionFactory接口的实现类DefaultSqlSessionFactory构造出SqlSessionFactory对象。 XMLConfigBuilder对象在调用parser()方法时,会读出所有所有配置文件,将配置文件解析后保存在Configuration对象中。(代码看起来很长,其实原理很简单,就是对那颗DOM树进行遍历)XMLConfigBuilder的parseConfiguration(XNode)方法把XML全局配置文件中每一个节点的信息都读取出来,保存在一个Configuration对象中,Configuration分别对以下内容做出了初始化:properties全局参数。settings设置。typeAliases别名。typeHandler类型处理器。ObjectFactory对象。plugin插件。environment环境。DatabaseIdProvider数据库标识。Mapper映射器。2、构建Mapper映射器 Mapper映射器是Configuration初始化组件的最后一道工序了,它就是加载我们sql映射文件或者是Mapper接口的地方,来,我们看看MyBatis为我们做了什么,如下图所示。 初始化Mapper的逻辑也不难理解:由parseConfiguration传入mappers结点,之后在mapperElement方法中遍历mappers结点下的所有子节点:如果遍历到package子节点,是以包名引入映射器,则将该包下所有Class注册到Configuration的MapperRegistry中。如果遍历到mapper子节点的class属性,则将指定的接口注册到Configuration的MapperRegistry中。如果遍历到resource或者url子节点,麻烦一点,需要借助XMLMapperBuilder来解析SQL映射文件: Mapper映射问文件最终还是通过JDk的XPathParser解析成一颗DOM树,在构造方法中完成初始化之后将调用XMLMapperBuilder的parse方法遍历这颗DOM树获取并解析其中的SQL语句。 在解析映射文件的时候首先它判断在当前configuration对象中是否已经有此资源了,这个在Configuration是叫一个Set类型的loadedResource变量控制的,这样设计是为了防止资源重复加载。当然如果没有加载,那就要执行加载操作了,进入到configurationElement方法中,如下图所示。 源码的逻辑也是不难的,首先获取namespace结点的值,这个namespace一般就是和这个Mapper相关联的那个Mapper接口,如果它没有的话,那就会在这里直接报错。否则namespace属性保存在XMLMapperBuilder的MapperBuilderAssistant对象中,以便其他方法调用。 之后方法对mapper映射文件每个标签逐一解析并保存到Configuration和MapperBuilderAssistant对象中,最后调用buildStatementFromContext方法解析select、insert、update和delete节点。 最终的一切都还是落到了XMLStatementBuilder类的parseStatementNode方法上,这个方法完成对每条SQL语句的解析并且把每一套SQL语句封装到一个SqlSource对象,SqlSource是个接口,如果是动态SQL就创建DynamicSqlSource实现类,否则普通SQL创建StaticSqlSource实现类。如下图。 这个方法就是把一条SQL语句色剂的所有可能的属性全部解析出来,并且如上面所说,它把每条SQL语句进行了封装用SqlSource来表示一条Sql语句,最后把这条Sql语句通过MapperBuilderAssistant的addMappedStatement方法将MappedStatement对象放入Configuration对象的mappedStatements容器中,得到了最终的Configuration对象后传入SqlSessionFactoryBuilder的另一个build方法中,生成我们需要的DefaultSqlSessionFactory对象。 这里只分析了MyBatis是如何解析resource和url方式指定的mapper配置文件,如果是通过指定Mapper接口的package或者class全限定名配置方式,则Configuration对象会通过addMappers方法将接口注册,再通过Java反射技术和JDK动态代理技术,根据接口class的全限定名找到对应的XML配置文件或者注解进行解析,如果是非注解模式的xml配置文件必须和这个class在同一级目录,且与class同名,这里不再详解。最后一图总结SqlSessionFactory的构建流程: 流程还是比较简单的,就是围绕着SqlSessionFactoryBuilder类该加载配置加载配置,该解析解析,最后得到Configuration对象返回给SqlSessionFactoryBuilder,之后SqlSessionFactoryBuilder拿着Configuration对象new了一个SqlSessionFactory的默认实现类DefaultSqlSessionFactory最后返回该对象。
LoveIT 2020-07-02MyBatis -
MyBatis架构设计及源代码分析(一):MyBatis架构
一、概述MyBatis是一个轻量级的ORM框架,其官方首页是这么介绍自己。MyBatis数据映射器框架使将关系数据库与面向对象的应用程序结合使用变得更加容易。MyBatis使用XML描述符或注释将对象与存储过程或SQL语句耦合。相对于对象关系映射工具,简单性是MyBatis数据映射器的最大优势。而在其官方文档中介绍“WhatisMyBaits”中说到MyBatis是一款优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。MyBatis可以通过简单的XML或注解来配置和映射原始类型、接口和JavaPOJO(PlainOldJavaObjects,普通老式Java对象)为数据库中的记录。二、整体架构从功能流程层次描述MyBatis的整体架构图如下图所示:1、接口层我们知道,在不考虑与Spring集成的情况下,使用MyBatis执行数据库操作的代码如下:上面的代码中就使用了接口层的两个核心接口:SqlSessionFactory和SqlSession,其中SqlSessionFactory用于构建一个SqlSession(openSession()方法)以及生成MyBatis获取(getConfiguration()方法);SqlSession接口中定义了MyBatis暴露给应用程序调用CRUD的API,也就是上层应用与MyBatis交互的桥梁。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作。这两个类都在MyBatis的session包下,这个包的主体类结构图如下:两个核心接口:SqlsessionFactory和SqlSession。Configuration是MyBatis的配置信息存放的地方,在Configuration类中还引用了一个Environment对象,Environment中可以配置数据源和事物管理。由上图可以看到,Configuration对象与DefaultSqlSessionFactory是1:1的关联关系,这也就意味着在一个DefaultSqlSessionFactory衍生出来的所有SqlSession作用域里,Configuration对象是全局唯一的。同时SqlSessionFactory提供了getConfiguration()接口来公开Configuration对象,因此开发者除了配置文件之外,还可以在程序里动态更改Configuration的属性项以达到动态调整的目的,但此时不仅要考虑到执行完reset,同时还要考虑在修改过程中会可能影响到其他SqlSession的执行。2、核心层(1)配置解析在MyBatis初始化过程中,会加载mybatis-config.xml配置文件、映射配置文件以及Mapper接口中的注解信息,解析后的配置信息会形成相应的对象并保存到Configuration对象中。之后,利用该Configuration对象创建SqlSessionFactory对象。待MyBatis初始化之后,开发人员可以通过初始化得到Sq!SessionFactory创建SqlSession对象并完成数据库操作。1)全局配置文件mybatis-config.xml是在XMLConfigBuilder中完成解析的,有关类图大致如下:xml解析常见的方式有三种:DOM(DocumentObjectModel)、SAX(SimpleAPIforXML)、StAX(StreamingAPIforXML)MyBatis在初始化过程中处理mybatisConfig.xml配置文件及映射配置文件时,使用的是DOM解析方式,并结合使用XPath(JDK提供的一个解析xml的类)解析XML配置文件。DOM是基于树形结构的XML解析方式,它会将整个XML文档读入内存并构建一个DOM树,基于这棵树形结构对各个节点(XNode)进行操作。如下图所示:2)SQL映射文件是在XMLMapperBuilder这个类中完成解析的,其中把对Statement的解析(即XxxMapper.xml中SELECT|INSERT|UPDATE|DELETE定义部分)委托给XMLStatementBuilder来完成。XxxMapper.xml的解析比较复杂的,涉及到PreparedMapping、ResultMapping、LanguageDriver、Discriminator、缓存、自动映射等一系列对象的构造。(2)SQL执行SQL语句的执行涉及多个组件,其中比较重要的是Executor、StatementHandler、ParameterHandler和ResultSetHandler。1)Executor主要负责维护一级缓存和二级缓存,并提供事务管理的相关操作,它会将数据库相关操作委托给StatementHandler完成。下面是Executor的类图从上图中可以看到,Executor主要提供了一下功能:CRUD接口,从方法定义中可看到,它需要MappedStatement、parameter、resultHandler这几个实例对象,这几个也是SQL执行的主要部分,详细实现在后面专题中再介绍。事务提交/回滚(commit()/rollback()),这委托给Transaction对象来完成缓存,createCacheKey()/isCached()延迟加载,deferload()关闭(close()),主要是事务回滚/关闭①BaseExecutor:它内部维护了localCache来localOutputParameterCache来处理缓存,以及线程安全的延迟加载列表deferredLoads、事务对象Transaction。②BatchExecutor:它内部维护了statementList批量提交并通过batchResultList保存执行结果③ResueExecutor:它内部维护了java.sql.Statement对象缓存,以重用Statement对象(对于支持预编译的数据库而言,在创建PreparedStatement时需要发送一次数据库请求预编译,而重用Statement对象主要是减少了这次预编译的网路开销)。2)**StatementHandler**首先通过**ParameterHandler完成SQL语句的实参绑定。然后通过java.sql.Statement对象执行SQL语句并得到结果集ResultSet。最后通过ResultSetHandler**完成结果集的映射,得到结果对象并返回。下图所示就是一条SQL语句在MyBatis中的执行流程:3)SOL解析与scripting模块:MyBatis有动态SQL语句的功能,提供了多种动态SQL语句对应的节点,例如,<where>节点、<if>节点、<foreach>节点等。通过这些节点的组合使用,开发人员可以写出几乎满足所有需求的动态SQL语句。MyBatis中的scripting模块会根据用户传入的实参,解析映射文件中定义的动态SQL节点,并形成数据库可执行的SQL语句。之后会处理SQL语句中的占位符,绑定用户传入的实参。3、基础层3.1、logging:MyBatis使用了自己定义的一套logging接口,根据开发者常使用的日志框架——Log4j、Log4j2、ApacheCommonsLog、java.util.logging、slf4j、stdout(控制台)——分别提供了适配器。由于各日志框架的Log级别分类法有所不同(比如java.util.logging.Level提供的是All、FINEST、FINER、FINE、CONFIG、INFO、WARNING、SEVERE、OFF这九个级别,与通常的日志框架分类法不太一样),MyBatis统一提供trace、debug、warn、error四个级别,这基本与主流框架分类法是一致的(相比而言缺少Info,也许MyBatis认为自己的日志要么是debug需要的,要么就至少是warn,没有Info的必要)。在org.apache.ibatis.logging里还有个比较特殊的包jdbc,这不是按字面意义理解把日志通过jdbc记录到数据库里,而是将jdbc操作以开发者配置的日志框架打印出来,这也就是我们在开发阶段常用的跟踪SQL语句、传入参数、影响行数这些重要的调试信息。3.2、IOMyBatis里的IO主要是包含两大功能:提供读取资源文件的API(我们写代码常用的Resource类就在这个包下)、封装MyBatis自身所需要的ClassLoader和加载顺序。3.3、reflection在MyBatis如参数处理、结果映射这些大量地使用了反射,需要频繁地读取Class元数据、反射调用get/set,因此MyBatis提供了org.apache.ibatis.reflection对常见的反射操作进一步封装,以提供更简洁方便的API。比如我们reflect时总是要处理异常(IllegalAccessException、NoSuchMethodException),MyBatis统一处理为自定义的RuntimeException,减少代码量。3.4、exceptions在以Spring为代表的开源框架中,对于应用程序中无法进一步处理的异常大都转成RuntimeException来方便调用者操作,另外如频繁遇到的SQLException,JDK约定其是个Exception,从JDK的角度考虑,强制要求开发者捕获SQLException是为了能在catch/finally中关闭数据库连接,而Spring之类的框架为开发者做了资源管理的事情,自然就不需要开发者再烦心SQLException,因此封装转换成RuntimeException。MyBatis的异常体系不复杂,org.apache.ibatis.exceptions下就几个类,主要被使用的是PersistenceException。3.5、缓存缓存是MyBatis里比较重要的部分,MyBatis提供两种级别的缓存:SESSION/一级缓存,默认生命周期是一次SESSION,BaseExecutor中根据MappedStatement的Id、SQL、参数值以及rowBound(边界)来构造CacheKey,并使用BaseExccutor中的localCache来维护此缓存。全局的二级缓存,通过CachingExecutor来实现,其委托TransactionalCacheManager来保存/获取缓存,这个全局二级缓存比较复杂,详细的内容大家可以参考我的另一篇博文深入理解MyBatis缓存机制。3.6、数据源/连接池MyBatis自身提供了一个简易的数据源/连接池,在org.apache.ibatis.datasource下。主要实现类是PooledDataSource,包含了最大活动连接数、最大空闲连接数、最长取出时间(避免某个线程过度占用)、连接不够时的等待时间,虽然简单,却也体现了连接池的一般原理。阿里有个“druid”项目,据他们说比proxool、c3p0的效率还要高,可以学习一下。3.7事务MyBatis对事务的处理相对简单,TransactionIsolationLevel中定义了几种隔离级别,并不支持内嵌事务这样较复杂的场景,同时由于其是持久层的缘故,所以真正在应用开发中会委托Spring来处理事务实现真正的与开发者隔离。分析事务的实现是个入口,借此可以了解不扫JDBC规范方面的事情。3.8Binding模块MyBatis通过绑定模块将用户自定义的SQL映射文件和混和Mapper接口关联起来了,系统可以通过调用自定义Mapper接口中的方法执行相应的SQL语句完成数据库操作。开发人员无须编写自定义Mapper接口的实现,MyBatis会通过动态代理自动为Mapper解口创建实现类实例。参考资料[1]https://blog.csdn.net/tiankong_12345/article/details/90813101#%E4%BA%8C%E3%80%81%E6%A0%B8%E5%BF%83%E5%A4%84%E7%90%86%E5%B1%82[2]https://www.cnblogs.com/mengheng/p/3739610.html[3]https://mybatis.org/mybatis-3/zh/configuration.html
LoveIT 2020-06-24MyBatis -
一文让你理解高并发缓存中的一致性Hash算法原理
一、从Web系统的演进说起单机时代在当今的互联网项目中,对于缓存的使用已经是”标配“了,我们开发一个平台刚开始访问量很小只需要一个缓存服务器就够用了(系统架构如下图所示)负载均衡随着系统的发展,访问量越来越大,这是我们的服务撑不住了,此时我们考虑给系统做负载均衡增加应用服务器来提高系统的并发量,此时缓存还够用,因此几个应用服务器共用一个缓存服务器就好了。分布式缓存可是好景不长,随着服务的时间越长,缓存的东西也越来越多,终于有一个天,缓存服务器撑不住了!此时我们考虑增加缓存服务器,并将缓存的数据拆分并缓存到不同的缓存服务器中。(下图中黄色背景的表示被拆分的数据)这样我们的系统就可以使用处理一定量的高并发和海量数据了。但是问题又来了,我们现在的系统有多个缓存服务器,我们的一个请求下来要到那个缓存服务器中去写\读数据呢?对于这个问题,有一个简单的方法就是使用数据的key的hashCode与缓存服务器结点个数取余。比如:现在有一个key="java"求hashCode:key.hashCode()=100求余:index=100%3==>index=1因此key="java"这个缓存数据就在第1个缓存服务器中现在系统改进的可以平稳运行了因对日常级别的大流量已经没有问题了,但是突然要搞活动,比如像双11这样的活动,系统的访问量邹增,为了系统能够平稳运行,这时我们考虑给系统增加配置,其中一项就是缓存服务器增加了缓存服务器之后,我们按照原来的对key的hashCode()取余的思路来计算一下:现在有一个key="java"求hashCode:key.hashCode()=100//hashCode()在一个系统不会改变取余:index=100%4==>index=0//由于缓存服务器结点增加了,因此问题就在这里这样一来,原本在第1个服务器中缓存的数据现在计算出来要去第0个服务器中拿,这就会引起错误通过上面的分析,我们就可以感受到,使用这种简单的hash算法就会导致增加或减少服务结点之后大部分结点之前的数据不可用,因此为了解决这个问题就出现了一致性hash算法,这个算法原本就是用来解决分布式缓存问题的。二、一致性hash算法背景 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点问题,初衷和CARP(CommonAccessRedundancyProtocol,共用地址冗余协议)十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得分布式哈希(DHT,DistributedHashTable)可以在P2P环境中真正得到应用。一致性hash算法提出了在动态变化(分布式系统每个节点都有可能失效,并且新的节点很可能动态的增加进来)的环境中,判定哈希算法好坏的四个定义:平衡性(Balance)平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。单调性(Monotonicity)单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。分散性(Spread)在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。负载(Load)负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。三、一致性Hash算法1、一致性Hash算法基本原理其实,一致性哈希算法也是使用取模的方法,只是,刚才描述的取模法是对服务器的数量进行取模,而一致性哈希算法是对2^32取模,什么意思呢?我们慢慢聊。首先,我们把二的三十二次方想象成一个圆,一个整数就是一个点,就像钟表一样,钟表的圆可以理解成由60个点组成的圆,而此处我们把这个圆想象成由2^32个点组成的圆,示意图如下:圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32^-1,也就是说0点左侧的第一个点代表2^32^-1,我们把这个由2^32^个点组成的圆环称为hash环。有了这个Hash环之后,我们首先按照一定的规则把服务节点通过普通的hash算法把结点记录到hash环的结点上,如下图所示:之后对数据的key同样做hash运算,拿到key的hash值,那么这个key必然就会在hash环上有一个“落点”,我们不要求它能一次性命中缓存结点,只需要顺着落点顺时针遍历每一个结点,当遍历到下一个缓存结点时就把值发入这个缓存结点后从这个缓存结点获取值。当一个key顺着hash环遍历到2^32^-1时又从0开始遍历,知道找到下一个结点。示意图如下:了解了一致性Hash算法的基本工作原理之后,我们来看看它是如何解决结点动态改变二造成大规模结点数据不同用的问题的。我们还是模拟上面的情景,在活动开始之前给系统增加缓存服务器,添加后的示意图:此时我们可以按照Hash一致性算法的原理分析不难看出:添加结点后可能产生问题的地方就在node3到node4之间,因为这个区间的数原本应该是映射到node1的,但是现在都要映射到node4新节点上去了,不过相对于简单的hash取余,这个方法使得动态增加或减少结点之后数据的可用性提升了不少。2、一致性Hash算法改进至此问题基本解决,使用Hash一致性算法可以很轻松的解决分布式缓存带来的问题。然而,我们的分析还只是一种过于理想化的模型,即服务节点均匀的分布在Hash环的4周,假如结点没有均匀分布呢?我们来看看下面的示意图:当结点没有均匀分布时,一旦添加新的结点,仍然会有大部分数据会失效,这肯定不行啊!那么与上面办法解决这个问题呢?目前常用的解决办法就是使用虚拟结点来解决服务节点分布不均匀的问题,具体原理是:给每个真实的物理结点增加一组虚拟结点,将虚拟结点也放置到Hash环上,这样当一个key遍历到虚拟结点了也就表示找到了真实的物理结点。还是用示意图来说明:通过示意图可以直观的看出来,增加虚拟结点之后,我们人为的让有限的物理结点均匀分布了,这样一来当添加新结点之后受影响的数据很少。通过分析我们不难得出结论:对于每个物理节点对应的虚拟节点越多,各个物理节点之间的负载越均衡,新加入物理服务器对原有的物理服务器的影响越保持一致(这就是一致性Hash这个名称的由来)。那么在实践中,一台物理服务器虚拟为多少个虚拟服务器节点合适呢?太多会影响性能,太少又会导致负载不均衡,一般说来,经验值是150,当然根据集群规模和负载均衡的精度需求,这个值应该根据具体情况具体对待。参考资料:[1]https://www.bilibili.com/video/BV1yb411u7bz/?spm_id_from=trigger_reload[2]https://www.cnblogs.com/lpfuture/p/5796398.html[3]https://blog.csdn.net/zhaohong_bo/article/details/90519123?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase[4]https://blog.csdn.net/jiankunking/article/details/85111367
LoveIT 2020-06-21Redis -
单线程的Redis为什么高并发场景下还是很快
缓存在高并发的场景的作用不言而喻,号称高并发架构的基石,其中最为典型代表非Redis莫属。无论你是想面试通关,还是实战中用好Redis,理解Redis的设计精髓,就变得很重要了。今天主要分享Redis关于单线程以及高并发场景的核心设计。一、Redis到底有多快?Redis采用的是基于内存的采用的是单进程单线程模型的KV数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。这个数据不比采用单进程多线程的同样基于内存的KV数据库Memcached差!有兴趣的可以参考官方的基准程序测试《HowfastisRedis?》(https://redis.io/topics/benchmarks),下图是Redi官方给出的一个在不同连接数下Redis吞吐量的曲线,横纵表示连接数,纵轴表示是吞吐量(q/s):这张图反映了一个数量级,希望大家在面试的时候可以正确的描述出来,不要问你的时候,你回答的数量级相差甚远!几个关键的数量级:最大QPS:100万+,30_0000连接:60_0000QPS,60_0000连接:50_0000万QPS一、Redis的高并发和快速原因(1)redis是基于内存的非关系型数据库,内存的读取速度非常快;(2)采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;(3)最重要的原因是redis使用了IO多路复用模型,非阻塞IO,可以处理并发的连接。下面我们重点来了解一下Redis设计者把Redis设计成单线程以及IO多路复用技术的原理。二、为什么Redis要采用单线程的模式1、官方的解释官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。可以参考:https://redis.io/topics/faq并且在截图中我们也可以看到,Redis官方给我们建议:为了最大程度地利用CPU,您可以在同一框中启动多个Redis实例,并将它们视为不同的服务器。在某个时候,单个Redis可能还不够,因此,如果您要使用多个CPU,则可以开始考虑更早地分片的某种方法。因此,Redis采用线程的原因就是因为Redis目前在爱单线程下性能已经很高了,也就没必要着急着使用多线程了。不过在截图的最后官方表示在Redis未来的版本中会逐渐的使用上多线程,这一目标在Redis6版本中首次被实现了,感兴趣的小伙伴可以去了解一下。注意!这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的Redis服务运行的时候肯定是不止一个线程的,这里需要大家明确的注意一下!例如Redis进行持久化的时候会以子进程或者子线程的方式执行....2、我的理解1)不需要各种锁的性能消耗Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。2)单线程多进程集群方案单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。所以单线程、多进程的集群不失为一个时髦的解决方案。3)CPU消耗采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU。但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。三、IO多路复用技术Redis采用网络IO多路复用技术来保证在多连接的时候,系统的高吞吐量。多路指的是多个socket连接,复用指的是复用一个线程。意思就是使用一个线程处理多个socket连接。Linux系统中IO多路复用提供了三个系统调用:select、poll、epoll。epoll是目前最先进的多路复用技术。select/poll/epoll核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数block,而不是被socket阻塞的。1、select机制函数定义如下:(1)基本原理select调用过程示意图:客户端在操作服务器的时候会产生三种文件描述符fd:writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞并分别监视这3类文件描述符,等有数据可读、可写、出异常或超时的时候就会返回。返回后通过遍历fd_set整个数组来找到就绪的描述符fd,然后进行对应的IO操作。(2)优点支持几乎所有的平台,跨平台性好。(3)缺点由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。每次调用select(),需要把fd集合从用户态拷贝到内核态,并进行遍历默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢2、poll机制和select用三组文件描述符不同的是,poll只有一个pollfd数组,数组中的每个元素都表示一个需要监听IO操作事件的文件描述符。events参数是我们需要关心的事件,revents是所有内核监测到的事件。(1)基本原理基本原理与select一致,也是轮询+遍历;唯一的区别就是poll采用链表的方式替换select中的fd_set(数组)数据结构,而使其没有连接数的限制3、epoll机制函数定义如下:epoll_create&epoll_create1用于创建一个epoll实例,而epoll_ctl用于往epoll实例中增删改要监测的文件描述符,epoll_wait则用于阻塞的等待可以执行IO操作的文件描述符直到超时。(1)基本原理epoll调用过程示意图:epoll也没有FD个数限制,用户态到内核态拷贝只需要一次,使用时间通知机制来触发。通过epoll_ctl()方法注册FD,一旦FD就绪就会通过回调机制来激活对应的FD,进行相关的IO操作。epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fdepoll_ctl()每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd,绑定一个callback函数epoll_wait()轮训所有的callback集合,并完成对应的IO操作(2)优点没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄效率提高,使用回调通知而不是轮询的方式,性能不会随着FD数目的增加效率下降内核和用户空间mmap同一块内存实现(mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间)例子:100万个连接,里面有1万个连接是活跃,我们可以对比select、poll、epoll的性能表现 select:不修改宏定义默认是1024,则需要100w/1024=977个进程才可以支持100万连接,会使得CPU性能特别的差。 poll:没有最大文件描述符限制,100万个链接则需要100w个fd,遍历都响应不过来了,还有空间的拷贝消耗大量的资源。 epoll:请求进来时就创建fd并绑定一个callback,只需要遍历1w个活跃连接的callback即可,即高效又不用内存拷贝。参考资料[1]Redis、Zookeeper、Kafka、Nginx、Netty、Epoll、NIO、分布式、Hbase技术串讲[2]Redis为什么是单线程,高并发快的3大原因详解[3]为什么说Redis是单线程的以及Redis为什么这么快![4]https://zhuanlan.zhihu.com/p/115220699[5]https://blog.csdn.net/nanxiaotao/article/details/90612404
LoveIT 2020-06-20Redis -
Redis缓存和MySQL数据一致性解决方案探究
在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。然而在工程技术领域并没有100%完美的解决方案,正如这里,读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。我们所说的一致性就是在要求:缓存不能读到脏数据缓存可能会读到过期数据,但要在可容忍时间内实现最终一致这个可容忍时间尽可能的小要想同时满足上面三条,可以采用读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。但是,串行化之后,就会导致系统的吞吐量会大幅度的降低,要用比正常情况下多几倍的机器去支撑线上请求。无脑增加机器肯定不是我们软件开发人员应该考虑的,所以,在这里,我们讨论三种常见方法:先更新数据库,再更新缓存先删除缓存,再更新数据库先更新数据库,再删除缓存在分析之前需要说明的是:如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,那就不要使用缓存方案来解决数据库的瓶颈问题,但是如果你的系统不是严格要求缓存和数据库必须保持一致性的话,那么可以尝试使用这里讨论的方案尝试解决缓存和数据库数据一致性问题。1、先更新数据库,再更新缓存这种方法是大家普遍反对的,原因集中在下面两点:从线程安全角度看:如果有同时两个线程A和B更新数据库,正常情况下我们希望A首先执行完毕,然后B再执行,但是由于网络抖动等原因,B先执行了更新,接着A又去更新,那么就会有如下情形:B更新了数据库------>B更新缓存---->A更新数据库------>A更新缓存这么操作加大了脏数据产生的可能性,因为A、B线程可能就是对同一个数据进行了更新,那么后发出B请求的数据就理论上就比A的新,因此按照这种策略解决缓存一致性不可取。从业务角度看:如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致数据压根还没读到,缓存就被频繁的更新,浪费性能。如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。相比之下,删除缓存更为适合。2、先删除缓存,再更新数据库该方案同样会导致不一致。同时有请求A和请求B进行更新操作,那么会出现:请求A进行写操作,检查发现缓存存在,于是首先了删除缓存请求B接下来查询发现缓存不存在请求B去数据库查询得到旧值请求B将旧值写入缓存请求A将新值写入数据库上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。解决方案:先删除缓存再写数据库休眠一定时间(例如1秒或200ms),再次删除缓存。这么做,可以将缓存脏数据再次删除。然而,这种方式回影响到系统的吞吐量,读入高并发场景不是特别适用。3、先更新数据库,再删除缓存这种方案是很多工程采用的方案,我们来看下是否一定安全。假设有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生:缓存刚好失效请求A查询数据库,得一个旧值请求B将新值写入数据库请求B删除缓存请求A将查到的旧值写入缓存这样,脏数据就产生了,然而上面的情况是假设在数据库写请求比读请求还要快。实际上,工程中数据库的读操作的速度远快于写操作的。如果缓存删除失败了怎么办?答:启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。读取binlog后分析,利用消息队列,推送更新各台的redis缓存数据。这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。阿里有一款开源框架—canal就是来做这个事情的,通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
LoveIT 2020-06-18Redis -
Redisson 实现分布式锁原理浅析
在上一文中分布式锁的三种实现方案总结了目前业界常见的实现分布式锁的三种方案:1、基于数据库表排他锁2、基于Redis的setNX命令+过期时间+lua脚本3、基于Zookeerper的临时结点。本文我们继续通过源码分析一下Redisson实现分布式锁的原理。回顾:Redis实现分布式锁主要步骤指定一个key作为锁标记,存入Redis中,并且指定一个唯一的用户标识作为value。当key不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足互斥性特性。给key设置一个过期时间,防止因系统异常导致没能删除这个key,满足防死锁特性。当处理完业务之后需要清除这个key来释放锁,清除key时需要校验value值,需要满足只有加锁的人才能释放锁。Redisson实现分布式锁下面从加锁机制、锁互斥机制、Watchdog机制、可重入加锁机制、锁释放机制等五个方面对Redisson实现分布式锁的底层原理进行分析。1、加锁原理Redisson加锁其实是通过一段lua脚本实现的,如下:我们可以把这一段lua脚本拿出来分析一下:在Redisson中,加锁需要以下三个参数:(1)KEYS[1]:需要加锁的key,这里需要是字符串类型。这个参数就是我们给Redisson加锁时传入的那个key,比如:(2)ARGV[1]:锁的超时时间,防止死锁。默认时间是30s,这个也可以在加锁的时候设置(3)ARGV[2]:锁的唯一标识,id(UUID.randomUUID())+“:”+threadId,比如:285475da-9152-4c83-822a-67ee2f116a79:52。通过这段脚本可以看到,Redsson在实现分布式锁的时候没有使用SETNX,而是使用了hincrby这个命令。命令语法:HINCRBYkeyfiledincrement这个命令可以为哈希表key中的域field的值加上增量increment。增量也可以为负数,相当于对给定域进行减法操作。如果key不存在,一个新的哈希表被创建并执行HINCRBY命令。如果域field不存在,那么在执行命令前,域的值被初始化为0。上面这一段加锁的lua脚本的作用是:第一段if判断语句,就是用existsproduct命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。如何加锁呢?使用hincrby命令设置一个hash结构,类似于在Redis中使用下面的操作:接着会执行pexpiremyLock30000命令,设置myLock这个锁key的生存时间是30秒。到此为止,加锁完成。有的小伙伴可能此时就有疑问了,如果此时有第二个客户端请求加锁呢?这就是下面要说的锁互斥机制。2、锁互斥机制此时,如果客户端2来尝试加锁,会如何呢?首先,第一个if判断会执行existsproduct,发现product这个锁key已经存在了。接着第二个if判断,判断一下,product锁key的hash数据结构中,是否包含客户端2的ID,这里明显不是,因为那里包含的是客户端1的ID。所以,客户端2最后会执行:返回的一个数字,这个数字代表了product这个锁key的剩余生存时间。了解了上面这些知识点以后,接下来分析一下阻塞锁和非阻塞锁的逻辑。阻塞锁从lock()方法开始追溯源码,RedissonLock类中有的两个重载的方法分别来自java的Lock接口和Redsson的RLock接口,他们处理逻辑都在下面那个私有的lock()方法中Redisson阻塞锁上锁的逻辑:可以看到代码很长,但是只要你清楚如何使用Redis实现一把分布式锁,看懂应该不难。大致流程如下:当一个线程尝试获取锁时,首先会尝试调用tryAcquire()方法尝试获取锁,如果获取锁成功就会返回null(这个在lua脚本中可以看到,成功加锁会返后nil)。如果首次尝试获取锁失败,表示已经有别的线程在使用锁,那就会进入到自旋过程中,直到加锁成功返回null。非阻塞锁流程分析:尝试获取锁,返回null则说明加锁成功,返回一个数值,则说明已经存在该锁,ttl为锁的剩余存活时间。如果此时客户端2进程获取锁失败,那么使用客户端2的线程id(其实本质上就是进程id)通过Redis的channel订阅锁释放的事件。如果等待的过程中一直未等到锁的释放事件通知,当超过最大等待时间则获取锁失败,返回false,也就是第286行代码。如果等到了锁的释放事件的通知,则开始进入一个不断重试获取锁的循环。循环中每次都先试着获取锁,并得到已存在的锁的剩余存活时间。如果在重试中拿到了锁,则直接返回。如果锁当前还是被占用的,那么等待释放锁的消息,具体实现使用了JDK的信号量Semaphore来阻塞线程,当锁释放并发布释放锁的消息后,信号量的release()方法会被调用,此时被信号量阻塞的等待队列中的一个线程就可以继续尝试获取锁了。特别注意:以上过程存在一个细节,这里有必要说明一下,也是分布式锁的一个关键点:当锁正在被占用时,等待获取锁的进程并不是通过一个while(true)死循环去获取锁,而是利用了Redis的发布订阅机制,通过await方法阻塞等待锁的进程,有效的解决了无效的锁申请浪费资源的问题。3、Watchdog机制客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?Redisson提供了一个续期机制,只要客户端1一旦加锁成功,就会启动一个WatchDog。从以上源码我们看到leaseTime必须是-1才会开启WatchDog机制,也就是如果你想开启WatchDog机制必须使用默认的加锁时间为30s。如果你自己自定义时间,超过这个时间,锁就会自定释放,并不会延长。WatchDog机制其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个RedissonLock.EXPIRATION_RENEWAL_MAP里面,然后每隔10秒(internalLockLeaseTime/3)检查一下,如果客户端1还持有锁key(判断客户端是否还持有key,其实就是遍历EXPIRATION_RENEWAL_MAP里面线程id然后根据线程id去Redis中查,如果存在就会延长key的时间),那么就会不断的延长锁key的生存时间。异步续命也是通过一段lua脚本实现的,lua脚本如下:注意:这里有一个细节问题,如果服务宕机了,WatchDog机制线程也就没有了,此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程就可以获取到锁。4、可重入加锁机制Redisson支持重入锁,比如下面的代码:我们再来分析一下那段加锁的lua脚本:第一个if判断肯定不成立,existsproduct会显示锁key已经存在。第二个if判断会成立,因为product的hash数据结构中包含的那个ID即客户端1的ID,此时就会执行可重入加锁的逻辑,使用:hincrbyproduct285475da-9152-4c83-822a-67ee2f116a79:521对客户端1的加锁次数加1。锁重入的逻辑演示如下:看到这里,小伙伴就应该明白了:Redisson分布式锁时使用Redishash结构的incrby命令来实现的,其中key表示锁的名字,filed表示客户端唯一标识(UUID:线程id),value表示锁重入次数,每重入一次就计数就加1。5、锁释放机制执行lock.unlock()就可以释放锁,我们来看一下释放锁的流程代码:大致流程:调用unlockInnerAsync异步释放锁关闭该线程的WatchDog,取消异步更新锁的过期时间上面得代码核心调用就是unlockInnerAsync,这个方法也是通过一段lua脚本实现实际解锁逻辑的:lua脚本的流程:判断要释放的锁标志key是否存在,如果不存在返回nil;如果要释放的锁标志key还存在,使用hincrbyKEYS[1]ARGV[3]-1减给锁重入次数减1如果锁的重入次数还是大于0就返回锁的过期时间;否者删除key,并且通过Redis的发布机制通知阻塞的进程去竞争锁从代码来看,释放锁的步骤也主要分三步:删除锁(这里注意可重入锁,在上面的脚本中有详细分析)。广播释放锁的消息,通知阻塞等待的进程(向通道名为redisson_lock__channelpublish一条UNLOCK_MESSAGE信息)。关闭这个线程WatchDog,即将RedissonLock.EXPIRATION_RENEWAL_MAP里面的线程id删除,并且cancel掉Netty的那个定时任务线程。方案优点Redisson通过WatchDog机制很好的解决了锁的续期问题。和Zookeeper相比较,Redisson基于Redis性能更高,适合对性能要求高的场景。通过Redisson实现分布式可重入锁,比原生的SETmylockuserIdNXPXmilliseconds+lua实现的效果更好些,虽然基本原理都一样,但是它帮我们屏蔽了内部的执行细节。在等待申请锁资源的进程等待申请锁的实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率。方案缺点使用Redisson实现分布式锁方案最大的问题就是如果你对某个RedisMaster实例完成了加锁,此时Master会异步复制给其对应的slave实例。但是这个过程中一旦Master宕机,主备切换,slave变为了Master。接着就会导致,客户端2来尝试加锁的时候,在新的Master上完成了加锁,而客户端1也以为自己成功加了锁,此时就会导致多个客户端对一个分布式锁完成了加锁,这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。所以这个就是RedisCluster或者说是RedisMaster-Slave架构的主从异步复制导致的Redis分布式锁的最大缺陷(在RedisMaster实例宕机的时候,可能导致多个客户端同时完成加锁)最后一张图总结:参考[1]https://zhuanlan.zhihu.com/p/135864820[2]https://juejin.im/post/5e828328f265da47cd355a5d#heading-6
LoveIT 2020-06-16Redis -
分布式锁的三种实现方案
一、为什么要使用分布式锁我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程的18般武艺进行处理,并且可以完美的运行!注意这是单机应用,也就是所有的请求都会分配到当前服务器的JVM内部,然后映射为操作系统的线程进行处理!而这个共享变量只是在这个JVM内部的一块内存空间!后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:上图我们可以看到,在集群环境下不同服务器中的变量是单独的JVM中用于一块独立的空间,如果仅仅使用Java内置的锁他只是JVM级别的锁,也即使说只有分布式服务里面,多个服务属于不同进程,用普通的同步锁会失效,因为不同进程里面,就没有什么内存共享之类的说法。因此为了保证一个方法或属性在高并发情况下的同一时间只能被一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的JavaAPI并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!二、分布式锁应该具备哪些条件分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些特性:1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;2、高可用的获取锁与释放锁;3、高性能的获取锁与释放锁;4、具备可重入特性;5、具备锁失效机制,防止死锁;6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。三、分布式锁的三种实现方式以及Redis分布式锁的具体实现目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、**可用性(Availability)**和分区容错性(Partitiontolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行,当前业内公认的实现分布式锁有三套方案:基于数据库实现分布式锁基于缓存(Redis等)实现分布式锁基于Zookeeper实现分布式锁1、基于数据库实现分布式锁方案一:基于数据库表的实现要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。创建这样一张数据库表:当我们想要获取锁时,执行以下SQL:因为我们对method_name做了唯一性约束,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行临界区内容。当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:上面这种简单的实现有以下几个问题:1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。当然,我们也可以有其他方式解决上面的问题。针对数据库是单点问题可以搞数据库集群,数据之前双向同步。一旦挂掉快速切换到备库上。针对没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。针对非阻塞的?搞一个while循环,直到insert成功再返回成功。针对非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。方案二:基于数据库排他锁的实现除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作(伪代码):在查询语句后面增加forupdate,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:这种数据库分布式锁方案有效解决上面提到的无法释放锁和阻塞锁的问题。阻塞锁:forupdate语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。服务宕机自动释放锁:使用这种方式,服务宕机之后数据库会自己把锁释放掉。但是还是无法直接解决数据库单点可用性和可重入问题。方案总结总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。这两种方式各有各自的优缺点,但是我想说使用数据库实现分布式锁的方案虽然可行但是你要想想,我们的业务系统都搞成分布式的了,那就说明我们的业务系统已经有很大的流量了,这时候数据库的压力本生就很大了,此时在人为的给数据库增加压力,我个人认为这种方式可行但不可取!2、基于缓存(Redis等)实现分布式锁相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的,可以解决单点问题。目前有很多成熟的缓存产品,包括Redis,memcached等。这里我主要讲解一下使用Redis做分布式锁的方式——通过Redis的SETNXkeyvalue命令配合lua脚本保证命令执行的原子性。下面我们来一步步分析这其中的”坑“,并解决它们。首先按照基本想法我实现一个简单版本的如下:这个版本中很明显的有很多错误,首先一个可能的错误就是获取锁的线程在执行程序是突然发生异常了咋办?那么解决问题我们可以使用try-catch-finally来解决,更该版本如下:这般代码还不够健壮,因为如果在线程释放锁的时候突然停电了或者其他意外导致这个锁标志没法移除,那么这就是比较糟糕的,对于其他线程来说这个锁就不可用了。解决这个问题我们可以在设置锁的时候给锁设置一个过期时间,比如30s过期。这样一来就保证了锁的可用性,即使一个节点突然发生问题导致锁没有主动释放,那么也可通过Redis的超时机制来保护锁的可用性。由于我们目前这个分布式锁还只是一个非阻塞锁,也就是当线程没有获取到锁就立即返回失败,这在有些场景下是非常不友好的一种设计,因此我们需要把它改造成阻塞式锁。现在这把Redis实现的分布式锁足够安全了吗?我认为还不够!因为在释放锁的时候没有控制,任何线程都可以把这个key为“product”的锁标志删除了,这样看来,我们的这个锁还是不够安全。解决这个问题的方法是在设置key的时候向vlaue写一个唯一的随机值(比如UUID),在删除的时候需要比对key的值是否是在上锁的时候的值一样,如果一样才可以删除这个值,否者不允许删除。现在我们实现这把锁安全了吗?表面上看似安全了,但是仔细一想就会发现:我们的锁是有一个过期时间的,在比较繁杂的业务执行过程中执行业务的时间可能就会超过锁的过期时间,锁突然过期了这对于业务系统安全也是一种实实在在的威胁。解决这个问题的方法就是在主线程执行业务的时候再开一个线程当检测到锁快过期了就异步的为锁”续命“,直到主线程把任务执行完之后,异步线程死亡。这里我直接使用一个定时任务线程池,每当锁时间大约剩下1/3的时候就给锁续命,而后当执行完任务之后主线程关闭定死任务并释放锁,下一个线程就可以进来处理业务了。下面是执行截图:方案总结到这里,我们已经基本实现了redis分布式锁,Redis实现分布式锁主要步骤如下:指定一个key作为锁标记,存入Redis中,并且指定一个唯一的用户标识作为value。当key不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足互斥性特性。给key设置一个过期时间,防止因系统异常导致没能删除这个key,满足防死锁特性。当处理完业务之后需要清除这个key来释放锁,清除key时需要校验value值,需要满足只有加锁的人才能释放锁。3、利用zookeeper实现分布式锁让我们来回顾一下Zookeeper节点的概念:Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。Znode有可以分为四种类型:持久节点(PERSISTENT):默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在。持久节点顺序节点(PERSISTENT_SEQUENTIAL):所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号。临时节点(EPHEMERAL):和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。临时顺序节点(EPHEMERAL_SEQUENTIAL):临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。Zookeeper分布式锁的原理恰恰应用了临时顺序节点的特性,并且根据CAP理论,它是基于CP的一种分布式锁实现。具体实现原理和代码可以参考我的另一篇博文Zookeeper分布式锁的原理和具体实现参考链接[1]https://blog.csdn.net/wuzhiwei549/article/details/80692278[2]https://blog.csdn.net/weixin_42567141/article/details/103730590?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-1[3]https://www.bilibili.com/video/BV1iE411b7Fv
LoveIT 2020-06-14Redis -
SpringBoot项目更换启动时控制台打印的banner
更换Banner1、在resources目录下新建banner.txt文件2、制作Banner之后制作Banner,这里我推荐几个网站:-http://patorjk.com/software/taag-http://www.network-science.de/ascii/-http://www.degraeve.com/img2txt.php把banner字符输入之后提交就可以得到想要的Banner了,下面是我的网站的启动效果:感兴趣的小伙伴赶快去尝试一下吧!
LoveIT 2020-06-11Spring Boot -
布隆过滤器(Bloom Filter)的原理和实现
海量数据处理以及缓存穿透这两个场景让我认识了布隆过滤器,我查阅了一些资料来了解它,但是很多现成资料并不满足我的需求,所以就决定自己总结一篇关于布隆过滤器的文章。希望通过这篇文章让更多人了解布隆过滤器,并且会实际去使用它!下面我们将分为几个方面来介绍布隆过滤器:什么是布隆过滤器?布隆过滤器的原理介绍。布隆过滤器使用场景。通过Java编程手动实现布隆过滤器。利用Google开源的Guava中自带的布隆过滤器。Redis中的布隆过滤器。一、什么是布隆过滤器?在学习任何东西之前我们都应该先去了解一下它是什么?能干什么?布隆过滤器(BloomFilter)是一个叫做Bloom的老哥于1970年提出的。布隆过滤器是一个神奇的数据结构,可以用来判断一个元素是否在一个集合中。很常用的一个功能是用来去重\过滤。布隆过滤器的本质是由一个位数组和一系列哈希函数组成的数据结构。所谓位数组,就是指数组中的每个元素只占用1bit,每个元素只能是0或1。因此相对于List、Set、Map等结构,位数组的显著优势就是占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。二、布隆过滤器的原理介绍。当一个元素添加进入布隆过滤器的时候户进行如下操作:使用K个哈希函数根据元素计算出K个hash值根据得到的hash值,将位数组中对应的下标值置为1举个例子,比如现在有一个布隆过滤器有3个哈希函数:hash1,hash2,hash3和一个位数组array,现在要把www.easyblog.top插入到布隆过滤器中,则有如下操作:对字符串进行三次hash计算,得到3个hash值:h1,h2,h3根据hash值将对应位数组中的下标值置为1:array[h1]=1,array[h2]=1,array[h3]=1当需要判断在布隆过滤器中是否存在某个关键字的时候,只需要对关键字再次hash,得到值之后判断位数组中的每个元素是否都为1,如果值都为1,那么说明这个值在布隆过滤器中,如果存在一个值不为1,说明该元素不在布隆过滤器中。看不懂文字?没关系,让灵魂画手画给你看看到这里,我们就会发现一个问题:当插入的元素原来越多,位数组中被置为1的位置就越多,当一个不在布隆过滤器中的元素,经过哈希计算之后,得到的值在位数组中查询,有可能这些位置也都被置为1。这样一个不存在布隆过滤器中的也有可能被误判成在布隆过滤器中。但是如果布隆过滤器判断说一个元素不在布隆过滤器中,那么这个值就一定不在布隆过滤器中。简单来说:布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。三、布隆过滤器使用场景。判断给定数据是否存在:比如判断一个数字是否在于包含海量数字的数字集中(数字集很大,亿级以上!)防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)邮箱的垃圾邮件过滤、黑名单功能等等。去重:比如爬给定网址的时候对已经爬取过的URL去重。四、通过Java编程手动实现布隆过滤器。上面我们了解布隆过滤器的原理,知道了布隆过滤器的原理之后就可以自己手动实现一个了。根据原理我们向手动实现一个布隆过滤器的话,我们需要考虑:1、需要K个哈希函数2、一个合适大小的位数组3、添加元素到位数组(布隆过滤器)的方法4、判断关键字在位数组(布隆过滤器)中是否存在如果明白了原理实现起来还是比较简单的,实现如下:测试执行结果:布隆过滤器会有一定的误判,它说一个元素在其内部存在不一定存在,但是如果他说不能存在,那这个元素就一定不存在。五、使用Google开源的Guava中自带的布隆过滤器。自己实现的目的主要是为了让自己搞懂布隆过滤器的原理,Guava中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。首先我们需要在项目中引入Guava的依赖:使用方法如下:我们创建了一个最多存放10000个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01)执行结果:在我们的示例中,当mightContain()方法返回true时,我们可以99%确定该元素在过滤器中,当过滤器返回false时,我们可以100%确定该元素不存在于过滤器中。Guava提供的布隆过滤器的实现还是很不错的,但是它有一个重大的缺陷就是只能单机使用,而现在互联网一般都是分布式的场景。为了解决这个问题,我们可以使用Redis实现的布隆过滤器。六、Redis中的布隆过滤器。Redis4.0之后有了Module(模块/插件)功能,RedisModules让Redis可以使用外部模块扩展其功能。布隆过滤器就是其中的Module。详情可以查看Redis官方对RedisModules的介绍:https://redis.io/modules。RedisBloom提供了多种语言的客户端支持,包括:Python、Java、JavaScript和PHP。6.1、使用Docker启动RedisBloom可以参考dockerhub上给出的示例:https://hub.docker.com/r/redislabs/rebloom/第一次上面命令Redis会从Docker仓库下载RedisBloom。启动成功之后进入容器的交互界面RedisBloom常用命令注意:key表示布隆过滤器名,value是添加的元素(1)BF.ADD{key}{value}:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。(2)BF.MADD{key}{value}[value...]:将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式BF.ADD与之相同,只不过它允许多个输入并返回多个值。(3)BF.EXISTS{key}{value}:确定元素是否在布隆过滤器中存在。(4)BF.MEXITS{key}{vlaue}[value...]:确定一个或者多个元素是否在布隆过滤器中存在。(5)BF.RESERVE{key}{error_rate}{capacity}[EXPANSIONexpansion]:创建一个布隆过滤器。key:表示布隆过滤器的名字error_rate:表示误报的期望概率,这应该是介于0到1之间的十进制值。例如,对于期望的误报率0.1%(1000中为1),error_rate应该设置为0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的CPU使用率越高。capacity:过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。expansion:可选参数。如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以expansion。默认扩展值为2。这意味着每个后续子过滤器将是前一个子过滤器的两倍使用示例:添加元素到布隆过滤器,添加成功返回1,失败返回0批量添加元素到布隆过滤器判断元素在布隆过滤器中是否存在,存在返回1,否者返回0批量判断元素在布隆过滤器中是否存在6.2整合SpringBoot使用详情请参考我在GitHub上的Demohttps://github.com/LoverITer/redis-blooomFilter参考链接[1]https://segmentfault.com/a/1190000021194652[2]https://segmentfault.com/a/1190000016721700[3]https://blog.csdn.net/lifetragedy/article/details/103945885
LoveIT 2020-06-11Redis -
Docker常见异常处理(持续更新)
1.启动docker时映射到宿主机时出现/usr/bin/docker-current:Errorresponsefromdaemon:driverfailed...的解决方案启动docker映射到宿主机时出现如下错误时:这是由于来自守护进程的错误响应,而致使外部连接失败。解决的办法就是将其docker进程kill掉,然后再清空掉iptables下nat表下的所有链(规则)。最后,将docker的网桥删除,并重启docker服务.重启后不出以意外的话就可以了。2.serviceendpointwithnamexxxalreadyexists(docker已删除的容器却依旧存在)(1)启动服务报错如题确认我已经dockerrm-fXXX了。也确认各个容器端口并不重复。重新启动容器服务依旧报错:意思是这个端口已经被名为xxx的容器占用了。而执行dockerps又找不到这个容器,这种情况,通常是xxx容器没有正常删除导致的。这时我们可以使用命令:可以看到,此时stoic_varahamihira这个容器都还和docker保持连接的。(2)断开其网络连接:再次查看,stoic_varahamihira已不存在:之后重新启动服务,发现可以成功启动,问题解决
LoveIT 2020-06-10Docker -
秒懂,Redis缓存穿透、缓存击穿、缓存雪崩概念以及应对策
如上图所示,我们在应用程序和Mysql数据库中建立一个中间层,即Redis缓存。通过Redis缓存可以有效减少查询数据库的时间消耗,这极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求极高,那么就不能使用缓存。另外的一些典型问题就是,缓存穿透、缓存击穿和缓存雪崩。本文将简单介绍缓存穿透、缓存雪崩和缓存击穿这三者之间的区别以及这三类问题的解决方法。一、缓存穿透缓存穿透是指用户访问了不存在的数据,导致缓存无法命中,大量的请求都要穿透到数据库进行查询,从而使得数据库压力过大,甚至挂掉。比如:数据库使用了id为正整数作为键,但是黑客使用负整数向服务器发起请求,这时所有的请求都没有在缓存中命中,从而导致大量请求数据库,如果超过了数据库的承载能力,会导致数据库服务器宏机。一般解决缓存穿透的方法有:(1)缓存空对象这是一个简单粗暴的方法,方法是如果一个查询返回的结果是空的,仍然把这个空结果进行缓存,这样的话缓存层就存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个比较短的过期时间,让其自动过期。(2)使用布隆过滤器拦截这是一种常见而且有效的策略,它将所有可能存在的数据哈希到一个足够大的bitmap中,当查询一个不存在的key时会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。Redis实现了布隆过滤器,我们可以直接使用来达到过滤的目的。具体的原理和使用方法可以参考我的另一篇博文布隆过滤器(BloomFilter)的原理和实现二、缓存击穿(缓存并发)缓存击穿,是指当某个key在过期的瞬间,有大量的请求并发访问过期的键,这类数据一般是热点数据,由于缓存过期了,会同时访问数据库来查询数据,并写回缓存,从而导致数据库瞬间压力过大。缓存击穿的解决方法也有两个:(1)设置热点数据永不过期当遇到这种情况的时候,数据库很难扛下来这么大的并发。最简单的方法就是将热点数据缓存永不过期就好了。(2)使用互斥锁业界比较常用的方法,使用锁控制访问后盾服务的并发量。分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其线程没有获取到分布式锁等待就好了。这种方式将高并发的压力转移到了分布式锁,因此对系统的分布式锁是否合格考验很大。关于分布式锁内容可以参考我的另一篇博文分布式锁的三种实现方案本地锁:与分布式锁的作用类似,我们通过本地锁的方式来限制只有一个线程去数据库中查询数据,而其他线程只需等待,等前面的线程查询到数据后再访问缓存。但是,这种方法只能限制一个服务节点只有一个线程去数据库中查询,如果一个服务有多个节点,则还会有多个数据库查询操作,也就是说在节点数量较多的情况下并没有完全解决缓存并发的问题。基于Redis实现的分布式锁伪代码:三、缓存雪崩缓存雪崩是指设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致所有的查询同一时刻都落到了数据库上,造成了数据库压力过大。缓存雪崩与缓存击穿的区别在于这里针对很多key同时失效,前者则是针对某一个热点key失效解决方案:(1)不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀(2)在缓失效后,通过分布式锁或者分布式队列的方式控制数据库写缓存的线程数。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(和缓存击穿中使用互斥锁类似)(3)如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。随机过期时间伪代码:解释:在同一分类中的key,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。这种情况就应该考虑使用主备缓存策略了。最后再介绍几个有关缓存的概念缓存预热缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!解决思路:1、直接写个缓存刷新页面,上线时手工操作下;2、数据量不大,可以在项目启动的时候自动进行加载;3、定时刷新缓存;缓存更新除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:(1)定时去清理过期的缓存;(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。缓存降级当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。总结本文介绍了缓存应用中比较典型的是三个问题:缓存穿透,缓存击穿和缓存雪崩的概念和基本的解决方法。最后还介绍了缓存预热、缓存更新以及缓存降级等概念。在缓存的应用中还有一个比较典型的问题就是如何保证数据的一致性问题,也欢迎大家参考我的另一篇博文Redis缓存和MySQL数据一致性方案详解参考[1]https://www.pieruo.com/13549.html[2]https://www.cnblogs.com/midoujava/p/11277096.html
LoveIT 2020-06-09Redis -
Nginx配置实例—反向代理
1、配置示例1要求:在浏览器地址栏中输入www.123.com,跳转到Linux服务器中的tomcat主页面声明:本次实验在我电脑的Linux虚拟机上进行,Linux系统信息如下:图1实验系统信息1.1准备工作(1)安装nginx(参考:Nginx快速入门—基本概念以及在Linux上安装Nginx(2)启动一个tomcat(注意:在启动之前一定要配置好Java环境变量,参考:记一次在云服务器上部署项目的实践经历)(3)tomcat启动后在本地host文件中配置域名映射:配置完成后在浏览器中输入www.123.com:8080如果能看到tomcat的页面,就说明配置的没有问题。图2host配置测试1.2nginx配置反向代理接下来我们来配置nginx,nginx一入门我们都知道在nginx的安装目录下的conf/nginx.conf是nginx的默认配置文件,因此打开它默认配置文件并简单配置如下内容(当然,你也可以使用nginx-c指定配置文件),简单配置如下:测试结果:配置完成后重新加载配置文件后,直接访问www.123.com,如果可以看到tomcat的首页那就大功告成啦!图2nginx反向代理配置示例1测试2、配置示例2要求:在浏览器地址栏中输入www.123.com/login,请求跳转到127.0.0.1:8080在浏览器地址栏中输入www.123.com/user,请求跳转到127.0.0.1:80812.1准备工作启动两个tomcat,并在监听8080端口的tomcat的webapps目录下新建文件夹login以及文件index.html;在监听8081端口的tomcat的webapps目录下新建文件夹user以及文件index.html;2.2配置nginx按照要求,nginx服务器要根据不同的请求路径把不同的请求转发到不同的tomcat中处理,这里就需要用到正则表达式来匹配路径,具体的配置如下:测试结果:图3nginx反向代理配置示例2测试3、location匹配规则详解在上面的配置中使用到了正则匹配,nginx官方文档给出location语法如下:location语法:location[=|~|~*|^~]/uri/{…}其中,方括号中的四种标识符是可选项,用来改变请求字符串和uri的匹配方式。uri是待匹配的请求字符串,可以是不包含正则的字符串,这种模式被称为“标准的uri";也可以包含正则,这种模式被称为"正则uri",具体可用的标识符如下:**=**:必须与指定的模式严格匹配,如果匹配成功就停止向下搜索,并立即处理代理逻辑~:必须以指定模式开始,并且区分大小写~*:必须以指定模式开始,并且不区分大小写**^~**:前缀匹配,不支持正则,如果模式匹配,那么就停止搜索其他模式了。无:普通匹配(最长字符匹配);与location顺序无关,是按照匹配的长短来取匹配结果。若完全匹配,就停止匹配。备注:1、如果uri里面包含正则表达式,就必须使用~或~*标识符;2、针对~和~*匹配标识符,可以在前面加上!来取反,如下:*!~表示正则不匹配,区分大小写。*!~*表示正则不匹配,不区分大小写。3.1精确匹配(=)示例3.2区分大小写正则匹配(~)示例3.3不区分大小写正则匹配(~*)示例3.4标准匹配(^~)示例3.5普通匹配(最长字符匹配)示例4、location匹配优先级1、如果有精确匹配,会先进行精确匹配,匹配成功,立刻返回结果。2、普通匹配与顺序无关,因为按照匹配的长短来取匹配结果。3、正则匹配与顺序有关,因为是从上往下匹配。(首先匹配,就结束解析过程)。4、在location中,location/可以匹配所有的请求,但是他的优先级是最低的。总结匹配规则如下:(location=)>(location完整路径)>(location^~路径)>(location~,~*正则顺序)>(location部分起始路径)>(location/)即(精确匹配)>(最长字符串匹配,但完全匹配)>(非正则匹配)>(正则匹配)>(最长字符串匹配,不完全匹配)>(location通配)
LoveIT 2020-05-31Nginx -
SpringBoot网站基于OAuth2添加第三方登录之GitHub登录
一、OAuth2简介OAuth是目前最流行的授权机制,用来授权第三方应用,获取用户数据。OAuth在全世界已经得到广泛应用,目前的版本是2.0版。简单说,OAuth就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。因此令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异:令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。令牌可以被数据所有者撤销,会立即失效。令牌有权限范围(scope)因为令牌有着和密码一样的功能,所以对于令牌也必须严格保密,泄漏令牌和泄漏密码的后果是一样的。这也就是为啥令牌的有效期很短的原因了。二、GitHub第三方登录的原理第三方登录实际就是OAuth授权认证。用户想要登录A网站,A网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要OAuth授权。以使用GitHub为第三方登录为例:用户在网站点击使用GitHub登录,A网站跳转到GitHub(会带上回调URI和ClientID)GitHub要求用户登录,然后询问"A网站要求获得xx权限,你是否同意?"(对于同一个网站,同意一次之后,下次再登录就不在需要用户授权)用户同意,GitHub就会重定向回A网站,同时发回一个授权码。A网站使用授权码,向GitHub请求令牌。GitHub返回令牌.A网站使用令牌,向GitHub请求用户数据。使用GitHub做第三方登录认证的具体的流程可如下图所示:好了,原理大致了解到这里,接下来动手搭建一下GitHub第三方登录。三、GitHub第三方登录搭建示例1、创建应用①登录github后台(地址:https://github.com/settings/developers),创建应用你需要添加GitHub登录的应用。或者在GitHub的settings中点击Deveplopersettings②之后直接点击NewGitHubApp③之后填写你的App有关的信息即可在注册成功之后,GitHub会返回客户端ID(clientID)和客户端密钥(clientsecret),这就是应用的身份识别码,在授权的时候会用到。一个填写示例:2、几个重要的URL登录页面授权URL:https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&state=%s获得Token的URL:https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s&redirect_uri=%s&state=%s获得用户信息的URL:https://api.github.com/user?access_token=%s3、设计数据库设计一个数据表用于存放用户的认证记录,用于认证之后就将他的认证信息存放到这个表中,并和主用户表(User)绑定。之后Mapper(DAO)、Service层自己按照自己业务逻辑自行实现,准备好有关接口等待Controller的调用。4、放置GitHub登录按钮点击GitHub登录之后的授权页面:5、后台代码具体实现①首先在SpringBoot配置文件配置如下信息:②之后写一个配置类用于获取这些文件中的这些信息③为了提高系统的扩展性,比如以后还可以做个QQ登录、微信登录.....,因此这里提供一个接口用于认证服务GitHub认证授权实现类:④GitHub认证控制器(Controller)GitHub返回的用户信息中的id在GitHub上是一个唯一的Id,这个可以作为用户是否在使用GitHub在系统登录过的依据。好了,这里我们的Github授权登录功能就算完成了。哪里有不清楚的地方可以在评论区留言。
LoveIT 2020-05-31Spring Boot -
卸载Linux服务器上安装的Docker
有时会用到,特此记录一下在使用后3条命令时如果遇到如下错误:这是由于挂载的目录没有停止,最简单的方法是重启服务器,然后在执行命令即可。
LoveIT 2020-05-05Docker -
Docker进阶之容器数据卷(Volume)
一、什么是数据卷?什么是容器数据卷? 数据卷就是一个文件或者文件夹。 Docker的理念之一是将应用与其运行的环境打包,docker容器的生命周期是与其运行的程序一致的,而对数据的要求是持久化,docker容器之间也需要有共享数据的渠道。 数据卷是特殊的目录,可以绕过联合文件系统,为一个或多个容器提供访问。数据卷设计的目的是数据的持久化,数据卷的生命周期完全独立于容器,不会在容器删除时删除其挂载的数据卷,也不会存在类似垃圾收集机制,对容器引用的数据卷进行处理。 数据卷存在于宿主机中,独立于容器,和容器的生命周期是分离的,数据卷存在于宿主机的文件系统中,数据卷可以是目录也可以是文件,容器可以利用数据卷与宿主机进行数据共享,实现了容器间的数据共享和交换。 通俗地来说,docker容器数据卷可以看成使我们生活中常用的u盘,它存在于一个或多个的容器中,由docker挂载到容器,但不属于联合文件系统,Docker不会在容器删除时删除其挂载的数据卷。二、Volume的作用/特点通过数据卷可以在容器之间实现共享和重用对数据卷的修改会立马生效(非常适合作为开发环境)对数据卷的更新,不会影响镜像卷会一直存在,直到没有容器使用三、Docker添加数据的两种方式1、使用命令的方式添加数据卷在使用dockerrun的时候我们可以通过-v来创建一个数据卷并挂载到容器上,在一次run中多次使用可以挂载多个容器。命令语法:dockerrun-it-v/宿主机绝对路径:/容器绝对路径镜像名打开命令行终端,使用docker命令以交互式的方式来运行centos如果发现挂载在容器中的目录没有访问权限,可以加上--privileged=true这个参数。启动成功后我们可以用前面我们学过一个命令dockerinspect容器名/容器ID来查看关于一个容器的有关细节,打开我们发现主机和容器之间的共享已经建立起来了,而且默认都是可读可写的:接下来然容器和主机之间互相共享数据:在另一个终端中来到/home/huangxin/hostdata目录下,新建text1.txt,然后在里面写点东西:随便写点东西:然后来到容器里,就可以看到主机在主机端的共享文件夹中写的数据在容器中可以看到了:接着我们反向操作看看能不能在容器中写的数据在主机可以共享到。还是随便写了点东西在新建的Demo1.text中,然后在主机端去看看有没有。可以看到主机和容器之间可以相互共享文件了;而且对这些文件都具有读写的权限。即使容器停止后,这些共享的数据还是同步的。然而有时我们不需要让容器写数据,那么我们就可以用带权限的命令:dockerrun-it-v/宿主机绝对路径:/容器绝对路径:ro镜像名,ro表示readonly。四、使用DockerFile添加数据卷编写自定义的Dockerfile,可以使用VOLUME[]命令可以挂载任意多个共享目录,以json的格式,多个目录逗号隔开。使用dockerbuild命令通过Dockerfile构建一个镜像dockerbuild的基本语法:dockerbuild[OPTIONS]PATH|URL|-*-f,--file指定Dockerfile的路径名*-t,--tag指定镜像的REPOSITORY和标签常用的写法:dockerbuild-fDcokerfile的路径-t仓库:标签可以看到我们的镜像构建成功了,下来我们来启动基于我们构建镜像的一个容器。使用ls查看,发现容器中自动就挂载了两个文件夹data1,data2但是这两个文件夹对应在主机上的什么地方呢?使用dockerinspect来看看这个运行中的容器的详细信息可以看到,docker对于使用VOLUME挂载的共享目录,在主机中会有一个默认的文件夹——"/var/lib/docker/volumes/。在这个文件夹下你有挂载几个,docker就默认给你生成几个对应的共享目录。还是一样随便在Dem01.txt中写点东西,然后去容器中的data1目录看看有没有Demo1.txt以及内容。五、容器之间共享数据让容器之间共享数据,有一种方法是可以通过数据卷来实现,首先启动一个父容器(启动我们刚才制作的那个镜像)在centos01.txt中随便写点东西,然后在启动子容器时候使用命令参数:--volumes-from来和父容器共享数据,最终达到的效果:父子容器都可以互相共享各自的数据:那么如果我们删除了父容器,父容器以前共享的数据还在吗?子容器之间的共享还会有吗?来,一张图回答这个问题:
LoveIT 2020-05-01Docker -
手把手教你在Centos 7上安装和使用Docker
在上一节我们了解了什么是Docker、以及Docker的三个核心概念、Docker的作用。本节我们就上机实操,在Linux上安装Docker、学习一下Docker常用的命令、最后我们使用Docker启动一个Tomcat容器感受一下Docker的强大。好了,我们开始吧!一、CentOS7安装配置Docker1、检查系统版本。在安装前一定要先检查一下你的CentOS的版本:Docker运行在CentOS7上,要求系统为64位、系统内核版本为3.10以上。查看的命令是:2、使用yum命令安装docker3、启动Docker,并设置以后开机Docker自动启动4、这时我们可以查看一下docker的有关配置5、配置一下阿里云的镜像源这一步的配置不是必须的,只是配置一下以后使用的时候下载的速度会快很多,而且有时候如果直接使用国外的镜像出现下载失败的情况也可以这么配置一下。配置的具体的方法是:5.1、到阿里云的官网直接搜索容器镜像服务5.2、点开后一顿注册设置后点击最下面的镜像加速器,然后复制加速器地址5.3、复制后来到虚拟机是用vim打开/etc/docker/daemon.json添加刚才阿里云上的加速地址,格式如下:注意:这里格式一定要正确,标准的json格式,不然服务不能启动配置后保存并退出vim界面,输入以下命令重新加载配置并重启Docker6、停止docker在这里对于Docker的配置就大致完成了,如果我们想停止docker,可以使用一下命令:二、Docker常用命令和操作2.1镜像操作1、检索关键字(镜像)2、拉取镜像这个命令中的tag就是镜像的版本,具体这个镜像有哪些版本可以下载,可以去docker.hub查看。tag可以省略,省略后默认下载最新的版本。3、查看本地所有的镜像4、删除指定本地的镜像2.2容器操作(以tmocat为例)1、搜索镜像搜索镜像使用命令:dockersearch镜像名一般查出来会有一大堆的东西,其中OFFICIAL字段标记为[OK]表示是官方版本,一般我们就下载官方的就好了。2、拉取镜像拉取镜像使用命令:dockerpull镜像名在这一步配置阿里云镜像的效果就显现出来了,下载速度还是非常快的。3、查看已下载的镜像我们可以使用命令:dockerimages查看我们已经安装的镜像。执行时候会列出当前已经安装的docker镜像,各个字段的含义如下:REPOSITORY:镜像名TAG:镜像的版本,latest表示是最新版本IMAGEID:标识这个镜像的唯一标识符CREATED:安装日期SIZE:镜像的大小4、启动容器启动容器使用命令:dockerrun[参数]镜像名dockerrun命令通常会跟一些启动参数:--name参数后面指定你为这个容器起的名字-d参数表示启动后在后台运行-p参数作用是将主机的端口映射到容器的一个端口:-p主机端口:容器内部的端口tomcat:latest就是要启动的镜像名,冒号后面指定这个镜像的版本,latest可以省略,省略后默认启动latest版本的。启动成功就会产生一串16进制数字,这串数字可以在当前docker环境中唯一标识这个运行中的容器实例。5、查看运行中的容器和所有容器之后我们可以尝试访问一下这个在虚拟机中启动的tomcat,有两种方法,一种是在图像化的Linux终端中打开浏览器访问localhost:25050或者我们也可以先使用ifconfig命令查看虚拟机的ip,然后使用宿主机的浏览器中访问虚拟机ip:250506、停止运行中的容器停止容器使用命令::dockerstop容器名或容器id7、删除容器删除容器使用命令:dockerrm容器名或容器id8、查看容器的日志查看容器日志使用命令:dockerlogs容器名或容器id后记这里我只是借助安装Tomcat简单使用了一下docker,不得不说确实非常方便,然而这里仅仅是介绍了一些常用的命令,Docker中还有许多的命令这里没有介绍到,具体的可以参考Docker官方文档,而且我们也可以参考docker.hub上每个镜像官方给出的配置方法,方法很多这里就不一一介绍了。
LoveIT 2020-04-30Docker -
Docker入门基本核心概念
一、认识容器Docker是世界领先的软件容器平台,所以想要搞懂Docker的概念我们必须先从容器开始说起。1、什么是容器?先来看看容器较为官方的解释。一句话概括容器:容器就是将软件打包成标准化单元,以用于开发、交付和部署。容器镜像是轻量的、可执行的独立软件包,包含软件运行所需的所有内容:代码、运行时环境、系统工具、系统库和设置。容器化软件适用于基于Linux和Windows的应用,在任何环境中都能够始终如一地运行。容器赋予了软件独立性 ,使其免受外在环境差异(例如,开发和预演环境的差异)的影响,从而有助于减少团队间在相同基础设施上运行不同软件时的冲突。再来看看容器较为通俗的解释(说人话)如果需要通俗的描述容器的话,我觉得容器就是一个存放东西的地方,就像书包可以装各种文具、衣柜可以放各种衣服、鞋架可以放各种鞋子一样。我们现在所说的容器存放的东西可能更偏向于应用比如网站、程序甚至是系统环境。2、虚拟机与容器的关系?容器和虚拟机具有相似的资源隔离和分配优势,但功能有所不同,因为容器虚拟化的是操作系统,而不是硬件,因此容器更容易移植,效率也更高。什么是虚拟机?虚拟机(VM)是计算机系统的仿真。简而言之,它可以在实际上是一台计算机的硬件上运行看起来很多单独的计算机。操作系统(OS)及其应用程序从单个主机服务器或主机服务器池共享硬件资源。每个VM都需要自己的底层操作系统,并且硬件是虚拟化的。管理程序或虚拟机监视器是创建和运行VM的软件,固件或硬件。它位于硬件和虚拟机之间,是虚拟化服务器所必需的。自从负担得起的虚拟化技术和云计算服务出现以来,大大小小的IT部门都将虚拟机(VM)作为降低成本和提高效率的一种方式。但是,VM可能占用大量系统资源。每个VM不仅运行操作系统的完整副本,还运行操作系统需要运行的所有硬件的虚拟副本。这很快就会增加大量的RAM和CPU周期。与运行单独的实际计算机相比,这仍然是经济的,但对于某些应用程序来说,这可能是过度的,这导致了容器的开发。什么是容器?使用容器,而不是像虚拟机(VM)那样虚拟化底层计算机,只是虚拟化操作系统。容器位于物理服务器及其主机操作系统之上-通常是Linux或Windows。每个容器共享主机操作系统内核,通常也包括二进制文件和库。共享组件是只读的。共享操作系统资源(如库)可以显着减少重现操作系统代码的需要,并且意味着服务器可以通过单个操作系统安装来运行多个工作负载。因此容器非常轻-它们只有几兆字节,只需几秒钟即可启动。与容器相比,VM需要几分钟才能运行,并且比同等容器大一个数量级。与VM相比,容器所需的全部功能都足以支持程序和库以及运行特定程序的系统资源。实际上,这意味着您可以将容器上的应用程序的容量设置为使用容器的两到三倍,而不是使用VM。此外,使用容器,您可以为开发,测试和部署创建可移植,一致的操作环境。容器与虚拟机总结容器是一个应用层抽象,用于将代码和依赖资源打包在一起。多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行。与虚拟机相比,容器占用的空间较少(容器镜像大小通常只有几十兆),瞬间就能完成启动。虚拟机(VM)是一个物理硬件层抽象,用于将一台服务器变成多台服务器。管理程序允许多个VM在一台机器上运行。每个VM都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此占用大量空间。而且VM启动也十分缓慢。通过Docker官网,我们知道了这么多Docker的优势,但是大家也没有必要完全否定虚拟机技术,因为两者有不同的使用场景。虚拟机更擅长于彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而Docker通常用于隔离不同的应用,例如前端,后端以及数据库。二、Docker基本概念1、什么是Docker?Docker使用Google公司推出的使用Go语言进行开发实现,基于Linux内核的cgroup,namespace,以及AUFS类的UnionFS等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。Docker在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得Docker技术比虚拟机技术更为轻便、快捷。记住最重要的一点,Docker实际是宿主机的一个普通的进程,这也是Dokcer与传统虚拟化技术的最大不同。我们着重可以从下四个方面理解Docker是什么:Docker是世界领先的软件容器平台。Docker是一个构建在LXC(LinuxContainer容器)之上,基于进程容器的轻量级VM解决方案,实现了一种应用程序级别的资源隔离及配额。Docker起源于PaaS提供商dotCloud基于go语言开发,遵从Apache2.0开源协议Docker能够自动执行重复性任务,例如搭建和配置开发环境,从而解放了开发人员以便他们专注在真正重要的事情上:构建杰出的软件。用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。2、为什么要用Docker?一致的运行环境:Docker的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现“这段代码在我机器上没问题啊”这类问题;更快速的启动时间:可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。——更快速的启动时间隔离性:避免公用的服务器,资源会容易受到其他用户的影响。弹性伸缩,快速扩展:善于处理集中爆发的服务器使用压力;动态迁移成本低:可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。持续交付和部署:使用Docker可以通过定制应用镜像来实现持续集成、持续交付、部署。三、Docker三个核心概念Docker中有非常重要的三个核心的概念,理解了这三个概念,就理解了Docker的整个生命周期。镜像(Image)容器(Container)仓库(Repository)理解了这三个概念,就理解了Docker的整个生命周期1、镜像(Image)操作系统分为内核和用户空间。对于Linux而言,内核启动后,会挂载root文件系统为其提供用户空间支持。而Docker镜像(Image),就相当于是一个root文件系统。Docker镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。Docker设计时,就充分利用**UnionFS技术**,将其设计为分层存储的架构。镜像实际是由多层文件系统联合组成。镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。 比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。2、容器(Container)镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。前面讲过镜像使用的是分层存储,容器也是如此。容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。按照Docker最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器可以随意删除、重新run,数据却不会丢失。3、仓库(Repository)镜像构建完成后,可以很容易的在当前宿主上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,DockerRegistry就是这样的服务。一个DockerRegistry中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。所以说:镜像仓库是Docker用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以latest作为默认标签。目前最大最常用的公开DockerRegistry就是Docker官方提供的**DockerHub**,官方是这样介绍DockerHub的:DockerHub是Docker官方提供的一项服务,用于与您的团队查找和共享容器镜像在DockerHub中我们几乎可以搜索到目前市面上所有的软件产品的镜像。在DockerHub的搜索结果中,有几项关键的信息有助于我们选择合适的镜像:OFFICIALImage:代表镜像为Docker官方提供和维护,相对来说稳定性和安全性较高。Stars:和点赞差不多的意思,类似GitHub的Star。Dowloads:代表镜像被拉取的次数,基本上能够表示镜像被使用的频度。在国内访问DockerHub可能会比较慢国内也有一些云服务商提供类似于DockerHub的公开服务。比如时速云镜像库、网易云镜像服务、DaoCloud镜像市场、阿里云镜像库等。具体如何配置,可以参考下一节:手把手教你在Centos7上安装和使用Docker
LoveIT 2020-04-30Docker -
基于Linux平台运行的Nginx配置阿里云 SSL证书实现HTTPS访问
1、前提工作1.1准备一台具有公网IP的Linux服务器1.2申请一个域名,并将此域名和服务器的公网IP绑定1.3在阿里开发者平台上申请并获取SSL证书(1)在开启SSL后,来到下图所示页面,选择一个证书类型后,不用管他上面的报价,点击申请。(2)在接下来的这个页面中,选择你想要的证书服务类型(3)购买成功之后,跟随系统的引导完成证书的签发之后点击下载SSL证书。这里我下载了Nginx。证书有两个,一个是证书文件*.pem,另一个是证书密钥文件*.key。1.4在服务器上安装Nginx服务器2、配置SSL证书登录Nginx服务器,在Nginx安装目录(默认Nginx安装目录为/usr/local/nginx/conf)下创建cert目录,并将下载的证书文件和密钥文件拷贝到cert目录中。上传好证书和秘钥文件后,我们打开改Nginx安装目录/conf/nginx.conf文件。找到以下配置信息:按照下文中注释内容修改nginx.conf文件:修改后保存之后别急着重启,先检查一下配置是否正确:[root@ecs-sn3-medium-2-linux-20191128162047conf]#../sbin/nginx-tnginx:theconfigurationfile/usr/local/nginx/conf/nginx.confsyntaxisoknginx:configurationfile/usr/local/nginx/conf/nginx.conftestissuccessful没问题后再重启Nginx,就可以使用https了。注意!如果检查时提示nginx:[emerg]the"ssl"parameterrequiresngx_http_ssl_modulein/usr/local/nginx/conf/nginx.conf:xxx,那就说明我们在安装Nginx的时候没有安装http_ssl_module模块,这一点我们可以使用nginx-V命令来查看。解决办法:重新配置Nginx安装http_ssl_module模块即可。(1)进入到nginx的源码包(2)在源码包下执行如下命令:(3)使用make命令编译(不要执行makeinstall,执行后就会覆盖安装已经存在Nginx)最终效果:
LoveIT 2020-03-23Nginx -
Spring从入门到精通—SSM整合实例
0、什么是SSM?在软件开发领域里SSM是对Spring、SpringMVC、MyBatis这三大框架的简称。0.1、SpringSpring是一个开源框架,Spring是于2003年兴起的一个轻量级的Java开发框架,由RodJohnson在其著作ExpertOne-On-OneJ2EEDevelopmentandDesign中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。简单来说,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。0.2、SpringMVCSpringMVC属于SpringFrameWork的后续产品,已经融合在SpringWebFlow里面。SpringMVC分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。0.3、MyBatisMyBatis本是Apache的一个开源项目iBatis,2010年这个项目由Apachesoftwarefoundation迁移到了googlecode,并且改名为MyBatis。MyBatis是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQLMaps和DataAccessObjects(DAO)MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis使用简单的XML或注解用于配置和原始映射,将接口和Java的POJOs(PlainOldJavaObjects,普通的Java对象)映射成数据库中的记录。接下来我们来一步步把他们整合在一起。1、创建基本的MavenWeb项目 在IDEA中创建一个基本的Maven项目。具体过程不是这里的重点,如有有不清楚,请自行百度或Google~。创建好项目后,在pom.xml文件中配置依赖(导包),在pom.xml文件中添加依赖配置如下:注意!配置的时候要特别注意mybatis-spring这个包的版本和你使用的MyBatis以及Spring版本是否兼容,特别适当发生java.lang.AbstractMethodError:org.mybatis.spring.transaction.SpringManagedTransaction.getTimeout()错误的时候,还就更要怀疑是这个问题了:适配的环境:mybatis-spring对JDK、mybatis、spring都有要求2、配置环境 配置文件的编写是SSM整合的关键,需要配置的东西主要有web.xml、Sprng配置文件applicationContext.xml、SpringMVC配置文件applicationContext-mvc.xml、MyBatis全局配置文件MyBatis-config.xml以及后面的Mapper映射文件的编写。2.1配置web.xml文件web.xml文件的基本配置如下:2.2配置MyBatis-config.xml全局配置文件 在MyBatis的全局配置文件中配置一些基本对settings,环境就不要在这里配置了,交给Spring管理就好了(本身MyBatis这个配置文件都是可有可无的,但是用它来写一个settings配置可以使结构清晰,方便修改)。2.3配置applicationContext-mvc.xml文件2.4配置applicationContext.xml文件2.5jdbc.properties数据库资源文件对于SSM的配置基本上完成了,下面将使用这个搭建好的框架对对book表进行简单的CRUD操作。3、一个简单CRUD3.1基本准备创建数据库和表:新建tx数据库,并在tx数据库中新建book表。具体的数据库脚本如下:3.2编写SQL映射文件以及对应的Mapper接口在src/main/resources目录下新建mapper目录,在mapper目录下新建BookMapper.xmlSQL映射文件。并在源码包的mappr包下新建对应的BookMapper.java接口。(1)SQL映射文件BookMapper.xml(2)Mapper接口3.3开发业务层(Service层)为了遵循SOA的开发理念和规范,我们这里还是写一个接口,并实现接口在com.xust.iot.service包下新建impl包,然后新建BookService接口的实现类BookServiceImpl。3.4开发控制层(Controller层)在com.xust.iot.controller包下新建BookController类3.5开发视图层(View层)(1)index.html(2)添加图书的页面:add.jsp(3)展示所有图书信息的页面:bookInfo.jsp(4)修改图书信息的页面:edit.jsp最终的测试结果总结 经过几个小时的斗争,SSM三大框架的简单整合算是完成了,期间也遇到了这样那样的问题,但都一一解决了,从测试结果来看还是比较成功的。然而代码没有写几行,配置文件写了一大堆,我只想说SpringBoot真香!!!
LoveIT 2020-03-17Spring -
Nginx配置实例—nginx+keepalived实现主备服务器的高可用集群
1、什么是负载均衡高可用?nginx作为负载均衡服务器,所有请求都到了nginx,可见nginx处于非常重要的位置,如果nginx服务器宕机将会导致后端web服务将无法提供,后果严重。为了屏蔽负载均衡服务器的宕机,需要建立一个备份机。主服务器和备份机上都运行高可用(HighAvailablitity)监控程序,通过传送诸如“Iamalive”这样的信息来监控对方的运行状况,当备份机不能再一定时间内收到这样的信息,它就接管主服务器的服务ip并继续提供负载均衡服务;当备份管理器又从主管服务器收到“Iamalive”这样的信息时,它就释放服务ip地址,这样的主服务器就开始再次提供负载均衡服务。2、Nginx+Keepalived实现主备2.1什么是keepalived?(1)keepalived是进群工作管理中保证集群高可用的一个服务软件,用来防止单点故障。(2)keepalived的作用是检测web服务器的状态,如果有一台web服务器死机,或工作出现故障,keepalived将检测到,并将有故障的web服务器从系统中剔除,当web服务器工作正常后keepalived自动将web服务器加入到服务器群中,这些工作全部自动完成,不需要人工干涉,需要人工做的只是修复故障的web服务器。2.2keepalived的工作原理keepalived是以vrrp协议为实现基础的,vrrp全称VirtualRouterRedundancyProtocol,即虚拟路由冗余协议。虚拟路由冗余协议,可以认为是实现路由器高可用的协议,即将N台提供相同功能的路由器组成一个路由器组,这个组里面有一个master和多个backup,master上面有一个对外提供服务的vip(VIP=virtualIPAddress,即虚拟ip地址,该路由器所在局域网内其他机器的默认路由为该vip),master会发组播,当backup收不到VRRP包时就认为master宕掉了,这时就需要根据VRRP的优先级来选举一个backup当master。这样的话就可以保证路由器的高可用了。keepalived主要有三个模块,分别是core、check、VRRP。core模块为keepalived的核心,负载进程的启动、维护以及全局配置文件的加载和解析。check负责健康检查,包括常见的各种检查方式。VRRP模块是来实现VRRP协议的。3、配置Nginx高可用集群图nginx高可用架构示意图我的实验环境:1).两台nginx,一主一备分别在独立的Linux系统上运行,ip分别是:192.168.92.128和192.168.92.1342).分别在192.168.92.128和192.168.92.134两台服务器上安装nginx和keepalived3.1安装nginxnginx的安装请参考Nginx快速入门—基本概念以及在Linux上安装Nginx这里为了体现效果,我把nginx的默认index.html页面修改一下图3.1.1masternginx的修改图3.1.2backupnginx的修改3.2安装和配置keepalived直接输入下面的命令安装keepalived:安装完成后会在/etc下生成一个配置文件,它的绝对路径是/etc/keepalived/keepalived.conf,它就是用来配置nginx主备关系的配置文件。主nginx的keepalived配置备份nginx的keepalived配置之后在/etc/keepalived目录下新建检测脚本,内容如下:之后给脚本权限:keepalived的相关命令keepalived的启动、重启、和停止命令:1.systemctlstartkeepalived.service #启动keepalived2.systemctlrestartkeepalived.service #重启keepalived3.systemctlstopkeepalived.service #停止keepalived启动后可以使用:systemctlstatuskeepalived.service查看keepalived的状态,如果发现异常及时修改直到正常启动,正常运行截图:图3.2.1正常启动keepalived或者可以使用命令:`ps-ef|grepkeepalived`两边的nginx和keepliaved都启动后,在浏览器中输入我们刚才配置的虚拟ip,不出意外应该走的是主nginx,如下图:图3.2.2配置成功4、测试故障转移4.1主服务器正常提供服务初始时候,nginx主服务器正常,将vip绑定到自身,对外提供服务,从服务器始终与主服务器保持通信,监测主服务器的健康状态。图4.1主服务器正常服务4.2主服务器宕机,备份服务器接替主服务器的工作当nginx主服务器宕机或发生异常,总之以任何理由造成服务器上的健康监测程序发生异常,无法和从服务器上的健康监测程序通信,此时从服务器上的健康监测机制就会认为主服务器挂了,从而将vip绑定到自身,成功上位,充当主服务器的角色。图4.2.1主服务器宕机后验证:关闭主服务器(192.168.92.128)上的nginx:/usr/local/nginx/sbin/nginx-squit,之后在浏览器地址栏中输入vip:192.168.92.150图4.2.1切换到备用服务器(192.168.92.134)4.3“一山不容二虎”:主服务器恢复后,备份服务器主动让位在keepalive机制中,主服务器终究是主服务器,一旦主服务器恢复,边从新绑定vip,继续充当主服务器,而从服务器又成为了热备。图4.3主服务器恢复验证:重启主服务器(192.168.92.128)和主服务器的keepalived,再次输入192.168.92.150,主服务器恢复服务:图3.2.2配置成功
LoveIT 2020-02-13Nginx -
Nginx配置实例—动静分离
1、动静分离概念重温Nginx动静分离可以简单的理解为把动态跟静态分离开来,不能理解成只是单纯的把动态页面和静态页面物理上的分离。严格意义上来说应该是把动态请求和静态请求分离,即nginx处理静态页面,tomcat等应用服务器处理动态页面。动静分离从目前的实现角度来讲大致分为两类:一种是纯粹把静态文件放在一个独立的服务器上,有一个独立的域名,也是目前主流推崇的方案;另一种是动态文件和静态文件混在一起发布,通过nginx分离开来,通过location配置不同的后缀名实现不同的请求转发。2、Nginx动静分离实战配置2.1准备静态资源首先在Liunx系统根目录下新建一个文件夹/resources/static用于存放静态文件2.2准备动态资源在tomcat的webapps/ROOT目录下新建test.jsp,内容如下:2.3在nginx中配置对静态请求和动态请求的处理逻辑完成资源文件的准备后,启动两个tomcat,分别监听8080和8081,之后在nginx的配置文件做如下简单配置:配置好后重启nginx,然后在浏览器地址栏中输入www.123.com/nginx.png,能看到nginx的Logo图,说明对静态资源的配置没有问题;之后再输入www.123.com/test.jsp,能看到Randomnumber:xxx,说明对动态请求的配置也能正常工作,运行截图如下:图1nginx处理静态请求图2nginx处理动态请求整合动态资源和静态资源上面通过一个简单的小测试体验了一下nginx的动静分离的效果,测试结果达到了预期效果。接下来我们来把他们整合在一起,即在一个页面中即有动态的也有静态的资源,通过nginx中配置帮我们完成对资源的请求,而我们所需做的就是输入www.123.com这个地址即可。(1)在/rsources目录下新建目录template,并在template中新建文件:index.html,文件内容如下:保存文件之后修改nginx配置文件如下:配置好后重启nginx,然后在浏览器地址栏中输入www.123.com,如果在网页上能看到如下图所示内容表示配置成功,运行截图如下:图3动静资源正常加载
LoveIT 2020-02-12Nginx -
Nginx配置实例—负载均衡
1、Nginx负载均衡配置预期实现效果:在浏览器地址栏中输入:http://www.123.com,nginx收到请求后把请求平均分匹配到Linux服务器上监听8080和8081端口的两个tomcat服务器上。1.1准备工作(1)首先在host文件中配置域名映射:192.168.92.128www.123.com(2)准备两个tomcat服务器,分别监听8080和8081端口,并在各自目录下新建一个edu目录,并创建index.html测试页面,配置完成后启动,让两个tomcat跑起来。1.2nginx负载均衡配置在nginx的http模块下配置upstream模块,upstream的语法是:注:[]中的内容是可选的。简单配置如下:测试结果在浏览器地址栏中输入www.123.com/edu,结果和预期一样,nginx让两个tomcat轮询处理请求:负载均衡配置运行结果2、upstream模块详解upstream是Nginx中配置负载均衡的关键模块,基本语法:upstream有关的参数如下:server:用于配置反向服务地址和端口,语法:serveraddress[parameters];weight:权重,语法:weight=number,默认是1max_conns:语法:max_conns=number,用于限制到代理服务器的同时活动连接的最大数量,默认是0max_fail:语法:max_fails=number,允许请求失败的次数。经常和fail_timeout参数配合使用。fail_timeout:语法:fail_timeout=time,经过max_fails失败后,服务暂停的时间(默认10s)backup:预留的备份服务器(当其他的节点挂掉,备份服务器启动)down:当前的server暂时不参与负载均衡(不对外提供服务)slow_start:语法:slow_start=time,设置服务器不正常运行时,或者在一段时间后服务器变为不可用时,服务器将其权重从零恢复到指定值的时间。默认值为零,即禁用慢速启动。service:语法:service=name,启用DNSSRV记录的解析并设置服务名称。为了使此参数起作用,必须为服务器指定resolve参数,并指定不带端口号的主机名。resolve:监视与服务器域名相对应的IP地址的更改,并自动修改upstream配置,而无需重新启动nginx。配置示例:max_fail=3fail_timeout=100的含义是:当一台服务器在100s内如果出现了3次请求失败,就会报机器的状态设置为down,并且会在100s后尝试重新启用这个服务器。3、nginx负载均衡调度算法详解Nginx目前支持的调度策略有以下5种:(1)轮询(默认)每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。#####(2)加权轮询指定轮询权重,权重(weight)和访问比率成正比,用于后端服务器性能不均的情况。#####(3)ip_hash每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session共享的问题。应用场景:保持session一至性#####(4)url_hash(第三方)按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。应用场景:静态资源缓存,节约存储,加快速度其中,hash_method为使用的hash算法,需要注意的是:此时,server语句中不能加weight等参数。#####(5)fair(第三方)按后端服务器的响应时间来分配请求,响应时间短的优先分配。
LoveIT 2020-02-11Nginx -
Nginx配置实例—反向代理
1、配置示例1::要求:在浏览器地址栏中输入www.123.com,跳转到Linux服务器中的tomcat主页面声明:本次实验在我电脑的Linux虚拟机上进行的,Linux系统信息如下:图1实验系统信息1.1准备工作(1)安装nginx(参考:Nginx快速入门—基本概念以及在Linux上安装Nginx(2)启动一个tomcat(注意:在启动之前一定要配置好Java环境变量,参考:记一次在云服务器上部署项目的实践经历)(3)tomcat启动后在本地host文件中配置域名映射:配置完成后在浏览器中输入www.123.com:8080如果能看到tomcat的页面,就说明配置的没有问题。图2host配置测试1.2nginx配置反向代理接下来我们来配置nginx,nginx一入门我们都知道在nginx的安装目录下的conf/nginx.conf是nginx的默认配置文件,因此打开它默认配置文件并简单配置如下内容(当然,你也可以使用nginx-c指定配置文件),简单配置如下:测试结果:配置完成后重新加载配置文件后,直接访问www.123.com,如果可以看到tomcat的首页那就大功告成啦!图2nginx反向代理配置示例1测试2、配置示例2::要求:在浏览器地址栏中输入www.123.com/login,请求跳转到127.0.0.1:8080在浏览器地址栏中输入www.123.com/user,请求跳转到127.0.0.1:80812.1准备工作启动两个tomcat,并在监听8080端口的tomcat的webapps目录下新建文件夹login以及文件index.html;在监听8081端口的tomcat的webapps目录下新建文件夹user以及文件index.html;2.2配置nginx按照要求,nginx服务器要根据不同的请求路径把不同的请求转发到不同的tomcat中处理,这里就需要用到正则表达式来匹配路径,具体的配置如下:测试结果:图3nginx反向代理配置示例2测试3、location匹配规则详解在上面的配置中使用到了正则匹配,nginx官方文档给出location语法如下:location语法:location[=|~|~*|^~]/uri/{…}其中,方括号中的四种标识符是可选项,用来改变请求字符串和uri的匹配方式。uri是待匹配的请求字符串,可以是不包含正则的字符串,这种模式被称为“标准的uri";也可以包含正则,这种模式被称为"正则uri",具体可用的标识符如下:**=**:必须与指定的模式严格匹配,如果匹配成功就停止向下搜索,并立即处理代理逻辑~:必须以指定模式开始,并且区分大小写~*:必须以指定模式开始,并且不区分大小写**^~**:前缀匹配,不支持正则,如果模式匹配,那么就停止搜索其他模式了。无:普通匹配(最长字符匹配);与location顺序无关,是按照匹配的长短来取匹配结果。若完全匹配,就停止匹配。备注:1、如果uri里面包含正则表达式,就必须使用~或~*标识符;2、针对~和~匹配标识符,可以在前面加上!来取反,如下:<spanstyle="color="rgb(192,0,0)">!~表示正则不匹配,区分大小写。<spanstyle="color="rgb(192,0,0)">!~表示正则不匹配,不区分大小写。3.1精确匹配(=)示例3.2区分大小写正则匹配(~)示例3.3不区分大小写正则匹配(~*)示例3.4标准匹配(^~)示例3.5普通匹配(最长字符匹配)示例4、location匹配优先级1、如果有精确匹配,会先进行精确匹配,匹配成功,立刻返回结果。2、普通匹配与顺序无关,因为按照匹配的长短来取匹配结果。3、正则匹配与顺序有关,因为是从上往下匹配。(首先匹配,就结束解析过程)。4、在location中,location/可以匹配所有的请求,但是他的优先级是最低的。总结匹配规则如下:<spanstyle="color="rgb(192,0,0)">*(location=)>(location完整路径)>(location^~路径)>(location~,~正则顺序)>(location部分起始路径)>(location/)即<spanstyle="color="rgb(192,0,0)">(精确匹配)>(最长字符串匹配,但完全匹配)>(非正则匹配)>(正则匹配)>(最长字符串匹配,不完全匹配)>(location通配)**
LoveIT 2020-02-10Nginx -
Nginx配置文件详解(配置文件结构)
Nginx配置文件结构在nginx的安装目录下的conf目录下有一个nginx.conf,这个就是nginx默认的配置文件,默认的内容如下:Nginx配置文件结构:1、main:配置影响nginx全局的指令。一般有运行nginx服务器的用户组,nginx进程pid存放路径,日志存放路径,配置文件引入,允许生成workerprocess数等。可配置的参数如下:(1)user:来指定NginxWorker进程运行用户以及用户组,默认由nobody账号运行(2)worker_processes:指定了Nginx要开启的子进程数。每个Nginx进程平均耗费10M~12M内存。根据经验,一般指定1个进程就足够了。如果是多核CPU,建议指定和CPU的数量一样的进程数即可。(3)error_log:用来定义全局错误日志文件。日志输出级别有debug、info、notice、warn、error、crit可供选择,其中,debug输出日志最为最详细,而crit输出日志最少。(4)pid:用来指定进程id的存储文件位置。(5)worker_rlimit_nofile:用于指定一个nginx进程可以打开的最多文件描述符数目,这里是65535,需要使用命令“ulimit-n65535”来设置。配置示例:worker_processes1;error_loglogs/error.log;error_loglogs/error.lognotice;error_loglogs/error.loginfo;pidlogs/nginx.pid;2、events块:配置影响nginx服务器或与用户的网络连接。有每个进程的最大连接数,选取哪种事件驱动模型处理连接请求,是否允许同时接受多个网路连接,开启多个网络连接序列化等。(1)use:用来指定Nginx的工作模式。Nginx支持的工作模式有select、poll、kqueue、epoll、rtsig和/dev/poll。其中select和poll都是标准的工作模式,kqueue和epoll是高效的工作模式,不同的是epoll用在Linux平台上,而kqueue用在BSD系统中,对于Linux系统,epoll工作模式是首选。(2)worker_connections:用于定义Nginx每个进程的最大连接数,即接收前端的最大请求数,默认是1024。最大客户端连接数由worker_processes和worker_connections决定,即Max_clients=worker_processes*worker_connections,在作为反向代理时,Max_clients变为:Max_clients=worker_processes*worker_connections/4。进程的最大连接数受Linux系统进程的最大打开文件数限制,在执行操作系统命令“ulimit-n65536”后worker_connections的设置才能生效。配置示例:3、http块:可以嵌套多个server,http模块负责HTTP服务器相关属性的配置,有server和upstream两个子模块的配置。如文件引入,mime-type定义,日志自定义,是否使用sendfile传输文件,连接超时时间,单连接请求数等。(1)include:设定文件的mime类型,类型在配置文件目录下的mime.type文件定义,来告诉nginx来识别文件类型。(2)default_type:设定了默认的类型为二进制流,也就是当文件类型未定义时使用这种方式,例如在没有配置asp的locate环境时,Nginx是不予解析的,此时,用浏览器访问asp文件就会出现下载了。(3)log_format:用于设置日志的格式,和记录哪些参数,这里设置为main,刚好用于access_log来纪录这种类型。配置示例:'$status$body_bytes_sent"$http_referer"''"$http_user_agent""$http_x_forwarded_for"';access_loglogs/access.logmain;sendfileon;tcp_nopushon;keepalive_timeout65;gzipon;4、server块:配置虚拟主机的相关参数,一个http中可以有多个server。(1)listen:用于指定虚拟主机监听的服务端口。(2)server_name:用来指定IP地址或者域名,多个域名之间用空格分开。(3)root:表示在这整个server虚拟主机内,全部的rootweb根目录。注意要和locate{}下面定义的区分开来。(4)index:全局定义访问的默认首页地址。注意要和locate{}下面定义的区分开来。(5)charset:用于设置网页的默认编码格式。(6)access_log:用来指定此虚拟主机的访问日志存放路径,最后的main用于指定访问日志的输出格式。配置示例:listen80;server_namelocalhost;root/Users/hk/www;indexindex.phpindex.htmlindex.htm;charsetutf-8;access_loglogs/host.access.logmain;aerror_loglogs/host.error.logmain;}5、location块:location模块负载均衡,反向代理,虚拟域名等配置。是来定位的,定位URL,解析URL,它也提供了强大的正则匹配功能,也支持条件判断匹配,可以通过location指令实现Nginx对动,静态网页进行过滤处理。(1)location/表示匹配访问根目录。(2)root:用于指定访问根目录时,虚拟主机的web目录,这个目录可以是相对路径(相对路径是相对于nginx的安装目录)。也可以是绝对路径。(3)proxy_pass:代理转发,如果在proxy_pass后面的url加/,表示绝对根路径;如果没有/,表示相对路径,把匹配的路径部分也给代理走。(4)proxy_set_header:允许重新定义或者添加发往后端服务器的请求头。(5)include:加载配置文件,后面介绍nginx多个配置文件时候会提到。(6)index:定义页面显示html,一般和alias配合使用。(7)root:定位localtion匹配的url资源路径。配置示例:roothtml;indexindex.htmlindex.htm;}6、upstream:模块负债负载均衡模块,通过一个简单的调度算法来实现客户端IP到后端服务器的负载均衡。Nginx的负载均衡模块目前支持4种调度算法:(1)weight轮询(默认)。每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某台服务器宕机,故障系统被自动剔除,使用户访问不受影响。weight指定轮询权值,weight值越大,分配到的访问机率越高,主要用于后端每个服务器性能不均的情况下。(2)ip_hash。每个请求按访问IP的hash结果分配,这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题。(3)fair。比上面两个更加智能的负载均衡算法。此种算法可以依据页面大小和加载时间长短智能地进行负载均衡,也就是根据后端服务器的响应时间来分配请求,响应时间短的优先分配。Nginx本身是不支持fair的,如果需要使用这种调度算法,必须下载Nginx的upstream_fair模块。(4)url_hash。按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。Nginx本身是不支持url_hash的,如果需要使用这种调度算法,必须安装Nginx的hash软件包。在HTTPUpstream模块中,可以通过server指令指定后端服务器的IP地址和端口,同时还可以设定每个后端服务器在负载均衡调度中的状态。常用的状态有:down,表示当前的server暂时不参与负载均衡。backup,预留的备份机器。当其他所有的非backup机器出现故障或者忙的时候,才会请求backup机器,因此这台机器的压力最轻。max_fails,允许请求失败的次数,默认为1。当超过最大次数时,返回proxy_next_upstream模块定义的错误。fail_timeout,在经历了max_fails次失败后,暂停服务的时间。max_fails可以和fail_timeout一起使用。
LoveIT 2020-02-10Nginx -
Nginx快速入门—基本概念以及在Linux下安装
1、什么是Nginx?百度百科:Nginx(enginex)是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Рамблер)开发的,第一个公开版本0.1.0发布于2004年10月4日。我的理解:Nginx(enginex)是一个高性能的HTTP反向代理服务器,特点是占用内存少,并发能力强,事实上nginx的并发能力确实是同类型网页服务器中中表现最好的。Nginx专门为性能优化而开发,性能是其最重要的考量,能经受住高并发的考验,有报告表明能支持高达50000个并发连接数。2、Nginx中重要的概念反向代理负载均衡动静分离2.1反向代理(1)正向代理 正向代理,也就是传说中的代理,他的工作原理就像一个跳板,简单的说,我是一个用户,我访问不了某网站,但是我能访问一个代理服务器,这个代理服务器呢,他能访问那个我不能访问的网站,于是我先连上代理服务器,告诉他我需要那个无法访问网站的内容,代理服务器去取回来,然后返回给我。从网站的角度,只在代理服务器来取内容的时候有一次记录,有时候并不知道是用户的请求,也隐藏了用户的资料,这取决于代理告不告诉网站。 简单说正向代理就是一个位于客户端和原始服务器(originserver)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。图2.1.1正向代理示意图(2)反向代理反向代理正好相反,对于客户端而言反向代理服务器就像是原始服务器(实质并不是),并且客户端不需要进行任何特别的设置。客户端向反向代理服务器(例如:Nginx)的命名空间(name-space)中的内容发送普通请求,接着反向代理服务器将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容原本就是它自己的一样。图2.1.2反向代理示意图(3)正向代理和反向代理的区别从用途上来讲:**正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径。**正向代理还可以使用缓冲特性减少网络使用率。**反向代理的典型用途是将防火墙后面的服务器提供给Internet用户访问。反向代理还可以为后端的多台服务器提供负载平衡,或为后端较慢的服务器提供缓冲服务。**另外,反向代理还可以启用高级URL策略和管理技术,从而使处于不同web服务器系统的web页面同时存在于同一个URL空间下。 从安全性来讲:正向代理允许客户端通过它访问任意网站并且隐藏客户端自身,因此你必须采取安全措施以确保仅为经过授权的客户端提供服务。反向代理服务器对外都是透明的,访问者并不知道自己访问的是一个代理服务器。2.2负载均衡单个服务器解决不了问题时,我们通过增加服务器的数量,然后将任务分发到各个的服务器上,将原先请求集中到一个服务器上改变为分发到多个服务器,这一过程就是负载均衡。图2.2.1负载均衡示意图2.3动静分离为了加快网站的解析速度,可以把静态资源和动态资源分开部署到不同的服务器上进行解析,以加快解析速度,降低了原先单个服务器解析的压力。图2.3.1动静分离示意图3、Linux上安装Nginx3.1安装Nginx所需依赖Nginx安装需要的依赖:pcre、openssl、zlib、和gcc,因此我们需要在安装nginx之前先安装所需的依赖,下面是一条一键安装上面四个依赖的命令:3.2安装Nginx依赖安装完毕后,我们可以到Nginx官网:http://nginx.org/download/找到合适的Nginx版本下载下来进行安装。图3.2.1解压成功进入到解压出来的文件夹中执行以下命令;安装成功后就会在/usr/local/nginx下生成Nginx的相关文件conf、html、logs、sbin,如图:图3.2.1Nginx目录结构目录结构说明:-conf:Nginx的配置文件夹,它里面有一个nginx.conf,这是配置Nginx的重要文件-sbin:Nginx命令的目录,如Nginx的启动命令nginx-logs:Nginx默认的日志路径,包括错误日志及访问日志-html:这是编译安装时Nginx的默认站点目录,类似Apache的默认站点htdocs目录3.3启动Nginx进入到sbin目录,目录下只有一个执行脚本nginx,执行命令./nginx,然后找到Linux虚拟机的ip直接访问,如果看到下面的画面,证明Nginx就安装成功了:图3.2.2安装成功注意:由于Nginx的配置文件中默认的端口是80,因此当没法访问的时候请先检查你的配置以及对应的端口是否在防火墙中是否是开放的。1.检查防火墙端口是否开放:firewall-cmd--list-all2.增加端口:sudofirewall-cmd--add-port=803.重启防火墙:firewall-cmd--reload
LoveIT 2020-02-09Nginx -
SpringBoot—WebMvcConfigurer详解
1.简介WebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的xml配置文件形式进行针对框架个性化定制,可以自定义一些Handler,Interceptor,ViewResolver,MessageConverter。基于java-based方式的springmvc配置,需要创建一个配置类并实现WebMvcConfigurer接口;在SpringBoot1.5版本都是靠重写WebMvcConfigurerAdapter的方法来添加自定义拦截器,消息转换器等。SpringBoot2.0后,该类被标记为@Deprecated(弃用),官方推荐直接实现WebMvcConfigurer或者直接继承WebMvcConfigurationSupport,方式一实现WebMvcConfigurer接口(推荐),方式二继承WebMvcConfigurationSupport类2.WebMvcConfigurer接口常用方法:2.1addInterceptors:拦截器addInterceptor:需要一个实现HandlerInterceptor接口的拦截器实例addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截excludePathPatterns:用于设置不需要拦截的过滤规则拦截器主要用途:进行用户登录状态的拦截,日志的拦截等。下面是一个配置示例:2.2addViewControllers:页面跳转以前使用Servlet的时候,如果需要访问一个页面,必须要写Servlet类,然后再写一个方法跳转到页面,感觉好麻烦,后来有了SpringMVC我们就可以在xml配置文件中配置视图的直接映射view-controller命名空间:来避免这些方法。再后来有了SpringBoot,我们可以可以重写WebMvcConfigurer中的addViewControllers方法即可达到效果了:值的指出的是,在这里重写addViewControllers方法,并不会覆盖WebMvcAutoConfiguration(Springboot自动配置)中的addViewControllers(在此方法中,SpringBoot将“/”映射至index.html),这也就意味着自己的配置和SpringBoot的自动配置同时有效,这也是我们推荐添加自己的MVC配置的方式。2.3addResourceHandlers:静态资源比如,我们想自定义静态资源映射目录的话,只需重写addResourceHandlers方法即可。注:如果继承WebMvcConfigurationSupport类实现配置时必须要重写该方法,具体见其它文章addResoureHandler:指的是对外暴露的访问路径addResourceLocations:指的是内部文件放置的目录2.4configureDefaultServletHandling:默认静态资源处理器此时会注册一个默认的Handler:DefaultServletHttpRequestHandler,这个Handler也是用来处理静态文件的,它会尝试映射/。当DispatcherServelt映射/时(/和/是有区别的),并且没有找到合适的Handler来处理请求时,就会交给DefaultServletHttpRequestHandler来处理。注意:这里的静态资源是放置在web根目录下,而非WEB-INF下。可能这里的描述有点不好懂(我自己也这么觉得),所以简单举个例子,例如:在webroot目录下有一个图片:1.png我们知道Servelt规范中web根目录(webroot)下的文件可以直接访问的,但是由于DispatcherServlet配置了映射路径是:/,它几乎把所有的请求都拦截了,从而导致1.png访问不到,这时注册一个DefaultServletHttpRequestHandler就可以解决这个问题。其实可以理解为DispatcherServlet破坏了Servlet的一个特性(根目录下的文件可以直接访问),DefaultServletHttpRequestHandler是帮助回归这个特性的。2.5configureViewResolvers:视图解析器这个方法是用来配置视图解析器的,该方法的参数ViewResolverRegistry是一个注册器,用来注册你想自定义的视图解析器等。2.6addCorsMappings:跨域2.7configureMessageConverters:信息转换器参考【1】zhangpower1993.SpringBoot---WebMvcConfigurer详解.CSDN【2】小仙.SpringBoot——》WebMvcConfigurerAdapter详解.CSDN
LoveIT 2019-11-21Spring Boot -
Redis的Java客户端—Jedis和Lettuce
Jedis连接Redis1.添加Jedis依赖2.在虚拟机端配置:将bind注释掉,然后改protected-mode为no改了之后保存并重启Redis。3.使用Jedis提供的Jedis这个工具类来连接Jedis,首先在虚拟机使用ifconfig命令查看虚拟机的ip,然后向Redis发送一个ping命令,测试一下是否可以连接上远程的Redis:可以连接上Redis的标志是程序运行后打印”PONG“如果程序运行出现了JedisConnectionException,这种情况要么是你代码中把ip或端口写错了,要不就是由于Linux的防火墙导致的,在你确定你没有写错ip或端口的前提下,你可以直接关闭防火墙或者为了安全你可以开放6379这个端口给远程,Centos7上开放端口有关的命令操作如下:Jedis常用APIJedis操作Redis的常用API几乎和Redis的命令是一样的,比如操作String:可以看到通过Jedis操作Redis所调用的API和Redis的命令是一样的,所以只要熟悉Redis的关于5大常用数据类型的命令,那么使用Jedis操作Redis就没有大的问题。如果你还不是熟悉Redis的关于5大常用数据类型的命令,可以参考我的这篇笔记:Redis五大常用数据类型Jedis事务Redis中和事务有关的命令:mulit、exec,discard,watch和unwatch,然而Jedis中操作Redis事务的API也和这几个命令是一样的,比如我们实现一个简单的事务:有两个关键字balance表示信用卡的余额(初始值为1000),debt表示信用卡的欠额(初始值为0),使用redis提供的乐观锁watch来实现对消费的记录正常情况下(就是没有别的线程干扰):异常情况下:解释一下这两种不同的结果:当开启监控后,如果期间别的线程把监控的关键字的值改变了,那么Redis就会在本次事务期间不执行任何操作,即使使用exec提交事务了,也不会执行(这时返回exec的返回值是null),这种基于CAS的监控,不仅保证了共享数据的安全,而且还提高了响应速速。这也正是程序所体现的,当jedis.mset("balance","100","debt","400");这条语句被注释掉以后,程序可以正常执行,执行后返回true,程序结束;当jedis.mset("balance","100","debt","400");语句没有注释以后,在开启watch以后,相当于别的进程改变了监控关键字的值,那么这时Redis就不会在执行事务了,exec就会返回false,然后while(!txTest.coustmer());就又再次调用方法,直到执行成功,然后结束程序。JedisPool连接池类似于mysql的数据库连接池c3p0、Durid等,JedisPool是java连接Redis的连接池,基本的使用方式如下:一般我们可以各种配置的代码抽取出来写一个工具类,下面是一个基于单例模式的JedisPoolUtils:首先我们需要一个redis.properties的配置文件,用于配置JedisPool的一些属性:JedisPoolUtils.java然后测试一下我们的JedisPoolUtils工具类:测试结果:SpringBoot连接Redis导入redis的相关依赖在application.yml中配置redis的有关连接信息自定义RedisTemplate SpringBoot自动帮我们在容器中生成了一个RedisTemplate和一个StringRedisTemplate。我们可以使用RedisTemplate来像Jedis一样操作Redis。但是,这个RedisTemplate的泛型是<Object,Object>,写代码不方便,需要写好多类型转换的代码;我们需要一个泛型为<String,Object>形式的RedisTemplate。并且,这个RedisTemplate没有设置数据存在Redis时,key及value的序列化方式。 看到这个@ConditionalOnMissingBean注解后,就知道如果Spring容器中有了RedisTemplate对象了,这个自动配置的RedisTemplate不会实例化。因此我们可以直接自己写个配置类,配置RedisTemplate。RedisConfig.java
LoveIT 2019-10-14Redis -
SpringBoot从入门到精通—Spring Boot 错误处理机制
1、SpringBoot默认错误处理机制(现象) 当我们使用SpringBoot发生错误的时候,如果我们没有配置错误的处理规则,那么SpringBoot就会启用内部的默认错误处理办法。比如当发生404错误的时候,网页端的效果如下:而在别的客户端访问的时候如果出现了404错误,默认会给客户端发送一串错误消息的JSON数据客户端的测试使用到了一个工具:Postman,感兴趣的小伙伴可以去Postman官网下载后来测试。2、SpringBoot默认错误处理机制(原理) 看到这些现象我们不禁会有疑问,SpringBoot的底层是如何生成不同错误的默认错误页面的?还有他是如何区分浏览器和其他客户端的?带着疑问我们继续往下看。 我们参照源码来分析一下(SpringBoot2.1.7版本),具体在ErrorMvcAutoConfiguration这个错误处理自动配置类,下面是在这个类中注册的几个重要的组件的源码:2.1ErrorMvcAutoConfiguration源码片段可以看到,ErrorMvcAutoConfiguration这个错误处理类中配置了几个重要的组件:*DefaultErrorAttributs:见名知意,这是SpringBoot定义的默认错误属性,他就是和错误信息的填充有关。*BasicErrorController:他是SpringBoot中默认处理/error请求的Controller*ErrorPageCustomizer:系统出现错误以后来到error请求进行处理*DefaultErrorViewResolver:默认的出现错误后的视图解析器继续跟踪源码(1)DefaultErrorAttributs源码片段(2)BasicErrorController源码片段首选通过判断媒体的类型来选择不同的错误处理方法,核心就是下面两个方法在BasicErrorController类的源码我们看到它调用了父类AbstractErrorControlle的方法resolveErrorView来处理ModelAndView,具体的实现细节如下:AbstractErrorController源码片段(3)ErrorPageCustomizer源码片段(4)DefaultErrorViewResolver源码片段 大致分析源码后可以总结SpringBoot对错误的处理流程如下:如果系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效(定制错误的响应规则),就会发出/error请求,然后就会被BasicErrorController处理并返回ModelAndView(网页)或者JSON(客户端)。3、使用SpringBoot默认错误处理机制来处理我们程序中的异常 通过分析源码我们可以发现,如果要使用SpringBoot默认的错误处理机制,我们可以把我们定制的错误页面放在/templates/error目录下的,交给模板引擎来处理;或者不使用模板引擎那就放在static/error目录下。并且给这些错误页面命名为错误码.html或4xx.html、5xx.html。SpringBoot就可以自动帮我们映射到错误页面。例如,处理404错误:在/templates/error目录下放404.html 访问浏览器,在地址栏中随便输入一个地址让他发生404错误,结果来到了我们定制的404错误页面,而不是SpringBoot默认的那个难看的白板页面。4xx.html测试结果:4、定制自己的错误信息默认情况下,SpringBoot的错误页面中可以可得一下错误信息:timestamp:时间戳status:状态码error:错误提示exception:异常对象message:异常消息errors:JSR303数据校验的错误都在这里4.1第一种方式:使用SpringMVC的异常处理器这样无论是浏览器还是别的客户端,只要出错了就全部返回的JSON数据。4.2第二种方式:转发到/error请求进行自适应效果处理4.3第三种方式:编写一个MyErrorAttributes继承DefaultErrorAttributes并重写其getErrorAttributes方法 前两种虽然都可以解决错误,但是当我们自己定义一个错误属性(比如上面的code属性)就没办法带到页面,因此我们设置的信息也就无法被带到页面显示。我们可以编写一个MyErrorAttributes继承自DefaultErrorAttributes重写其getErrorAttributes方法将我们的错误数据添加进去。最终的效果:响应是自适应的,以后可以通过定制ErrorAttributes改变需要返回的内容。
LoveIT 2019-10-13Spring Boot -
SpringBoot从入门到精通—Spring Boot 对静态文件的默认映射规则
1、Webjars(官网:http://www.webjars.org/)webjars:以jar包的方式引入静态资源;SpringBoot中所有/webjars/**,都去classpath:/META-INF/resources/webjars/找资源。推荐使用Webjars的三大理由:将静态资源版本化,更利于升级和维护。剥离静态资源,提高编译速度和打包效率。实现资源共享,有利于统一前端开发。1.1使用Webjars引入公共的静态资源使用Webjars在POM文件中很方便的引入静态资源文件,比如JQuery、Vue.js.....例如引入jquery和BootStrap,这样引入后,我们可以像管理Java依赖一样管理静态文件依赖。引入之后我们可以尝试访问一下BootStrap里面的东西,在地址栏输入localhost/webjars/bootstrap/4.0.0/webjars-requirejs.js2、引入自己的静态资源文件使用Webjars是好,但是没办法引入我们自己编写的静态资源文件。别急!SpringBoot开发者早已想到这个问题了,SpringBoot规定可以放在以下几个地方放置静态文件:3、给网页标签栏设置小图标SpringBoot支持给网页标签栏设置图标的功能,方法十分简单只需要将图标命名为favicon.ico,然后放在静态资源文件夹根目录下,SpringBoot在启动的时候就会扫描到并设置到网页中。在classpath:/public/放一个我自己的图标启动来看看效果:很nice!我们的小图标被用上了。4、使用spring.resources.static-locations改变静态资源文件夹位置在SpringBoot的主配置文件中,我们可以使用spring.resources.static-locations来指定静态资源文件的位置,可以指定多个,多个路径之间用","(逗号)隔开。需要注意的是,我们这么指定后,那些默认的资源文件夹就会失效。在resources下新建webapps文件夹,把静态资源放在里面。启动来看看效果:
LoveIT 2019-10-12Spring Boot -
Redis主从复制的几种形式和原理详解
主从复制概述 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(salve)。数据的复制是单向的,只能从主节点到从结点。 默认情况下,每台Redis服务器都是主节点,且一个主节点可以有多个从结点(或没有从结点),但是一个从结点只能有一个主节点。主从复制的作用数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。读写分离:可以用于实现读写分离,主库写、从库读,读写分离不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量;高可用的基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。主从复制启用前面提到过,默认情况下每台Redis服务器都是主节点(master),而且如果没有配置的话,是没有从服务器的。(可以使用inforeplication命令查看一个Redis服务器的复制有关的信息)有三种方式可以开启主从:配置文件:在从服务器的配置文件中加入slaveof<主节点ip><主节点port>。启动命令:redis-server启动命令后面加入--slaveof<主节点ip><主节点port>。客户端命令:Redis服务器启动后,直接通过客户端执行命令slaveof<主节点ip><主节点port>,返回OK后该Redis实例就成为了从节点。Redis常用的主从拓扑1主N从所谓的1主N从指的是一个主Redis服务器(master)可以有一个或多个从Redis服务器(salve),这种拓扑关系的特点是:只有一个主节点,有一个或多个从结点;主节点可读可写,从结点只能从主节点读数据,不能自己写数据;当主服务节点宕机后(无论各种原因,反正主节点不能正常运行了),从服务节点不会自动变成主节点,而是保持自己从结点的身份继续运行(而且他从主节点复制的数据不会丢失,可一继续对外提供服务),直到主节点恢复后这些从结点又可一继续从主节点读数据;当一以从服务器结点“挂掉”以后,再次重启后,他与先前的主节点没有任何关系了(在没有在配置文件中配置的前提下),除非在配置文件中配置过或者再次使用命令slaveof<主节点ip><主节点port>连上主节点。演示一:在客户端使用命令行在127.0.0.1:3679开启master,然后在127.0.0.1:3680/3681开启两个salve然后关闭master节点,查看从服务节点,发现从服务节点没有自动升级为master,并且他之前从主节点复制来的数据还在,还可以向外提供服务:之后重新启动master,查看从服务节点,发现从服务节点有重新连接上主服务节点了:演示二:恢复到127.0.0.1:3679是master,127.0.0.1:3680/3681是127.0.0.1:3679的两个salve的状态,然后任意重启一个从服务器,观察发现这个服务器结点如果之前没有在配置文件中配置过,那么他将和master没有任何关系了:"薪火相传"所谓”薪火相传“,指的是那种一个master连接了一个slave,然后这个slave结点有作为另一个slave的master结点.......依次向链表一样传递下去,这种拓扑的特点是:只有这个传递链上的第一个master结点具有写的权限,其他的结点都是由读的权限;这种模式下,减轻了master结点的压力,但是与之而来的问题是越往后的结点同步延时越大;如果其中一个节点“挂了”,那么他后面的结点就无法同步到最新的数据了演示:让127.0.0.1:3679作为127.0.0.1:3680的master,然后让127.0.0.1:3680作为127.0.0.1:3681的master:"反客为主"“反客为主”说的就是当主服务器结点“挂了”以后,可以手动将一个从服务器节点指定为主服务器节点,然后让其他的从服务节点从这个新的master上复制:演示:在客户端使用命令行在127.0.0.1:3679开启master,然后在127.0.0.1:3680/3681开启两个salve然后主服务节点突然“挂了”,手动使用命令slaveofnoone将原本的slave转成master,停止与其他数据库的同步,然后将其他的slave和这个新的master交互:这种模式下,在之后以前的主服务器再次启动后,它就与这个新建立的主从关系没有任何关系了。Redis哨兵模式 Sentinel(哨兵)是用于监控redis集群中Master状态的工具,是Redis的高可用性解决方案。Sentinel可以让redis实现主从复制,当一个集群中的master失效之后,sentinel可以选举出一个新的master用于自动接替master的工作,集群中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。其结构如下: Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-Slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换,而Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自动切换。Sentinel由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。说的简单点,哨兵模式就是监控+自动版“反客为主”比如下图的过程:演示:首先我们在新建一个sentinel.conf配置文件,配置有关哨兵监控的信息,然后保存退出让127.0.0.1:3679作为127.0.0.1:3680的master,然后让127.0.0.1:3680作为127.0.0.1:3681的master一切设置好后,使用redis-sentinelsentinel.conf配置文件路径启动哨兵,让他监控master的状态:之后关闭master,模拟服务器突然宕机等情况,发现哨兵自动通过投票选举出了新的master,并且把其他从服务器(slave)都拉到了这个新的master“旗下”:那么如果之前的master重启回来,会不会有两个master冲突?答案是不会,之前的master会在哨兵模式下变为slave从机:主从复制的原理Redis主从复制的过程大体可以分成3个阶段:建立连接阶段、数据同步阶段和命令传播阶段。建立连接step1:保存主节点信息从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。(用inforeplication命令就可以查看)slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的复制操作在这之后才开始进行。step2:建立socket连接 从节点(slave)每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。如果连接成功:*从节点(slave):为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。主节点(master):接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。step3:发送ping命令 从节点(slave)成为主节点(master)客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。从节点发送ping命令后,可能出现3种情况:*(1)返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。(2)超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。(3)返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。step4:身份验证如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的masterauth的值。如果主节点设置密码的状态,与从节点masterauth的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。step5:发送从节点端口信息身份验证之后,从节点会向主节点发送其监听的端口号,主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中;该端口信息除了在主节点中执行infoReplication时显示以外,没有其他作用。数据同步阶段 主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。redis同步有2个命令:sync和psync,前者是redis2.8之前的同步命令,后者是redis2.8为了优化sync新设计的命令。 数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。 在数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端;而到了这一阶段及以后,主从节点互为客户端。原因在于:在此之前,主节点只需要响应从节点的请求即可,不需要主动发请求,而在数据同步阶段和后面的命令传播阶段,主节点需要主动向从节点发送请求(如推送缓冲区中的写命令),才能完成复制。全量复制和增量复制 在Redis2.8以前,从结点向主节点发送的是sync命令同步数据的,这种同步方式是全量复制;但是在Redis2.8以后,从结点可以发送psync命令请求同步数据,此时根据主节点当前状态的不同,同步方式可能是全量复制和增量服饰:*全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,这是一个非常耗费资源的操作。流程如下:发送psync命令(spync?-1)主节点根据命令返回FULLRESYNC从节点记录主节点ID和offset主节点bgsave并保存RDB到本地主节点发送RBD文件到从节点从节点收到RDB文件并加载到内存中主节点在从节点接受数据的期间,将新数据保存到“复制客户端缓冲区”,当从节点加载RDB完毕,再发送过去。(如果从节点花费时间过长,将导致缓冲区溢出,最后全量同步失败)从节点清空数据后加载RDB文件,如果RDB文件很大,这一步操作仍然耗时,如果此时客户端访问,将导致数据不一致,可以使用配置slave-server-stale-data关闭.从节点成功加载完RBD后,如果开启了AOF,会立刻做bgrewriteaof。以上红色字体的部分是整个全量同步耗时的地方。增量复制:当从节点正在复制主节点时,如果出现网络闪断和其他异常,从节点会让主节点补发丢失的命令数据,主节点只需要将复制缓冲区的数据发送到从节点就能够保证数据的一致性,相比较全量复制,成本小很多。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行增量复制,仍使用全量复制。当从节点出现网络中断,超过了repl-timeout时间,主节点就会中断复制连接。主节点会将请求的数据写入到“复制积压缓冲区”,默认1MB。当从节点恢复,重新连接上主节点,从节点会将offset和主节点id发送到主节点。主节点校验后,如果偏移量的数后的数据在缓冲区中,就发送cuntinue响应—表示可以进行部分复制。主节点将缓冲区的数据发送到从节点,保证主从复制进行正常状态。命令传播阶段 数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONFACK。心跳机制主从节点在建立复制后,他们之间维护着长连接并彼此发送心跳命令。心跳的关键机制如下:主从都有心跳检测机制,各自模拟成对方的客户端进行通信,通过clientlist命令查看复制相关客户端信息,主节点的连接状态为flags=M,从节点的连接状态是flags=S。主节点默认每隔10秒对从节点发送ping命令,可修改配置repl-ping-slave-period控制发送频率。从节点在主线程每隔一秒发送replconfack{offset}命令,给主节点上报自身当前的复制偏移量。主节点收到replconf信息后,判断从节点超时时间,如果超过repl-timeout60秒,则判断节点下线。注意事项: 延迟与不一致:命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的repl-disable-tcp-nodelay配置等有关。 repl-disable-tcp-nodelayno:该配置作用于命令传播阶段,控制主节点是否禁止与从节点的TCP_NODELAY;默认no,即不禁止TCP_NODELAY。当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与Linux内核的配置有关,默认配置为40ms。当设置为no时,TCP会立马将主节点的数据发送给从节点,带宽增加但延迟变小。一般来说,只有当应用对Redis数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为yes;多数情况使用默认值no。
LoveIT 2019-10-12Redis -
Redis的事务控制
事务的基本概念事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发来的命令请求打断。事务是一个原子操作:事务中的命令要么全部执行,要么全部不执行。Redis事务相关的几个命令multi:MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。exec:执行所有的事物命令,EXEC命令的回复是一个数组,数组中的每个元素都是执行事务中的命令所产生的回复。其中,回复元素的先后顺序和命令发送的先后顺序一致。当客户端处于事务状态时,所有传入的命令都会返回一个内容为QUEUED的状态回复(statusreply),这些被入队的命令将在EXEC命令被调用时执行。discard:通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务。watchkey[key...]:监视一个或多个key(类似于乐观锁)unwatch:取消watch对所有key的监视Redis事务的三个阶段开始事务:使用multi命令开启一个事务,当一个事务被exec或discard后,改事务就宣告结束(无论有没有成功执行),下次在向开启事务就必须在使用这个命令开启事务。命令入队:简单点说就是,开启事务后输入的命令不会立即执行,而是先入队,执行当exec后在一次性执行。执行事务:使用exec命令执行事务Redis事务使用示例:1、正常执行2、取消事务3、事务在执行EXEC之前,入队的命令可能会出错(语法上就是错误的),执行exec时,整个事务都会失败。 对于发生在EXEC执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:如果命令入队时返回QUEUED,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。 不过,从Redis2.6.5开始,服务器会对命令入队失败的情况进行记录,并在客户端调用EXEC命令时,拒绝执行并自动放弃这个事务。4、事命令可能在EXEC调用之后失败(语法上没有错误,但是调用执行的时候出错了),在执行exec命令时,其他正确的命令可以正确执行,错误命令抛出错误为什么Redis不支持事务回滚?通过上面的案例我们可以看到redis在事务中发生错误后是没有回滚的,而是继续执行余下的命令,那么redis为什么不支持事务回滚呢?从各方面考虑有以下两点原因:Redis命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面,也就是说,从实用的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发过程中别发现,而不应出现在生产环境中。而且需要注意的是在通常情况下,回滚并不能解决编程错误带来的问题。因为redis不需要支持事务回滚,所以他可以在内部保持简单和快捷。5、使用watch监控WATCH命令可以为Redis事务提供check-and-set(CAS)行为。被WATCH的键会被监视,并会发觉这些键是否被改动过了。如果有至少一个被监视的键在EXEC执行之前被修改了,那么整个事务都会被取消,EXEC返回nil-reply来表示事务已经失败。下面使用很典型的账户和消费问题来展示一下watch的作用。案例一:使用watch检测balance,事务期间balance数据未变动,事务执行成功案例二:使用watch检测balance,在开启事务后(标注1处),在新窗口执行标注2中的操作,更改balance的值,模拟其他客户端在事务执行期间更改watch监控的数据,然后再执行标注1后命令,执行EXEC后,事务未成功执行。
LoveIT 2019-10-11Redis -
神秘的Redis持久化机制
Redis与传统数据的一个主要区别就是Redis将所有的数据都存储在内存中,而传统数据库通常只会把数据存储到磁盘上,这就使得Redis的数据存储和读取有着极快的速度,但是由于内存属于易失存储器,它记录的所有数据一旦断电就消失了,这对于想把Redis作为数据库而不仅仅是缓存的用户来说是不能容忍的。为了满足不同场景下的持久化需求,Redis提供了RDB(RedisDataBase)持久化、AOF(AppendOnlyFile)持久化和RDB-AOF混和持久化等多种持久化机制。如果用户不需要持久化,也可以完全关闭持久化功能。一、RDB(RedisDataBase)持久化RDB持久化是Redis默认使用的持久化方式,它是将当前Redis进程中的数据生成快照保存到硬盘,默认会在当前工作目录下生成dump.rdb文件,当Redis重启的时候会自动读取快照文件来恢复数据。Redis提供了多种创建RDB文件的方法,用户既可以使用SAVE命令或BGSAVE命令手动创建RDB文件,也可以在redis.conf配置文件中设置save来配置选项让服务器在满足指定条件后自动触发BGSAVE命令。1、手动输入命令创建RDB文件SAVE命令和BGSAVE命令都可以生成RDB文件。SAVE和BGSAVE的区别是:SAVE:接收到SAVE命令后,Redis服务器将遍历数据库包含所有数据库,并将各个数据库包含的键值对全部记录到RDB文件中。在SAVE命令执行期间,Redis服务器将阻塞,直到RDB文件创建完毕为止。如果在创建RDB文件时已经有了对应的RDB文件,那么服务器将会新创建RDB文件代替已有的RD文件。BGSAVE:BGSAVE是SAVE命令的异步版本,当Redis服务器收到BGSAVE命令是,将会执行以下操作:1)创建一个子进程(拷贝一份父进程)。2)子进程执行SAVE命令,创建RDB文件。3)RDB文件创建完毕之后,子进程退出并通知Redis服务器进程(父进程)RDB文件已经创建完毕。一般来说,在生产环境很少直接使用SAVE命令,因为它会阻塞Redis服务器进程,保存RDB文件的任务通常由BGSAVE命令异步地执行。然而,如果负责保存数据的后台子进程不幸出现问题时,SAVE可以作为保存数据的最后手段来使用。2、通过配置选项自动创建RDB文件用户除了可以使用SAVE命令和BGSAVE命令手动创建RDB文件之外,还可以通过在配置文件中设置save选项,让Redis服务器在满足指定条件后自动触发BGSAVE命令:save<seconds><changes>save选项接收两个参数:seconds和changes,即服务器的数据只要在指定的seconds秒内只要发生了changes次变化后,就会触发BESAVE。Redis默认的配置有三个:save9001//900秒内Redis数据发生了至少1次变化,则执行bgsavesave30010//300秒内Redis数据发生了至少10次变化,则执行bgsavesave6010000//60秒内Redis数据发生了至少10000次变化,则执行bgsave注意,为了避免由于同时使用多个触发条件而导致服务器频繁的执行BGSAVE命令,Redis服务器会在每次成功创建RDB文件之后将负责自动触发BGSAVE命令的时间计数器清0并重新开始计数:无论这个RDB文件是由自动触发的BGSAVE命令创建的,还是有用户执行的SAVE或BGSAVE命令创建的,都是如此。3、自动触发的原理Redis的自动触发是通过serverCron函数、dirty计数器和lastave时间戳来实现的。serverCron是Redis服务器的周期性操作函数,默认每隔100ms执行一次;该函数对服务器的状态进行维护,其中一项工作就是检查save<seconds><changes>配置的条件是否满足,如果满足就执行bgsave。dirty计数器是Redis服务器维持的一个状态,记录了上一次执行bgsave/save命令后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。lastsave时间戳也是Redis服务器维持的一个状态,记录的是上一次成功执行save/bgsave的时间。也就是每隔100ms,执行serverCron函数;在serverCron函数中,遍历save<seconds><changes>配置的保存条件,只要有一个条件满足,就进行bgsave。对于每一个save<seconds><changes>条件,只有下面两条同时满足时才算满足: (1)当前时间-lastsave>seconds (2)dirty>=changes4、其他自动触发机制除了在配置文件中配置save<seconds><changes>来触发BGSAVE以外,还有别的情况会触发BGSAVE:在主从复制场景下,如果从结点执行全量复制操作,则主结点会执行BGSAVE命令,并将dump.rdb文件发送给从结点。在执行shutdown命令时,会自动执行RDB持久化,这一点通过redis的日志看到5、RDB文件结构RDB的文件格式RDB文件是经过压缩的二进制文件,RDB的的文件格式如下图所示其中重要字段的含义说明如下:Redis文件标识符:文件最开头是RDB文件标识符,这个标识符内容为"REDIS"5个字符。Redis会在尝试加载RDB文件的时候通过标识符快速判断这是不是一个RDB文件。版本号:跟在RDB文件标识符后面的是RDB文件的版本号,这个版本号是一个字符串数字,长度为4个字符。比如Redis5.0的RDB版本号是"0009",不同版本的RDB文件结构会有些许不同,但是新版的RDB都会在旧版本上添加更多信息,而且新版的Redis服务器总是能够向下兼容旧版的Redis服务器生成的RDB文件。比如在Redis5.0的服务器可以正常读取Redis4.0生成的"0008"版本的RDB文件。数据库数据:SELECTDB0pairs表示一个完整的数据库(0号数据库),同理SELECTDB3pairs表示3号数据库;只有当数据库中有键值对时,RDB文件中才会有该数据库的信息(上图所示的Redis中只有0号和3号数据库有键值对);如果Redis中所有的数据库都没有键值对,则这一部分直接省略。其中:SELECTDB是一个常量,代表后面跟着的是数据库号码;0和3是数据库号码;pairs则存储了具体的键值对信息,包括key、value值,及其数据类型、内部编码、过期时间、压缩信息等等。EOF:常量,标志RDB文件正文内容结束,他的实际值为二进制0xFF。当Redis读取到EOF,它就知道RDB文件的正文部分已经全部读取完毕了。CRC64校验和:RDB文件的末尾是一个一无符号64整数表示的CRC64校验和,在载入RBD文件时,会重新计算校验和并与CRC64值比较,判断文件是否损坏。RDB文件的存储路径RDB文件的存储路径既可以在redis.conf配置文件中配置,也可以在客户端通过命令动态设定:在配置文件中可以设置dir来指定RDB文件的存放路径,redis默认是存放在当前工作目录下。也可以在配置文件中通过设置dbfilename指定RDB文件的名字,redis默认的文件名是dump。动态设定:Redis启动后也可以在客户端使用configsetdir/path来动态的改变RDB的存放路径,当然也可以通过configsetdbfilenamenewfilenaem来设置RDB文件的名字。RDB文件的压缩Redis默认采用LZF算法对RDB文件进行压缩。虽然压缩会有一定的性能消耗,但是这样可以大大减小RDB文件的大小。但是需要特别注意的是:RDB文件的压缩并不是针对整个文件进行的,而是对数据库中的字符进行的,且只有在字符串达到一定长度(20字节)时才会进行压缩。6、载入RDB文件RDB文件的载入工作是在服务器启动的时候自动进行的,并没有专门的命令。但是当开启AOF后,由于AOF文件的优先级更高,Redis会优先加载AOF文件来恢复数据,只有当AOF关闭时,才会在Redis服务器启动的时候检测RDB文件,并自动加载。服务器载入RDB文件期间处于阻塞状态,直到加载完毕阻塞解除。Redis载入RDB文件时,会对RDB文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。具体步骤如下:首先在工作目录中寻找是否有RDB文件出现,如果有就打开它,,然后读取文件的内容并执行以下载入操作1)检查文件开头的表示符是否是"REDIS",如果是则据需后续载入操作,不是则抛出错误终止载入操作。2)检查RDB文件版本号,判断当前Redis服务器能否读取这一版本的RDB文件。3)根据RDB文件中记录的设备附加信息,执行相应的操作和设置。4)检查数据库数据部分是否为空,如果不为空着执行以下操作(1)根据文件记录的数据库号码,切换到对应数据库。(2)根据文件记录的键值对总数量以及带有过期时间的监视对数量,设置数据库底层数据结构。(3)一个接一个的载入文件记录的所有键值对数据,并在数据库中重建这些键值对。5)如果服务器启动了复制功能,那么将之前缓存的Lua脚本重新载入缓存中。6)遇到EOF结束标记符,确认RDB文件已经全部读取完毕。7)载入CRC64校验和,把它和载入数据期间计算出的CRC64比对,以此判断被载入的数据是否完好。8)RDB文件载入完毕,等待客户端的请求。7、RDB优缺点总结最后以一幅图的方式总结RDB的优缺点:二、AOF(AppendOnlyFile)持久化方式与全量式的RDB持久化功能不同,AOF提供的是增量式持久化功能,这种持久化的核心原理在于:服务器每次执行完写命令之后,都会将命令追加到AOF文件尾部。这样一来服务器停机之后,只要重新执行AOF文件中保存的Redis命令,就可以将数据恢复值停机之前的状态。与RDB相比较AOF具有更好的实时性,也是当前主流的持久化方案1、开启AOF持久化功能Redis服务器默认只开启了RDB持久化方式,要开启AOF,需要在redis.conf中修改appendonly为yes,并且还可以在配置文件中修改AOF文件的名字等等,具体的可以参考我的这篇笔记:Redis配置文件redis.conf详解2、AOF的执行流程AOF的执行流程包括:命令追加(append):将Redis的写操作追加到缓冲区aof_buf文件写入(write)和文件同步(sync):根据不同的同步策略将aof_buf中的内容同步带硬盘文件重写(rewrite):当AOF文件过大的时候重写AOF文件,达到压缩的目的。命令追加(append)Redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次有写命令都直接写入硬盘,导致硬盘IO成为Redis负载的瓶颈。命令追加的格式是Redis命令请求的协议格式,它是一种纯文本格式,具有兼容性好、可读性强、容易处理、操作简单避免二次开销等优点。在AOF文件中,除了用于指定数据库的select命令(如select0为选中0号数据库)是由Redis添加的,其他都是客户端发送来的写命令。文件写入(write)和文件同步(sync)Redis提供了多种AOF缓存区的同步文件策略,策略涉及到操作系统的write函数和fsync函数:write函数:为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失fsync函数:为了解决write函数数据丢失的问题,因此系统提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。为了消除操作系统的写缓存机制带来的不确定性,Redis向用户提供了appendfsync选项,以此来控制系统写AOF的频率:appendfsync<value>appendfsync选项拥有always、no、everysec3个值可选,他们代表的含义分别为:always命令写入aof_buf后立即调用系统的fsync函数同步到AOF文件,fsync完成后线程返回。这种情况下,每次有写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,Redis只能支持大约几百TPS写入,严重降低了Redis的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低SSD的寿命。no命令写入aof_buf后调用系统的wirte函数,不对AOF文件做fsync同步,同步操作由系统负责,通常同步周期为30s。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。everysec命令写入aof_buf后调用系统的write函数,write完成后返回,fsync同步文件操作,有专门的线程每一秒调用一次。everysec是前面两种策略的折中,兼顾了性能和数据安全,也是Redis的默认配置。3、AOF文件重写(rewrite)AOF文件重写主要的作用就是对AOF文件进行压缩,减小AOF文件的体积。需要注意的是,AOF重写只会把Redis进程内的数据转化为写命令,同步到新的AOF文件;不会对旧的AOF文件进行任何读取、写入操作。为什么文件重写可以压缩AOF文件?过期的数据不需要再写入文件无效的命令不再写入文件多条命令可以合并为一条命令,比如saddstuv1,saddstuv2,saddstuv2,这三条操作可以合并为一条saddstuv1v2v3。不过为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset类型的key,并不一定只使用一条命令;而是以某个常量为界将命令拆分为多条。这个常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定义总之压缩的原理就是通过重写减小命令的数量从而减少了文件的大小。文件重写的触发用户可以通过执行BGREWRITEAOF命令或者在配置文件中设置对应的选项来触发AOF文件重写操作。1、BGREWRITEAOF命令用户可以通过在Redis客户端手动指定BGREWRITEAOF命令显式的触发AOF操作,该命令是一个无参数命令。BGREWRITEAOF命令是一个异步命令,Redis服务器在接收到命令后会创建一个子进程,由他扫描数据库并生成新的AOF文件。当新的AOF文件生成完毕,子进程就会退出并通知主进程,然后主进程会使用新的AOF文件代替旧旧的AOF文件。BGREWRITEAOF命令执行时服务器的日志另外关于BGREWRITEAOF命令需要注意两点:首先,如果用户发送BGREWRITEAOF命令请求时,服务器正在创建RDB文件,那么服务器将会把AOF的重写操作延后到RDB文件创建完成之后再执行,以此避免两个写操作同时执行导致性能的下降;其次,如果服务器在执行重写的过程中,又接收到了新的BGREWRITEAOF命令请求,那么服务器将会返回错误信息。2、AOF重写配置选项除了可以手动执行BGREWRITEAOF命令重写AOF文件之外,还可以通过配置选项自动的触发BGREWRITEAOF命令重写,主要配置的参数如下:auto-aof-rewrite-min-size<value>:执行AOF重写时,文件体积最小体积,默认为64MB。auto-aof-rewrite-percentage<value>:执行AOF重写时,当前AOF大小和上一次重写AOF大小的比值,默认大小100。这些参数都可通过`configget参数`来查看。其中auto-aof-rewrite-min-size用于设置自动触发BGREWRITEAOF命令的最小AOF文件体积,当AOF文件的体积喜小于给定值时,服务器将不会自动执行BGREWRITEAOF命令。默认值是64MB,含义就是如果AOF文件的体积超过64MB后就会自动触发BGREWRITEAOF命令执行AOF重写。另一个选项auto-aof-rewrite-percentage它控制的是触发自动AOF文件重写所需的文件体积增大比例。默认值是100,表示如果当前AOF文件的体积比最后一次AOF文件重写后的体积增大了一倍(100%),那将自动触发BGREWRITEAOF命令执行AOF重写。如果之前还没有执行过重写,那么服务器启动时的AOF文件大小会被当做最后AOF重写的体积。文件重写的流程对照上图,可以总结出AOF文件的重写流程如下:Redis父进程首先判断当前是否存在正在执行bgsave/bgrewriteaof的子进程,如果存在则bgrewriteaof命令直接返回,如果存在bgsave命令则等bgsave执行完成后再执行。父进程执行fork操作创建子进程,这个过程中会阻塞Redis主进程。父进程fork后,执行bgrewriteaof命令返回”Backgroundappendonlyfilerewritestarted”信息并不再阻塞父进程,此时Redis主进程恢复可以响应其他命令。Redis的所有写命令依然写入AOF缓冲区,并根据appendfsync策略同步到硬盘,保证原有AOF机制的正确。由于fork操作使用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然在响应命令,因此Redis使用AOF重写缓冲区(图中的aof_rewrite_buf)保存这部分数据,防止新AOF文件生成期间丢失这部分数据。也就是说,bgrewriteaof执行期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。子进程根据内存快照,按照命令合并规则写入到新的AOF文件。子进程重写完新的AOF文件后,向父进程发信号,父进程更新统计信息,具体可以通过infopersistence查看。接着父进程把AOF重写缓冲区的数据写入到新的AOF文件,这样就保证了新AOF文件所保存的数据库状态和服务器当前状态一致。4、AOF文件启动加载当开启AOF后,Redis重启会默认优先加载AOF文件来恢复数据;只有当AOF关闭时参会加载RDB文件。Redis加载AOF文件时,会对AOF文件进行校验,如果文件损坏,则日志中就会打印错误,并且Redis会启动失败。当AOF文件损坏后,我们可以使用redis-check-aof这个工具来修复AOF文件。5、AOF优缺点总结最后以一幅图片总结AOF的优缺点:三、RDB-AOF混合持久化在前面我们分析了Redis两种持久化方式,他们都有各自的优缺点:RDB持久化可以生成紧凑RDB文件,并且使用RDB文件进行数据恢复的速度也非常快,但是RDB的全量持久化模式可能会让服务器在宕机是丢失大量数据。与RDB相比,AOF持久化可以将丢失数据的时间窗口限制在1s内,但是AOF文件的体积要比RDB文件的体积大的多,并且数据恢复过程也相对较慢。为了解决RDB和AOF两种持久化方式选择问题,Redis从4.0版本开始引入RDB-AOF混合持久化模式,这种模式是基于AOF持久化模式构建而来的,在打开AOF持久化的基础上,在配置上一下信息就可以打开混合持久化:aof-use-rdb-preamble<value>选项如果设置成为了yes,那么Redis服务器将在执行AOF重写操作时,向执行BGSAVE命令那样,根据数据库当前状态生成出相应的RDB数据,并将这些数据写入新建的AOF文件。换句话说,如果开启了混合持久化功能之后,服务器生成的AOF文件将由廊部分组成,其中AOF开头的是RDB格式的数据,RDB-AOF混合持久化生成的AOF文件通过使用RDB-AOF混合持久化功能,用户可以同时获得RDB持久化和AOF持久化的优点:服务器既可以通过AOF文件包含的RDB数据快速恢复数据,有可以通过AOF文件包含的AOF数据来讲数据丢失控制到1s之内。需要注意的是,RDB-AOF混合持久化生成的AOF文件同时包含RDB格式的数据和AOF个格式的数据,而传统的AOF持久化生成的AOF文件只包含AOF格式的数据。四、同时使用RDB持久化和AOF持久化在Redis4.0的RDB-AOF混合持久化出现之前,许多追求安全性的Redis使用者都会同时开启RDB和AOF阆中持久化方式,但是随着Redis4.0的RDB-AOF混合持久化的推出,同时使用两种持久化机制已经没有必要。对于使用Redis4.0的系统,优先使用RDB-AOF混合持久化是个不错的选择。如果使用的是Redis4.0之前的版本,那么在RDB和AOF之间如何选择,下面是Redis官方给出的建议:原文链接我的理解就是Redis4.0版本之前具体选择哪种持久化方式要看你的业务类型,如果你的业务对于数据一致性没有那么高的要求、网站访问量非常有限,那么仅仅开启RDB就足够了,比如像我的这个博客网站;但是如果你的业务对数据一致性要求非常高、网站访问量有十分巨大,那么RDB配合AOF是个不错的选择。特别的,Redis官方建议不要单独使用AOF,因为定时生成RDB快照(snapshot)非常便于进行数据库备份,并且RDB恢复数据集的速度也要比AOF恢复的速度要快。参考【1】黄健宏.Redis使用手册[M].北京:机械工业出版社,2019:380-399.【2】Redis官网.RedisPersistence
LoveIT 2019-10-09Redis -
手把手教你在Centos 7上安装、配置、启动Redis
一、什么是Redis?Redis是RemoteDictionarySevery的缩写,中文名称:远程字典服务器,它是一个基于C语言编写的完全开源免费,并且遵循BSD协议的一个高性能的key-value型分布式内存数据库。Redis使用key-value的形式保存值,常用的数据类型有String、list、hash、set、zset等数据结构,因此也被人们称为数据结构服务器。二、在Linux(Centos7)安装Redis1、首先去Redis的官方网站下载需要redisredis官网的镜像网址:http://download.redis.io/releases/,在这里有redis的各个版本:在Liunx上使用wget命令下载(我这个下载它当前的最新的镜像redis-5.0.5-tar.gz版本):wgethttp://download.redis.io/releases/redis-5.0.5.tar.gz2、下载好后解压tar-zxvfredis-5.0.5.tar.gz三、安装、配置、启动Redis1、执行make命令编译redis源文件在编译之前请检查一下你的Linux上是否已经安装了GCC编译器,如果没有请先安装GCC编译器。在命令行输入gcc-v后如果打印出如下类似信息说明你的Linux上已经安装了gcc了如果没问题了,进入到刚刚解压出来的文件夹根目录下执行make命令编译Redis2、makeinstallPREFIX=/usr/local/redis安装Redismake执行成功后文件夹找就会多一个src文件夹,进入src文件夹,执行makeinstallPREFIX=/usr/local/redis命令,把redis安装到/usr/local/redis/其实1、2两步可以合并到一步执行:make&&makeinstallPREFIX=/usr/local/redis3、拷贝配置文件到安装目录安装好的redis目录下默认没有配置文件,我们需要复制一份到安装目录下4、配置redis为后台启动将刚在复制到安装目录的那个redis.conf打开,并把其中的daemonizeno改成daemonizeyes5、设置redis开机自动启动打开/etc/rc.local在里面添加:/usr/local/redis/bin/redis-server/usr/local/redis/redis.conf(rc.local这个脚本会在开机的时候执行)6、启动redis服务执行命令redis-server/usr/local/redis/redis.conf启动redis服务。注意!如果按照上面的正常的流程安装下来,但是在执行redis-server启动redis的时候提示redis-server不是命令,不要慌张,这是由于这个redis-server不是全局的命令不能在每一个目录下使用,当在别的目录下使用的时候系统在/usr/bin/找不到这个命令,此时我们需要把安装目录下的redis-server链接到到到/usr/bin目录下就可以了。比如我的redis安装目录是/home/myredis/redis/redis-5.0.5/src/redis-server,那就可以执行下面的命令:ln-s/home/myredis/redis/redis-5.0.5/src/redis-server/usr/bin/redis-server解决问题后再来执行上面那个命令启动redis服务,启动后我们可以使用命令ps-ef|grepredis来查看服务有没有启动:redis启动成功了,之后执行redis-cli-p6379进入redis的命令行模式四、redis-benchmarkredis自带了一个性能测试工具redis-benchmark,他有丰富的模拟组件和指令可以使用。中文链接如下:http://www.redis.cn/topics/benchmarks.html。redis-benchmark程序模拟N个客户端同时发出M个请求来测试在本机上redis可以达到的吞吐量从而间接的对给定硬件条件下面的机器计算出性能参数。表现为Responsetime和完成request的数量等等。redis-benchmark可以使用到的参数:五、redis入门必会必知(0)什么是Redis?redis是一个key-value型的高性能内存数据库,同时Redis也是一个优秀的缓存中间件,类似于memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过10万次读写操作,是已知性能最快的Key-ValueDB。(1)redis的默认端口是多少?redis出厂的默认端口是6379,这个端口可以在redis的配置文件中修改。(2)redis的常用五大数据类型Redis的五大常用数据类型是:string(字符串)、list(列表)、hash(散列表)、set(集合)、zSet(sortedSet,有序集合)(3)redis是以单线程来处理客户端的请求。对读写等事件的响应式通过对epoll函数的包装来实现到的。Redis的实际处理速度完全依靠主进程的执行效率。epoll是Linux内科为处理大批量文件描述符伟做了改进的epoll,是Linux下多路复用IO接口select/poll的增强版本,他能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。(4)默认有16个数据库。默认的数据库从DB0开始(默认登录也是0号库),切换可以使用select<dbid>,这些在redis.conf这个文件中有详细的说明:(5)一个字符串类型的值能存储最大容量是多少?512MB(6)一个Redis实例最多能存放多少的keys?List、Set、SortedSet他们最多能存放多少元素?和内存大小有关,内存越大可以存放的key就越多,能存储的元素就越多。(7)Redis集群最大结点数是多少?16384个(8)Redishash槽的概念?Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
LoveIT 2019-10-08Redis -
Redis配置文件redis.conf详解
Redis脚本简介在我们介绍Redis的配置文件之前,我们先来说一下Redis安装完成后生成的几个可执行文件:redis-server、redis-cli、redis-benchmark、redis-stat、redis-check-dump、redis-check-aof:Redis配置文件详解开头说明开头说明中主要就是要注意在redis中内存大小写k和kb是不一样的,前者是1000的倍数,后者超时1024的倍数。INCLUDES INCLUDES的作用就是把其他关于redis的配置文件引入到redis.conf文件中使其生效,redis.conf就作为一个总闸一样,配置的方法是使用include来引入一个路径下配置文件(比如:include/path/aaa/other.conf)。 需要注意的是,如果将此配置写在redis.conf文件的开头,那么后面的配置会覆盖引入文件的配置,如果想以引入文件的配置为主,那么需要将include配置写在redis.conf文件的末尾。MODULES这个部分是用来引入自定义的模块的。通过这里的`loadmodule`配置将引入自定义模块来新增一些功能。NETWORKbind:绑定redis服务器网卡IP,默认为127.0.0.1,即本地回环地址。这样的话访问redis服务只能通过本机的客户端连接,而无法通过远程连接。如果bind被注掉了或者为空时会接收所有来自于可用网络的连接。port:指定redis运行的端口,默认的是6379。由于redis是单线程模型,因此单机开多个redis运行的时候会修改端口,除此而外一般保持默认的即可。protected-mode:是否开启保护模式,默认是yes表示开启保护模式timeout:设置客户端连接时的超时时间,单位:秒。当客户端在这段时间没有任何操作(空闲的),那么就会关闭连接。默认为0,表示永不关闭。tcp-backlog:此参数确定了TCP连接中已完成的队列长度,这个值必须不能大于Linux系统中定义的/proc/sys/net/core/somaxconn值,默认是511。tcp-keepalive:表示将周期性的使用SO_KEEPALIVE检测客户端是否还处于健康状态,避免服务器一直阻塞,官方默认是300s,如果设置为0,表示不周期性检测。GENERALdaemonize:设置为yes表示指定Redis以守护进程的方式启动(后台启动)。默认为nopidfile:配置pid文件路径,当Redis作为守护进程运行的时候,会把pid默认写到/var/redis/run/redis_6379.pid文件里面loglevel:定义日志级别。默认为notice。Redis中有4中日志级别:debug:记录详细的日志,使用与开发、测试阶段varbose:较多的日志notice:适量的日志信息,适用于生产环境warning:仅有部分重要、关键的才会被记录logfile:配置日志文件默认存放的位置,默认会直接打印在终端的屏幕上databases:设置数据库的数目。默认的数据库是DB0,有16个,可以使用select<dbid>命令选择不同的数据库。always-show-logo:是否在启动的时候显示Redis的logo,默认为yes,即显示logo。SNAPSHOTTINGSnapshotting:快照。主要是用来配置持久化策略的。save:用来配置触发Redis的做持久化的条件,也就是什么时候将内存中的数据保存到硬盘中。默认配置如下:*save9001:表示900s内如果有1个key变化,到时间(900s)后就把这段时间内的变化保存到磁盘*save30010表示300s内如果有10个key变化,到时间(300s)后就把这段时间内的变化保存到磁盘*save6010000表示60s内如果有10000个key变化,到时间(60s)后就把这段时间内的变化保存到磁盘当然如果只是使用Redis的缓存功能,不需要持久化,那么可以把这些save注释掉,然后使用一个空字符串实现停用:save""stop-writes-on-bgsave-error:当启用了RDB且最火一次后台保存数据失败,Redsi是否停止接收数据。默认值为yes,这会让用户意识到数据没有正确持久化到硬盘上,从而可以排错,否者没有人会注意到灾难发生了。rdbcompression:对于存储到磁盘中的快照,可以设置时候惊进行压缩存储。默认值是yes,redis会使用LZF算法进行压缩。但是压缩会带来一定的CPU消耗,如果关闭后存储在磁盘上的快照将会非常大。rdbchecksum:在存储快照后,我们还可以让Redis使用CRC64算法来进行数据校验。默认是yes,这样会带来10%的性能消耗。dbfilename:设置快照的文件名,默认名字是dump.rdb。dir:设置快照文件的存放路径,这个配置项必须自定的是一个目录,而不能是一个文件名。保存的是上面dbfilename,默认保存到当前目录下。REPLICATIONslave-serve-stale-data:当一个slave和一个master失去联系,或者复正在进行的时候,slave可能会有两种表现:*如果是yes,slave任然会应答客户端请求,但是返回的数据是过时的。*如果是no,在执行除了infohesalvaof之外的其他命令时,slave都将返回一个“SYNCwithmasterinprogress”错误slave-read-only:配置Redis的Slave示例是否接受写操作,即Slave是否为只读Redis。默认值是yes,Slave为只读。repl-diskless-sync:主从复制是否使用无硬盘复制功能。默认值为no。repl-disless-sync-delay:当启用无硬盘备份,服务器等待一段时间后才会通过套接字向从站传送RDB文件,这个等待时间是可配置的。这一点很重要,因为一旦传送开始,就不可能再为一个新到达的从站服务。从站则要排队等待下一次RDB传送。因此服务器等待一段时间以期更多的从站到达。延迟时间以秒为单位,默认为5秒。要关掉这一功能,只需将它设置为0秒,传送会立即启动。默认值为5。repl-disalbe-tcp-nodelay:同步之后是否禁用从站上TCP_NODELAY。如果yes,表示redis会使用较少的TCP包和带宽向从站发送数据。但是这回导致从站增加数据延时;如果选择no,从站的数据延时不会那么多,但备份需要的带宽相对较多。Redis默认设置是no。SECURITYrename-command:从命名命令。例如对于一些危险的命令:*flushdb:清空当前数据库*flushall:清空所有数据库*config:客户端连接后可配置服务器*keys:查看数据库中所有的键requirepass:设置Redis连接密码,如果配置了连接密码,客户端在连接Redis的时候需要通过auth<password>命令来验证。默认是关闭的。作为服务端redis-server,常常需要禁用以上命令来使得服务器更加安全,禁用的具体做法是:(比如禁用FLUSHALL命令):*rename-commandFLUSHALL""也可以保留这个命令但是把它重命名,一般人没有权限使用:*rename-commandFLUSHALLsfr443g432这样,重启服务器后则需要使用新命令来执行操作,否则服务器会报错unknowncommand。CLIENTSmaxclients:设置客户端最大的连接数,默认是10000个连接。当客户端连接数到达限制是,Redis会关闭新的连接并向客户端返回maxnumberofclientsreached错误信息。如果设置为0,表示不作限制。MEMORYMANAGEMENTmaxmemory:指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区maxmemory-policy:当内存使用达到最大值时,redis应该采用的内存清理策略。有以下几种可以选择:volatile-lru:从所有配置了过期时间的键中移除最近很少使用的键allkeys-lru:从所有键中移除任何最近很少使用过的键volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键allkeys-lfu:从所有键中驱逐使用频率最少的键volatile-random:随机移除设置了过期时间的keyallkeys-random:随机移除任何keyvolatile-ttl:移除距过期时间最近的keynoeviction:不移除任何key,仅仅返回写错误,默认值。APPENDONLYMODEappendonly:默认redis使用的是RDB方式持久化,这种方式在许多应用中已经足够用了。但是对于数据一致性要求很高的应用,如果还是只使用RDB,一旦redis宕机,会导致可能有几分钟的数据丢失,这种场景下就需要使用AOF(另一种持久化方式),可以提供更好的持久化特性以及更高的数据一致性。将appendonly置为yes开启AOF,Redis将会把每次写入的数据在接收后都写入appendonly.aof文件(默认的文件名),每次启动的时候会优先加载appendonly.aof这个文件到内存中。默认值是no。appendfilename:aof文件的默认文件名,默认值是appendonly.aofappendfsync:aof持久化化策略配置。有三个值可以选:*no:不执行fsync,有操作系统保证数据同步到磁盘,速度最快*always:每次写入都执行fsync,以保证数据同步到磁盘,速度最慢*everysec:每秒执行一次fsync,这样aof就可能会对时1s的数据(默认值,通常来说能在速度和数据安全性之间取得比较好的平衡。)no-appendfsync-on-rewirite:如果有子进程在进行保存操作,那么Redis就处于"不可同步"的状态。这实际上是说,在最差的情况下可能会丢掉30秒钟的日志数据。(默认Linux设定)如果把这个设置成"yes"带来了延迟问题,就保持"no",这是保存持久数据的最安全的方式。auto-aof-rewrite-percentage:aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写。默认值是100auto-aof-rewrite-min-size:AOF文件到达重写的阈值,避免了达到约定百分比但尺寸仍然很小的情况还要重写,默认64m,这点内存啥都干不了,一般设置都是以GB为单位。aof-load-truncated:如果设置为yes,如果一个因异常被截断的AOF文件被redis启动时加载进内存,redis将会发送日志通知用户;如果设置为no,erdis将会拒绝启动。此时需要用"redis-check-aof"工具修复文件。LUASCRIPTINGlua-time-limit:一个lua脚本执行的最大时间,单位:ms。默认值5000。REDISCLUSTERcluster-enable:是否开启集群,默认是不开启的。cluster-config-file:集群配置文件名称,每个节点都有一个集群相关的配置文件,持久化保存集群的信息。这个文件不需要手动配置,它由redis生成并更新。默认配置为nodes-6379.confcluster-node-timeout:可以设置值为15000。节点互连超时的阈值,集群节点超时毫秒数。cluster-slave-validity-factor:在进行故障转移的时候,全部slave都会请求申请为master,但是有些slave可能与master断开连接一段时间了,导致数据过于陈旧,这样的slave不应该被提升为master。该参数就是用来判断slave节点与master断线的时间是否过长。判断方法是:比较slave断开连接的时间和(node-timeout*slave-validity-factor)+repl-ping-slave-period如果节点超时时间为三十秒,并且slave-validity-factor为10,假设默认的repl-ping-slave-period是10秒,即如果超过310秒slave将不会尝试进行故障转移。cluster-migration-barrier:master的slave数量大于该值,slave才能迁移到其他孤立master上,如这个参数若被设为2,那么只有当一个主节点拥有2个可工作的从节点时,它的一个从节点会尝试迁移。cluster-require-full-coverage:默认情况下,集群全部的slot有节点负责,集群状态才为ok,才能提供服务。设置为no,可以在slot没有全部分配的时候提供服务。不建议打开该配置,这样会造成分区的时候,小分区的master一直在接受写请求,而造成很长时间数据不一致。
LoveIT 2019-10-08Redis -
Redis五种常用数据类型及命令详解
Redis键(key)的通用命令,所有的数据类型都可以使用string有关命令字符串类型是Redis中最为基础的数据存储类型,是一个由字节组成的序列,他在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据货Json对象描述信息等,是标准的key-value,一般来存字符串,整数和浮点数。value最多可以容纳的数据长度为512MB应用场景:很常见的场景用于统计网站访问数量,当前在线人数等。incr命令(++操作)SETkeyvalue[Options]将字符串value关联到键key。如果key已经持有其他值,SET就覆写旧值,无视类型。可选参数:EXseconds:将键的过期时间设置为seconds秒。执行SETkeyvalueEXseconds的效果等同于执行SETEXkeysecondsvalue。PXmilliseconds:将键的过期时间设置为milliseconds毫秒。执行SETkeyvaluePXmilliseconds的效果等同于执行PSETEXkeymillisecondsvalue。NX:只在键不存在时,才对键进行设置操作。执行SETkeyvalueNX的效果等同于执行SETNXkeyvalue。XX:只在键已经存在时,才对键进行设置操作。当SET命令对一个带有生存时间(TTL)的键进行设置之后,该键原有的TTL将被清除。GETkey返回与键key相关联的字符串值,如果没有这个key,返回nil。APPENDkeyvalue如果键key已经存在并且它的值是一个字符串,APPEND命令将把value追加到键key现有值的末尾。如果key不存在,APPEND就简单地将键key的值设为value,就像执行SETkeyvalue一样。执行成功后会返回当前value的长度。STRLENkey返回与key关联的value的字符串的长度,当key不存在的时候返回0,当key不是字符串的时候使用这个命令会报错。INCRkey为键key储存的数字值加上一。如果键key不存在,那么它的值会先被初始化为0,然后再执行INCR命令。如果键key储存的值不能被解释为数字,那么INCR命令将返回一个错误。INCRBYkeyincrement为键key储存的数字值加上增量increment。如果键key不存在,那么键key的值会先被初始化为0,然后再执行INCRBY命令。如果键key储存的值不能被解释为数字,那么INCRBY命令将返回一个错误DECRkey为键key储存的数字值减去一。如果键key不存在,那么键key的值会先被初始化为0,然后再执行DECR操作。如果键key储存的值不能被解释为数字,那么DECR命令将返回一个错误。DECRBYkeydecrement将键key储存的整数值减去减量decrement。如果键key不存在,那么键key的值会先被初始化为0,然后再执行DECRBY命令。如果键key储存的值不能被解释为数字,那么DECRBY命令将返回一个错误。注意:上面这4个命令只能用于value是数字值的,而且这些操作执行后都会返回加/减操作后的值,且仅仅支持64位(bit)有符号数字表示之内。GETRANGEkeystartend返回键key储存的字符串值的指定部分,字符串的截取范围介于start和end两个偏移量。SETRANGEkeyoffsetvalue从偏移量offset开始,用value参数覆写(overwrite)键key储存的字符串值。不存在的键key当作空白字符串处理。Redis允许的字符串最大的512M,即:能够使用的最大偏移量为2^29-1(536870911),但是请别这样做,除非你想上午还在写码,下午就被开除!!!MSETkeyvalue[keyvalue…]同时为多个键设置值。如果某个给定键已经存在,那么MSET将使用新值去覆盖旧值。MSET是一个原子性(atomic)操作,所有给定键都会在同一时间内被设置,不会出现某些键被设置了但是另一些键没有被设置的情况。MGETkey[key…]返回给定的一个或多个字符串键的值。如果给定的字符串键里面,有某个键不存在,那么这个键的值将以特殊值nil表示MSETNXkeyvalue[keyvalue…]当且仅当所有给定键都不存在时,为所有给定键设置值。即使只有一个给定键已经存在,MSETNX命令也会拒绝执行对所有键的设置操作。MSETNX是一个原子性(atomic)操作,所有给定键要么就全部都被设置,要么就全部都不设置,不可能出现第三种状态。列表listRedis的列表允许用户从序列的两端推入或者弹出元素,列表由多个字符串值组成的有序可重复的序列,是链表结构,所以向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的。List中可以包含的最大元素数量是4294967295。应用场景:1.最新消息排行榜。2.消息队列,以完成多程序之间的消息交换。可以用push操作将任务存在list中(生产者),然后线程在用pop操作将任务取出进行执行。(消费者)LPUSHkeyvalue[value…]将一个或多个值value插入到列表key的表头。如果有多个value值,那么各个value值按从左到右的顺序依次插入到表头,List中允许有重复的值。RPUSHkeyvaluevalue…]将一个或多个值value插入到列表key的表尾(最右边)。如果有多个value值,那么各个value值按从左到右的顺序依次插入到表尾LRANGEkeystartend返回列表key中指定区间内的元素,区间以偏移量start和end指定。下标(index)参数start和end都以0为底,也就是说,以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。也可以使用负数下标,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。LPUSHXkeyvalue当且仅当key存在并且是一个列表时才将值value插入到列表key的表头。RPUSHkeyvalue当且仅当key存在并且是一个列表时才将值value插入到列表key的表尾。LPOPkey移除并返回列表key的头元素。当元素不存在时返回nil。RPOPkey移除并返回列表key的尾元素。当元素不存在时返回nil。RPOPLPUSHsourcedestination将source弹出的元素插入到列表destination,作为destination列表的的头元素。如果source和destination相同,则列表中的表尾元素被移动到表头,并返回该元素,可以把这种特殊情况视作列表的旋转(rotation)操作。LLENkey返回列表key的长度。如果key不存在,则key被解释为一个空列表,返回0。如果key不是列表类型,返回一个错误。LREMkeycountvalue当count>0表示从表头开始搜索并删除count个和value相等的元素当count<0表示从表尾开始搜索并删除count个和vlaue相等的元素当count=0表示删除表中所有的和vlaue相等的元素Listtrimstartend对一个列表进行修剪(trim),让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。LINDEXkeyindex返回列表key中,下标为index的元素。下标(index)参数以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。也可以使用负数下标,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。LINSERTkeyBEFORE|AFTERpivotvalue将值value插入到列表key当中,位于值pivot之前或之后。当pivot不存在于列表key时,不执行任何操作。当key不存在时,key被视为空列表,不执行任何操作LSETkeyindexvlaue将列表key下标为index的元素的值设值为value。当index参数超出范围,或对一个空列表(key不存在)进行LSET时,返回一个错误。字典hashRedis中的hash可以看成具有Stringkey和Stringvalue的map容器,可以将多个key-value存储到一个key中。每一个Hash可以存储4294967295个键值对。应用场景:例如存储、读取、修改用户属性(name,age,pwd等)HSETkeyfiledvlaue[filedvalue...]将哈希表key中域field的值设置为value。如果给定的哈希表并不存在,那么一个新的哈希表将被创建并执行HSET操作。如果域field已经存在于哈希表中,那么它的旧值将被新值value覆盖。HGETkeyfiled返回哈希表中给定域的值。如果给定的域或者hash表不存在,返回nil。HGETALLkey返回哈希表key中,所有的域和值。在返回值里,紧跟每个域名(fieldname)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。HMSETkeyfiledvalue[filedvalue...]同时将多个field-value(域-值)对设置到哈希表key中。此命令会覆盖哈希表中已存在的域。如果key不存在,一个空哈希表会被创建并执行HMSET操作。这个的使用和HSET的用法一样。HMGETkeyfiled回哈希表key中,一个或多个给定域的值。如果给定的域不存在于哈希表,那么返回一个nil值。这个命令的作用和HEGT的作用一样。HSETNXkeyfiledvalue当且仅当域field尚未存在于哈希表key中的情况下,将它的值设置为value。如果给定域已经存在于哈希表当中,那么命令将放弃执行设置操作。如果哈希表hash不存在,那么一个新的哈希表将被创建并执行HSETNX命令HEXISTSkeyfiled检查给定域field是否存在于哈希表hash当中。存在返回1,不存在返回0。HLENkey返回哈希表key中域的数量。HSTRLENkeyfiled返回哈希表key中,与给定域field相关联的值的字符串长度(stringlength)。如果给定的键或者域不存在,那么命令返回0。HINCRBYkeyfiledincrement为哈希表key中的域field的值加上增量increment。增量也可以为负数,相当于对给定域进行减法操作。如果key不存在,一个新的哈希表被创建并执行HINCRBY命令。如果域field不存在,那么在执行命令前,域的值被初始化为0。HINCRBYFLOATkeyfiledincrement和HINCRBYkeyfiledincrement的作用一样,都是哈希表key中的域field的值加上增量increment,但是这里的增量是浮点数。HKEYSkey返回哈希表key中的所有域。HVALSkey返回哈希表key中的所有域的值。集合setRedis的集合是无序不可重复的,和列表一样,在执行插入和删除和判断是否存在某元素时,效率是很高的。集合最大的优势在于可以进行交集并集差集操作。Set可包含的最大元素数量是4294967295。应用场景:1.利用交集求共同好友。2.利用唯一性,可以统计访问网站的所有独立IP。3.好友推荐的时候根据tag求交集,大于某个threshold(临界值的)就可以推荐。SADDkeymember[member…]将一个或多个member元素加入到集合key当中,已经存在于集合的member元素将被忽略。假如key不存在,则创建一个只包含member元素作成员的集合。会返回被添加到集合中元素的个数,不包括重复的元素。SMEMBERSkey返回集合key中的所有成员,不存在的key被视为空集合。SISMEMBERSkeymember判断member元素是否集合key的成员。如果member元素是集合的成员,返回1。如果member元素不是集合的成员,或key不存在,返回0。SCARDkey返回集合key的基数(集合中元素的数量)。SREMkey[key...]移除集合key中的一个或多个member元素,不存在的member元素会被忽略。SRANDMEMBERkeycount如果命令执行时,只提供了key参数,那么返回集合中的一个随机元素。从Redis2.6版本开始,SRANDMEMBER命令接受可选的count参数:如果count为正数,且小于集合基数,那么命令返回一个包含count个元素的数组,数组中的元素各不相同。如果count大于等于集合基数,那么返回整个集合。如果count为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为count的绝对值。SPOPkeycount移除并返回集合中的一个或多个随机元素。SMOVEsourcedestination将member元素从source集合移动到destination集合。SMOVE是原子性操作。如果source集合不存在或不包含指定的member元素,则SMOVE命令不执行任何操作,仅返回0。否则,member元素从source集合中被移除,并添加到destination集合中去。当destination集合已经包含member元素时,SMOVE命令只是简单地将source集合中的member元素删除。集合的数学操作命令SDIFFkey[key…]返回一个集合的全部成员,该集合是所有给定集合之间的差集。不存在的key被视为空集。SINTERkey[key...]返回一个集合的全部成员,该集合是所有给定集合的交集。不存在的key被视为空集。当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。SUNIONkey[key...]返回一个集合的全部成员,该集合是所有给定集合的并集。不存在的key被视为空集。有序集合zset和set很像,都是字符串的集合,都不允许重复的成员出现在一个set中。他们之间差别在于有序集合中每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。尽管有序集合中的成员必须是唯一的,但是分数(score)却可以重复。应用场景:可以用于一个大型在线游戏的积分排行榜,每当玩家的分数发生变化时,可以执行zadd更新玩家分数(score),此后在通过zrange获取几分topten的用户信息。ZADDkeyscoremember[scoremember...]将一个或多个member元素及其score值加入到有序集key当中。如果某个member已经是有序集的成员,那么更新这个member的score值,并通过重新插入这个member元素,来保证该member在正确的位置上。score值可以是整数值或双精度浮点数。如果key不存在,则创建一个空的有序集并执行ZADD操作。ZRANGEkeystartend[withscore]返回有序集key中,指定区间内的成员。其中成员的位置按score值递增(从小到大)来排序。具有相同score值的成员按字典序(lexicographicalorder)来排列。如果需要成员按score值递减(从大到小)来排列,可以使用ZREVRANGEkeystartendwithscoreZREVRANGEkeystartend[withscores]返回有序集key中,指定区间内的成员。其中成员的位置按score值递减(从大到小)来排列。具有相同score值的成员按字典序的逆序(reverselexicographicalorder)排列。ZRANGEBYSCOREkeyminmax[withscores][limitoffsetcount]返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列。具有相同score值的成员按字典序(lexicographicalorder)来排列(该属性是有序集提供的,不需要额外的计算)。可选的LIMIT参数指定返回结果的数量及区间(就像SQL中的SELECTLIMIToffset,count),注意当offset很大时,定位offset的操作可能需要遍历整个有序集,此过程最坏复杂度为O(N)时间。可选的WITHSCORES参数决定结果集是单单返回有序集的成员,还是将有序集成员及其score值一起返回。ZREMkeymember[menber...]移除有序集key中的一个或多个成员,不存在的成员将被忽略。ZCARDstu当key存在且是有序集类型时,返回有序集的基数。当key不存在时,返回0。ZCOUNTstustartend返回有序集key中,score值在min和max之间(默认包括score值等于min或max)的成员的数量。ZRANKkeymember返回有序集key中成员member的排名。其中有序集成员按score值递增(从小到大)顺序排列。排名以0为底,也就是说,score值最小的成员排名为0。ZREVRANKkeymember返回有序集key中成员member的排名。其中有序集成员按score值递减(从大到小和默认的逆序))排序。排名以0为底,也就是说,score值最大的成员排名为0。ZRANGEBYSOCREkeyminmax[withscores][limitoffsetcount]返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列。具有相同score值的成员按字典序(lexicographicalorder)来排列:*可选的LIMIT参数指定返回结果的数量及区间(就像SQL中的SELECTLIMIToffset,count),注意当offset很大时,定位offset的操作可能需要遍历整个有序集,此过程最坏复杂度为O(N)时间。*可选的WITHSCORES参数决定结果集是单单返回有序集的成员,还是将有序集成员及其score值一起返回。ZREVRANGESCOREkeyminmax[withsocres][limitoffsetcount]使用方法和ZRANGEBYSOCRE的用法一样,还是这个是前者的逆序更多Redis命令参考:http://redisdoc.com/。另外这里只是介绍了一下Redis中5种常用数据类型的有关命令和简单的使用场景,关于他们的底层实现我把他放到了下一节:Redis五种常见数据结构的实现及使用场景,感兴趣的小伙伴可以去看看,图文配合非常简单易懂~~参考【1】dijia478.Redis的五种数据类型及方法.博客园
LoveIT 2019-10-08Redis -
SpringBoot从入门到精通—SpringBoot异步任务、定时服务和邮件服务
1、SpringBoot—异步任务 异步调用是相对于同步调用而言的,同步调用是指程序按预定顺序一步步执行,每一步必须等到上一步执行完后才能执行,异步调用则无需等待上一步程序执行完即可执行。 实现异步处理任务的方式有很多,我们可以自己通过多线程来实现或者也可以使用SpringBoot提供的@EableAysnc和@Aysnc这两个注解来实现。1.1通过多线程来实现异步处理任务直接在需要异步任务处理的方法中开启新的线程来处理任务。执行结果如下:1.2使用SpringBoot提供的注解处理异步任务首先在需要异步任务处理的方法上加上@Async注解告诉SpringBoot这个方法需要异步处理然后在主配置类使用@EnableAsync注解开启异步注解功能执行结果如下,SpringBoot也是开启了一个新的线程task-1来处理这个任务的:2、SpringBoot—定时任务定时任务就是提前设置好时间点,然后每到这个时间点就会执行的任务。SpringBoot中可以通过@Scheduled和@EableScheduled这两个注解来实现定时任务。同样在需要定时任务对方法上标注@Scheduled注解,然后在主配置类上标注@EableScheduled注解开启定时注解功能执行结果:从上面这个例子中可以看到,对于定时任务使用主要就是对cron表达式的编写,cron允许的值可以有以下几种:字段允许值允许的特殊字符秒0-59,-*/分0-59,-*/小时0-23,-*/日1-31,-*/?LWC月1-12,-*/星期0-7或SUN-STA,-*/?LWC#解释:,:表示枚举,可以用它在一个一段上枚举多个值—:表示一个区间*:表示任意/:步长?:日/星期冲突匹配L:最后W:工作日C:和Calendar联系后计算后的值#:星期,例如4#2表示第二个星期四下面是几个用法示例:3、SpringBoot—邮件任务 SpringEmail抽象的核心是MailSender接口,MailSender的实现能够把Email发送给邮件服务器,由邮件服务器实现邮件发送的功能。 Spring自带了一个MailSender的实现JavaMailSenderImpl,它会使用JavaMailAPI来发送Email。Spring或SpringBoot应用在发送Email之前,我们必须要JavaMailSenderImpl装配为Spring应用上下文的一个bean。3.1首先引入邮件服务的starter3.2邮件配置3.3简单邮件服务—SimpleMessage使用简单邮件服务只能发送文本消息执行的结果,成功收到了邮件:3.4复杂邮件服务—MimeMessage使用复杂邮件服务可以发送文本消息、HTML语句、甚至支持上传附件执行结果:在GMail收到了邮件,并且设置HTML样式起作用了,附件也上传成功了。
LoveIT 2019-10-01Spring Boot -
SpringBoot从入门到精通—Spring Boot + Eureka 实现微服务负载均衡
1、什么是Eureka? Eureka这个单词原本的意思就是“我发现了,我找到了”,他在Spring中的功能也和他的本意是一样的。Eureka是netflix的一个子模块,也是核心模块之一,Eureka是一个基于RESTful的服务,用于定位服务,以实现云端中间层服务发现和故障转移。服务注册与发现对于微服务架构来说是非常重要的,有了服务发现和注册,只需要使用服务的标识符,就可以访问到服务,而不需要修改服务,而不需要修改服务调用的配置文件了,功能类似于dubbo的注册中心,比如zookeeper。 SpringCloud封装了Netflix公司开发的Eureka模块来实现服务注册时和发现。 Eureka采用了C/S设计架构。EurekaServer作为服务注册功能的服务器,它是服务注册时中心。而系统中的其他微服务,使用eureka的客户端连接到eurekaserver并维持心跳连接。这样系统的维护人员就可以通过eurekaserver来监控系统中各个微服务是否正常运行。SpringCloud的一些其他模块就可以通过eurekaserver来发现系统中的其他微服务,并执行相关的逻辑。 Eureka包含两个组件:EurekaServer和EurekaClient。 EurekaServer提供服务注册服务。各个节点启动后,会在EurekaServer中进行注册,这样Eurekaserver中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。 Eurekaclient是一个Java客户端,用于简化eurekaserver的交互,客户端同时也具备一个内置的,使用轮询负载算法的负载均衡器。在应用启动后,将会向EurekaServer发送心跳。如果EurekaServer在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表把这个服务节点移除。Eureka的三大角色:1.Eurekaserver提供服务注册和发现2.ServiceProvider服务提供方将自身服务注册到Eureka,从而使服务消费方能够找到。3.ServiceConsumer服务消费方从Eureka获取注册服务列表,从而能够消费服务。总结起来就是说1.Eureka是Netflix开源的一个RESTful服务,主要用于服务的注册和发现。2.Eureka由两个组件组成:Eureka服务器和Eureka客户端。Eureka服务器用作服务注册服务器。3.Eureka客户端是一个Java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。4.Netflix在其生产环境中使用的是另外的客户端,它提供基于流量、资源利用率以及出错状态的加权负载均衡。2、搭建一个基于SpringBoot+Eureka的微服务工程工程的搭建我使用IDEA,首先新建一个空工程【Emptyproject】,选择空工程点击【next】,之后我们需要在这个工程中建立三个子模块,分别是euraka-server注册中心,poervider服务提供者,customer消费者。新建一个空工程在新建项目的时候我们可以使用【SpringInitializr】,在新建eurake-server模块的时候选上EurekaServer,新建provider和customer模块的时候可以选上Web的satrter和EurakeDiscoveryClient的satrter建好后的项目结构:3、EurekaServer—注册中心的配置在EurekaServer的启动类上使用@EnableEurekaServer开启Eureka服务配置好后启动enureka-server,它将会在配置的端口8761启动,输入http://localhost:8761就可一个看到enureka-server的管理界面:4、ServiceProvider—服务提供方的配置在服务提供方的配置文件中配置如下信息:实现一个服务,TicketServiceEureka的底层还是基于HTTP协议的,在消费者端要调用服务提供方的服务时,实际是通过HTTP请求的方式来调用的,因此需要在服务提供方给对应的service提供对应的controller。TicketController如下:最后在服务提供方的启动类上使用@EnableEurekaClient来告诉Spring这是Eureka的Cilent端,这个服务要注册到注册中心上去。(也可以使用@EnableDiscoveryClient注解,这两个注解的作用是相同的,但是还是有差别的,具体的可以参考@EnableDiscoveryClient与@EnableEurekaClient区别)。配置完成后启动ServerProvider(注意在启动Client端的服务时EurekaServer要保持运行),启动后可以正常访问http://localhost:8080/ticket,并且在注册中心可以看到8080端口的PROVIDER实例已经注册了就算是成功了。我们可以开启多个服务提供方,方法是把当前的provider使用Maven命令(mvninstall)打包成可执行jar包,然后在把server.port改成另一个端口再使用Maven命令打包后运行...下面是我启动了两个provider,分别在8080和8081端口5、ServiceCustomer—服务消费方的配置在服务消费方的配置文件中配置如下内容:在服务消费方的启动类上使用@EnableDiscoveryClient注解告诉SpringBoot把这个服务注册到注册中心。并且注册RestTemplate到IoC容器中,可以使用他来远程调用服务提供方的服务。编写一个controller使用RestTemplate来调用服务提供方的注册在服务中心的服务。最后启动服务消费方看看效果吧!首先我们可以访问http://localhost:8761/,可以在注册中心看到消费方也在注册中心注册了:我们接着访问http://localhost:8020/buy?name=李四&num=6就可以看到下面的页面:微服务负载均衡的体现把另一个打包的可执行jar包在命令行使用java-jar命令运行,注意不要让端口冲突。让两个服务提供方同时运行,然后我们访问http://localhost:8020/buy?name=李四&num=6,不断改变num的值查看控制台的打印发现这两个端口的服务是轮流工作的,从而达到了一个负载均衡的作用。
LoveIT 2019-09-30Spring Boot -
SpringBoot从入门到精通—自定义starter
SpringBoot中提供了各种starter,starter可以理解为一个可拔插式的插件,当我们要使用的时候只用导入需要的starter即可。例如:你想使用jdbc插件,那么可以使用spring-boot-starter-jdbc;如果想使用mongodb,可以使用spring-boot-starter-data-mongodb。但是当我们需要的场景没有的时候我们可以来定制starter。首先在IDEA中创建一个maven工程,在其中创建两个Model,一个是hello-spring-boot-starter,另一个是hello-spring-boot-starter-configurer,目结构如下:要特别注意artifactId的命名规则:Spring官方starter通常命名为spring-boot-starter-{name},如spring-boot-starter-webSpring官方建议非官方Starter命名应遵循{name}-spring-boot-starter的格式,如mybatis-spring-boot-starter。一般我们不会直接在starter中写配置,starter一般只一个空项目,然后主要的配置写在这个starter对应的autoconfigurer中,让starer依赖configurer就可以了。1、在starter中引入对应的configurer依赖starter一般是一个空模块,真正的实现放在configurer中,让starter依赖configurer,以后需要使用这个模块的时候只用引入starter就可以引入它所依赖的configurer。2、在hello-spring-boot-starter-configurer模块中写我们需要的配置2.1XxxPrperoties2.2核心业务类2.3自动配置类XxxAutoConfigurer2.4spring.factories然后需要在hello-spring-boot-starter-configurer的src/main/resources文件夹下新建META-INF文件夹然后新建spring.factories文件,配置这个类让他可以自动启动。2.5打包mvncleaninstall使用Maven命令mvncleaninstall或直接使用IDEA提供的Maven插件执行install命令把我们的这个starter安装到本地Maven仓库。注意:如果提示没有找到pom文件的错误,那就使用命令行找到对应的项目执行Maven命令测试一下之后我们再新建一个普通的SpringBoot项目,引入我们自定义的starter引入后的效果:写一个controller来测试一下我们的starter是否有效可以在主配置文件中配置UserProperties中的属性(不配置将会使用默认值)启动运行看看效果:
LoveIT 2019-09-29Spring Boot -
SpringBoot从入门到经通过—Spring —Cache
1、JSR-107规范1.1JSP-107是什么? 要回答这个问题,首先要知道JSR是什么,JSR是JavaSpecificationRequests的缩写,Java规范请求,故名思议就是Java规范,大家一同遵守这个规范的话,会让大家‘沟通’起来更加轻松。规范是很重要的,举个例子大家都知道红灯停,路灯行吧,如果每个城市的信号灯代表不一样,那就麻烦了,B城市红灯行,绿灯停,C城市甚至出现紫灯行,闪灯行,想想都知道,如果我们保证不出问题,必须知道每个城市的信号等代表的意义。我们一直使用的JDBC就一个访问数据库的一个规范的例子。而JSR-107呢就是关于如何使用缓存的规范。1.2JSR-107核心APIJavaCaching定义了5个核心接口,分别是CachingProvider,CacheManager,Cache,Entry和Expiry。CachingProvider用于定义创建、配置、获取、管理和控制Cachemanager。CacheManager用于定义了建立,配置,得到,管理和控制有着唯一名字的Cache,一个CacheManager被包含在单一的CachingProvider。CacheCache是一个Map类型的数据结构,用来存储基于键的数据,很多方面都像java.util.Map数据类型。一个Cache存在在单一的CacheManager。EntryEntry是一个存在于Cache的key-value键值对Expiry每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy来设置。这些接口之间的关系可以用下图表示:2、Spring缓存抽象Spring3.1从开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并使用JCache(JSR-107)注解简化我们的开发。Cache接口为缓存的组件规范定义,包含缓存的各种操作集合Cache接口下Spring提供了各种xxxCache的实现,如RedisCache、EncheCache、ConcurrentMapCache......每次调用需要缓存功能的方法是,Spring会检查指定的参数的是定目标方法时候已经被调用过,如果有就直接从缓存中获取方法调用后对结果,如果没有就调用方法去数据库查询并缓存结果后返回给用户,下次回直接从缓存中获取。重要的缓存注解注解功能Cache缓存接口,定义缓存操作。CacheManager缓存管理器。管理和中缓存组件@Cacheable主要用于方法,能够根据方法的请求参数对其进行缓存@CachePut方法被调用,并且在调用后结果被缓存,主要用于更新操作@CacheEvict清除缓存@Caching配置复杂对缓存策略@CacheConfig同一配置本类的缓存注解额属性@EnableCaching开启基于注解的缓存serialize缓存数据的value序列haul策略@Cacheable/@CachePut/@CacheEvict主要的参数参数解释value/cacheNames缓存的名字。必须指定至少一个,可以配置多个例如:@Cacheable(value={"cache1","cache2"})key缓存的key。可以为空,如果指定要使用SpEL。默认将方法的所有参数组合起来作为key。例如:@Cacheable(value="cache1",key="#id")keyGenerator定义自动生成主键的策略,使用的时候key和keyGenerator二选一condition作缓存的条件。可以为空,使用SpEL表达式指定,返回true表示作缓存,否者不缓存。例如:@Cacheable(vlaue="cache",condition="#id>0")unless也是作缓存的条件。当条件为true时,就不缓存(和condition的效果是反的)。例如:@Cacheable(value="cache",unless="#idsync(@Cacheable)是否使用异步支持,这是Spring4.3以后才支持的,默认值false,不开启异步模式例如:@Cacheable(value="cache",sync=true)//开启异步模式allEntries(@CacheEvict)是否清空所有缓存内容。默认为false,如果指定为true,则方法调用后将立即清空所有缓存。beforeInvocation(@CacheEvict)是否在方法执行前清空缓存。默认为false,如果指定为true,则方法还没有执行的时候就清空缓存。默认情况下如果方法抛出异常,就没有办法清空缓存了。SpEL上下文数据,Spring提供了一些供我们使用的SpEL表达式,名称位置描述用法示例methodName(方法名)root对象当前被调用的方法名#root.methodnamemethod(方法)root对象当前被调用的方法#root.method.nametarget(当前对象)root对象当前被调用的目标对象实例#root.targettargetClass(目标类)root对象当前被调用的目标对象的类#root.targetClassargs(参数列表)root对象当前被调用的方法的参数列表#root.args[0]caches(缓存列表)root对象当前方法调用使用的缓存列表#root.caches[0].nameArgumentName(参数名)执行上下文当前被调用的方法的参数,如findArtisan(Artisanartisan),可以通过#artsian.id获得参数#artsian.idresult(方法返回值)执行上下文方法执行后的返回值(仅当方法执行后的判断有效,如unlesscacheEvict的beforeInvocation=false)#result3、Spring缓存的配置和使用3.1@Cacheable@Cacheable注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存。在CacheAspectSupport这个类的findCachedItem上打上断点,观察发下我们配置的key是有效:3.2自定义主键生成策略在方法中可以这么使用:同样,通过打断点发现我们配置的主键生成也起作用了:其他参数的用法可以参考下面的程序:3.3@CachePut@CachePut注解的作用主要用于对方法配置,能够根据方法的请求参数对其结果进行缓存,和@Cacheable不同的是,它每次都会触发真实方法的调用。简单来说就是用户更新缓存数据。但需要注意的是该注解的value和key必须与要更新的缓存相同,也就是与@Cacheable相同。@CachePut的其他参数的用法和一样@Cacheable参数的用法一样。这里不再赘述。3.4@CacheEvict@CachEvict的作用主要用于方法的配置,能够根据一定的条件对缓存进行清空。3.5@Caching有时候我们可能组合多个Cache注解使用,此时就需要@Caching组合多个注解标签了。3.6@CachingConfig当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames={"cacheName"})注解来统一指定value/cacheNames的值,这时可省略value/cacheNames,如果你在你的方法依旧写上了value/cacheNames,那么依然以方法的@CacheConfig配置的值为准。使用方法如下:@CacheConfig可以配置单的属性如下:
LoveIT 2019-09-28Spring Boot -
Spring从入门到精通—自动配置Spring MVC的原理
1、SpringMVC自动配置SpringBoot对SpringMVC自动配置的详细可以参考管方文档。SpringBoot为SpringMVC提供的AutoConfiguration适用于大多数应用场景,SpringBoot对SpringMVC做了以下默认的自动配置:引入ContentNegotiatingViewResolver和BeanNameViewResolver。对静态资源的支持,包括对WebJars的支持自动注册Converter,GenericConverter,Formatter。对HttpMessageConverters的支持。自动注册MessageCodeResolver。对静态资源的支持对自定义Favicon的支持自动使用ConfigurableWebBindingInitializerbean。SpringBoot默认情况下是自动配置好SpringMVC的,可以直接使用,但是SpringBoot也支持我们修改SpringBoot对SpringMVC的配置。如果保留SpringBootMVC特性,我们只需添加其他的MVC配置(拦截器,格式化处理器,视图控制器等)。我们可以添加自己的WebMvcConfigurerAdapter类型的@Configuration类(配置类),而不需要注解@EnableWebMvc。如果希望使用自定义的RequestMappingHandlerMapping,RequestMappingHandlerAdapter,或ExceptionHandlerExceptionResolver,我们可以声明一个WebMvcRegistrationsAdapter实例提供这些组件。但是如果想全面控制SpringMVC,我们可以添加自己的@Configuration类,并使用@EnableWebMvc注解。这样SpringBoot就不会对MVC进行配置了。然后我们就可以像刚开始使用SpringMVC那样对他进行配置。2、SpringMVC自动配置原理细节SpringBoot对SpringMVC的自动配置主要是通过WebMvcAutoConfiguration这个类实现的,接下来我们就结合这个类来简单分析一下自动配置的细节。2.1ContentNegotiatingViewResolver和BeanNameViewResolver这两个一听名字就知道是和视图解析器有关,也确实是这样的,他们自动配置了ViewReslover,然后由ViewReslover得到View对象,View对象调用他的render方法渲染页面等等。其中BeanNameViewResolver就是SpringMVC中的一个视图解析器,他可以通过视图名来获得视图解析器,而ContentNegotiatingViewResolver的作用就是组合所有的视图解析器,下面他们的源码:问题:ContentNegotiatingViewResolver是如何组合所有视图解析器的因此,我们可以实现自己的视图解析器,然后ContentNegotiatingViewResolver把它注册到容器中。定制自己的视图解析器,我们可以在启动类中实现ViewResolver接口,编写我们自己的视图解析器,然使用@Bean标签配置给IOC容器。3、Converter,GenericConverter,Formatter这些功能在SpringBoot中也有默认的自动配置,这里我们要了解的是如何扩展配置Converter和Formatter。源码:我们也可以定制自己的转换器4、HttpMessageConvertersSpringMVC使用HttpMessageConverter接口转换HTTP请求和响应,合适的默认配置可以开箱即用,例如对象自动转换为JSON(使用Jackson库)或XML(如果JacksonXML扩展可用,否则使用JAXB),字符串默认使用UTF-8编码。可以使用SpringBoot的HttpMessageConverters类添加或自定义转换类:5、扩展SpringBoot对SpringMVC的配置想要扩展SpringBoot的MVC功能,我们要WebMvcConfigurer接口,但是这样太麻烦了,因此SpringBoot提供了一个适配器类WebMvcConfigurerAdapter,它里面全部是一些空方法,我们可以继承WebMvcConfigurerAdapter类,然后我们只需要按照我们的需要重写里面的方法就好了。6、全面接管SpringBoot对SpringMVC的自动配置官网中的一句话:IfyouwanttotakecompletecontrolofSpringMVC,youcanaddyourown@Configurationannotatedwith@EnableWebMvc.意思就是我们可以配置类上加上EnableWebMvc来全面接管SpringMVC,这样一来SpringBoot就不会对SpringMVC进行配置了,一切都需要我们来配置。
LoveIT 2019-09-27Spring Boot -
SpringBoot从入门到精通—MyBatis的配置和使用(注解+XML配置)
关于MyBatis,大部分人都很熟悉。MyBatis是一款优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。这篇文章主要介绍了SpringBoot集成MyBatis的两种方式(注解和XML文件配置),需要的朋友可以参考下.1、使用XML配置MyBatis1.1在pom.xml文件中引入MyBatis的依赖1.2编写MyBatis的主配置文件Mybatis-config.xml在src/main/resources目录下新建mybatis文件夹,然后新建Mybatis-config.xml文件并配置如下:1.3编写Mapper接口以及对应的Mapper映射文件在src/main/java下新建mapper包,然后新建ArticleMapper接口如下:在src/main.resoures下新建mapper文件夹,然后在mapper文件夹中新建对应的Mapper映射文件ArticleMapper.xml如下:然后在application.yml文件中配置mybatis这两个文件的位置:编写一个controller,简单测试一下看看我们配置的MyBatis能不能用。这里为了简单就没有写对应的service。测试结果:2、使用注解配置MyBatisMyBatis提供了以下几个CRUD基本注解:*@Select*@Update*@Insert*@Delete增删改查占据了绝大部分的业务操作,掌握这些基础注解的使用还是很有必要的,例如下面这段代码无需XML即可完成数据查询:MyBatis提供的映射注解:@Results用于填写结果集的多个字段的映射关系.@Result用于填写结果集的单个字段的映射关系.@ResultMap根据ID关联XML里面.在启动类上使用@MapperScan扫描mapper包下的所有的Mapper接口,这样做的好处是可以省去在每个Mapper接口上写@Mapper注解。还可以写一个配置类彻底取代MyBatis的所有XML文件。MyBatisConfig.java编写一个controller,看看实际的效果:测试结果:
LoveIT 2019-09-26Spring Boot -
SpringBoot从入门到精通—SpringBoot启动流程剖析
上图为SpringBoot启动结构图,我们发现启动流程主要分为三个部分,第一部分进行SpringApplication的初始化模块,配置一些基本的环境变量、资源、构造器、监听器,第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块,第三部分是自动化配置模块,该模块作为springboot自动配置核心,在后面的分析中会详细讨论。在下面的启动程序中我们会串联起结构中的主要功能。1、基于源码简单剖析SpringBoot的启动流程Springboot的启动过程:SpringBoot应用启动后首先执行SpringBoot启动类的main方法中的publicstaticConfigurableApplicationContextrun(Class<?>primarySource,String...args)这个run方法,他又调用了重载方法的publicstaticConfigurableApplicationContextrun(Class<?>[]primarySources,String[]args),在这个run方法中调用了真正的run方法。他们的调用关系如下图:在调用正真干活的run方法之前,SpringAppication先进行了初始化:最终真正干活的run方法该方法中有几个关键的步骤:(1)创建了应用监听器SpringApplicationRunListeners并开始监听(2)加载SpringBoot配置环境ConfigurableEnvironment,如果是通过web容器发布,会加载StandardEnvironment,其最终也是继承了ConfigurableEnvironment,类图如下:(3)将从配置文件中加载进来的配置环境(Enviroment)加入到监听器中(4)创建应用配置上下文对象ConfigurableApplicationContext,我们可以到其具体的实现中看看:(5)回到run方法继续往下走,prepareContext()方法将listeners、environment、applicationArguments、banner等组件与上下文对象关联(6)调用refreshContext()方法是SpringBoot启动中最重要的一步,包括spring.factories的加载,bean的实例化等核心工作。这一步完成后SpringBoot的启动工作基本就完成了。下面是最终真正干活的refresh()方法的截图:方法首先获取显式获取并设置了应用上下文(applicationContextClass),如果不存在,再根据不同的Web环境选择实例化不同的上下文对象,默认选择AnnotationConfigApplicationContext注解上下文(通过扫描所有注解类来加载bean),最后通过BeanUtils实例化上下文对象,并返回。配置结束后,Springboot做了一些基本的收尾工作,返回了应用环境上下文。回顾整体流程,Springboot的启动,主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean,至此,通过SpringBoot启动的程序已经构造完成。启动过程中的一些细节代码说明:(1)在run方法一开始执行他就new了一个StopWatch,这个类有啥用呢?开启单步调试,最终发现这个类的功能就是统计一下SpringBoot启动所用的时间并在最会在启动完成打印出来。(2)在启动中有一个Banner,他的用处就是打印一下SpringBoot的logo
LoveIT 2019-09-25Spring Boot -
SpringBoot从入门到精通—嵌入式Servlet容器的配置、切换
声明:以下的所有方法、原理、源码全部是建立在SpringBoot2.1.7版本。1、修改SpringBoot对嵌入式Server容器的默认配置SpringBoot默认使用Tomcat作为嵌入式的Servlet容器。实际应用中我们需要对他进行专门的定制。定制的方式不外乎两种:在配置文件中配置或在配置类中注册组件的方式配置1.1直接在application.properties/application.xml文件中配置和server有关的属性配置方式是server.属性名=值,下面是ServerProperties类中定义的绝大多数可以配置的属性配置示例:1.2向IoC容器中添加servlet容器工厂定制器WebServerFactoryCustomizer运行结果:如果使用的是SpringBoot1.x版本可以参考下面的方法来使用配置1.3向IoC容器中添加可配置的servlet容器工厂ConfigurableServletWebServerFactory运行自然是没有什么问题,在地址栏输入http://localhost:8089/springboot/也来到了目标页面:1.4在SpringBoot中注册Servlet三大组件【Servlet、Filter、Listener以前注册这些组件都是在web.xml中配置,由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件。注册三大组件用以下方式:①、借助ServletRegistrationBean注册自定义Servlet组件;自定义【MyServlet】:定义【ServletRegistrationBean】并放入容器中:②、借助FilterRegistrationBean注册自定义Filter组件;(自定义的MyFilter忽略)③、借助ServletListenerRegistrationBean注册自定义Listener组件;(自定义的MyListener忽略)2、嵌入式Servlet容器切换2.1三大容器比较容器优点缺点默认tomcat功能齐全庞大,荣泽truejetty轻量功能不全falseundertow异步,高效不支持jspfalse2.2容器切换TomcatSpringBoot引入web模块默认就是使用嵌入式的Tomcat作为Servlet容器。`Jettyundertow3、嵌入式Servlet容器自动配置原理主要看三个类:*ServletWebServerFactoryConfiguration*ServletWebServerFactoryAutoConfiguration*ServletWebServerFactoryCustomizer3.1ServletWebServerFactoryConfiguration有三个:EmbeddedUndertow,EmbeddedJetty,EmbeddedTomcat@Conditionalxxx标注的类;条件满足才向容器中添加组件。ServletWebServerFactory:嵌入式Servlet工厂。作用:创建嵌入式Servlet容器在判断@ConditionalOnClass({Servlet.class,Tomcat.class,UpgradeProtocol.class})是否导入依赖,满足条件后returnnewTomcatServletWebServerFactory();添加对应的Servlet容器工厂;通过工厂的唯一方法getWebServer获取对应的Servlet容器TomcatServer.TomcatServletWebServerFactoryTomcatWebServer3.2ServletWebServerFactoryAutoConfiguration修改定制Servlet容器的方法: 1、配置文件中添加配置。 2、ServerProperties绑定/修改定制组件WebServerFactoryCustomizer他们的本质是一样的:在ServletWebServerFactoryAutoConfiguration配置类中@EnableConfigurationProperties({ServerProperties.class})。导入了BeanPostProcessorsRegistrar,在这个类的方法中添加了组件WebServerFactoryCustomizerBeanPostProcessor(定制器后置处理器)。也就是说一旦容器中添加任何组件都会启动定制后置处理器,进行Servlet的赋值。后置处理器起作用的过程:3.3ServletWebServerFactoryCustomizer在这个配置类中使用customize(ConfigurableServletWebServerFactoryfactory)这个方法完成了Tomcat的各项配置的修改和定制总结配置修改定制原理:以tomcat为例总结配置修改定制原理:以tomcat为例1、SpringBoot根据根据导入依赖的情况,给容器中添加相应的ServletWebServerFactory(比如tomcat就会添加TomcatServletWebServerFactory)2、如果使用的是通过application.propertoes修改配置,那么server相关的配置修改是与ServerProperties类绑定的,所以相关的修改会直接通过Serverproperties的方法实现【相关的配置类:ServletWebServerFactoryCustomizer】3、如果使用的是修改定制器WebServerFactoryCustomizer的方法来配置server,那么定制器会创建ConfigurableWebServerFactory对象,这样一来就会触发WebServerFactoryCustomizerBeanPostProcessor后置处理器,判断是否为WebServerFactory类型;满足条件后,就会获取容器中的所有定制器(customizer.cutomize(bean)),为Servlet容器修改和定制配置【相关的配置类ServletWebServerFactoryAutoConfiguration,导入了定制处理器】
LoveIT 2019-09-24Spring Boot -
SpringBoot从入门到精通—整合Druid数据源
java实现的数据库连接池有很多,比如c3p0,dbcp等,还有号称速度最快的HikariCP,并且springboot2.0.2版本默认使用的就是HikariCP。为什么选用Druid呢?-性能够好,比c3p0,dbcp强一些-经过考验,毕竟是阿里开源出来的项目-最关键的是带一个强大的数据库监控这些特性的加持下足以支撑起我使用Druid的理由。1、Druid能监控那些数据数据源SQL监控,对执行的MySQL语句进行记录,并记录执行时间、事务次数等SQL防火墙,对SQL进行预编译,并统计该条SQL的数据指标Web应用,对发布的服务进行监控,统计访问次数,并发数等全局信息URI监控,对访问的URI进行统计,记录次数,并发数,执行jdbc数等Session监控,对用户请求后保存在服务器端的session进行记录,识别出每个用户访问了多少次数据库等Spring监控,(按需配置)利用aop对各个内容接口的执行时间、jdbc数进行记录2、如何配置使用Druid想达到的目标效果,监控sql,监控sql防火墙,监控url,监控session,监控spring其中监控sql、监控url、基础信息,几乎不怎么需要配置,集成好druid,配置好监控页面,就可以显示。需要我们配置的大概分为3部分,基础连接池配置,基础监控配置,定制化监控配置2.1引入Druid的依赖2.2连接池基础配置主要配置用户名,密码,数据库驱动、数据库连接.....以及和连接池有关的配置2.3导入Druid数据源的其他属性上面的配置文件中和数据库连接池有关的配置属性在SpringBoot是没有的,因此我们需要告诉SpringBoot,让他在启动的时候去找加载这些属性,因此我们需要写一个配置类。没有写配置类之前的效果:写一个配置类写配置类后的效果,配置类起作用了:2.4配置Durid监控主要就是配置一个后台管理的Servlet—StarViewServlet和Web监控过滤器—WebStatFilter。具体有哪些初始化参数可以设置,可以参考这两个类以及他们的父类,都是以常量的方式出现的,配置的时候配置他们的值就可以了。写一个controller启动看看效果:启动后在浏览器地址栏输入localhost:8080/query,执行一次请求启动后在浏览器地址栏输入localhost:8080/druid,可以看到Druid后台管理的登录界面输入刚刚设置的用户名个密码,来到后台管理页面,点击SQL监控就可以看到刚才执行的SQL被记录了。其实主打开箱即用的SpringBoot,对于上面Druid监控的配置其实我们还可以直接在SpringBoot的配置文件中配置,下面是关于Druid监控的配置示例:
LoveIT 2019-09-15Spring Boot -
SpringBoot从入门到精通—Thymeleaf模板引擎快速上手
1、引言 在做WEB开发的时候,我们不可避免的要在前端页面之间进行跳转,中间进行数据的查询等等操作。我们在使用SpringBoot之前包括我在内其实大部分都是用的是JSP页面,可以说使用的已经很熟悉。但是我们在使用SpringBoot开发框架以后我们会发现一个致命的问题,就是SpringBoot对Jsp的支持可以说是惨不忍睹,因此官方推荐我们使用Thymeleaf模板引擎来解决问题。目前而言,当然模板引擎有很多,比如Velocity、Freemarker、等等,但是我这一直感觉thymeleaf相对于其他的来说好用的还是不少的。在这里我们就来了解一下thymeleaf这个模板引擎的用法!2、什么是模板引擎? 模板引擎是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档。用于网站的模板引擎(比如Thymeleaf模板引擎)就是将模板文件和数据通过模板引擎生成一个HTML文本。 简单说,通过使用模板引擎,可以将后端的数据填充到前端写好的页面模板中,作用类似JSP。和Thymeleaf类似的模板引擎还有Velocity、FreeMarker,它们完全可以替代JSP。3、SpringBoot中使用Thymeleaf模板引擎SpringBoot中提供了对模板引擎的支持,并且SpringBoot也建议我们使用模板引擎,常用的Thymeleaf使用的方法很简单,首先在pom.xml文件中引入thymeleaf的starter3.1引入Thymeleaf模板引擎的POM依赖 一般SpringBoot引入的都是依赖文件的最新的版本,如果发现不是最新的依赖版本或者你想更改依赖的版本,可以在<properties>标签中修改,比如修改thymeleaf的版本为3.0.10版本3.2使用Thymeleaf 引入Thymeleaf的依赖后,我们就可以使用了。在学习SpringBoot时无论是啥新东西,我们都可以打开XxxxAutoConfigration和XxxxProperties,从中我们可以基本了解SpringBoot对这个东西的默认配置。好了我们来看看Thymeleaf的配置类当然,我们也可以在SpringBoot的主配置文件中进行thymeleaf相关配置:#thymeleaf有关设置spring.thymeleaf.prefix=classpath:/templatesspring.thymeleaf.suffix=.htmlspring.thymeleaf.mode=HTML5spring.thymeleaf.encoding=UTF-8spring.thymeleaf.servlet.content-type=text/html#关闭模板缓存,避免更新无效spring.thymeleaf.cache=false(1)在控制层写一个跳转到hello.html页面的处理器方法(2)在html页面中导入thymeleaf名称空间,名称空间的导入不是必须的,但是导入后可以有提示,提示还是挺好用的。(3).然后在页面只用Thymeleaf语法获取服务器响应的数据测试结果:4、Thymeleaf模板引擎的基本语法4.1Thymeleaf常用属性下面列举一些Thymeleaf常用的属性,全部的属性还请参考Thymeleaf官方手册。Thymeleaf的属性都有一个优先级,优先级从1~8,数字越低优先级越高就会被先执行。(以下用order表示优先级大小)1、th:text:设置当前元素的文本内容,相同功能的还用th:utext,两者的区别是前者会转义特殊字符,后者不会。order=72、th:value:设置当前元素的value值,类似修改指定属性的还有th:src,th:herf,优先级order=63、th:each:遍历循环元素,和th:value或th:value一起使用。order=24、th:if:条件判断,类似的还有th:unless,th:switch,th:case。order=35、th:insert:代码块引入,类似的还有th:replaceth:include,三者的区别较大,若使用不恰当会破坏html结构,常用于公共代码块提取的场景。优先级最高:order=16、th:fragment:定义代码块,方便被th:insert引用。优先级最低:order=87、th:object:声明变量,一般和*{}一起配合使用,达到偷懒的效果。优先级一般:order=48、th:attr:修改任意属性,实际开发中用的较少,因为有丰富的其他th属性帮忙,类似的还有th:attrappend,th:attrprepend。优先级一般:order=5除了这些Thymeleaf定义的一些属性外,在HTML中所有的结点属性都可以使用th:属性名=值的方式来动态设定值,具体使用方法参考下面的例子。4.2Thymeleaf标准表达式语法${.....}变量表达式(VariableExpressions)#{.....}消息表达式(MessageExpressions)@{.....}链接表达式(LinkURLExpressions)~{.....}代码块表达式(FragmentExpressions)*{.....}选择变量表达式(SelectionVariableExpressions)1、${}变量表达式Thymeleaf的变量表达式使用的其实是OGNL表达式,变量表达式的功能非常丰富。(1)可以获取对象的属性、调用方法等(OGNL可以做的事他都可以做)(2)可以使用内置对象。官方手册上给的可以使用的内置对象如下:(3)可以使用一些内置的工具对象(方法)2、*{}选择表达式对于变量我们还可以使用*{}来处理,它和${}在功能上是一样的。但是有一个重要的区别:*{}评估所选对象上的表达式而不是整个上下文。也就是说,只要没有选定的对象,${}和*{}语法就会完全相同。下面是官网给的一个例子,可以说明这个问题:3、#{}消息表达式:主要用于处理资源国际化的。4、@{}链接表达式Thymeleaf中专门用于定义超链接的表达式。配合Thymeleaf中的th:href属性一起使用链接表达式@{}基本用法:链接表达式@{}高级用法:动态拼接URL请求路径:比如要拼接一个如下的动态路径:127.0.0.1/article/index/{id}?articleType=xxx&page=xxx,其中id是项目中每篇文章的id,articleType是文章的类型,page是控制分页的参数,现在要在页面提供这么一个连接来实现分页。实现结果:5、~{}代码块表达式Thymeleaf中专门用于引用片段的表达式。经常配合th:insert和th:replace属性使用,例如:合理的使用代码块表达式可以极大的减少我们项目中的冗余代码,尤其是页面中的冗余代码。我们可以把几个页面中公共使用的代码抽象成一个片段,然后在页面需要的位置引入代码块即可,而且更厉害的是Thymeleaf可以把页面的的head部分或者是JS脚本部分抽取出来统一管理,这样当我们项目发生改变的时候只用改一处就可以了。
LoveIT 2019-09-11Spring Boot -
SpringBoot从入门到精通—自动配置原理(深入源码)
1、引言不论在工作中,亦或是求职面试,SpringBoot已经成为我们必知必会的技能项。除了某些老旧的政府项目或金融项目持有观望态度外,如今的各行各业都在飞速的拥抱这个已经不是很新的Spring启动框架。当然,作为SpringBoot的精髓,自动配置原理的工作过程往往只有在“面试”的时候才能用得上,但是如果在工作中你能够深入的理解SpringBoot的自动配置原理,将无往不利。SpringBoot的出现,得益于“约定优于配置”的理念,没有繁琐的配置、难以集成的内容(大多数流行第三方技术都被集成),这是基于Spring4.x提供的按条件配置Bean的能力。2、SpringBoot自动配置原理2.1配置文件到底能写什么?怎么写?我们一接触SpringBoot的时候就了解到:SpringBoot有一个全局配置文件application.properties或application.yml。我们的各种属性都可以在这个文件中进行配置,最常配置的比如:server.port、logging.level.*、数据库配置、Redis配置等等,然而我们实际用到的往往只是很少的一部分,而且可以配置的属性都可以在官方文档中查找到。2.2自动配置原理?(1)@SpringBootApplication每个SpringBoot应用都有一个启动类,所有的SpringBoot项目必须从启动类启动(一般情况下SpringBoot启动类类名就是项目名+Application),启动类上必须使用@SpringBootApplication注解标注而且主程序类必须位于项目基包的根目录。这个注解是SpringBoot的核心注解,标有这个注解的应用就是基于SpringBoot的应用,我们的自动配置原理和这个注解还有着千丝万缕的联系!我们可以点看进去看看。@SpringBootApplication注解是一个复合注解,这里有三个重要注解:@SpringBootConfiguration:这也是一个复合注解,底层使用的是Spring的基础注解**@Configuration**,它的作用就是让SpringBoot引用支持JavaConfig的配置方式进行配置@EnableAutoConfiguration:开启自动配置,后面着重说明它的原理。@ComponentScan:开启包扫描,也是Spring的基础注解。默认扫描的是当前类下的包。这就是要把启动类放置到基包根目录下的原因,这样启动类就可以在启动的时候把包下的@Controller/@Service/@Component/@Repository等注解类加载到IOC容器中(2)@SpringBootConfiguration注解:@SpringBootConfiguratuon注解的作用是用来告诉SpringBoot这是一个SpringBoot的配置类,从他的源码可以看出它是@Configuration注解的派生注解,对于这个注解我们应该不会陌生,这个类是Spring的核心注解之一。因此可以知道的是这两个注解的功能大致是一样的,只不过一个是专门用于SpringBoot的配置类,另一个则在Spinrg体系中是通用的。(3)@EnableAutoConfiguration注解我们点进去看一下,发现有两个比较重要的注解:@AutoConfigurationPackage:自动配置包@Import:给IOC容器导入组件(3.1)@AutoConfigurationPackage注解这个注解的作用即使自动配置包,查看源码我们可以发现,依靠的还是**@Import**注解,它使用@Import注解导入再点进去查看,我们发现重要的就是以下的代码:在默认的情况下就是将主配置类(@SpringBootApplication)的所在包及其子包里边的组件扫描到Spring容器中。看完这句话,会不会觉得,这不就是@ComponentScan注解的功能吗?这俩不就重复了吗?我开始也有这个疑问,直到我看到文档的这句话:itwillbeusedwhenscanningforcode@Entityclasses.ItisgenerallyrecommendedthatyouplaceEnableAutoConfiguration(ifyou'renotusing@SpringBootApplication)inarootpackagesothatallsub-packagesandclassescanbesearched.比如说,你用了SpringDataJPA,可能会在实体类上写@Entity注解。这个@Entity注解由@AutoConfigurationPackage扫描并加载,而我们平时开发用的@Controller/@Service/@Component/@Repository这些注解是由ComponentScan来扫描并加载的。因此这二者扫描的对象是不一样的。(3.2)@Import注解SpringBoot自动配置功能就是由Import注解后面那个类AutoConfigurationImportSelector实现的(1.5版本以前使用EnableAutoConfigurationImportSelector类,1.5以后这个类废弃了使用的是AutoConfigurationImportSelector类)。这个类会调用SpringFactoriesLoader.loadFactoryNames()方法来扫描定义在MEAT-INF/spring.factories文件中的jar包AutoConfigurationImportSelector实现了ImportSelector接口,这个接口中只有一个方法selectImports(),它的作用是控制哪些类自动的实例加入到容器中,在SpringBoot2.1.7版本中得源码如下:在selectImports()方法的源码中使用了getAutoConfigurationEntry()这个方法,其中configurations存放的数据就是加入容器的自动配置类的完整包路径,继续追踪源码,进入getAutoConfigurationEntry()源码:从getCandidateConfigurations()方法的实现中,我们发现他调用SpringFactoriesLoader.loadFactoryNames反返回了List集合,返回的东西是啥呢?我们直接追踪源码:通过DEBUG可以看到,返回对List中全部是一些类的全限定名。如果继续往下追踪发现他最终在loadFactories方法中使用本类的一个私有静态方法loadSpringFactories加载了META-INF/spring.factories这个配置文件。查看spring-boot-autoconfigure包下META-INF/spring.factories:看到的非常多的xxxxAutoConfiguration类,这些类都是容器中的一个组件,加入到容器中,用他们做自动配置。2.2SpringBoot自动配置原理总结@SpringBootApplication等同于下面三个注解:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan其中@EnableAutoConfiguration是关键(启用自动配置),内部实际上就去加载META-INF/spring.factories文件的信息,然后筛选出以EnableAutoConfiguration为key的数据,加载到IOC容器中,实现自动配置功能!官网参考文档:https://docs.spring.io/spring-boot/docs/2.2.0.BUILD-SNAPSHOT/reference/html/using-spring-boot.html#using-boot-structuring-your-code3、@Conditional派生注解SpringBoot在启动的时候为我们加载了这么多组件,我们不可能全部用得上,那如果用不上的还注册进容器,岂不是耗费资源。其实底层使用了条件装配@Conditional,在我们需要的情况下才会注册对应的组件。比如,SpringBoot中对AOP的自动配置如下所示:AOP的自动配置中使用了非常多的条件注解:@ConditionalOnClass({EnableAspectJAutoProxy.class,Aspect.class,Advice.class,AnnotatedElement.class})表示当类路径上存在EnableAspectJAutoProxy、Aspect、Advice、AnnotatedElement时才会自动配置@ConditionalOnProperty(prefix="spring.aop",name={"auto"},havingValue="true",matchIfMissing=true)表示当配置文件中存在配置项spring.aop.auto=true时会自动配置,不过这里默认值就是true,所以默认会配置@ConditionalOnProperty(prefix="spring.aop",name={"proxy-target-class"},havingValue="true/false",matchIfMissing=true/false)表示当配置文件中存在配置项spring.aop.proxy-target-class并且当它的值为true的时候会使用CGLib代理,否则默认会使用JDK动态代理在SporingBoot源码中下方这些Condition注解比较常见:Conditional扩展注解作用(判断是否满足当前指定条件)@ConditionalOnJava系统的java版本是否符合要求@ConditionalOnBean容器中存在指定Bean;@ConditionalOnMissingBean容器中不存在指定Bean;@ConditionalOnExpression满足SpEL表达式指定@ConditionalOnClass类路径上有指定的类@ConditionalOnMissingClass系统中没有指定的类@ConditionalOnSingleCandidate容器中只有一个指定的Bean,或者这个Bean是首选Bean@ConditionalOnProperty系统中指定的属性是否有指定的值@ConditionalOnResource类路径下是否存在指定资源文件@ConditionalOnWebApplication当前是web环境@ConditionalOnNotWebApplication当前不是web环境@ConditionalOnJndiJNDI存在指定项Q:如何知道哪些自动配置类生效了,哪些配置类没有生效?A:我们可以通过在主配置文件中配置debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效。如下:
LoveIT 2019-09-10Spring Boot -
SpringBoot从入门到精通—配置文件详解
1、SpringBoot配置文件 SpringBoot支持两种形式的配置文件,分别是.properties、和.yml,而且配置文件的名字是固定不可变的:*application.properties*application.yml 配置文件的作用是修改SpringBoot自动配置的默认值。相对于properties文件而言,yml文件更年轻,也有很多的坑。下面我们就来一一学习一下。 注意:当application.properties文件和application.yml同时存在的时候,application.properties会优先加载,application.yml则后加载,而且在applicatin.properties中已经加载的属性如果在application.yml中再次出现会被忽略,如果是application.yml中的独有的属性则会加载。2、YAML/YML文件简介2.1什么是YAML/YML文件 YAML是"YAMLAin'taMarkupLanguage"(YAML不是一种置标语言)的递回缩写。YAML是一个可读性高,用来表达资料序列的编程语言。YAML是一种直观的能够被电脑识别的的数据数据序列化格式,容易被人类阅读,容易和脚本语言交互的,可以被支持YAML库的不同的编程语言程序导入,比如:C/C++,Ruby,Python,Java,Perl,C#,PHP等。YAML以数据为中心,比json、xml等更适合做配置文件。2.2YAML/YML的基本语法基本语法:key:value,key:和value中间要有一个空格,而且key:value的形式可以写无限层。还有一下规则:1.大小写敏感2.使用缩进表示层级关系3.缩进时不允许使用Tab键,只允许使用空格。4.缩进的空格数目不重要,只要相同层级的元素左侧对齐即可5.只有一种注释方式,使用#2.3YAML/YML支持的数据结构YML支持3中类型的数据结构类型1.字面量2.对象(属性和值),Map(键值对)3.数组(List、Set)1、字面量具体包括:字符串、布尔值、整数、浮点数、Null、时间、日期字面量可以直接使用,但是特别注意字符串的字面量在写的时候是不需要引号的(无论单引号还是双引号)。单/双引号在yml语法中有特殊的含义:*“”:双引号;会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思name:“zhangsan\nlisi”:输出;zhangsan换行lisi*”:单引号;不会转义特殊字符,特殊字符最终只是一个普通的字符串数据name:‘zhangsan\nlisi’:输出;zhangsan\nlisi2.对象(属性和值),Map(键值对)基本的语法还是key:value,以一个例子来说明:对应的行内写法:3.数组(List、Set)用-值表示数组中的一个元素行内写法举个栗子:配置一个Person的信息我们可以导入配置文件处理器,编写配置的时候就有提示了在application.yml配置文件中配置preson信息:在SpringBoot测试类中测试IOC是否可以拿到在配置文件中配置的信息测试结果:3、properties文件简介 properties文件大家经常用,这里就简单介绍一下。其语法结构形如:key=value。注意中文乱码问题,需要转码成ASCII。在IDEA中可以设置自动转换把uft-8格式自动转成ASCII,设置方式是:依次点击【File】=>【OtherSettings】=>【settigsfornewprojects】,搜索FileEncodings,然后做如下配置:上面Person信息的properties配置如下:4、配置文件值注入4.1@Value获取值和@ConfigurationProperties获取值比较 SpringBoot通过ConfigurationProperties注解从配置文件中获取属性。从上面的例子可以看出ConfigurationProperties注解可以通过设置prefix指定需要批量导入的数据。支持获取字面值,集合,Map,对象等复杂数据。ConfigurationProperties注解还有其他特么呢?它和Spring的Value注解又有什么区别呢?带着这些问题,我们继续往下看。比较@ConfigurationProperties@Value功能批量注入配置文件中的属性一个个指定松散绑定(松散语法)支持不支持SpEL不支持支持JSR303数据校验支持不支持复杂类型封装支持不支持**何时使用@Value何时使用@ConfigrationProperties?**如果说,我们只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用@Value。如果说,我们专门编写了一个javaBean来和配置文件进行映射,我们就直接使用@ConfigurationProperties。4.1配置文件注入值数据校验使用@Vaildatad可以给配置的属性数据校验功能。可以用在类、方法、参数上。可以加的验证注解如下:下面举个例子:4.2@PropertySource&@ImportResource&@Bean@PropertySource:加载指定的配置文件;person.properties@ImportResource:导入Spring的配置文件,让配置文件里面的内容生效;在src/main/resources下新建一个Spring配置文件applicationContext.xml在SpringBoot的主程序类中使用@ImportResource引入配置这个配置文件然后在测试类中自动注入ApplicationContext来拿这个组件测试结果:虽然这种方式可以用,但是一般我们不这么用,而且SpringBoot也不推荐这么使用,SpringBoot推荐使用全注解的方式来添加配置文件。*1、配置类**@Configuration**------>Spring配置文件2、使用**@Bean**给容器中添加组件接下类,我们在com.xust.iot基包下新建配置类ApplicationConfig.java运行后得到的效果一样:5、配置文件占位符在application.properties和application.yml文件可以使用${random}来设置随机值5.1常用随机设值如下在application.yml文件中写如下配置:在测试类中打印person的信息,结果如下:6、SpringBootprofile Spring支持对不同环境,提供不同配置,可以通过激活、指定参数等方式快速的切换环境。环境profile可以是开发环境(develop)、测试环境(fuy)、生产环境(production)等6.1配置多个profile在主配置文件编写的时候,文件名可以是application-{profile_name}.properties/yml,比如下面分别编写的application-develop.properties和application-production.propertiesapplication-develop.propertiesapplication-production.properties目录结构:在主配置文件application.properties中,使用spring.profiles.active=[profile]来激活不同的环境,如下激活develop环境:启动SpringBoot观察Tomcat启动的端口激活生产环境:spring.profiles.active=production可以看到,配置的不同环境被激活后都起作用了。这在以后的开发中无疑是个十分强大而且方便的功能。6.2YML配置多个profile然而使用properties文件配置不同环境还是太麻烦了,YML对多profile的支持更加简单粗暴,直接通过连续的三个-就可以划分出不同的文件,配置示例:注意:SpringBoot启动的时候会优先加载.properties的文件,然后才来加载.yml文件,如果要想使applicaton.yml中配置的信息可以使用,那么.properties中不能有和.yml相同的配置。6.3激活指定profile的不同方式1、在主配置文件中指定spring.profiles.active=dev,上面的配置方式就是这种。2、命令行:可以在IDEA中配置--spring.profiles.active=develop来启动开发环境,操作如下:也可以在命令行中使用java-jarspring_boot_config-0.0.1-SNAPSHOT.jar--spring.profiles.active=develop来启动开发环境3、虚拟机参数:可以在IDEA中做如下配置:-Dspring.profiles.active=dev,示例如下:7、配置文件的加载位置springboot启动会扫描以下位置的application.properties或者application.yml文件作为Springboot的默认配置文件–file:./config/–file:./–classpath:/config/–classpath:/这些位置的配置文件加载的优先级由高到底,而且高优先级的配置如果在低优先即中出现,那么低优先优先级中重复配置不会生效。但是低优先级中独有的配置还是会生效。也就是说SpringBoot会从这四个位置全部加载主配置文件;并且会互补配置;==我们还可以通过spring.config.location来改变默认的配置文件位置==比如:项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默认加载的这些配置文件共同起作用形成互补配置;首先在硬盘的另一个地方(我这里在E://)创建application.properties。添加如下配置:然后可以在命令行输入java-jarspring_boot_config-0.0.1-SNAPSHOT.jar--spring.config.location=E://application.properties####8、外部配置加载顺序**==SpringBoot也可以从以下位置加载配置,加载顺序的优先级从高到低;高优先级的配置会覆盖低优先级的配置,所有的配置会形成互补配置==**。**1.命令行参数**所有的配置都可以在命令行上进行指定java-jarspring_boot_config-0.0.1-SNAPSHOT.jar--server.port=8087--server.context-path=/abc多个配置用空格分开;`--配置项=值`2.来自java:comp/env的JNDI属性3.Java系统属性(System.getProperties())4.操作系统环境变量5.RandomValuePropertySource配置的random.*属性值==**由jar包外向jar包内进行寻找;**====**优先加载带profile的配置文件**==**6.jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件****7.jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件**==**再来加载不带profile的配置文件**==**8.jar包外部的application.properties或application.yml(不带spring.profile)配置文件****9.jar包内部的application.properties或application.yml(不带spring.profile)配置文件**10.@Configuration注解类上的@PropertySource11.通过SpringApplication.setDefaultProperties指定的默认属性所有支持的配置加载来源[参考官方文档](https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#boot-features-external-config)。
LoveIT 2019-09-09Spring Boot -
SpringBoot与日志
一、日志框架分类目前,日志框架有很多,例如:JUL(java.util.logging)、JCL(ApacheJakartaCommonsLogging)、Log4j、Log4j2、LogBack、SLF4J、jboss-logging等等。日志门面日志实现JCL(ApacheJakartaCommonsLogging)SLF4J(SimpleLoggingFacadeforJava)jboss-loggingLog4jJUL(java.util.logging)Log4j2Logback 日志门面就是日志的抽象层,里面只是定义了日志的规范,日志实现就是来具体实现日志门面的。日志门面中这里重点来介绍一下SLF4J。1、SLF4J slf4j是对所有日志框架制定的一种规范、标准、接口,并不是一个框架的具体的实现。因为接口并不能独立使用,需要和具体的日志框架实现配合使用(如log4j、logback)。那么问题来了,我们有了日志的实现,为什么还需要日志门面(日志抽象层)? 我们都知道使用一个接口实际上使用的是这个接口的实现类,那好了,我只要在程序中使用接口中的API来操作日志,然后导入实现了这个日志接口的日志实现类,程序就可以正常的记录日志;下次我们导入了另一套基于这个接口实现的日志实现类,不用改我们在程序中写的任何日志代码程序还是可以正常的记录日志。原理很简单,日志实现类实现了日志接口的规范。因此这个问题的回答总结为一句话:使用日志框架接口更便于更换为其他的日志框架。 log4j、logback、log4j2都有SLF4J的具体实现,他们既可以单独使用,也可以结合SLF4J框架使用。相关的Maven依赖写法:2、Log4J log4j是apache实现的一个开源日志组件。通过使用log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件、甚至是套接口服务器、NT的事件记录器、UNIXSyslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。然而log4j已经很多年没有更新过了,小项目可以使用,大项目还是算了吧。相关的Maven依赖写法:3、Logback logback同样是由log4j的作者设计完成的,拥有更好的特性,是用来取代log4j的一个日志框架,是slf4j的原生实现,所以logback与slf4j的结合最好。 Logback,一个“可靠、通用、快速而又灵活的Java日志框架”。logback当前分成三个模块:logback-core,logback-classic和logback-access。logback-core是其它两个模块的基础模块。logback-classic是log4j的一个改良版本。此外logback-classic完整实现SLF4JAPI使你可以很方便地更换成其它日志系统如log4j或JDKLogging。logback-access访问模块与Servlet容器集成提供通过Http来访问日志的功能。因此在log4j和logback之间选择的话,我们应该选择更强大的logback。理由如下:1.logback比log4j要快大约10倍,而且消耗更少的内存。2.logback-classic模块直接实现了SLF4J的接口,所以我们迁移到logback几乎是零开销的。3.logback不仅支持xml格式的配置文件,还支持groovy格式的配置文件。相比之下,Groovy风格的配置文件更加直观,简洁。4.logback-classic能够检测到配置文件的更新,并且自动重新加载配置文件。5.logback能够优雅的从I/O异常中恢复,从而我们不用重新启动应用程序来恢复logger。6.logback能够根据配置文件中设置的上限值,自动删除旧的日志文件。7.logback能够自动压缩日志文件。8.logback能够在配置文件中加入条件判断(if-then-else)。可以避免不同的开发环境(dev、test、uat…)的配置文件的重复。9.logback带来更多的filter。10.logback的stacktrace中会包含详细的包信息。11.logback-access和Jetty、Tomcat集成提供了功能强大的HTTP-access日志。相关的Maven依赖写法:4、log4j2引用官网的一句话ApacheLog4j2isanupgradetoLog4jthatprovidessignificantimprovementsoveritspredecessor,Log4j1.x,andprovidesmanyoftheimprovementsavailableinLogbackwhilefixingsomeinherentproblemsinLogback’sarchitecture.翻译过来就是说:ApacheLog4j2是对Log4j的升级,它比其前身Log4j1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些固有问题。Log4j2的特性:1.插件式结构。Log4j2支持插件式结构。我们可以根据自己的需要自行扩展Log4j2.我们可以实现自己的appender、logger、filter。2.配置文件优化。在配置文件中可以引用属性,还可以直接替代或传递到组件。而且支持json格式的配置文件。不像其他的日志框架,它在重新配置的时候不会丢失之前的日志文件。3.Java5的并发性。Log4j2利用Java5中的并发特性支持,尽可能地执行最低层次的加锁。解决了在log4j1.x中存留的死锁的问题。4.异步logger。Log4j2是基于LMAXDisruptor库的。在多线程的场景下,和已有的日志框架相比,异步的logger拥有10倍左右的效率提升。相关的Maven依赖写法:二、日志框架的使用1、SLF4J2、什么时候使用SLF4J比较合适呢? 如果你开发的是类库或者嵌入式组件,那么就应该考虑采用SLF4J,因为不可能影响最终用户选择哪种日志系统。在另一方面,如果是一个简单或者独立的应用,确定只有一种日志系统,那么就没有使用SLF4J的必要。3、如何在系统中使用SLF4J?(官网:https://www.slf4j.org/manual.html)下面是官网给的一个使用实例,以后如果要是用SLF4J,那就不要直接调用实现类的API,而是应该调用SLF4J里面的API,当然只是使用日志实现类另当别论!4、Log4J log4j是Apache的一个开放源代码项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、数据库等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。 Log4j有7种不同的log级别,按照等级从低到高依次为:TRACE、DEBUG、INFO、WARN、ERROR、FATAL、OFF。如果配置为OFF级别,表示关闭log。Log4j支持两种格式的配置文件:properties和xml。包含三个主要的组件:Logger、appender、Layout。一个简单的log4j配置文件在SpringBoot中使用log4j 首先创建一个SpringBoot工程,在创建SpringBoot工程时,我们引入了spring-boot-starter,其中包含了spring-boot-starter-logging,由于SpringBoot默认的日志框架是Logback,所以我们在引入log4j之前,需要先排除该包的依赖,再引入log4j的依赖。配置log4j-spring.properties 配置文件的编写方法上面介绍的没有啥区别,但是配置文件的名字可以是log4j-spring.properties也可以是log4j.properties,但是SpringBoot官方推荐的命名方式是第一种方式。配置文件的内容参考上面。在SpringBoot主配置文件中指定日志文件的配置5、Logbacklogblack的加载顺序SLF4J+Logback是SpringBoot默认的日志策略,logback支持xml和groovy形式的配置文件,而且还支持编程式地配置,它加载配置文件的顺序:在classpath中寻找logback-test.xml文件如果找不到logback-test.xml,则在classpath中寻找logback.groovy文件如果找不到logback.groovy,则在classpath中寻找logback.xml文件如果上述的文件都找不到,则logback会使用JDK的SPI机制查找META-INF/services/ch.qos.logback.classic.spi.Configurator中的logback配置实现类,这个实现类必须实现Configuration接口,使用它的实现来进行配置如果上述操作都不成功,logback就会使用它自带的BasicConfigurator来配置,并将日志输出到console在SpringBoot中使用LogBack(官网:https://logback.qos.ch/documentation.html)前面说过,logback是SpringBoot默认的日志系统,假如对日志没有特殊要求,可以完全零配置使用SLF4J(SimpleLoggingFacadeForJava)的logback来输出日志。 上面这段程序,不经过任何配置,默认的日志系统是完全可以正常运行的。但是SpringBoot也支持我们修改默认配置,修改的方式有两种,一种是在src/main/resources下新建logback-spring.xml文件(logback.xml也可以,但官方推荐前面的写法);第二种是直接在SpringBoot主配置文件中配置,下面是简单的配置示例:logbac-spring.xmlapplication.properties6、扩展使用springProperty 在logback-spring.xml中可以使用SpringBoot扩展的,使用它可以把在application.properties中定义的属性值映射为logback环境中的一个属性,从而可以在logback的上下文使用。举个例子: 下面的配置中定义了属性appName对应于SpringBoot的Environment中的app.name(由source属性指定),当未指定时默认使用defaultValue属性指定的TEST;属性name对应于SpringBoot的Environment中的logging.path,未指定时使用/logs/${appName}.log,其中的${appName}又对应于变量appName的值。定义好的变量可以在logback的配置文件中以${varName}的形式进行引用。三、多个不同的日志框架共用SLF4J 经常在实际的开发环境中,我们会使用到不同的框架,而且不同的框架默认的日志系统不同,那么如何在SpringBoot中设置一下,让这些不同框架的默认日志系统可以借助于SpringBoot的默认日志系统来工作?这是可以办到的,具体的步骤如下:1、在pom.xml文件中导入依赖的时候排除所使用框架对默认日志系统的依赖2、用SLF4J提供的中间包替换原有日志框架下图有详细的配置方法:
LoveIT 2019-09-07Spring Boot -
SpringBoot从入门到精通—SpringBoot快速入门
1、入门环境准备在本地安装3.3版本以上的Maven,以及JDK1.7以上的java环境,然后在IDEA【settings】=>【File|Settings|Build,Execution,Deployment】=>【Maven】,设置如下内容:2、使用Maven构建SpringBoot工程 第一个程序我们先创建一个Maven工程。创建好Maven项目后,首先导入SpringBoot的依赖包,他的依赖可以在SpringBoot的官网找到。注意: 1.spring-boot-starter-parent的父项目是spring-boot-dependencies(SpringBoot的版本仲裁中心),他的作用是用来管理SpringBoot应用里面的所有依赖版本 2.spring-boot-starter-web:spring-boot-starter我们称作spring-boot场景启动器,SpringBoot将所有的功能场景都抽取出来,做成一个个的starters(启动器),只需要在项目里面引入这些starter,相关场景的所有依赖都会导入进来(要用什么功能就导入什么场景的启动器)。spring-boot-starter-web的作用是帮我们导入了web模块正常运行所依赖的组件;完成上面的操作后,在src/main/java下新建一个主程序类Application.java类注意! 1、主程序类必须位于项目基包的根目录(具体原因下面会说),而且要使用@SpringBootApplication来告诉SpringBoot这是一个主程序类(配置类),这么标注后SpringBoot就会从这个类的main方法启动并加载配置。 2、@SpringBootApplication是基于SpringBoot基本注解的一个复合注解,源码片段如下: 2.1、@SpringBootConfiguration:标注在某个类上,表示这是一个SpringBoot的配置类;底层实际还是使用的是Spring的底层注解@Configuration 2.2、@ComponnetScan:标注在类上,就是我们熟悉的包扫描 2.3、@EnableAutoConfiguration:开启自动配置功能;以前我们需要配置的文件,在类上写上这个注解,SpringBoot帮我们自动配置。自动配置是SpringBoot的一大特性,关于它的原理可以参看我的另一篇文章SpringBoot从入门到精通—自动配置原理(深入源码)然后在src/main/java包下新建controller包,在这个包下新建HelloWordController.java这样一个简单的SpringBoot程序就写好了,在主程序类中点击运行按钮启动它。下面是运行后的结果:关于运行结果的说明:启动后我们访问http://localhost:8080,回车后就会出现WhitelableErrorPage提示,这是SpringBoot默认的错误页面,由于我们没有指定初始页面,出错很正常,之后在地址栏中请求hello,就可以看到我们写的Helloword,helloSpringBoot!1.2SpringBoot使用可执行jar文件部署应用 基于SpringBoot开发项目,不仅开发起来十分容易,而且由于他内置了Tomcat、Jetty等应用服务器,因此使得我们的部署也不再需要外部的应用服务器了,我们可以把SpringBoot使用Maven打包成jar文件后直接执行起来就可以了。 把SpringBoot打包成jar文件需要spring-boot-maven-plugin这个插件(在上面有依赖文件中已经导入)。在POM文件中导入后,我们在IDEA有侧边栏点击【Maven】=>【项目名】=>【Lifecycle】=>【package】,双击package就会运行插件就可以我们的应用打包成一个可运行的jar包(或者在IDEA自带的控制台中在当前项目的根目录下使用命令mvncleanpackage)。打包后我们在target目录下可以找到他,然后复制在里一个目录中在命令行中cd到这个目录,执行java-jarjar包名这个命令可以执行打包后的应用。下面是执行后命令行的打印:2、使用SpringInitializer快速创建SpringBoot项目【推荐】除了使用Maven构建一个SpringBoot项目之外,我们还可以使用Spring官方提供的SpringInitializer来快速构建一个SpringBoot项目。在IDEA依次点击【New】=>【Project】然后选择【SpringInitializr】使用SpringInitializr构建的SpringBoot项目中主程序类已经生成好了,我们只需要我们实现业务逻辑resources文件夹中目录结构static:保存所有的静态资源,如:jscssimages;templates:保存所有的模板页面,SpringBoot默认jar包使用嵌入式的Tomcat,默认不支持JSP页面,可以使用模板引擎(freemarker、thymeleaf);application.properties:SpringBoot的配置文件;可以修改一些默认设置。这个文件也支持YAML格式的文件,用YAML格式配置的文件结构更加简洁,清晰。
LoveIT 2019-09-07Spring Boot -
SpringMVC文件下载和上传
1、文件下载文件下载的最重要的一点是设置响应头的Content-disposition为attachmen;filename=要下载的文件的名字,然后得到文件的输入流写入本地即可1.常规方法2.使用SpringMVC提供的ResponseEntity<T>类型,使用它可以很方便地定义返回的HttpHeaders和HttpStatus。下载页面测试结果2、文件上传文件上传这里使用的是commons-fileupload-1.4,他需要依赖commons-io,他们的Maven依赖如下:(1)单文件上传上传表单页面文件上传最重要的一点就是将表单的提交方法设置为post,并且将enctype的值设置为"multipart/form-data"。控制器在Controller的处理方法中,使用MultipartFile对象作为参数接收前端上传过来的文件在springmvc配置文件中注册文件上传组件使用MultipartFile对象接收前端上传过来的文件,还需要在springmvc的配置文件中进行如下配置:(2)多文件上传其实多文件上传也很简单,单文件上传是在Controller的处理方法中使用MultipartFile对象作为参数接收前端上传过来的文件,而多文件上传则使用MultipartFile对象数组来接收。页面该页面中有几个name值一样的file类型的input标签,其他跟单文件上传的页面没区别。控制器同样的,使用MultipartFile数组接收前端上传过来的多个文件,也需要在springmvc的配置文件进行配置,具体配置与上述单文件上传的springmvc.xml配置没差别。这样,就可以进行多文件上传了。多种文件上传情景综合当然,项目开发中,场景可能并不是这么简单,上述的多文件上传是一个个文件选择后一起上传(即多个name相同的input标签),那要是我项目中只要一个input标签就可以一次性多个文件呢?又或者一个页面中既要一个个选择的多文件上传,又要一次性选择的多文件上传,还要有单文件上传呢?没问题,MultipartFile[]通吃,代码也很easy,下面直接上代码。页面控制器测试结果MultipartFile[]就是如此强大,不管单个多个,逻辑处理一样,所以建议在项目开发中使用MultipartFile[]作为文件的接收参数。3、重要方法和参数1、MutipartFile类的一些常用方法:StringgetContentType()//获取文件MIME类型InputStreamgetInputStream()//获取文件流StringgetName()//获取表单中文件组件的名字StringgetOriginalFilename()//获取上传文件的原名longgetSize()//获取文件的字节大小,单位bytebooleanisEmpty()//是否为空voidtransferTo(Filedest)//保存文件到服务器指定路径2、CommonsMultipartResolver的属性解析defaultEncoding:表示用来解析request请求的默认编码格式,当没有指定的时候根据Servlet规范会使用默认值ISO-8859-1。当request自己指明了它的编码格式的时候就会忽略这里指定的defaultEncoding。uploadTempDir:设置上传文件时的临时目录,默认是Servlet容器的临时目录。maxUploadSize:设置允许上传的总的最大文件大小,以字节为单位计算。当设为-1时表示无限制,默认是-1。maxUploadSizePerFile:跟maxUploadSize差不多,不过maxUploadSizePerFile是限制每个上传文件的大小,而maxUploadSize是限制总的上传文件大小。maxInMemorySize:设置在文件上传时允许写到内存中的最大值,以字节为单位计算,默认是10240。resolveLazily:为true时,启用推迟文件解析,以便在UploadAction中捕获文件大小异常。
LoveIT 2019-08-12Spring MVC -
SpringMVC拦截器(Interceptor)详解
1、拦截器概述1.1什么是拦截器? SpringMVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),但是比过滤器的功能更加强大,它主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。要使用SpringMVC中的拦截器,就需要对拦截器类进行定义和配置。通常拦截器类可以通过两种方式来定义:1.通过实现HandlerInterceptor接口,或继承HandlerInterceptor接口的实现类(如HandlerInterceptorAdapter)来定义。2.通过实现WebRequestInterceptor接口,或继承WebRequestInterceptor接口的实现类来定义。2、自定义拦截器以实现HandlerInterceptor接口方式为例,自定义拦截器类的代码如下:上述代码中,自定义拦截器实现了HandlerInterceptor接口,并实现了接口中的三个方法:preHandle():在目标方法执行之前回执行,有个boolean类型的返回值,当返回true表示放行,即允许执行目标方法;当返回false,表示不放行,即不运行执行目标方法,此时会中断以后的所有过程postHandle():在目标方法执行结束后会执行,且解析视图之前执行afterCompletion():在请求到达页面,即视图渲染完成后执行开发拦截器就像开发servlet或者filter一样,都需要在配置文件进行配置,配置代码如下:上面的代码中,<mvc:interceptors>元素用于配置一组拦截器,子元素<bean>中定义的是全局拦截器,它会拦截所有的请求;而也可以使用<mvc:interceptor>元素中定义指定路径的拦截器,它会对指定路径下的请求生效。<mvc:interceptor>元素的子元素<mvc:mapping>用于配置拦截器作用的路径,该路径在其属性path中定义。如果在请求路径中包含不需要拦截的内容,还可以通过<mvc:exclude-mapping>元素进行配置。注意:<mvc:interceptor>中的子元素必须按照上述代码中的配置顺序进行编写,即<mvc:mapping><mvc:exclude-mapping><bean>,否则文件会报错。下面写一个控制器来测试一下正常情况下单个拦截器的工作流程:目标页面success.jsp测试结果2.1单个拦截器正常情况下的工作流程:拦截器preHandle方法执行,返回true继续以后的过程控制器目标方法执行拦截器postHandle方法执行页面渲染完成来到页面拦截器afterCompletion方法执行单个拦截器非正常情况下的工作流程:单个拦截器的非正常情况分为两种情况: 1、拦截器中的preHandler方法返回false; 2、虽然preHandler方法返回了true,但是其中有一个过程“炸了”,比如发生了异常没有处理接下来通过代码来测试:-第一种情况:preHandler返回false测试结果:可以看到,后面的过程直接无法执行第二种情况:preHandle方法放行了,但是有一个过程“炸了”,比如我们在控制器目标方法中制造一个异常:测试结果:可以看到,只要拦截器的prehandle方法放行了,拦截器的afterCompletion方法总会执行/font>#####2.2多个拦截器正常情况下的工作流程:实现第二个自定义拦截器:```javapackagecom.xust.iot.interceptor;importorg.springframework.web.servlet.HandlerInterceptor;importorg.springframework.web.servlet.ModelAndView;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;publicclassMySecondInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{System.out.println("1.执行目标方法之前......MySecondInterceptor");returntrue;}@OverridepublicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{System.out.println("3.执行目标方法之后......MySecondInterceptor");}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{System.out.println("5.来到页面之后......MySecondInterceptor");}}```配置第二个拦截器```xml可以具体配置拦截器拦截那些请求>-->```执行结果:交换拦截器在springmvc配置文件中的定义顺序:```xml可以具体配置拦截器拦截那些请求>-->```执行结果:测试结果表明:多个拦截器是有执行的先后顺序的,这个顺序就是定义的先后顺序拦截器的preHandle方法:按照定义顺序顺序执行的拦截器的postHandle方法:按照定义顺序逆序执行的拦截器的preHandle方法:按照定义顺序顺逆序执行的***多个拦截器非正常情况下的工作流程**:1、一个拦截器的preHandle方法返回false的情况:2、所有拦截器的preHandle方法都返回true,但是中间有过程发生了异常:####3、小结#####3.1单拦截器的执行顺序*正常情况下会按照:preHandle--->目标方法--->postHnadle--->页面渲染--->afterCompetion执行*当preHandle返回false,就没有以后流程的事儿了当preHandler返回了true,但是中间过程发生异常,会直接结束以后的流程但是afterCompetion总会执行3.2多个拦截器的执行顺序正常情况会按照配置中定义的顺序顺序执行所有拦截器的preHandle方法,然后执行控制器中目标方法,之后按照定义顺序的逆序执行postHandle方法,然后渲染页面,最后按照定义的顺序的逆序执行afterComprtion方法有拦截器返回false在多个拦截器中只要有一个拦截器的preHandle方法返回了false,那么以后的流程都没有了,会直接回按照这些拦截器配置的定义顺序的逆序执行afterCompetion方法
LoveIT 2019-08-11Spring MVC -
SpringMVC对Ajax异步请求的支持
1、Ajax异步请求概念1.1AJAX:AnsycJavascriptAndXml(异步请求) 异步是指基于Ajax的应用与服务器通信的方法。对于传统的Web应用,每次用户发送请求或向服务器请求获得新数据时,浏览器都会完全丢弃当前页面,而等待重新加载的页面。在服务器完全响应之前,用户浏览器将是一片空白,用户的动作必须中断。异步是指用户发送请求后,完全无须等待,请求在后台发送,不会阻塞用户的当前活动,用户无须等待第一次请求得到完全响应,就可以立即发送第二次请求。简单的说,异步请求不会刷新当前html页面。异步指的是服务器端响应数据的获取方式。同步:异步:#####1.2异步&同步的区别1.同步请求:>请求的过程:浏览器(当前的html页面会丢弃)--->http协议--->Web服务器(tomcat)响应的过程:Web服务器(tomcat)--->http协议-->返回一个新html页面.2.异步请求:>请求的过程:浏览器(当前的html页面不会丢弃)--->Ajax引擎(http协议)--->Web服务器(tomcat)响应的过程:Web服务器(tomcat)--->准备部分数据-->Ajax引擎(http协议)-->DOM编程.总而言之,异步请求只是局部刷新页面,同步请求会全部刷新当前的页面####2、jQuery框架的异步请求和处理1.$.ajax([settings])—jQuery核心处理异步请求的方法:语法:>$.ajax([settings])最简单的情况下,$.ajax()可以不带任何参数直接使用。具体语法格式都有哪些参数的参照:https://www.w3school.com.cn/jquery/ajax_ajax.asp2.$.post()$.post()方法通过HTTPPOST请求从服务器上请求数据。语法:>$.post(url,data,function(data,status){>//status(状态码):success、error>//data:响应数据>},dataType);必需的URL参数规定您希望请求的URL。可选的data参数规定连同请求发送的数据。可选的function参数是请求成功后所执行的函数名,其中data是响应的数据,status是状态码可选的dataType参数是服务器响应返回的数据3.$.get()$.get()方法通过HTTPGET请求从服务器上请求数据。>$.get(url,data,function(data,status){>//status(状态码):success、error>//data:响应数据>},dataType);必需的URL参数规定您希望请求的URL。可选的data参数规定连同请求发送的数据。可选的function参数是请求成功后所执行的函数名,其中data是响应的数据,status是状态码可选的dataType参数是服务器响应返回的数据####3、SpringMVC支持ajax异步请求和处理返回json数据#####3.1数据绑定@RequestBody/@ResponseBody***@RequestBody** 功能:用于将HttpServletRequest的getInputStream()的内容绑定到方法入参例如:>@RequestMapping(value="/hello")>publicStringhandleRequest(@RequestBodyStringbody){>//body参数就被请求参数自动绑定>}***@ResponseBody** 功能:被ResponseBody修饰的方法的返回值会被作为响应体>@RequestMapping(value="/hello")>@ResponseBody>publicUserhandleRequest(Ueseruser){>>returnUser;//返回值会被作为响应体,而且如果返回值是对象时SpringMVC会自动转换成JSON给页面>}#####3.2使用@RequestBody/@ResponseBody来支持Ajax可以使用@RequestBody来自动获取Ajax上传的数据,同时也可以使用@ResponseBody,把要返回的对象自动拼成JSON的格式返回。当然,需要加入几个jackson的包,这里加入了:jackson-core-2.9.3.jar、jackson-annotations-2.9.3.jar、jackson-databind-2.9.3.jar,Maven依赖如下:```xmlcom.fasterxml.jackson.corejackson-databind2.9.3com.fasterxml.jackson.corejackson-core2.9.3com.fasterxml.jackson.corejackson-annotations2.9.3```Controller```java/***ResponseBody:用于将ResponseBody方法的返回值作为响应体*RequestBody:用于将HttpServletRequest的getInputStream()内容绑定到入参**@paramusers*@return*/@ResponseBody@RequestMapping(value="/getAllUserByAJAX",produces="application/json;charset=UTF-8")publicListgetAllUserByAJAX(@ModelAttribute("users")Listusers){returnusers;}/***RequestBody:将请求体的数据绑定到入参**@paramuser*@return*/@ResponseBody@RequestMapping("/testRequestBody")publicUsertestRequestBody(@RequestBodyUseruser,Modelmodel){System.out.println("请求的数据:"+user);model.addAttribute("requestInfo",user);returnuser;}/***提前把全部信息查询好放在隐含模型中*@parammodel*/@ModelAttribute("users")publicvoidgetAll(Modelmodel){IUserServiceuserService=newIUserServiceImpl();Listlists=userService.getUser(null,null,null);model.addAttribute("users",lists);}```1.通过AJAX获得服务器数据的页面:```jspajax获取全部用户信息```测试结果2.通过AJAX向服务器发JSON数据,服务器返回JSON数据```jspTitleAJAX发送JSON数据给服务器```测试结果
LoveIT 2019-08-10Spring MVC -
SpringMVC异常处理
1、重要的接口和类1.1HandlerExceptionResolver 他是SpringMVC“九大组件”之一,SpringMVC异常处理核心接口。该接口定义了1个解析异常的方法:1.2ExceptionHandlerExceptionResolver 继承自AbstractHandlerMethodExceptionResolver,该类主要处理Controller中用@ExceptionHandler注解定义的方法。该类是<annotation-driven/>配置中定义的HandlerExceptionResolver实现类之一,大多数异常处理都是由该类操作。 在Controller中使用@ExceptionHandler处理异常异常后的页面:测试结果: 像这样在Controller中借助@ExceptionHandler这个注解定义的异常处理方法只能在定义的那个Controller中使用,当Controller类十分多的时候,那么写异常处理方法就是个体力活了,因此SpringMVC就提供了@ControllerAdvice这个注解,它只能用在类上,而这个类中的异常处理方法都是全局范围,如下定义一个类专门集中处理异常:1.3DefaultHandlerExceptionResovler HandlerExceptionResolver接口的默认实现之一 ,基本上是SpringMVC内部使用,用来处理Spring定义的各种标准异常,将其转化为相对应的HTTPStatusCode。其处理的异常类型有:1.4ResponseStatusExceptionResovler 用来支持@ResponseStatus的使用,处理使用了ResponseStatus注解的异常,根据注解的内容,返回相应的HTTPStatusCode和异常页面给客户端。如果Web应用程序中配置了ResponseStatusExceptionResolver,那么我们就可以使用ResponseStatus注解来注解我们自己编写的异常类,并在Controller中抛出该异常类,之后ResponseStatusExceptionResolver就会自动帮我们处理剩下的工作。<annotation-driven/>配置中定义的HandlerExceptionResolver实现类之一。 自定义一个异常另一个自定义异常在Controller中主动抛出这个异常看看效果:测试结果:ExceptionHandlerExceptionResolver、DefaultHandlerExceptionResolver和ResponseStatusExceptionResolver这三个类是<mvc:annotation-driver>配置后默认的3个实现类,他们的优先级是按书写的顺序由高到底。1.5SimpleMappingExceptionResovler提供了将异常映射为视图的能力,高度可定制化。其提供的能力有:1.根据异常的类型,将异常映射到视图;2.可以为不符合处理条件没有被处理的异常,指定一个默认的错误返回;3.处理异常时,记录log信息;4.指定需要添加到Modle中的Exception属性,从而在视图中展示该属性。
LoveIT 2019-08-09Spring MVC -
SpringMVC对资源国际化的支持
1、资源国际化开发1.1什么是资源国际化? 软件的国际化:软件开发时,要使它能同时应对世界不同地区和国家的访问,并针对不同地区和国家的访问,提供相应的、符合来访者阅读习惯的页面或数据。国际化(internationalization)又称为i18n(读法为i18n,据说是因为internationalization(国际化)这个单词从i到n之间有18个英文字母,i18n的名字由此而来)1.2国际化的基本规则 国际化信息”也称为“本地化信息”,一般需要两个条件才可以确定一个特定类型的本地化信息,它们分别是“语言类型”和“国家/地区的类型”。如中文本地化信息既有中国大陆地区的中文,又有中国台湾、中国香港地区的中文,还有新加坡地区的中文。Java通过java.util.Locale类表示一个本地化对象,它允许通过语言参数和国家/地区参数创建一个确定的本地化对象。 语言参数使用ISO标准语言代码表示,这些代码是由ISO-639标准定义的,每一种语言由两个小写字母表示。在许多网站上都可以找到这些代码的完整列表,下面的网址是提供了标准语言代码的信息:http://www.loc.gov/standards/iso639-2/php/English_list.php。 国家/地区参数也由标准的ISO国家/地区代码表示,这些代码是由ISO-3166标准定义的,每个国家/地区由两个大写字母表示。用户可以从以下网址查看ISO-3166的标准代码:http://www.iso.ch/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html。下面是常用的国家/地区语言参数:2、资源国际化简单示例编写国际化资源文件:国际化资源文件就是用一种key-value的形式把要显示的信息的不同语言的翻译版本写到properties资源文件中。(1)英文的资源文件(2)简体中文的资源文件注意:一般情况下我们用*._zh_CN.properties表示中文资源文件,*.properties表示默认的资源文件。在springmvc配置文件中简单的配置一下,把我们的资源文件交给SpringMVC管理编写一个用户登录表单上面的代码中用到了JSTL中的fmt标签<fmt:message>,因此需要导入对应的jar包。测试结果我的浏览器默认就是中文,因此显示的就是中文的欢迎信息:在火狐浏览器中手动更改语言为英文英文环境下就显示的是英文信息:3、资源文件的编码问题 一般我们采用properties文件来保存资源文件。properties文件是以key-value的形式来保存文件的。login_zh_CN.properties中保存的是经过utf-8编码字后的ASCII字符,Unicode字符中不允许出现中文、日文等其他字符的文字。但是Unicode编码后的文字阅读起来比较困难,在IDEA中,可以在File->Settings->Editor->FileEncodings设置中勾选Transparentnative-to-asciiconversion,如下图,设置好后点击Apply,然后回到刚才编写的中文资源文件发现中文字符全部乱码了,这时可以在编辑器中直接重新输入中文。虽然我们输入的是中文,但是IDEA已经帮我们做了中文转码。4、在程序中获取国际化信息在程序中我们可以通过ResourceBundleMessageSource来获取资源文件的信息:执行结果:可以看到在不同对语言环境下使用了不同的资源文件。5、自定义区域信息解析器3个步骤:1、写一个类实现LocaleResolver接口或他的子接口或继承他的实现类,最主要是要实现它的resolveLocale方法2、在springmvc的配置文件中注册自定义的区域信息解析器3、启动测试(1)写一个类实现LocalResolver接口(2.1)在springmvc的配置文件中注册自定义的区域信息解析器注意:解析器的id必须是localeResolver,如果写错了就没有效果了。至于为啥非要这么写请参考SpringMVC源码中的DispatcherServlet这个类。(2.2)在页面中加入可以切换语言的链接(3)测试6、SessionLocaleResolver从SpringMVC的区域信息的继承图中我么看到了几个特别的区域信息解析器:1.AcceptHeaderLocaleResolver:它是SpringMVC默认装配的区域信息解析器,他会默认从accept-language请求头信息进行解析处理,通常这个头信息包含客户端操作信息的本地标示。它不支持通过链接的方式改变locale信息。2.FixedLocaleResolver:从字面意思就可以知道,这也是一个不支持通过链接的方式改变locale信息的一个解析器,它默认会从操作系统拿locale信息。3.SessionocaleResolver:从session中拿locale信息,允许设置区域信息。4.CookieLocaleResolver:从Cookie中拿locale信息,允许设置区域信息。下面我们借助SessionLocaleResolver来实现我们上面自定义区域信息解析器的功能:测试结果:7、使用LocaleChangeInterceptor通过配置LocaleChangeInterceptor,我们可以动态改变本地语言。它会检测请求中的参数并且改变地区信息。它调用LoacalResolver.setLocal()进行配置。既然是拦截器,要使用他就需要在springmvc配置文件中配置它,配置也很简单:然后在配合SessionLocaleResovler,实现动态的改变本地信息控制器层的代码瞬间变得极其简单,就一个跳转页面对返回语句测试结果:
LoveIT 2019-08-08Spring MVC -
SpringMVC 数据绑定&数据格式化&数据校验
1、数据绑定流程SpringMVC将ServletRequest对象及目标方法的入参实例传给WebDataBinderFactory实例,创建出DataBinder(数据绑定的核心部件)DataBinder调用转配在SpringMVC上下文中的ConversionService组件进行数据类型转换、数据格式化。并将servlet中的请求信息填充到入参对象中调用Validator组件对已经绑定好的请求消息的入参进行数据合法性校验,并最终生成数据绑定结果BindingResult对象SpringMVC抽取BindingResult中的入参对象和检验错误对象,将他们赋给处理方法的响应入参SpringMVC通过反射机制对目标方法进行解析,将请求消息绑定到处理方法的入参中。2、数据转换2.1ConversionServiceSpringMVC上下文中内建了很多转换器,可完成大多数Java类型的转换工作。Spring3.0添加了一个通用的类型转换模块,位于org.springframework.core.convert包中ConversionService接口是类型转换的核心接口ModifierandTypeMethodandDescriptionbooleancanConvert(ClasssourceType,ClasstargetType)判断是否可以将一个java类转换为另一个java类booleancanConvert(TypeDescriptorsourceType,TypeDescriptortargetType)需转换的类将以成员变量的方式出现在宿主类中,TypeDescriptor不但描述了需转换类的信息,还描述了从宿主类的上下文信息,如成员变量上的注解,成员是否是数组、集合或Map的方式呈现等Tconvert(Objectsource,ClasstargetType)将原类型对象转换为目标类型对象.Objectconvert(Objectsource,TypeDescriptorsourceType,TypeDescriptortargetType)将对象从原类型对象转换为目标类型对象,此时往往会用到所在宿主类的上下文信息2.2自定义类型转换器 Spring在org.springframework.core.convert.converter包中定义了3种类型转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到ConversionServiceFactroyBean中:-Converter<S,T>:将S类型对象转换为T类型对象-ConverterFactory:将相同系列多个Converter封装在一起.如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将String转换为Number及Number子类(Integer、Long、Double等)对象)可使用该转换器工厂类-GenericConverter:会根据源类对象及目标类对象所在的宿主类找那个的上下文信息进行类型转换 ConverstionServiceFactoryBean的converters属性可以接受Converter、ConverterFactory、GenericConverter或ConditionalGenericConverter接口的实现类,并把这些转换器的转换逻辑统一封装到一个ConverstionService实例对象中(GenericConversionService),Spring在Bean属性配置及SpringMVC请求消息绑定时将利用这个ConversionService实例完成类型转换工作。实际应用中常用的是Converter<S,T>,下面通过他实现一个自定义的类型转换器。关键步骤:1.实现Converter接口,他有两个泛型,S:是转换前的了类型,T:是转换后的类型,实现Converter接口的conver方法,在方法中定制对S类型如何转换换成T类型的规则2.在springmvc配置文件中将自定义的Converter配置在ConversionService中3.告诉SpringMVC使用我们自定义的类型转换器假设处理方法有一个User类型的入参,我们希望将一个格式化的请求字符串直接转为User对象,该字符串格式如(小明:男:软件工程:软工3306班:1134556)-编写自定义类型转换器在SpringMVC配置文件中将自定义的Converter方在IOC容器中交给Spring管理用<mvc:annotation-drivenconversion-service=”xxx”/>覆盖默认的类型转换器控制器:测试结果3、数据格式化 Spring使用转换器进行源类型对象到目标类型对象的转换,Spring的转换不提供输入及输出信息格式化工作,像日期、时间、数字、货币等数据都具有一定格式的,在不同的本地化环境中,同一类型的数还会相应地呈现不同的显示格式。如何从格式化的数据中获取真正的数据以完成数据绑定,并将处理完成的数据输出为格式化的数据,是spring格式化框架要解决的问题,Spring引入了一个新的格式化框架,这个框架位于org.springframework.format类包中,其中最重要的一个接口FormatterSpring的org.springframework.format.datetime包中提供了一个用于时间对象格式化的DateFormatter实现类,而org.springframework.format.number包中提供了3个用于数字对象格式化的实现类。-NumberFormatter:用于数字类型对象的格式化-CurrencyFormatter:用于货币类型对象的格式化-PercentFormatter:用于百分数数字类型对象的格式化示例:有一个员工类employee.java要使注解可以发挥作用还需要在注解中配置如下信息: 对属性对象的输入/输出进行格式化,从其本质上讲依然属于“类型转换”的范畴。Spring在格式化模块中定义了一个实现ConversionService接口的FormattingConversionService实现类,该实现类扩展了GenericConversionService,因此它既具有类型转换的功能,又具有格式化的功能。 FormattingConversionService拥有FormattingConversionServiceFactroyBean工厂类,后者用于在Spring上下文中构造前者FormattingConversionServiceFactroyBean内部已经注册了:NumberFormatAnnotationFormatterFactroy:支持对数字类型的属性使用@NumberFormat注解JodaDateTimeFormatAnnotationFormatterFactroy:支持对日期类型的属性使用@DateTimeFormat注解装配了FormattingConversionServiceFactroyBean后,就可以在SpringMVC入参绑定及模型数据输出时使用注解驱动了。<mvc:annotation-driven/>默认创建的ConversionService实例即为FormattingConversionServiceFactroyBean.4、数据校验 应用程序在执行业务逻辑前,必须通过数据校验保证接收到的输入数据是正确合法的,如代表生日的日期应该是一个过去的时间、工资的数值必须是一个整数等。一般情况下,应用程序的开发时分层的,不同层的代码由不同的开发人员负责。很多时候,同样的数据验证会出现在不同的层中,这样就会导致代码冗余,违反了DRY原则。为了避免这样的情况,最好将验证逻辑和响应的域模型进行绑定,将代码验证的逻辑集中起来管理。4.1JSR-303规范 JSR-303规范是Java为Bean数据合法校验锁提供的标准框架,它已经包含在JavaEE6.0中。JSR-303通过在Bean属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。可以通过http://jcp.org/en/jsr/detail?id=303了解更多详细内容。JSR-303定义了一套可标注在成员变量、属性方法上的校验注解:JSR-303支持XML风格的和注解风格的验证,接下来我们首先看一下如何和Spring集成。1.导入jar包,此处使用Hibernate-validator实现(版本:hibernate-validator-6.0.17.Final-dist.zip),他的Maven依赖如下:在Spring配置中添加JSR-303验证框架支持通过ConfigurableWebBindingInitializer注册validator使用JSR-303验证框架注解为模型对象指定验证信息控制器通过在命令对象上注解@Valid来告诉SpringMVC此命令对象在绑定完毕后需要进行JSR-303验证,如果验证失败会将错误信息添加到Errors错误对象中。5.验证失败后回到填写表单的页面(/WEB-INF/jsp/pages/add.jsp)测试结果:4.2自定义国际化错误消息提示在上面的程序中有一个不好的地方,错误消息不是我们自定义的,而且都是英文的,下面我们来看看如何在通过国际化配置文件实现自定义国际化错误消息提示。使用System.out.println("错误码:"+fieldError.getCodes());可以得到错误的错误码,每种错误都定义了4中错误码,如下:他们从上到下所包含的范围由小到大,我们在写国际化配置文件的时候,每条配置的key必须是4个code中的一个code。`error_en_US.properties`error_zh_CN.properties在springmvc.xml文件中配置国际化资源:测试结果:
LoveIT 2019-08-07Spring MVC -
Spring MVC视图解析
对于控制器的目标方法,无论其返回值是String、View、ModelMap或是ModelAndView,SpringMVC都会在内部将它们封装为一个ModelAndView对象进行返回。 SpringMVC借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是JSP也可是Excell、JFreeChart等各种表现形式的视图。1、视图(View)视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。为了实现视图模型和具体实现技术的解耦,Spring在org.springframework.web.servlet包中定义了一个高度抽象的View接口。视图对象由视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题。所谓视图是无状态的,是指对于每一个请求,都会创建一个View对象。 JSP是最常见的视图技术。2、视图解析器(ViewResolver)和视图(View)springMVC用于处理视图最重要的两个接口是ViewResolver和View。所以视图解析器的作用就是通过视图名(处理方法的返回值)生成View对象,所有的视图解析器都必须实现ViewResolver接口。 SpringMVC为逻辑视图名的解析提供了不同的策略,可以在SpringWEB上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。程序员可以选择一种视图解析器或混用多种视图解析器。可以通过order属性指定解析器的优先顺序,order越小优先级越高,SpringMVC会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则抛出ServletException异常。在项目中可以配置InternalResourceViewResolver作为视图解析器,在springmvc.xml中可以做如下配置:3、forward:和redirect:一般情况下,控制器方法返回字符串类型的值会被当成逻辑视图名处理,会经过视图解析器拼串,但如果返回的字符串中带forward:或redirect:前缀时,SpringMVC会对它们进行特殊处理:将forward:和redirect:当成指示符,其后的字符串作为URL来处理。示例如下:index.htmlhello.jsp,在当前项目的根路径下,和index.html同级ViewTestController.java测试结果:按F12打开开发者工具,可以看到确实两次重定向4、SpringMVC视图的解析流程(结合源码分析)源码中把任何返回返回值封装为ModelAndView的实现:这里以发出了一个GET请求为例:首先FrameworkServlet类会来处理这个GET请求doGetprocessRequestDispatcherServlet类doService方法doDispatch方法processDispatchResult方法,这个方法就是最终将数据交给页面的方法DispatcherServlet类的render方法并没有继承View接口的render,和View接口的render不是一回事,这个render仅仅是为了命名统一而起的一个名字resolveViewName方法,循环遍历你配置的视图解析器,viewResolvers是进过order排序的,这一步就是ViewResolvers是如何通过视图名产生View对象的关键InternalResourceViewResolver继承了AbstractCachingViewResolver,resolveViewName方法首先会判断有没有缓存,要是有缓存,它会先去缓存中通过viewName查找是否有View对象的存在,要是没有,它会通过viewName创建一个新的View对象,并将View对象存入缓存中,这样再次遇到同样的视图名的时候就可以直接在缓存中取出View对象了createView的实现细节:以下都是解析视图名的实现细节,感兴趣的可以看一下。父类AbstractCachingViewResolver类的createView实现细节:InternalResourceViewResolver继承了UrlBasedViewResolverUrlBasedViewResolver类中loadView方法的实现:UrlBasedViewResolver的buildView方法会获取一个View对象,这个对象会将视图以什么格式呈现给用户,例如如果是jsp显示呈现给用户的话,那这个view对象就是JstlView,默认的是JstlView。在这个方法中我们看到了getPrefix()+viewName+getSuffix()这样一段代码,这就是对视图路径的一个拼接了,getPrefix()方法获取前缀,也就是我们在配置文件中配置的<propertyname="prefix"value="/WEB-INF/PAGE/"/>的value中的值了,getSuffix()方法就是获取后缀值了,也就是我们在配置文件中配置的<propertyname="suffix"value=".jsp"/>的value中的值。这样就将将视图的物理路径找到了,并赋值到View的URL属性中去。就这样我们得到了一个View对象,这个视图的name就是逻辑视图名,因为当将View对象放在缓存的时候,我们可以通过逻辑视图名在缓存中找出View对象。我们在获取到View对象的时候,我们还要将View进行渲染,并呈现给用户。View是个接口,AbstractView实现了render方法:最后一行的renderMergedOutputModel方法由AbstractView的孙子类InternalResourceView实现InternalResourceView的renderMergedOutputModel方法帮我们获取到视图的物理路径,然后将这段路径传给RequestDispatcher对象,再调用RequestDispatcher的forward方法将页面呈现给用户,这样就走完了视图的解析了。最后一句话总结:视图解析器只是为了得到视图对象;视图对象才是真正的转发(将模型数据发在request域中数据)或重定向到页面(视图对象才是真正的渲染视图)。
LoveIT 2019-08-06Spring MVC -
Spring MVC从入门到精通—向页面带回响应数据的方法
1、SpringMVC提供了以下几种途径输出模型数据:ModelAndView:处理方法返回值类型为ModelAndView时,方法体即可通过该对象添加模型数据Map、Model以及ModelMap:入参为org.springframework.ui.Model、org.springframework.ui.ModelMap或Java.uti.Map时,处理方法返回时,Map中的数据会自动添加到模型中。@SessionAttributes:将模型中的某个属性暂存到HttpSession中,以便多个请求之间可以共享这个属性@ModelAttribute:方法入参标注该注解后,入参的对象就会放到数据模型中。当然,除了上面这些SpringMVC提供的几种方法,SpringMVC支持直接使用Servlet几个原生API来给页面传值:HttpServletRequestrequest、HttpservletResponseresponse、HttpSessionsession、InputStream/Reader对应request.getInputStream()、OutputStream/Writer对应response.getOutputStram()1.1Servlet原生API给页面传值页面测试代码:success.jsp测试结果:1.2Model、Map、ModelMap首先通过通过源码看看他们三者的关系:(1)ModelMap类(2)Model接口(3)ExtendModelMap类(4)BindingAwareModelMap类通过打开源码,我们不难总结出如下继承关系:接下来看看他们的用法:示例代码页面代码和上面样测试结果(1)页面的显示:(2)控制台打印的信息:从测试结果可以总结出:Model(SpringMVC接口)其中一个实现类是ExtendedModelMapModelMap是Map(JDK的接口)Map的一个实现类,并且ModelMap被ExtendedModelMapExtendedModelMap被BindingAwareModelMap继承Model、Map、ModelMap不论用哪个,最终工作的都是BindingAwareModelMap,而且从测试结果可以看到通过这三个设置的值,SpringMVC都把他们放在了request域中。1.3ModelAndView目标方法的返回值可以是ModelAndView类型,从名字上就可以看到,这是一个既包括模型(Model)又有视图(View)的一个类,然而事实也确实如此,他的model就可以理解为送给页面的数据,他的View可以理解为目标页面地址。但我们在他的model中放入值后,SpringMVC会把ModelAndView的model中数据放在request域对象中。示例代码测试结果:1.4使用@SessionAttributes注解 如果希望在多个请求之间共用某个模型属性数据,则可以在控制器类标注一个@SessionAttributes,SpringMVC会将模型中对应的属性暂存到HTTPSession中。@SessionAttributes除了可以通过属性名指定需要放到会话中的属性外,还可以通过模型属性的对象类型指定哪些模型属性需要放到会话中。1.@SessionAttributes(types=User.class)会将隐含模型中所有类型为User的属性添加到会话中2.@SessionAttributes(value={"user1","user2"})将名为user1和user2的模型属性添加到会话中3.@SessionAttributes(types={"User.class","Dept.class"})将模型中所有类型为User及Dept的属性添加到会话中4.@SessionAtributes(value={"user1","user2"},types={Dept.class})将名为user1和user2的模型属性添加到会话中,同时将所有类型为Dept的模型属性添加到会话中总之:当使用@SessionAttributes注解时就是告诉SpringMVC,当@SessionAttributes中的value值和BindingAwareModelMap的key一样时,那么在session也你也给我保存一份相同的值示例代码:页面代码对success.jsp中的sessionScope稍作修改:测试结果:1.5使用@ModelAttribute注解先来看看ModelAttribute的定义:通过@ModelAttribute的定义可以看到这个注解可以用在方法和参数上。在SpringMVC的Controller中使用@ModelAttribute时,应用情况包括下面几种:1、应用在方法上。2、应用在方法的参数上。3、应用在方法上,并且方法也使用了@RequestMapping示例代码:修改图书信息的页面:提交图书修改信息后的页面:如果没有使用@ModelAttribute,那么要更新数据信息,必须要全字段更新,即使你不需要更新的的字段,你也要填写,这显然不和常理,因为如果你不填写这个值,值就会为null。最主要是因为SpringMVC在封装提交的信息的时候只会new一个Book对象,里面的属性的值初始就是null。你没有填写也只会以null存到数据库。不使用@ModelAttribute进行非全字段更新测试结果:页面的显示:看看控制台的打印信息:可以看到果然不出预料的出问题了,更新信息后书名和作者的信息没了。这就相当于你更改了一下你的QQ密码,然后你的QQ号没了!这是很可怕的事情。使用@ModelAttribute解决问题:测试结果:页面展示的结果:控制台打印的信息:而且从控制台打印的信息来看,被@ModelAttribute标识的方法确实是在处理器方法之前执行了1.6@Modelattribute的原理废话不多说,直接看代码测试结果:最后总结为一张图:
LoveIT 2019-08-04Spring MVC -
Spring MVC从入门到精通—@RequestMapping注解详解
在SpringMVC中@RequestMapping注解是一个十分强大的注解,SpringMVC使用@RequestMapping注解可以为控制器指定可以处理那些URL请求,在控制器的类上或类中的方法上均可以使用这个注解:在类上使用可以提供初步的映射信息。相当于一个根路径在方法上使用提供更进一步的细分映射信息。(1)这是一个只在方法上使用@RequestMapping注解的例子:(2)这是一个在类和方法同时使用@RequestMapping注解的例子:@Requestmapping的属性:属性作用value默认的属性就是value,他就是一个URL路径,可以在一个方法上或类上给多个value值method用来定义接收浏览器发来的何种请求。在Spring中,使用枚举类RequestMethod来封装了HTTP协议的所有请求方式。最基本的有GET、POST、DELETE、PUTparams表示请求参数,也就是追加在URL上的键值对,多个请求参数以&隔开headers该属性表示请求头,通过@RequestMapping中的headers属性,可以限制客户端发来的请求consumes规定请求头的Content-Typeproduces告诉浏览器返回的内容是什么,给响应头中加上Content-Type示例代码:method:限定请求方法parmars:限定参数parmars支持简单的表达式计算,例如:eg1:params={"username"},表示请求的路径中必须要有username这个关键字,如果没有就会报异常eg2:params={"!username"},表示请求的路径中不能有username这个关键字,如果有这个参数就会报异常eg2:params={"username","age=20","pwd"},表示请求的路径中必须要有username、age、pwd这三个关键字,并且age必须是20,如果没有或age的值不等就会报异常headers:限定请求头部示例一:测试结果:
LoveIT 2019-08-03Spring MVC -
Spring MVC从入门到精通—SpringMVC获取请求参数
第一种方式:方法的形参上给一个和请求参数同名的参数1.获得普通类型的参数值示例代码2.获得POJO类型的值示例代码新建Book.java以及Address.java两个POJOBook.javaAddress.java写一个简单的表单:index.html提交后的页面:book.jsp提供一个控制器:BookContorller.java测试结果:第二种方式:使用SpringMVC提供的注解1.使用@RequestParam获取参数@RequestParam的源码:示例代码:2.使用@RequestHeader获得请求的头部信息@RequestHeader和@ReuqestParma的实现方式如出一辙,使用方法也基本相同。示例代码1:使用RequestHeader注解获得浏览器的信息示例代码2:使用RequestHeader注解获取请求头部的Cookie信息3.使用@CookieValue获得请求头部的JSESSIONID示例代码:4.使用@RequestBody接收前端传给后端的JSON格式的数据 由于GET方式无请求体,所以使用@RequestBody接收数据时,前端不能使用GET方式提交数据,而是用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。后端@RequestBody注解对应的类在将HTTP的输入流(含请求体)装配到目标类(即:@RequestBody后面的类)时,会根据json字符串中的key来匹配对应实体类的属性,如果匹配一致且json中的该key对应的值符合(或可转换为)实体类的对应属性的类型要求时,会调用实体类的setter方法将值赋给该属性。使用示例;
LoveIT 2019-08-02Spring MVC -
Spring MVC快速入门—Spring MVC体系结构及工作流程
SpringMVC体系结构:SpringMVC是基于MVC软件架构实现的技术框架SpringMVC的执行流程具体步骤:第一步:客户端发起请求到前端控制器(DispatcherServlet)第二步:前端控制器请求HandlerMapping查找Handler(可以根据xml配置、注解进行查找)并返回一个Handler第三步:处理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略第四步:前端控制器调用处理器适配器HandlerAdapter去执行Handler,处理器适配器HandlerAdapter将会根据适配的结果去执行Handler第五步:Handler执行完成给适配器返回ModelAndView第六步:处理器适配器向前端控制器返回ModelAndView(ModelAndView是springmvc框架的一个底层对象,包括Model和view)第七步:前端控制器请求视图解析器ViewResolver对ModelAndView进行解析(根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可。视图解析器最终向前端控制器返回View第八步:前端控制器进行视图渲染(视图渲染将模型数据(在ModelAndView对象中)填充到request域)第九步:前端控制器向用户响应结果第一个SpringMVC程序第一步:配置环境(新建Web工程+导包+配置Tomcat)在IDEA中新建MavenWeb工程,新建好后导包,使用SpringMVC所需要的基本的Maven依赖如下:第二步:配置DispatcherServlet通过DispatcherServlet这个名字可以大概了解到,这就是一个servlet,因此要想使一个sevlet起作用无非两种方法:一种是在servle类的头部加@WebServlet注解,二是在web.xml文件中配置<servlet>和servlet-mapping,第一中方法在这里显然不可行,人家源码肯定不能让改,那就需要在web.xml配置它,配置如下:接着在src/resources下新建springmvc.xml,配置springmvc:第三步:在webapp包下新建一个index.html以及hello.jsp,随便写点啥第四步:新建一个Controller,比如就叫HelloController测试结果:总结在做的时候的几细节1.SpringMVC下Web项目的运行流程:1.点击http://localhost/SpringMVC_01_war_exploded/后浏览器把请求给tomcat服务器2.服务器中由于配置了SpringMVC的Dispatcherservlet,他可以收到所有的请求3.Dispatcherservlet拿到请求后查看请求地址和@RequestMapping()中的那个地址(对应的方法)匹配4.前端控制器找到目标处理器和方法后,直接利用反射调用方法5.方法执行完成会有返回值(视图名),SpringMVC认为这就是方法执行完后要去的页面6.拿到方法返回值后,视图解析器拼接触页面地址7.拿到页面地址后,前段控制器就会使用转发的方式到目标页面2.关于springmvc.xml配置文件的配置:在web.xml配置前段控制器又这么一段配置,他的作用是告诉服务器去哪里加载对于前端控制器的配置文件:<init-param><param-name>contextConfigLocation</param-name><param-value>classpath:springmvc.xml</param-value></init-param>如果我们没有写他会发生什么呢?会发生异常:一大堆异常信息最终就是告诉中我们你没有给配置文件的路径,那我(SpringMVC)就会在/WEB-INF目录下默认加载一个文件叫app-servlet.xml的文件,然而也没有找到,从而无法完成初始化。从这里我们知道,在配置前端控制器的时候也可以不写初始化参数,但是我们必须将springmvc配置文件放在WEB-INF目录下,并且文件的必须名字是:前端控制器的<servlet-name>app</servlet-name>+-servlet.xml,这是规定,不可随便来,你想节省一些操作就得按人家的要求来。
LoveIT 2019-08-01Spring MVC -
MyBatis分页插件pageHelper配置和使用
1、PageHelper简介这是一个基于MyBatis开源的分页插件,使用非常方便,支持各种复杂的单表、多表分页查询,让你在写sql时无需考虑分页问题,PageHelper帮你搞定。项目托管在github上https://github.com/pagehelper/Mybatis-PageHelper。2、在项目中引入PageHelperPageHelper是一个通用的MyBatis分页插件,在使用的时候除了要导入MyBatis和数据库驱动的jar包外,还要导入PageHelper的依赖包。3、分页插件的配置配置PageHepler插件对方法有很多种,这里我有最基础的一种方法,使用mybatis-config.xml全局配置文件来配置。配置也很简单,只需要在mybatis-config.xml添加如下配置:配置参数详解:helperDialect:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。你可以配置helperDialect属性来指定分页插件使用哪种方言。配置时,可以使用下面的缩写值:oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012,derby特别注意:使用SqlServer2012数据库时,需要手动指定为sqlserver2012,否则会使用SqlServer2005的方式进行分页。也可以实现AbstractHelperDialect,然后配置该属性为实现类的全限定名称即可使用自定义的实现方法。offsetAsPageNum:默认值为false,该参数对使用RowBounds作为分页参数时有效。当该参数设置为true时,会将RowBounds中的offset参数当成pageNum使用,可以用页码和页面大小两个参数进行分页。rowBoundsWithCount:默认值为false,该参数对使用RowBounds作为分页参数时有效。当该参数设置为true时,使用RowBounds分页会进行count查询。pageSizeZero:默认值为false,当该参数设置为true时,如果pageSize=0或者RowBounds.limit=0就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是Page类型)。reasonable:分页合理化参数,默认值为false。当该参数设置为true时,pageNum<=0时会查询第一页,pageNum>pages(超过总数时),会查询最后一页。默认false时,直接根据参数进行查询。params:为了支持startPage(Objectparams)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值,可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值,默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero。supportMethodsArguments:支持通过Mapper接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面params配置的字段中取值,查找到合适的值时就会自动分页。使用方法可以参考测试代码中的com.github.pagehelper.test.basic包下的ArgumentsMapTest和ArgumentsObjTest。autoRuntimeDialect:默认值为false。设置为true时,允许在运行时根据多数据源自动识别对应方言的分页(不支持自动选择sqlserver2012,只能使用sqlserver),用法和注意事项参考下面的场景五。closeConn:默认值为true。当使用运行时动态数据源或没有设置helperDialect属性自动获取数据库类型时,会自动获取一个数据库连接,通过该属性来设置是否关闭获取的这个连接,默认true关闭,设置为false后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。4、在代码中使用PageHelper完成上面的操作后对分页插件的配置就完成了,使用方法也很简单:使用PageHelper.statPage静态方法来分页,使用PageInfo来获取分页信息注意: 1.PageHelper.startPage(pageNum,pageSize);语句之后,第一个SQL语句,必须为要进行分页的(全)查询语句。追注:如果在PageHelper.startPage()方法之后,先执行了其他的sql语句,然后再执行的要分页的查询的话;那么分页会失效。 2.由于分页插件是通过拦截器,在原有SQL上进行追加约束条件,所以使用分页插件时,应注意:保证原有SQL不会受后面追加的条件的影响。给出一个反例:原有SQL中使用变量计算排名时,如果在后面追加了LIMIT的话,那么排名就会受到影响,因为SELECT的优先级在LIMIT之后。 3.PageInfo是比Page信息更丰富的一个类;我们可以直接返回Page,也可以使用PageInfo包装一下返回PageInfo,甚至也可以自定义一个类来存放结果信息(只需将返回的Page中的信息取出来,再setter放入我们自己的类即可)。5、PageHelper常用的APIPageMethod的APIPageHelper继承了抽象类PageMethod,而使用PageHelper进行分页操作的方法实际是在PageMethod中的方法,具体如下:PageInfo中的成员变量每一个成员变量都有对应的get和set方法,使用这些方法可以获得分页的任何信息,这个类十分强大追加:现在SpringBoot已经是Java开发中非常流行的一个框架了,如果我们需要在基于SpringBoot开发的项目中使用pageHelper,只需要更换pageHelper的pom依赖,然后我们无需在写繁杂的xml配置文件了,只需要在SpringBoot的配置文件中写如下配置即可。(1)引入pageHelper依赖(2)编写配置
LoveIT 2019-07-30MyBatis -
MyBatis动态SQL
MyBatis的强大特性之一便是它的动态SQL。如果你有使用JDBC或其他类似框架的经验,你就能体会到根据不同条件拼接SQL语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态SQL这一特性可以彻底摆脱这种痛苦。MyBatis动态SQL元素和使用JSTL或其他类似基于XML的文本处理器相似。在MyBatis之前的版本中,有很多的元素需要来了解。MyBatis3大大提升了它们,现在用不到原先一半的元素就可以了。MyBatis采用功能强大的基于OGNL的表达式来消除其他元素。主要的有以下几个:if可以筛选一到多条SQL分支where一般配合if使用,作用相当于SQL语句中的where关键字choose、when、otherwise筛选一条SQL分支trim类似于replace效果,可以完成set或者是where标记的功能set相当于SQL中的set关键字foreach遍历集合一、if元素使用if可以用条件的筛选SQL语句分支,只有条件满足的时候才会执行。实例:这条sql语句就会动态的根据传入的参数值来查询,比如当只传了empAge=23,其他都为空,那么生成的sql语句就是:select*fromemployeewhereemp_age=23。其实管使用if标签这么写是有问题的,下面有具体分析以及改进方法。二、choose、when、otherwise元素MyBatis提供了choose元素,它有点像Java中的switch语句,即在choose中的SQL分支是由一条会被执行。如下:测试代码:可以看到,即使在条件中给了两个不为空的,但是由于EmpId在最前面,因此首先匹配上后就不在往下找了,和switch很像。最终的运行结果:三、trim,where元素看看下面的这条这种情况(一中的例子):如果三个分之没有一条分支匹配上,那么最终的sql语句会变成:这显然是会导致查询失败的。如果仅仅第二条或第三条匹配,那么sql语句会变成这样:不论是那种情况,都会导致由于sql语句错误而查询失败。MyBatis有一个简单的处理,这在大多数情况下都会有用——那就是使用where元素,where元素知道只有在一个以上的if条件有值的情况下才去插入“WHERE”子句。而且,若最后的内容是“AND”或“OR”开头的,where元素也知道如何将他们去除。例如:这样即使三条语句都不满足条件,那么最终的sql语句是:select*fromemployee,也不会影响正常的查询。如果where元素没有按正常套路出牌,我们还是可以通过自定义trim元素来定制我们想要的功能。比如,和where元素等价的自定义trim元素为:prefixOverrides属性会忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它带来的结果就是所有在prefixOverrides属性中指定的内容将被移除,并且插入prefix属性中指定的内容。类似的用于动态更新语句的解决方案叫做set。set元素可以被用于动态包含需要更新的列,而舍去其他的.trim标签的全部属性prefix:前缀增加的内容suffix:后缀增加的内容prefixOverrides:前缀覆盖第一个判断的条件suffixOverrides:后缀覆盖最后一个判断的条件四、set元素MyBatis中用于动态更新语句的解决方案叫做set。set元素可以被用于动态包含需要更新的列,而舍去其他的.示例:测试代码:set元素会动态前置SET关键字,同时也会消除无关的逗号,因为用了条件语句之后很可能就会在生成的赋值语句的后面留下这些逗号五、foreach元素动态SQL的另外一个常用的必要操作是需要各种集合集合进行遍历,通常是在构建各种条件语句以及批量插入数据的时候的时候使用较多。foreach元素有许多属性,在使用之前先了解一下foreach元素的属性:属性作用collectioncollection用于遍历的集合的名字,MyBatis支持任何可迭代对象的迭代,常见的比如List、Set、Map对象或者数组等item表示本次迭代获取到的元素index遍历过程的索引值。open前缀close后缀separator分隔符,表示迭代时每个元素之间以什么分隔注意!当使用Map对象(或者Map.Entry对象的集合)迭代时,index是键,item是值1、foreach遍历传递进来的集合,构建条件有时候我们可能会有下面的需求,根据多个id查询对应的信息,这多个id的数量是不固定的。这时候我们可以通过使用foreach标签来遍历集合中的参数,完成多个id之间的拼接。Mapper接口:SQL映射文件:2、使用foreach元素批量插入数据当需要插入的数据比较多的时候,此时再一条一条的插入数据就显得比较慢了,MySQL提供了一次插入多条数据的语法insertintotableName(......)values(......)[,(......),(......)],使用这条语句可以一次插入多条数据Mapper接口:SQL映射文件:总结这篇博文主要对常用的MyBatis动态SQL元素进行了介绍与其使用场景的应用,MyBatis还提供了其他的元素来支持动态SQL,熟练使用这些元素,才能在开发的时候写出简洁高效的SQL语句,不常用的元素信息可以到官方文档进行深入了解,希望这篇博文能够为你提供一些帮助。MyBatis动态SQL官方文档链接:http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html
LoveIT 2019-07-29MyBatis -
Spring从入门到精通—Spring事务详解
1、Spring事务核心接口Spring事务管理涉及的接口的联系如下: Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。此接口的内容如下:2、基本事务属性的定义—TransactionDefinition 上面讲到的Spring的事务管理器核心接口PlatformTransactionManager通过getTransaction(TransactionDefinitiondefinition)方法来得到事务,这个方法里面的参数是TransactionDefinition类,这个类就定义了一些基本的事务属性。那么什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面,如图所示:TransactionDefinition接口内容如下:2.1事物的传播行为 事务的传播行为(propagationbehavior)是指当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:传播行为含义PROPAGATION_REQUIRED表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务PROPAGATION_SUPPORTS表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行PROPAGATION_MANDATORY表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常PROPAGATION_REQUIRED_NEW表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManagerPROPAGATION_NOT_SUPPORTED表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManagerPROPAGATION_NEVER表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常PROPAGATION_NESTED表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务2.2事物的隔离级别 事物的隔离级别(isolationlevel)定义了一个事务可能受其他并发事务影响的程度。在并发事务中,经常会引起以下问题:脏读(Dirtyreads)——脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。不可重复读(Nonrepeatableread)——不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。幻读(Phantomread)——幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。 所以为了解决这些问题,引入了数据库的事务隔离级别的概念。Spring定义的事务隔离级别和数据库中定义的事务隔离级别是对应的,具体如下:隔离级别含义ISOLATION_DEFAULT使用后端数据库默认的隔离级别ISOLATION_READ_UNCOMMITTED最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读ISOLATION_READ_COMMITTED允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生ISOLATION_SERIALIZABLE最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的2.3事务只读 事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。在TransactionDefinition中以boolean类型来表示该事务是否只读。2.4事务超时 为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。在TransactionDefinition中以int的值来表示超时时间,其单位是秒。2.5回滚规则 这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚3、事务状态——TransactionStatus TransactionStatus接口用来记录事务的状态该接口定义了一组方法,用来获取或判断事务的相应状态信息.PlatformTransactionManager.getTransaction(…)方法返回一个TransactionStatus对象。返回的TransactionStatus对象可能代表一个新的或已经存在的事务(如果在当前调用堆栈有一个符合条件的事务)。TransactionStatus接口接口内容如下:
LoveIT 2019-07-22Spring -
Spring从入门到精通—Spring事务配置使用实例
1、Spring事务控制概述 Spring支持编程式事务管理和声明式事务管理两种数据库事务管理方式。编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager,通过硬编码的方式来管理数据库的事物。对于编程式事务管理,Spring推荐使用TransactionTemplate。声明式事务管理是建立在AOP(面向切面编程)之上的。其本质是对将要执行的SQL与语句进行拦截,然后在目标SQL开始之前创建或者加入一个事务,在执行完目标SQL之后根据执行情况提交或者回滚事务。声明式事务管理有两种常用的使用方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用。 显然声明式事务管理要优于编程式事务管理,这也正是Spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。 说了这么多,不如来点实际的,下面我们就来看看Spring推荐的声明式事物如何在我们的项目中使用。我将以注解和XML两种配置方式来一一为大家介绍。2、搭建实验环境首先新建一个Maven项目,导入需要的依赖:创建数据库和表:新建tx数据库,并在tx数据库中新建三张表account、book、book_stock。具体的数据库脚本如下:导入后应该是这样的三张表:3、基于Annotation的声明式事务管理配置和使用(1)首先配置数据源,这里使用的阿里的Druid数据连接池(2)数据库配置jdbc.properties如下:(3)配置JdbcTemplate,这里出于学习的目的先使用JdbcTemplate。(4)配置事物管理器(5)配置开启注解事物管理完整的ApplicationContext.xml配置文件如下: 完成上面的操作后,我们就可以在需要事物管理的地方使用@Transactional注解标注在类或方法上,以此告诉Spring这里需要事物管理。当作用于类上时,该类的所有public方法将都具有该类型的事务属性。另外,@Transactional注解应该只被应用到public方法上,这是由SpringAOP的本质决定的。如果你在protected、private或者默认可见性的方法上使用@Transactional注解,这将被忽略,也不会抛出任何异常。@Transactional注解的属性总结属性需要的参数的类型描述valueString指定使用的事务管理器(必须)propagationenum:Propagation事务传播行为设置(可选)isolationenum:Isolation事务隔离级别设置(可选)readOnlyboolean读写或只读事务,默认(false)读写timeoutint事务超时时间设置,单位:秒rollbackForClass[],必须继承自Throwable指定发生异常后哪些异常要回滚rollbackForClassNameString[],必须继承自Throwable指定发生异常后哪些异常要回滚的异常类名字数组noRollbackForClass[],必须继承自Throwable指定发生异常后哪些异常不回滚noRollbackForClassNameString[],必须继承自Throwable指定发生异常后哪些异常不回滚的异常类名字数组使用示例:DAO层,直接使用JdbcTemplate操作数据库,写3个方法分别用于:更新用户余额、更新图书库存、以及查询价格,具体如下:业务层,写一个结账的方法,使用@Transactional告诉Spring要帮我们控制事务4、基于XML的声明式事务管理配置和使用其他的步骤和使用注解一样,不同点的核心的在下面:测试:在src/main/test包下新建测试类TxTest.java测试
LoveIT 2019-07-21Spring -
Spring从入门到精通—Spring IOC高级依赖注入配置使用实例
1、环境与Profile 在开发中我们测试用一套数据库,开发用一套数据库,而且要将应用程序从一个环境迁移到另一个环境,Spring允许我们定义多套配置,可以配置声明应用哪套配置的Bean#####1.1Profile***Spring中的Profile是什么?** Spring中的Profile功能其实早在Spring3.1的版本就已经出来,它可以理解为我们在Spring容器中所定义的Bean的逻辑组名称,只有当这些Profile被激活的时候,才会将Profile中所对应的Bean注册到Spring容器中。举个更具体的例子,我们以前所定义的Bean,当Spring容器一启动的时候,就会一股脑的全部加载这些信息完成对Bean的创建;而使用了Profile之后,它会将Bean的定义进行更细粒度的划分,将这些定义的Bean划分为几个不同的组,当Spring容器加载配置信息的时候,首先查找激活的Profile,然后只会去加载被激活的组中所定义的Bean信息,而不被激活的Profile中所定义的Bean定义信息是不会加载用于创建Bean的。***Profile有什么用?** 由于我们平时在开发中,通常会出现在开发的时候使用一个开发数据库,测试的时候使用一个测试的数据库,而实际部署的时候需要一个数据库。以前的做法是将这些信息写在一个配置文件中,当我把代码部署到测试的环境中,将配置文件改成测试环境;当测试完成,项目需要部署到现网了,又要将配置信息改成现网的,真的好烦。。。而使用了Profile之后,我们就可以分别定义3个配置文件,一个用于开发、一个用户测试、一个用户生产,其分别对应于3个Profile。当在实际运行的时候,只需给定一个参数来激活对应的Profile即可,那么容器就会只加载激活后的配置文件,这样就可以大大省去我们修改配置信息而带来的烦恼。下面我将以两个例子介绍一下目前常用的两种配置方式下如何配置Springprofile方式一:用xml配置profile方式二:用Annotation配置profile,这种方式配置和用xml配置是等价的在同一个类的不同方法上使用@Profile注解与@Bean一起使用配置出不同的profile实例激活profile Spring在确定哪个profile处于激活状态时,需要依赖两个独立属性:sping.profiles.active和spring.profiles.default。Spring提供了@ActiveProfiles用来指定运行测试时要激活哪个profile,如果没有指定sping.profiles.active,会采用spring.profiles.default的默认值。测试代码:2、条件化Bean Spring4引入了一个新的@Conditional注解,它可以用到带@Bean注解的方法上,如果条件计算结果为true,就会创建个Bean设置给@Conditional的类可以是任意实现了Condition接口的类型,如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。若返回false,将不会创建这些bean。其中:ConditionContext:getRegistry():返回的BeanDefinitionRegistry检查Bean定义:getBeanFactory():返回ConfigurableListableBeanFactory检查Bean是否存在getEnvironments():返回Environment检查环境变量是否存在以及它的值是什么getResourceLoader():返回ResourceLoader所加载的瓷源getClassLoader():返回ClassLoder加载并检查是否存在AnnotatedTypeMetadata:可以让我们检查带@Bean注解的方法上还有什么其它注解,它也是一个接口举个栗子:写个条件类,实现Condition接口的matches方法,简单的判断一下当前的系统是不是Windows7的,如果是返回true,否则返回false测试代码:测试代码:程序运行结果:如果把“Windows7”改成“Windows10”就是条件Bean就会返回false,由于无法正常的注入就会出现以下的异常:
LoveIT 2019-07-20Spring -
Spring从入门到精通—基于Annotation配置和使用AOP
1、AspectJ通知类型AOP联盟定义通知类型,AOP联盟的jar都是接口,必须要有实现类。AspectJ通知类型只定义类型名称,以及方法格式,总共有6种;1.brfore:前置通知(应用:各种校验)在方法执行前执行,如果通知抛出异常,将不会执行方法2.afterReturning:后置通知(应用:常规数据处理)方法正常返回后执行,如果方法中抛出异常,通知将无法执行3.around:环绕通知(应用:十分强大,可以做任何事)方法执行前后分别执行,可阻止方法执行4.afterThrowing:抛出异常通知(应用:包装异常信息)方法抛出异常后执行,如果方法没有抛出异常,无法执行5.after:最终通知(应用:清理现场)方法执行完毕后执行,无论方法中是否出现异常都会执行(类似于finally代码块)2、回顾基于XML配置AOP在上一篇笔记*Spring从入门到精通—SpringAOP的XML配置和使用*中讲了如何使用xml的方式配置和使用Spring的AOP,这里我们再回顾一下:首先编写一个service接口,模拟要处理的业务实现sevice接口:编写切面类:写一个方法before(),他是在目标方法执行需要增强的功能在XML中的配置如下:测试:测试结果:如下图所示,前置通知确实起作用了,在目标方法执行之前就执行了下面我们在来测测<aop:advisor>中的其他通知方式<aop:after-returning> 在切面类中增加方法:afterReturning(JoinPointjp,Objectobj),其中第一个参数是连接点,第二个参数是目标方法运行后的返回值。要获得返回返回值需要在配置中设置returning="obj",就是把这个第二个参数的名字放进去,Spring就会把返回值注入。配置新增的切面类方法:测试代码不变,测试结果如下:#####<aop:around>around具有before和after-returning两者的功能,这里就不在重复测试了。所以一般使用了around就不在使用brfore和after-returning<aop:after-throwing>、<aop:after>切面类中增加方法afterThrowing和after配置xml并在deleteUser方法中主动抛出异常:测试结果如下:如果去掉异常的测试结果如下:2、基于Annotation配置AOP既然使用注解配置,那就全部用注解,包括配置文件都用注解+Java类来实现*编写配置类Appconfig.java替代xml文件当然这段java代码可以用下面这段XML配置文件替代编写一个日志记录的切面类LoggerApsect.java在切面类中可以使用如下几个注解来定制一个切面:@Aspect:告诉Spring这是切面类@Brfore:前置通知@AfterRuning:返回后通知@Around:环绕通知,是@Brfore和@AfterRuning@After-Throwing:抛出异常后的通知,没有异常不会执行@After:最终通知,无论有没有异常一定会执行的@PointCut:定义切点具体用法如下:测试类如下:测试结果:现在去掉before、after-return以及在deleteUser中抛出一个异常:
LoveIT 2019-07-19Spring -
Spring从入门到精通—基于XML配置和使用AOP
1、SpringAOP半自动编程核心步骤:1.创建一个接口以及它的实现类2.编写切面类,实现MethodInterceptor接口的invoke方法3.配置Spring的配置文件,xml文件中的配置重要是:【重要】 1).配置目标类的bean 2).配置切面类的bean 3).配置代理对象,其中代理对象中主要的配置如下' a.配置接口 b.配置目标类 c.配置切面类 d.还可以用<propertyname="optimize"value="true"/>,指明使用cglib的代理对象1.1首先编写一个接口1.2编写接口的实现类:1.3编写一个切面类:让这个切面类实现MethodInvocation接口的invoke方法1.4在Spring的配置文件Application.xml中注册代理对象1.5测试:测试结果:2、SpringAOP全自动编程主要步骤:1).实现切面类2).在bean配置文件中吧切点和切面关联起来2.1实现一个切面类,继承MethodInterceptor接口实现它的invoke方法接着在Spring配置文件Application.xml中配置aop的schema文件,如下红框的配置:接着在配置文件中添加如下内容:对于配置全自动AOP代理的一点说明,如下图所示<aop:config>标签中重要的三种元素:切点:<aop:pointcut>、切面:<aop:aspect>、通知:<aop:advisor>其中通知:``又有以下5种:测试:测试结果:写在最后 通过实际的操作可以明显感受到第二种方式实现AOP是省事儿又简单的一种方式,然而第二种方式也是Spring中使用到AOP是常用到的一种配置方式。既然有全自动xml的配置方式,那么一定就会有对应对一套使用注解配置对方式。下一节就讲讲如何用注解配置使用SpringAOP
LoveIT 2019-07-19Spring -
Spring从入门到精通—AOP与AspectJ的关系?原生JDK和CGLib手动实现AOP?
1、AOP和AspectJ概述(SpringAOP是什么?AOP有什么用?)1.1AOP简介在软件行业,AOP为AspectOrientedprogramming的缩写,意为:面向切面编程,它是一种编程思想。AOP是OOP(面向对象编程)思想的延续。AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码的编写方式(例如性能监视、事务管理、安全检查、缓存、日志记录等)AOP的核心思想:基于代理思想,对目标对象创建代理对象,在不修改原目标对象的情况下,通过代理对象,调用增强功能代码,从而对原有业务方法的功能进行增强。1.2AOP的作用利用AOP可以对业务逻辑的各个部分进行分离,从而使得业务逻辑各个部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。AOP采用横向抽取机制,取代了传统继承体系的纵向机制。AOP的经典应用场景:事务管理、性能监视、安全、缓存、日志.....1.3SpringAOP编程的两种方式SpringAOP使用纯Java代码实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码。AspectJ是一个基于Java的AOP框架,Spring2.0开始,SpringAOP引入AspectJ的支持,AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的织入。1.4AspectJ是什么?能做什么?AspectJ是一个易用且功能强大的AOP框架,并不是Spring的一部分。AspectJ的全称是EclipseAspectJ,其官网地址是:www.eclipse.org/aspectj/.AspectJ的特性如下:是一种基于Java平台的面向切面编程的语言兼容Java,可以无缝扩展易学易用,可以单独使用,也可以整合到其他框架中,单独使用AspectJ需要使用专门的编译器ajc1.5AspectJ和SpringAOP的区别?两者的区别如下:SpringAOP:基于动态代理来实现,默认如果使用接口,那么会用JDK提供动态代理实现,如果是方法则使用CGLib提供代理实现。SpringAOP需要依赖IOC容器来管理,并且只能用于Spring容器,使用纯Java代码实现。在性能上,由于SpringAOP是基于动态代理实现的,在容器启动时需要生成代理实例,在方法调用上也会增加桟的深度,使用SpringAOP的性能不如AspectJ好。AspectJ:AspectJ属于静态织入,通过修改代码来实现,一般AOP有三种织入的方式:编译期织入(Compile-timeweaving):类A中使用了AspectJ添加了一个属性,类B引用了它,这个场景就需要在编译时进行织入,否则没法编译类B。编译后织入(Post-compileweaving):当目标代码已经编译成.class字节码文件了的时候可以使用此方法来织入增强的代码类加载后织入(Load-timeweaving):在类加载的时候进行织入,要实现这个时期的织入一般有两种办法:自定义类加载器专门来进行织入;在JVM启动的时候指定AspectJ提供agent:-javaagent:xxx/xxx/aspectjweaver.jarAspectJ是AOP编程的完全解决方案,SpringAOP则只是致力于解决企业级开发中最普遍的AOP(方法织入)由于AspectJ在实际运行之前就完成了织入,因此由AspectJ生成的类是没有额外的运行时开销的。2、AOP的一些术语【了解】AOP(AspectOrientedProgramming)像大多数技术一样形成了自己的术语,而且这些术语比较难理解,不论是否理解都对编程影响不太大,但是最起码要清除有这样一个东西。连接点(Joinpoint)程序执行的某个特定位置:如类开始初始化前、类初始化后、类某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些点中的特定点就称为“连接点”。Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强。切点(Pointcut)每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。AOP通过“切点”定位特定的连接点。连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。通知或增强(Advice)增强是织入到目标类连接点上的一段程序代码,在Spring中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。结合执行点方位信息和切点信息,我们就可以找到特定的连接点。目标对象(Target)增强逻辑的织入目标类。如果没有AOP,目标业务类需要自己实现所有逻辑,而在AOP的帮助下,目标业务类只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑则可以使用AOP动态织入到特定的连接点上.引介(Introduction)引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。织入(Weaving)织入是将增强添加对目标类具体连接点上的过程。AOP像一台织布机,将目标类、增强或引介通过AOP这台织布机天衣无缝地编织到一起。根据不同的实现技术,AOP有三种织入的方式:代理(Proxy)一个类被AOP织入增强后,就产出了一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以我们可以采用调用原类相同的方式调用代理类。切面(Aspect)切面由切点和增强(引介)组成,它既包括了横切逻辑的定义,也包括了连接点的定义,SpringAOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。对于Spring中的AOP术语,确实不好理解,为了帮助大家理解这些术语,我们想象一下这样的场景: 有2条高速公路可以通向北京,分别为A,B(相当于2个目标个业务Target(功能),每条高速公路上有3个服务站(相当于每个业务上的连接点(Joinpoint)有3个),在每条高速公路的第2个服务站需要测速(相当于一个切点(Pointcut),匹配了3个连接点),在第一条(A)进入服务站之前进行测速,在第二条(b)进入服务站之后测速(通知或增强Advice,其实也定义了调用测速功能进行测速,以及怎么测),每条高速路第2个服务站加入测速功能,这件事情相当于切面Aspect,它包含了测速功能的定义与切点的定义,将测速功能应用到服务站上,这个过程叫织入(Weaving)3、AOP的实现原理接下来是重点,我们来学习一下AOP的实现原理,并利用原理自己手动实现AOP。AOP底层采用代理机制进行实现,具体的实现方法有以下两种:1)接口+实现类:采用jdk的动态代理2)使用实现类:Spring采用了cglib字节码增强接下来我们就用这两个原理分别自己手动实现AOP。4、使用jdk的动态代理实现AOP实现思路:使用jdk中的Proxy类的newProxyInstance方法来获得一个代理对象,此后当使用某个对象的时候就使用这个代理对象而不直接去new对象,好了废话不多说,直接上代码步骤:1.写一个普通的接口以及这个接口的实现类2.写一个切面类(就是一个普通的java类,里面的方法写要增强的功能)3.上面的操作完成后,在工厂类(为了方便,当然也可以不写这个工厂类)中首先创建一个目标类对象(就是new一个业务类的对象),接着new一个切面类对象,使用动态代理把切面类中的增强功能织入到目标方法的前后,下面是示例代码:4.1首先写定义一个业务类接口:UserServiceBase.java4.2写一个业务接口的实现类:UserSeviceImpi.java4.3基于Java原生API写一个切面类:MyAspect.java4.4写一个工厂类,专门用于生产代理对象(可以没有)4.5测试:测试结果:5、使用Cglib实现CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。CGLIB作为一个开源项目,其代码托管在github,地址为:https://github.com/cglib/cglib下面是CGLib的组成结构:CGLIB底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类。除了CGLIB库外,脚本语言(如Groovy何BeanShell)也使用ASM生成字节码。ASM使用类似SAX的解析器来实现高性能。CGLIB相比于JDK动态代理更加强大,JDK动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。接下来我们就用CGLib手动实现AOP5.1首先,需要导入Cglib所需的jar包,Maven依赖写法如下:5.2实现业务类还是需要一个业务类(这次不需要接口了)以及一个切面类,前面写过,这里就跳过了。5.3基于cglib的API实现切面类接着在刚才的工厂类中再写一个方法createUserService2(),步骤还是那几步,只不过这次是用cglib提供的api来写,具体如下:5.4测试:测试结果:可以看到结果和刚才用JDK动态代理的结果一样,但是这里要特别注意:jdk代理只能动态代理接口+实现类的形式;Cglib代理的优势是可以直接代理普通的类,但同时接口也可以
LoveIT 2019-07-19Spring -
Spring从入门到精通—IOC详解
1、什么是SpringIOC/DI控制反转(InversionofControl,IoC)所谓控制反转就是应用本身不负责依赖对象的创建及维护,依赖对象的创建及维护是由外部容器负责的。这样控制权就由应用转移到了外部容器,控制权的转移就是所谓反转。依赖注入(DependencyInjection,DI)在运行期,由外部容器动态地将依赖对象注入到组件中。换句话说,就是在运行时能Bean对象设置属性值2、bean标签一个bean标签代表一个spring容器中的java对象,可以在bean中经常使用的属性如下:1.id属性:起名称不能包含特殊符号根据id获得配置对象2.class属性:创建对象所在全路径3.name属性:功能和id一样,id不能包含特殊符号,name可以(基本不用,为了满足struts1遗留问题)4.scope属性:Bean的作用范围,scope常用的值有:-singleton和-prototype,分别表示单例和多例,如果没写默认就是单例3、Bean的3种实例化方式1.直接使用bean标签来实例化pojo,这中方法Spring默认调用的是这个pojo的无参构造器来实例化bean对象的首先创建一个EmailDaoImpl.java在ApplicationContext.xml文件中使用<beanid=“"class=""/>标签配置bean:经过这两步就配置好了一个bean,测试代码简单,就是调用了一下sent方法,下面是执行的结果:2.使用静态工厂实例化pojo首先新建一个静态工厂DaoFactory.java接着在xml中配置如下:3.使用实例化工厂实例化pojo首先新建一个实例化工厂:在xml文件中配置如下:4、Spring依赖注入的2种常用方式构造方法注入setter方法注入首先,新建学生实体类Student:1.使用构造方法注入在xml中配置如下,正常情况下只用指定参数的名字和参数的值:<constructor-argname=""value=""/>,name就是构造方法中的参数名,value即为这个参数的值下面是一个简单的配置示例:程序运行的结果:当构造方法出现命名冲突的时候,可以使用type属性指定参数的数据类型:程序运行的结果:2.setter方法注入,这种方法和实例化bean相同,都是用了property属性程寻运行的结果:5、SpEL(Spring表达式) SpEL(SpringExpressionLanguage),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言。为什么要总结SpEL,因为它可以在运行时查询和操作数据,尤其是数组列表型数据,因此可以缩减代码量,优化代码结构。个人认为很有用。 SpEL有三种用法,一种是在注解@Value中;一种是用于XML配置;最后一种是在代码块中使用Expression。下面说一下它最基础最重要的一种用法:xml配置法。(使用注解的方式的语法和xml方式的语法一样的,只是使用注解会更方便)5.1SpEL的基本语法语法格式``#{123}、#{'字符串'}:数字、字符串#{beanId}:对另一个bean的引用,类似ref属性#{beanId.propName}::操作数据#{beanId.toString()}:执行方法#{T(类).字段|方法}:静态方法或字段Spring表达式支持大多数的数学操作符、逻辑操作符、关系操作符。1.关系操作符包括:等于(==,eq),不等于(!=,ne),小于(le),大于(>,gt),大于等于(>=,ge)2.逻辑操作符包括:and,or,andnot(!)3.数学操作符包括:加(+),减(-),乘(*),除(/),取模(%),幂指数(^)。新建一个实体Customer```javapackagecom.xzy.bean;importjava.util.Arrays;importjava.util.List;importjava.util.Map;importjava.util.Set;publicclassCustomer{privateStringname;privateStringsex="男";privatedoublepi;@OverridepublicStringtoString(){return"Customer{"+"name='"+name+'\''+",sex='"+sex+'\''+",pi="+pi'}';}```在配置文件中如下配置:```xml```程序的执行结果如下:剩下的各种运算就不在这里试了,有兴趣的话可以自己尝试。####6、Spring集合类型注入 官方的一句话:`Inthe,,,andelements,yousetthepropertiesandargumentsoftheJavaCollectiontypesList,Set,Map,andProperties,respectively.`就是说,你可以用``、`set`、`map`、`props`来配置对应的Java集合类型:List、Set、Map、以及Array(数组),以及Properties也可以配置。举个栗子:在上例Customer实体类的基础上修改,分别增加List属性、Set属性、Map属性和数组属性一个,具体代码如下:```javapackagecom.xzy.bean;importjava.util.Arrays;importjava.util.List;importjava.util.Map;importjava.util.Set;publicclassCustomer{privateStringname;privateStringsex="男";privateListshopCar;//购物车privateSetprice;//价格privateMapgoods;//物品privateString[]address;//地址//省略getter、setter....@OverridepublicStringtoString(){return"Customer{"+"name='"+name+'\''+",sex='"+sex+'\''+",shopCar="+shopCar+",price="+price+",goods="+goods+",address="+Arrays.toString(address)+'}';}}```-使用``标签给List类型注入初始值:```xml元素-->书手机衣服电脑```-使用``标签给Set类型注入初始值:```xml元素-->"#{3.5*6}""#{2000*2}""#{100*6}""#{5000*1}"```-使用``标签给Map类型注入初始值:```xml元素,特别注意,使用来指定一条数据的key和value-->```-使用``标签给List类型注入初始值:```xml元素-->书手机衣服电脑```-使用``标签给List类型注入初始值:```xml元素-->西安北京南京广州```最后,对于customer的DI配置如下:```xml元素-->书手机衣服电脑元素-->"#{3.5*6}""#{2000*2}""#{100*6}""#{5000*1}"元素,特别注意,使用来指定一条数据的key和value-->元素-->西安北京南京广州```程序执行结果:####7、使用Annotation自动装配扫描要使用Anntation配置spring容器,首先需要在ApplicationContext.xml文件中配置如下信息:```xml```设置组件与bean命名>1.@Repository,@Service,and@Controller,@Component这四个Annotation功能相同都是声明一个bean组件,不同的是>@Repository声明Dao层>@Service声明Service层>@controller声明控制器层,>@Component就是一个普通的组件,例如对pojo实体可以使用他>都是用在类上的Annotation,说明让Spring实例化此类的对像,并放入spring容器中>2.@componet(“id”)其中id声明bean对像的名字举个栗子:新建Student.java```javapackagecom.xzy.bean;importorg.springframework.stereotype.Component;@Component//这一句就是告诉Spring这是个普通的组件publicclassStudent{privateintage;privateStringname;publicStudent(){System.out.println("1.实例化了bean。。。。。");}//省略getter、setter@OverridepublicStringtoString(){return"Student{"+"age="+age+",name='"+name+'\''+'}';}}```测试代码:```javapackagecom.xzy;importcom.xzy.bean.Student;importorg.apache.log4j.Logger;importorg.junit.Test;importorg.junit.runner.RunWith;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.test.context.ContextConfiguration;importorg.springframework.test.context.junit4.SpringJUnit4ClassRunner;/***UnittestforsimpleApp.*/@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(value={"/ApplicationContext.xml"})publicclassAppTest{privatestaticLoggerlog=Logger.getLogger(AppTest.class);@Autowired//自动注入,Spring会为我们自动从容器中找到student的对象然后注入这里的变量privateStudentstudent;@Testpublicvoidtest01(){System.out.println(student);}}```程序运行结果:@Repository,@Service,and@Controller新建StudentDao以及StudentDaoImpl```javaStudentDaopackagecom.xzy.Dao;importcom.xzy.bean.Student;publicinterfaceStudentDao{/***增加一个学生*@paramstu*@return*/publicvoidaddStudent(Studentstu);}```写一个StudentDaoImpi,模拟DAO层,并使用@Respositiry告诉Spring这是DAO层的组件```javaStudentDaoImplpackagecom.xzy.Dao.DaoImpi;importcom.xzy.Dao.StudentDao;importcom.xzy.bean.Student;importorg.springframework.stereotype.Repository;@RepositorypublicclassStudentDaoImpiimplementsStudentDao{publicStudentDaoImpl(){System.out.println("Repository层实例化");}@OverridepublicvoidaddStudent(Studentstu){System.out.print("3.Dao层处理数据:");System.out.println("向数据库发一条insert语句添加一个学生:"+stu.getName());}}```新建StudentService.java,模拟Service层,使用@Service注解告诉Spring这是一个Service的组件```javaStudentService.javapackagecom.xzy.Service;importcom.xzy.Dao.StudentDao;importcom.xzy.bean.Student;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;@ServicepublicclassStudentService{publicStudentService(){System.out.println("Service层实例化");}@AutowiredprivateStudentDaostuImp;publicvoidadd(Studentstu){System.out.println("2.service层收到控制层的数据后发给Dao层");stuImp.addStudent(stu);}}```新建StudentServlet.java,模拟控制器层,并使用@Controller注解告诉Spring这是控制器。```javaStudentServlet.javapackagecom.xzy.Servlet;importcom.xzy.Service.StudentService;importcom.xzy.bean.Student;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;@ControllerpublicclassStudentServlet{publicStudentServlet(){System.out.println("Controller层实例化");}@AutowiredprivateStudentServicestuService;@AutowiredprivateStudentstu;publicvoidaddAction(){System.out.println("1.控制层发数据给sevice层");stu.setAge(23);stu.setName("狗子");stuService.add(stu);}}```最终的执行结果:可以看到这三个注解是由其存在的意义的,@Repository是告诉spring这是一个DAO层的类应该最先实例化(确实也应该如此),@Service层接着DAO层实例化完成后实例化,and@Controller层最后实例化,只用这样才可以确保不会空指针。设置组件扫描的base-packages>@Configuration@ComponentScan(“基包名”)PublicclassAppConfig{}>@Configuration@ComponentScan(basepackages=“基包名”)PublicclassAppConfig{}>@Configuration@ComponentScan(basepackages={“基包名”,”...”})PublicclassAppConfig{}>@Configuration@ComponentScan(basePackageClasses={App1Config.class,App2Config.class})PublicclassAppConfig{}以上App1Config与App2Config所在的包作为组件扫描的基础包8.3Annotation自动装配>1.@Autowired自动装配和JSR330’s@Inject对应,可用在构造方法、属性setter方法,有属性@Autowired(required=false)>@Primary用于声明bean的首先,用在多个bean,无法选择装配谁的情况可以指明使用哪个>2.@Required声明依赖必须提供用在setter方法>@RequiredpublicvoidsetMovieFinder(MovieFindermovieFinder){this.movieFinder=movieFinder;}>3.@Qualifiers注明要装配bean的标识,用于多个bean无法确定装配哪个的情况8.4处理自动装配的歧义Spring提供的自动装配是非常好用,可是用这么个问题:比如,一个接口有三个实现类,当要将接口类型自动装配置时,就出现不唯一的问题,Spring会抛出NoUniqueBeanDefinitionException。正如下面这种情况:写一个接口:```javapackagecom.xzy.utils;importorg.springframework.stereotype.Component;@ComponentpublicinterfaceReadData{publicvoidread();}```接口的三个实现类:```javapackagecom.xzy.utils;importorg.springframework.stereotype.Component;@ComponentpublicclassUSBReadimplementsReadData{@Overridepublicvoidread(){System.out.println("USB读取数据.....");}}``````javapackagecom.xzy.utils;importorg.springframework.stereotype.Component;@ComponentpublicclassSSDReadimplementsReadData{@Overridepublicvoidread(){System.out.println("SSD读取数据......");}}``````javapackagecom.xzy.utils;importorg.springframework.stereotype.Component;@ComponentpublicclassBlueReadimplementsReadData{@Overridepublicvoidread(){System.out.println("蓝牙读取数据.......");}}```这时如果让Spring给我们自动装配,他都懵逼了,因为这个接口有3个实现类,都可以装配,他不知道装配那个,如下图所示:此时如果直接运行就会发生如下异常:```accesslog2019-08-0121:49:51[ERROR]-[org.springframework.test.context.TestContextManager]CaughtexceptionwhileallowingTestExecutionListener[org.springframework.test.context.support.DependencyInjectionTestExecutionListener@685cb137]topreparetestinstance[com.xzy.AppTest@50a638b5]org.springframework.beans.factory.UnsatisfiedDependencyException:Errorcreatingbeanwithname'com.xzy.AppTest':Unsatisfieddependencyexpressedthroughfield'read';nestedexceptionisorg.springframework.beans.factory.NoUniqueBeanDefinitionException:Noqualifyingbeanoftype'com.xzy.utils.ReadData'available:expectedsinglematchingbeanbutfound3:blueRead,SSDRead,USBRead```解决办法解决方法1:在实现类的头上使用`@Primary`注解告诉Spring首选哪个装配,比如在USBRead类的头上加上@Primary:解决方法2:使用`@Qualifier`注解限定自动装配的Bean
LoveIT 2019-07-18Spring -
Spring从入门到精通-认识Spring框架
1.什么是Spring?Spring是一个J2EE框架,这个框架提供了对轻量级IoC(InversionofControl,控制反转)的良好支持,同时也提供了对AOP(AspectOrientedProgramming,面向切面编程)技术非常好的封装。相比其他框架,Spring框架的设计更加模块化,框架内的每个模块都能完成特定的工作,而且每个模块可以独立的运行,不会互相牵制。总结起来就是:Spring是一个支持IoC和AOP的轻量级开源框架,来源于RodJohnson在其著作《ExpertoneononeJ2EEdesignanddevelopment》中阐述的部分理念和原型衍生而来。Spring提倡以“最少侵入”的方式来管理应用中的代码,这意味着我们可以随时安装或者卸载Spring适用范围:任何Java应用Spring的根本使命:简化Java开发2.Spring中常用术语框架:是能完成一定功能的半成品。框架能够帮助我们完成的是:项目的整体架构、一些基础功能、规定了类和对象如何创建,如何协作等,当我们开发一个项目时,框架帮助我们完成了一部分功能,我们自己再完成一部分,那这个项目就完成了。非侵入式设计:从框架的角度可以理解为:无需继承框架提供的任何类这样我们在更换框架时,之前写过的代码几乎可以继续使用。轻量级和重量级:轻量级是相对于重量级而言的,轻量级一般就是非入侵性的、所依赖的东西非常少、资源占用非常少、部署简单等等,其实就是比较容易使用,而重量级正好相反。JavaBean:即符合JavaBean规范的Java类POJO:即PlainOldJavaObjects,简单老式Java对象它可以包含业务逻辑或持久化逻辑,但不担当任何特殊角色且不继承或不实现任何其它Java框架的类或接口。>注意:bean的各种名称——虽然Spring用bean或者JavaBean来表示应用组件,但并不意味着Spring组件必须遵循JavaBean规范,一个Spring组件可以是任意形式的POJO。容器:在日常生活中容器就是一种盛放东西的器具,从程序设计角度看就是装对象的的对象,因为存在放入、拿出等操作,所以容器还要管理对象的生命周期。3.使用Spring框架的好处是什么?轻量:Spring是轻量的,基本的版本大约2MB控制反转(IOC):Spring通过控制反转实现了松散耦合,对像们给出它们的依赖,而不是创建或查找依赖的对象们面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开容器:Spring包含并管理应用中对象的生命周期和配置MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品事务管理:Spring提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)异常处理:Spring提供方便的API把具体技术相关的异常(比如由JDBC,HibernateorJDO抛出的)转化为一致的unchecked异常4.Spring能帮我们做什么?Spring能帮我们根据配置文件创建及组装对象之间的依赖关系。Spring面向切面编程能帮助我们无耦合的实现日志记录,性能统计,安全控制。Spring能非常简单的帮我们管理数据库事务。Spring还提供了与第三方数据访问框架(如Hibernate、JPA)无缝集成,而且自己也提供了一套JDBC访问模板来方便数据库访问。Spring还提供与第三方Web(如Struts1/2、JSF)框架无缝集成,而且自己也提供了一套SpringMVC框架,来方便web层搭建。Spring能方便的与JavaEE(如JavaMail、任务调度)整合,与更多技术整合(比如缓存框架)。5.Spring由哪些模块组成?Spring的模块组成架构图大致如下图所示。Spring5框架体系结构示意图从上图可以看到,Spring框架主由5个模块组成,他们分别是:Spring核心容器、SpringAOP、数据访问\集成、Web、SpringTest。下面介绍这5个部分的作用。(1)Spring核心容器Spring框架的核心容器,它提供了Spring的基本功能。这个模块中最主要的一个组件是BeanFactory(IoC容器),它使用工厂模式来创建所需要的对象。同时BeanFactory使用IOC思想、通过读取XML文件的方式实例化对象,BeanFactory提供了组件生命周期的管理、组件的创建、装配、销毁等功能。Spring核心容器主要包含4个包:spring-core:依赖注入IoC与DI的最基本实现spring-beans:Bean工厂与bean的装配spring-context:扩展核心容器,提供了spring的上下文环境,给我们提供了非常有用的服务,比如:国际化、EMail、JNDI访问等。spring-expression:spring表达式语言SPEL它们的依赖关系#####(2)SpringAOPSpringAOP是对面向切面编程思想的实现,它使得Spring框架管理对象支持AOP。SpringAOP部分包含4个包:spring-aop:面向切面编程spring-aspects:集成AspectJspring-instrument:提供一些类级的工具支持和ClassLoader级的实现,用于服务器spring-instrument-tomcat:针对tomcat的instrument实现它们的依赖关系#####(3)数据访问/集成提供了对数据访问对象模式(DAO)、JDBC、事务控制、对现有ORM框架的集成的支持等功能。spring-jdbc:jdbc的支持spring-tx:事务控制spring-orm:对象关系映射,集成orm框架spring-oxm:对象xml映射spring-jms:java消息服务它们的依赖关系(4)Web;该层提供了创建Web应用程序的支持。web部分包含4个包:spring-web:提供了Servlet监听器的Context和Web应用的上下文。同时还集成了一些现有的Web框架。spring-webmvc:提供了一个构建Web应用的MVC实现。spring-webmvc-portlet:基于portlet的mvc实现spring-struts:与struts的集成,不推荐使用,spring4不再提供它们的依赖关系#####(5)TestSpring提供的一个类似junit的测试模块,用于测试Spring应用。test部分只有一个模块spring-test:spring测试,提供junit与mock测试功能它们的依赖关系
LoveIT 2019-07-16Spring -
MyBatis SQL映射文件配置详解
配置SQL映射文件MyBatis中的SQL映射文件只有很少的几个顶级元素(按照它们应该被定义的顺序如下):cache–给定命名空间的缓存配置。cache-ref–其他命名空间缓存配置的引用。resultMap–是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。sql–可被其他语句引用的可重用语句块。insert–映射插入语句update–映射更新语句delete–映射删除语句select–映射查询语句1、select元素select元素就是用来查询的,在select里嵌入SQLselect查询语句,就像下边这样:其中select元素中的id属性是必须的,它的值是对应Mapper接口中的一个方法,当调用这个接口就是调用这个sql。关于select元素常用的属性具体如下:属性描述id在命名空间中唯一的标识符,可以被用来引用这条语句。这个是必须的属性parameterType将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为MyBatis可以通过TypeHandler推断出具体传入语句的参数,默认值为unset。resultType返回的期望类型的类的完全限定名或别名。这个属性是可选的resultMap返回值类型是是个map集合,可用于多表联查后的结果MyBatis会封装成一个map返回,这个属性是可选的flushCache将其设置为true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。这个属性是可选的useCache将其设置为true,将会导致本条语句的结果被二级缓存,默认值:对select元素为true。这个属性是可选的timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为unset(依赖驱动)。这个属性是可选的2、insertupdatedelete insertupdatedelete元素分别对应SQL语句中的insert、update、delete,分别实现对数据库记录的插入、更新和删除。他们可以有的属性值如下:属性描述id在命名空间中唯一的标识符,可以被用来引用这条语句。这个是必须的属性parameterType将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为MyBatis可以通过TypeHandler推断出具体传入语句的参数,默认值为unset。useGeneratedKeys(仅对insert和update有用)这会令MyBatis使用JDBC的getGeneratedKeys方法来取出由数据库内部生成的主键,默认值为false,这个属性是可选的。keyProperty指定实体类中的主键属性,MyBatis会通过getGeneratedKeys的返回值或者通过insert语句的selectKey子元素设置它的键值,他和useGeneratedKeys配合起来才能工作flushCache将其设置为true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true。这个属性是可选的useCache将其设置为true,将会导致本条语句的结果被二级缓存,默认值:对增删改元素为false。这个属性是可选的timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为unset(依赖驱动)。这个属性是可选的keyColumn指定数据表中的主键字段名,这个设置仅在某些数据库(像PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。3、resultMapresultMap元素是MyBatis中最重要最强大的元素。ResultMap的设计就是简单语句不需要明确的结果映射,而很多复杂语句确实需要描述它们的关系。3.1简单映射例如下面这个例子::JavaBean是这样的:Mybatis会将基于JavaBean的规范,这些在select语句中会精确匹配到列名。这样一个语句简单作用于所有列被自动映射到HashMap的键上,这由resultType属性指定。也就是说,对于resultTypeMyBatis会结果封装一个map返回。3.2高级映射有时候我们避免不了多表联查,这样带来的问题是返回的结果类型中的一个字段在resultType中的不存在,这就会造成问题。MyBatis中使用resultMap来解决这个问题。resultMap元素有很多子元素和一个值得讨论的结构。下面是resultMap标签中可以使用的属性如下:resultMap:constructor-类在实例化时,用来注入结果到构造方法中idArg-ID参数;标记结果作为ID可以帮助提高整体效能arg-注入到构造方法的一个普通结果id–一个ID结果;标记结果作为ID可以帮助提高整体效能result–注入到字段或JavaBean属性的普通结果association–一个复杂的类型关联;许多结果将包成这种类型嵌入结果映射–结果映射自身的关联,或者参考一个collection–复杂类型的集嵌入结果映射–结果映射自身的集,或者参考一个discriminator–使用结果值来决定使用哪个结果映射case–基于某些值的结果映射嵌入结果映射–这种情形结果也映射它本身,因此可以包含很多相同的元素,或者它可以参照一个外部的结果映射。描述描述id当前命名空间中的一个唯一标识,用于标识一个resultmap.type类的全限定名,或者一个类型别名autoMapping如果设置这个属性,MyBatis将会为这个ResultMap开启或者关闭自动映射。这个属性会覆盖全局的属性autoMappingBehavior。默认值为:unset。下面是一个例子:学生实体:Student.java课程实体:Course.java测试类AppTest.java运行结果:cache和cache-ref的用法参考另一篇笔记MyBatis缓存配置4、cachecache标签是用于指定MyBatis的二级缓存的具体实现的。虽然MyBtis自带二级缓存的实现,但是MyBatis在缓存方面毕竟和专门的第三方缓存服务提供方还是有差距的,MyBatis官方也深知自己的不足,因此MyBatis提供了org.apache.ibatis.cache.Cache接口,任何只要实现了这个接口的缓存中间件都可成为MyBatis二级缓存,具体的用法请参考我的博客*MyBatis整合Redis作为二级缓存或MyBatis整合Encache作为二级缓存*。5、sql我们在写这个SQL映射文件的时候,有很多重复的SQL语句。将这些重复的SQL语句提取出来,称为SQL片段,给不同方法使用。把我们的SQL语句提取出来,用SQL标签包起来。然后再用include标签,导进语句中。像这些我们需要查询的字段,也可以这样子搞。
LoveIT 2019-07-12MyBatis -
MyBatis整合Redis作为二级缓存
Redis不像Ehcache一样提供了针对MyBatis的二级缓存的实现,因此需要我们自己来实现缓存的逻辑,但是归根到底原理是一样的,就是实现MyBatis的org.apache.ibatis.cache.Cache接口,在实现类中我们把数据的存取中间件变为了Redis而已,下面是一个实现的示例:之后在Mapper文件中指定缓存使用我们实现的这个缓存即可。
LoveIT 2019-07-11MyBatis -
MyBatis整合Ehcache作为二级缓存
0、Ehcache简介 Encache是一个纯粹的Java进程内的缓存框架,具有快速、精干等特点。具体来说,Encache主要特点如下。快速简单多种缓存策略(FIFO、LRU、LFU)缓存数据有内存和磁盘两级,无需担心容量问题缓存数据会在虚拟机重启的过程写入磁盘可以通过RMI、可插入API等方式进行分布式缓存具有缓存和缓存接口的侦听接口 因为以上诸多优点,MyBatis项目开发者最早提供了Ehcache的MyBatis二级缓存实现,该项目名Ehcache-cache,EhCache官方网址是http://www.mybatis.org/ehcache-cache/。下面,我来演示一下在一个标准的Maven项目中集成EhCache框架。1、添加Encache项目依赖除了基本的MyBatis依赖、数据库驱动以外还需要在pom.xml中添加如下依赖:####2、配置ehcache.xml2.1ehcache缓存配置文件编写 和MyBatis一样,EnCache也需要外部的配置文件,而且要求这个文件的名字必须是encache.xml,并且必须放在类路径的根目录下,即src/main/resources目录下 如果想增加一个针对某个MyBatisSQL映射文件的个性化缓存配置时,可以在ehcache.xml文件中添加一个和SQL映射文件命名空间一致的缓存配置,例如针对UserMapper,可以进行如下配置:2.2EhCache可配置信息总览nameCache的名称,必须是唯一的(ehcache会把这个cache放到HashMap里)。maxElementsInMemory在内存中缓存的element的最大数目maxElementsOnDisk在磁盘上缓存的element的最大数目,默认值为0,表示不限制。eternal设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。overflowToDisk如果内存中数据超过内存限制,是否要缓存到磁盘上。copyOnRead判断从缓存中读取数据是否是返回对象的引用还是赋值一个对象返回。默认是false,即返回数据的引用,这种情况和MyBatis默认的缓存中只读对象是相同的。如果为true,那就是可读写缓存,每次读取缓存是都赋值一个新的实例。copyOnWrite判断写入缓存时直接缓存对象的引用还是赋值一个对象后timeToIdleSeconds对象空闲时间,指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问。timeToLiveSeconds对象存活时间,指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问。diskPersistent是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false。diskExpiryThreadIntervalSeconds对象检测线程运行时间间隔。标识对象状态的线程多长时间运行一次。diskSpoolBufferSizeMBDiskStore使用的磁盘大小,默认值30MB。每个cache使用各自的DiskStore。memoryStoreEvictionPolicy如果内存中数据超过内存限制,向磁盘缓存时的策略。默认值LRU,可选FIFO、LFU。缓存清空策略:1、FIFO,firstinfirstout(先进先出).2、LFU,LessFrequentlyUsed(最少使用).意思是一直以来最少被使用的。缓存的元素有一个hit属性,hit值最小的将会被清出缓存。3、LRU,LeastRecentlyUsed(最近最少使用).(ehcache默认值).缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。3、修改MyBatisSQL映射文件Ehcache提供了如下两个可选的缓存实现:*org.mybatis.caches.ehcache.EhcacheCache*org.mybatis.caches.ehcache.LoggingEhcache 这两个缓存中,第二个是带日志的缓存,由于MyBatis初始化时,如果Cache不是继承自LoggingEhcache,MyBatis便会使用LoggingEhcache装饰代理缓存,所以上面两个缓存使用时并没有区别,都会输出命中率的日志。修改后的UserMapper.xml配置如下:在src/main/test目录下新建测试类AppTest.java测试是否用上了EhCache:运行结果截图:在配置的磁盘路径下确实有缓存文件:4、在SpringBoot2.x上整合ehcache作为MyBatis二级缓存 在SpringBoot上集成ehcache非常简单,其他的东西都一样,按照上面的方法配置即可,之后只需要在SpringBoot项目的配置文件中做如下配置:
LoveIT 2019-07-10MyBatis -
深入理解MyBatis缓存机制
使用缓存可以是应用更快的获取数据,避免频繁的数据库交互,尤其是在查询越多、缓存命中率越高的情况下,使用缓存的作用就越明显。MyBatis作为持久层框架,提供了强大的查询缓存特性,可非常方便的配置和使用。MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存。1、默认情况下,一级缓存(SqlSession级别的缓存,也称为本地缓存)是开启的,并且不能控制。2、二级缓存需要手动开启和配置,他是和命名空间绑定的,即二级缓存需要在全局配置文件中开启,并且在sql映射文件中指定二级缓存使用的缓存中间件(MyBatis有自己的实现,但是毫不客气的说和专业的缓存中间件一比就是个弟弟!!!)3、为了提高扩展性。MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存(或者使用第三方缓存)1、一级缓存先通过一个简单的演示看看MyBatis一级缓存是如何起作用的。运行结果截图如下: 可以看到,两次查询值MyBatis只给数据库发送了一次SQL语句,但是两次查询的结果都是一样的,而且再往下发现两个List<User>对象竟然是同一个对象,之所以这样就是MyBatis的一级缓存在起作用。 在同一个SqlSession中查询是MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入PerpetualCache的HashMap本地缓存对象中。如果同一个SqlSession中执行的方法和参数完全一致,闹通过算法就会生成相同的键值,当Map缓存对象中已经存在该键值时,就会返回缓存中的对象,而不会再给数据库发sql语句了。但是要注意,下面几种情况发生会使一级缓存会失效:1.不同的SqlSession用不同的一级缓存,他们之间的数据不能共享2.就算是同一个SqlSession对象,但是如果执行的方法和参数不同也不行3.默认情况下,在SqlSession期间执行任何一次增删改操作,一级缓存会被清空4.手动清空一级缓存,一级缓存也会失效下面分别举例说明:由于MyBatis的一级缓存存在于SqlSession生命周期中,一次不同的SqlSession当然会有不同的一级缓存。测试结果如下: MyBatis的<insert>、<delete>、<update>和<select>标签都有一个属性:flushCache,在默认情况下,对于增删改操作这个标签默认值是true,也就是每次操作后要清空缓存,而对于<select>操作这个属性默认值是false,也即不刷新缓冲。也就是下面这段代码要说明的:测试结果如下:下面的这种情况就更直观了,但本质上和上面那种情况是一样的——都是一级缓存被清空了。测试结果如下:2、二级缓存二级缓存默认也是采用PerpetualCache,HashMap存储;二级缓存的存储作用域为Mapper(确切说是namespace),即一个Mapper执行了insert、update或delete操作,不影响另外一个Mapper(不同namespace);二级缓存可自定义存储实现,如Ehcache、redis;二级缓存开启后,需要对应的javaBean实现,并且这个javaBean要实现Serializable接口进行序列化2.1配置二级缓存 二级缓存有两种配置方法,一种是基于Mapper.xml文件来配置;另一种就是基于Mapper.java接口来配置。下面分别来看看如何配置使用MyBatis的二级缓存: 首先,无论是通过Mapper.xml文件来配置,还是通过Mapper.java接口来配置,都需要在mybatis-config.xml文件中通过settings设置显式地开启二级缓存:2.2在Mapper.xml中配置二级缓存 在xml文件中配置的方法很简单,在保证二级缓存的全局配置开启的情况下,在UserMapper.xml中只需要添加<cache></cache>即可。<cacha>标签的属性:*eviction:缓存回收策略:flushInterval:缓存多长时间清空一次,默认不清空,单位毫秒ms LRU——最少使用的,移除最长时间不适用的对象; FIFO——先进先出 WEAK——弱引用,更积极的移除基于垃圾回收器状态和弱引用规则的对象 SOFT——软引用,更积极的移除基于垃圾回收器状态和弱引用规则的对象*flushInterval:缓存多久清空一次,默认不清空,时间毫秒ms*readOnly:缓存是否只读*size:缓存觉存放元素个数,默认1024*type:自定义缓存的全类名2.3在Mapper接口中配置二级缓存 在接口中配置主要是借助@CacheNamespace这个注解,但是要注意:配置文件和接口注释是不能够配合使用的。只能通过全注解的方式或者全部通过xml配置文件的方式使用。也就是说你用了配置文件就不要用这种方式,用了接口配置的方式就别用xml配置文件的方式。如下:并在mybatis-config.xml中重新配置Mapper映射文件:4、使用自带的MyBatis二级缓存 配置Mybatis二级缓存的方法有两种,只要配置好,二级缓存就可以工作了。但是在使用前需要注意的是,由于MyBatis使用SerializedCache序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。因此,如果配置了只读缓存,MyBatis就会使用Map来存储缓存值。而这个缓存类要求所有被序列化的对象必须实现Serializable接口。因此我们的javaBean需要实现Serializable接口。做好所有准备后,编写一个测试类来看看二级缓存的效果:测试结果:5、MyBatis缓存的执行逻辑1.当一个SqlSession第一次执行一次select后,查到数据后会首先把查询到的结果保存到一级缓存中2.当该SqlSession被关闭或者提交后,保存在一级缓存中的数据会转移到二级缓存中(前提是正确开启并配置了二级缓存)3.当另一个SqlSession第一次执行同样select时,首先会在二级缓存中找,如果没找到,就去自己的一级缓存中找,找到了就返回,如果没找到就去数据库查,MyBatis就是通过这样的机制从而减少了数据库压力提高了性能。MyBatis的执行流程总结起来就是:二级缓存-->一级缓存-->数据库注意事项:1.如果SqlSession执行了DML操作(insert、update、delete),并commit了,那么mybatis就会清空当前mapper缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读2.mybatis的缓存是基于[namespace:sql语句:参数]来进行缓存的,意思就是:SqlSession的HashMap存储缓存数据时,是使用[namespace:sql:参数]作为key,查询返回的语句作为value保存的。
LoveIT 2019-07-09MyBatis -
深入理解Mybatis中的一对一、一对多映射关系
1、搭建实验环境1).新建数据库mybatis62).新建表:sys_user、sys_role、sys_user_role,sys_privilege、sys_role_privilegesql脚本如下:3).新建每个表对应的实体JavaBeanSysUser.javaSysRole.javaSysUserRole.javaSysPrivilege.javaSysRolePrivilege.java4).新建SysUserMapper接口5).新建mybaits配置文件2、一对一映射Mybatis中的映射方式有两种,一种是通过resultType自动映射,另一种是通过resultMap自己设置映射规则。resultMap又有两种映射方式:嵌套结果映射和嵌套查询映射。嵌套结果映射:给数据库发一条复杂的sql语句把查询到的结果根据映射规则映射到不同的对象中嵌套查询映射:会发多条sql简单的语句,Mybatis会把多条sql语句的查询据结果封装到一个对象中。如果在mybatis全局配置中设置了延迟加载:配置xml文件:通过resultMap一对一映射,在SysUser实体类中新增一个属性privateSysRolerole标识用户的角色,然后在SysUserMapper.xml中写如下映射userRoleMap:在SysUserMapper接口中增加方法:publicSysUserselectUserAndRoleById(Longid);测试方法:这是日志的打印结果:改进上面的映射方法:上面的映射方法虽然可以使用,但是耦合性太高,而且最大的问题是配置文件不能复用,啥意思?就是说,如果我现在需要一个单独查用户的方法,那我还得专门为查用户写一个sql配置,这样做非常的糟糕,当项目非常大的时候,配置文件的重复配置代码将会非常的多,那么如何解决这个问题呢?我们接着往下看:在SysUserMapper.xml中增加一个专门为查用户的映射userMap:然后刚才的上面的userRoleMap就可以修改成下面的样子:使用association元素替代上面的role.XXX:可是这样还是不行,实际的开发中,肯定会有关于单独查询sys_role的需求,而且人家sys_role肯定也会有单独的mapper,这样就又会存在重复配置的问题,解决这个问题需要用到association元素的另一个功能,具体看代码:新建SysRoleMapper.xml,并配置roleMap如下:这样我们在SysUserMapper.xml就可以把刚才的配置彻底抽取了出来:这样就彻底把模块与模块分开了,当然我们也可以顺便实现以下selectRoleById,下面是配置后的运行时打印的日志的部分:可以看到,日志的打印结果相同,但是修改后的方式肯定比一开始的方法要好,因为这样就把各个查询模块化了,就像搭积木,一个个简单的“积木块”最后通过合理的组织,就可以实现不同的复杂查询。一对一的嵌套查询映射上面这种方法是嵌套结果映射,就是直接给数据库发一条sql语句,数据库返回数据后Mybatis根据映射规则,把数据映射到不同的对象中。而嵌套查询映射则是多次给数据库发简单的sql语句,然后把不同的数据映射到一个对象中。1.association元素的嵌套查询: select:另一个映射查询map的id column:将主查询的那个列的结果作为嵌套查询的参数传给嵌套查询方法 fetchType:数据加载的方式[lazy或eager],即延迟加载或积极加载, 配置这个属性会覆盖全局配置中飞lazyLoadingEnabled2.MyBatis的嵌套查询可以实现懒加载,简单点的说就是不用的时候就不给你加载,等用的时候才去给你加载,这样做的好处是可以降低数据库的压力,做到按需响应。那么要用懒加载必须在全局配置文件中设置如下:<settings><settingname="aggressiveLazyLoading"value="false"/><!--vallue=false时按需加载-,否者全部加载-><settingname="lazyLoadingEnabled"value="true"/><!--是否开启懒加载,true表示开启--><settingname="lazyLoadTriggerMethods"value="equals,clone,hashCode,toString"/><!--懒加载模式下如果调用value后的方法将全部加载--></settings>在SysUserMapper.xml中写一个id为userRoleMapSelecct的新的映射关系,并写SQL查询语句如下:配置好后我们在SysUserMapper接口中增加selectUserAndRoleById3方法,然后写测试:日志的打印结果:可以看到,MyBatis分别给数据库发了两条sql语句,这是因为直接打印,在配置文件的setting中有一个元素lazyLoadTriggerMethods默认值value="equals,clone,hashCode,toString",当程序中调用这些方法的时就会全部加载。但是如果我们在程序中只是用到User的一些属性,那么Mybatis就只发查user的sql语句,把测试代码中的System.out.println(u);改成System.out.println(u.getUserName()+","+u.getUserEmail()+","+u.getCreateTime());再次运行打印的日志部分如下:可以看到,只发了一条sql语句。这就是MyBatis的延迟加载(懒加载),也就是说,当你没用到的时候,MyBatis压根不会帮你去查这个数据。这样一来的好处是会减轻数据库的压力。3、一对多映射使用collection实现一对多映射,collection的属性和用法与association基本是一样的,只是collection是专门用来映射数据库中一对多的多方元素的一个集合。比如现在有这样的需求:查询所有用户以及每个用户在本系统中所拥有的角色。这是一个很典型的一对多的例子,一个用户在系统中有多个角色。举个栗子:在SysUser.java中增加属性List<SysRole>roleList在SysUserMapper.xml中增加reultMapuserRoleListMap,由于roleMap在前面已经定义过了,这里就可以直接使用测试代码:程序运行打印的日志:前面这个实现了一层嵌套,就是一个主查询下面只有一个层子查询然后就结束了,下面我们尝试来实现一个两层嵌套:比如现在有这样的需求:查询所有的用户的角色,以及每个角色拥有的权限。很好想,就是一个用户可以有多个角色,每个角色又有不同的权限。实现:首先在SysRole中增加一个属性privateList<SysPrivilege>privilegeList新建SysPrivilegeMapper.xml文件,增加一个resultMapprivilegeMap在SysRoleMapper.xml中增加一个resultMaprolePrivilegeListMap,由于roleMap在上面已经定义过了,,用extends继承他就可以直接使用了。在SysUserMapper.xml中增加reusltMap:userRoleListMapSelect在SysUserMapper接口中增加方法selectAllUserAndRole2,并且编写测试代码:程寻运行打印的日志的一部分:这样,我们就算是实现了一对多的两层嵌套结果映射的一个查询,这种方式在日常非常常见,也是MyBatis中非常强大的地方。最后我们看一下一对多的嵌套查询映射,和一对一的实现方法是类似的:前面我们写过一个resultMaprolePrivilegeListMap,但是还没有为他写接口方法,这里我们首先来实现这个:在SysRoleMapper.xml增加一个resultMap:rolePrivilegeListMap2在SysRoleMapper接口中增加一个方法selectRoleByUserId最后在SysUserMapper.xml中增加一个resultMap:userRoleListMap2在SysUserMapper接口中增加一个方法selectAllUserAndRole2:测试代码:程寻运行打印的日志:可以看到,程序运行时,MyBatis给数据库发了多条sql语句,最终通过预定的映射集合,把这些查出来的数据放进去,之后打包组合成一个List<SysRole>对象返回。
LoveIT 2019-07-08MyBatis -
MyBatis-配置SQL映射文件
MyBatis中的SQL映射文件只有很少的几个顶级元素(按照它们应该被定义的顺序如下):cache–给定命名空间的缓存配置。cache-ref–其他命名空间缓存配置的引用。resultMap–是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。sql–可被其他语句引用的可重用语句块。insert–映射插入语句update–映射更新语句delete–映射删除语句select–映射查询语句1、select元素select元素就是用来查询的,在select里嵌入SQLselect查询语句,就像下边这样:其中select元素中的id属性是必须的,它的值是对应Mapper接口中的一个方法,当调用这个接口就是调用这个sql。关于select元素常用的属性具体如下:属性描述id在命名空间中唯一的标识符,可以被用来引用这条语句。这个是必须的属性parameterType将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为MyBatis可以通过TypeHandler推断出具体传入语句的参数,默认值为unset。resultType返回的期望类型的类的完全限定名或别名。这个属性是可选的resultMap返回值类型是是个map集合,可用于多表联查后的结果MyBatis会封装成一个map返回,这个属性是可选的flushCache将其设置为true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。这个属性是可选的useCache将其设置为true,将会导致本条语句的结果被二级缓存,默认值:对select元素为true。这个属性是可选的timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为unset(依赖驱动)。这个属性是可选的2、insertupdatedelete insertupdatedelete元素分别对应SQL语句中的insert、update、delete,分别实现对数据库记录的插入、更新和删除。他们可以有的属性值如下:属性描述id在命名空间中唯一的标识符,可以被用来引用这条语句。这个是必须的属性parameterType将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为MyBatis可以通过TypeHandler推断出具体传入语句的参数,默认值为unset。useGeneratedKeys(仅对insert和update有用)这会令MyBatis使用JDBC的getGeneratedKeys方法来取出由数据库内部生成的主键,默认值为false,这个属性是可选的。keyProperty指定实体类中的主键属性,MyBatis会通过getGeneratedKeys的返回值或者通过insert语句的selectKey子元素设置它的键值,他和useGeneratedKeys配合起来才能工作flushCache将其设置为true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true。这个属性是可选的useCache将其设置为true,将会导致本条语句的结果被二级缓存,默认值:对增删改元素为false。这个属性是可选的timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为unset(依赖驱动)。这个属性是可选的keyColumn指定数据表中的主键字段名,这个设置仅在某些数据库(像PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。3、resultMapresultMap元素是MyBatis中最重要最强大的元素。ResultMap的设计就是简单语句不需要明确的结果映射,而很多复杂语句确实需要描述它们的关系。3.1简单映射例如下面这个例子::JavaBean是这样的:Mybatis会将基于JavaBean的规范,这些在select语句中会精确匹配到列名。这样一个语句简单作用于所有列被自动映射到HashMap的键上,这由resultType属性指定。也就是说,对于resultTypeMyBatis会结果封装一个map返回。3.2高级映射有时候我们避免不了多表联查,这样带来的问题是返回的结果类型中的一个字段在resultType中的不存在,这就会造成问题。MyBatis中使用resultMap来解决这个问题。resultMap元素有很多子元素和一个值得讨论的结构。下面是resultMap标签中可以使用的属性如下:resultMap:constructor-类在实例化时,用来注入结果到构造方法中idArg-ID参数;标记结果作为ID可以帮助提高整体效能arg-注入到构造方法的一个普通结果id–一个ID结果;标记结果作为ID可以帮助提高整体效能result–注入到字段或JavaBean属性的普通结果association–一个复杂的类型关联;许多结果将包成这种类型嵌入结果映射–结果映射自身的关联,或者参考一个collection–复杂类型的集嵌入结果映射–结果映射自身的集,或者参考一个discriminator–使用结果值来决定使用哪个结果映射case–基于某些值的结果映射嵌入结果映射–这种情形结果也映射它本身,因此可以包含很多相同的元素,或者它可以参照一个外部的结果映射。描述描述id当前命名空间中的一个唯一标识,用于标识一个resultmap.type类的全限定名,或者一个类型别名autoMapping如果设置这个属性,MyBatis将会为这个ResultMap开启或者关闭自动映射。这个属性会覆盖全局的属性autoMappingBehavior。默认值为:unset。下面是一个例子:学生实体:Student.java课程实体:Course.java测试类AppTest.java运行结果:cache和cache-ref的用法参考另一篇笔记MyBatis缓存配置4、cachecache标签是用于指定MyBatis的二级缓存的具体实现的。虽然MyBtis自带二级缓存的实现,但是MyBatis在缓存方面毕竟和专门的第三方缓存服务提供方还是有差距的,MyBatis官方也深知自己的不足,因此MyBatis提供了org.apache.ibatis.cache.Cache接口,任何只要实现了这个接口的缓存中间件都可成为MyBatis二级缓存,具体的用法请参考我的博客MyBatis整合Redis作为二级缓存或MyBatis整合Encache作为二级缓存。5、sql我们在写这个SQL映射文件的时候,有很多重复的SQL语句。将这些重复的SQL语句提取出来,称为SQL片段,给不同方法使用。把我们的SQL语句提取出来,用SQL标签包起来。然后再用include标签,导进语句中。像这些我们需要查询的字段,也可以这样子搞。
LoveIT 2019-07-08MyBatis -
MyBatis全局配置文件详解
MyBatis使用过程中主要需要配置两个xml文件,一个是全局配置文件,另一个是SQL映射文件。本片博文我们就来学习一下MyBatis的全局文件的使用配置方式。在官方文档中也有详细的解释:MyBatis全局配置文件的官方文档。通过看文档和写代码来学习全局配置文件的使用,全局配置文件的配置主要有以下:这些子元素的配置是有顺序的,只能按照上面的顺序配置。可缺省部分子元素。1、propertiesproperties提供了一个通过外部配置文件(例如数据库配置文件)来动态配置环境的方法。以动态配置数据库为例:数据库配置文件jdbc.properties的详细配置如下:driver=com.mysql.cj.jdbc.Driverurl=jdbc:mysql://127.0.0.1:3306/xust?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&setglobaltime_zone='+8:00'username=rootpassword=95162437那么我们可以在mybatis-config.xml中这么写:2、settingssetting可以调整MyBatis的一些重要的配置,它们会改变MyBatis的运行时的行为,具体都有哪些配置可以设置可以参考官网,这里不再赘述,这里说几个常用的:设置名描述有效值默认值cacheEnabled全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。true|falsetruelazyLoadingEnabled延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。true|falsefalseaggressiveLazyLoading开启时,任一方法的调用都会加载该对象的所有延迟加载属性。否则,每个延迟加载属性会按需加载(参考lazyLoadTriggerMethods)。true|falsefalse(在3.4.1及之前的版本中默认为true)multipleResultSetsEnabled是否允许单个语句返回多结果集(需要数据库驱动支持)。true|falsetrueuseColumnLabel使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。true|falsetrueuseGeneratedKeys允许JDBC支持自动生成主键,需要数据库驱动支持。如果设置为true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如Derby)。true|falseFalseautoMappingBehavior指定MyBatis应如何自动映射列到字段或属性。NONE表示关闭自动映射;PARTIAL只会自动映射没有定义嵌套结果映射的字段。FULL会自动映射任何复杂的结果集(无论是否嵌套)。NONE,PARTIAL,FULLPARTIALautoMappingUnknownColumnBehavior指定发现自动映射目标未知列(或未知属性类型)的行为。NONE:不做任何反应WARNING:输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior'的日志等级必须设置为WARN)FAILING:映射失败(抛出SqlSessionException)NONE,WARNING,FAILINGNONEdefaultExecutorType配置默认的执行器。SIMPLE就是普通的执行器;REUSE执行器会重用预处理语句(PreparedStatement);BATCH执行器不仅重用语句还会执行批量更新。SIMPLEREUSEBATCHSIMPLEdefaultStatementTimeout设置超时时间,它决定数据库驱动等待数据库响应的秒数。任意正整数未设置(null)defaultFetchSize为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。任意正整数未设置(null)defaultResultSetType指定语句默认的滚动策略。(新增于3.5.2)FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE|DEFAULT(等同于未设置)未设置(null)safeRowBoundsEnabled是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为false。true|falseFalsesafeResultHandlerEnabled是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为false。true|falseTruemapUnderscoreToCamelCase是否开启驼峰命名自动映射,即从经典数据库列名A_COLUMN映射到经典Java属性名aColumn。true|falseFalselocalCacheScopeMyBatis利用本地缓存机制(LocalCache)防止循环引用和加速重复的嵌套查询。默认值为SESSION,会缓存一个会话中执行的所有查询。若设置值为STATEMENT,本地缓存将仅用于执行语句,对相同SqlSession的不同查询将不会进行缓存。SESSION|STATEMENTSESSIONjdbcTypeForNull当没有为参数指定特定的JDBC类型时,空值的默认JDBC类型。某些数据库驱动需要指定列的JDBC类型,多数情况直接用一般类型即可,比如NULL、VARCHAR或OTHER。JdbcType常量,常用值:NULL、VARCHAR或OTHER。OTHERlazyLoadTriggerMethods指定对象的哪些方法触发一次延迟加载。用逗号分隔的方法列表。equals,clone,hashCode,toStringdefaultScriptingLanguage指定动态SQL生成使用的默认脚本语言。一个类型别名或全限定类名。org.apache.ibatis.scripting.xmltags.XMLLanguageDriverdefaultEnumTypeHandler指定Enum使用的默认TypeHandler。(新增于3.4.5)一个类型别名或全限定类名。org.apache.ibatis.type.EnumTypeHandlercallSettersOnNulls指定当结果集中值为null的时候是否调用映射对象的setter(map对象时为put)方法,这在依赖于Map.keySet()或null值进行初始化时比较有用。注意基本类型(int、boolean等)是不能设置成null的。true|falsefalsereturnInstanceForEmptyRow当返回行的所有列都是空时,MyBatis默认返回null。当开启这个设置时,MyBatis会返回一个空实例。请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于3.4.2)true|falsefalselogPrefix指定MyBatis增加到日志名称的前缀。任何字符串未设置logImpl指定MyBatis所用日志的具体实现,未指定时将自动查找。SLF4J|LOG4J|LOG4J2|JDK_LOGGING|COMMONS_LOGGING|STDOUT_LOGGING|NO_LOGGING未设置proxyFactory指定Mybatis创建可延迟加载对象所用到的代理工具。CGLIB|JAVASSISTJAVASSIST(MyBatis3.3以上)vfsImpl指定VFS的实现自定义VFS的实现的类全限定名,以逗号分隔。未设置useActualParamName允许使用方法签名中的名称作为语句参数名称。为了使用该特性,你的项目必须采用Java8编译,并且加上-parameters选项。(新增于3.4.1)true|falsetrueconfigurationFactory指定一个提供Configuration实例的类。这个被返回的Configuration实例用来加载被反序列化对象的延迟加载属性值。这个类必须包含一个签名为staticConfigurationgetConfiguration()的方法。(新增于3.2.3)一个类型别名或完全限定类名。未设置shrinkWhitespacesInSqlRemovesextrawhitespacecharactersfromtheSQL.NotethatthisalsoaffectsliteralstringsinSQL.(Since3.5.5)true|falsefalseMybatis中可配置的settings如下:注意:settings、properties这些标签配置的位置不是随便的,他们的位置必须放在environment标签之前!3、typeAliase类型别名是为Java类型设置一个短的名字。它只和XML配置有关,存在的意义仅在于用来减少类完全限定名的冗余,即人好记,好写。3.1给单独的一个类重命名这样做后,以后在程序中任何使用com.xzy.bean.Employee的地方都可以用Employee来代替,例如:在mybatis-config.xml中添加如下配置:那么,在EmpoyeeMapper.xml中可以这么用:3.2给整个包重命名使用package元素:<typeAliases><packagename="com.xzy.bean"/></typeAliases> 这样一来每一个在包com.xzy.bean中的JavaBean,在没有注解的情况下,会使用类名的首字母小写来作为它的别名。如果使用了注解,那就以注解为他的别名。3.3@Alias(name)注解给一个JavaBean重命名其中name就是你给这个JavaBean起的别名4、TypeHandler—类型处理器无论是MyBatis在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成Java类型。Mytais给了很多的类型处理器来实现java类型到数据库类型的转换,但是就是这样内置的类型处理器还是无法完全胜任,因此MyBatis允许我们重写类型处理器或创建自己的类型处理器来处理不支持的或非标准的类型。具体做法为:实现org.apache.ibatis.type.TypeHandler接口,或继承类org.apache.ibatis.type.BaseTypeHandler,然后可以选择性地将它映射到一个JDBC类型。比如,有一个Phone类型:在另一个javaBean中使用他:自定义类型处理器自定义一个类型处理器,让他实现Typehandler接口,这个接口有一个泛型,这个参数就是处理后需要的java中对应对类型。在mybatis-config.xml中注册这个类型处理器注册的方式有两种,一种是使用<typeHandlerhandler=""/>这个标签,在handler属性中执行处理器的全类名;另一种方式是直接使用<packgename=""/>标签在name属性中指定包名,然后整个包下的处理器都会被自动注册。具体如下:5、配置环境(environments)MyBatis可以配置成适应多种环境,这种机制有助于将SQL映射应用于多种数据库之中,现实情况下有多种理由需要这么做。不过要记住:尽管可以配置多个环境,但是每个SqlSessionFactory实例只能选择其一。环境配置实例:5.1事务管理器(transactionManager)在MyBatis中有两种类型的事务管理器(也就是type=”[JDBC|MANAGED]”):JDBC–这个配置就是直接使用了JDBC的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域。MANAGED–这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期5.1数据源(dataSource)dataSource元素使用标准的JDBC数据源接口来配置JDBC连接对象的资源。有三种内建的数据源类型(也就是type=”[UNPOOLED|POOLED|JNDI]”):UNPOOLED–这个数据源的实现只是每次被请求时打开和关闭连接。这就是每次操作就要和数据库发生物理连接,数据会慢很多。这种数据源的实现利用“池”的概念将JDBC连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。这种方式每次需要和数据可连接的时候就直接去连接池中去拿。这个数据源的实现是为了能在如EJB或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个JNDI上下文的引用。通过需要实现接口org.apache.ibatis.datasource.DataSourceFactory,也可使用任何第三方数据源:6、映射器(mappers)mappers的作用就是告诉mybatis去哪里找SQL语句,因此对他的配置是至关重要。mybatis提供了如下4种配置的方法:6.1使用resource配置6.2使用文件路径配置6.3使用class属性配置接口配置单个mapper接口6.4使用name属性注册一个包下的所有mapper接口至此对于mybatis的全局配置大致完成了,接下来就是对SQL映射文件的配置了。
LoveIT 2019-07-06MyBatis