SpringBootConfiguration 是 SpringBoot 项目的配置注解,这也是一个组合注解,SpringBootConfiguration 注解可以用 java 代码的形式实现 Spring 中 xml 配置文件配置的效果,并会将当前类内声明的一个或多个以 @Bean 注解标记的方法的实例纳入到 spring 容器中,并且实例名就是方法名。
SpringBootConfiguration 可以作为 Spring 标准中 @Configuration 注解的替代。SpringBoot 项目中推荐使用@SpringBootConfiguration 替代 @Configuration。
@Scope注解主要作用是调节Ioc容器中的作用域,在Spring IoC容器中主要有以下五种作用域:基本作用域:singleton(单例)、prototype(多例);Web 作用域(reqeust、session、globalsession),自定义作用域。
@Scope注解是springIoc容器中的一个作用域,在 Spring IoC 容器中具有以下几种作用域:基本作用域singleton(单例)、prototype(多例),Web 作用域(reqeust、session、globalsession),自定义作用域
singleton
单例模式(默认):全局有且仅有一个实例prototype
原型模式:每次获取Bean的时候会有一个新的实例request
: request表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效session
:session作用域表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP session内有效global session
: global session作用域类似于标准的HTTP Session作用域,不过它仅仅在基于portlet的web应用中才有意义直接使用字符串容易出问题,spring有默认的参数:
ConfigurableBeanFactory.SCOPE_PROTOTYPE
,即“prototype”ConfigurableBeanFactory.SCOPE_SINGLETON
,即“singleton”WebApplicationContext.SCOPE_REQUEST
,即“request”WebApplicationContext.SCOPE_SESSION
,即“session”1 | (ElementType.TYPE) |
定义在main方法入口类处,用于启动sping boot应用项目
让spring boot根据类路径中的jar包依赖当前项目进行自动配置
在src/main/resources的META-INF/spring.factories
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
加载xml配置,一般是放在启动main类上
1 | "classpath*:/spring/*.xml") 单个 ( |
application.properties定义属性,直接使用@Value注入即可
1 | public class A{ |
可以新建一个properties文件,ConfigurationProperties的属性prefix指定properties的配置的前缀,通过location指定properties文件的位置
1 | "person") (prefix= |
用 @EnableConfigurationProperties注解使 @ConfigurationProperties生效,并从IOC容器中获取bean。
https://blog.csdn.net/u010502101/article/details/78758330
组合@Controller和@ResponseBody,当你开发一个和页面交互数据的控制时,比如bbs-web的api接口需要此注解
用来映射web请求(访问路径和参数)、处理类和方法,可以注解在类或方法上。注解在方法上的路径会继承注解在类上的路径。
produces属性: 定制返回的response的媒体类型和字符集,或需返回值是json对象
1 | "/api2/copper",produces="application/json;charset=UTF-8",method = RequestMethod.POST) (value= |
获取request请求的参数值
1 | public List<CopperVO> getOpList(HttpServletRequest request, |
支持将返回值放在response体内,而不是返回一个页面。比如Ajax接口,可以用此注解返回数据而不是页面。此注解可以放置在返回值前或方法前。
1 | 另一个玩法,可以不用 。 |
@Bean(name=”bean的名字”,initMethod=”初始化时调用方法名字”,destroyMethod=”close”)
定义在方法上,在容器内初始化一个bean实例类。
1 | "close") (destroyMethod= |
用于标注业务层组件
用于标注控制层组件(如struts中的action)
用于标注数据访问组件,即DAO组件
泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。
spring容器初始化时,要执行该方法
1 |
|
用来获得请求url中的动态参数
1 |
|
注解会告知Spring扫描指定的包来初始化Spring
1 | "com.bbs.xx") (basePackages = |
路由网关的主要目的是为了让所有的微服务对外只有一个接口,我们只需访问一个网关地址,即可由网关将所有的请求代理到不同的服务中。Spring Cloud是通过Zuul来实现的,支持自动路由映射到在Eureka Server上注册的服务。Spring Cloud提供了注解@EnableZuulProxy来启用路由代理。
在默认情况下使用 @Autowired 注释进行自动注入时,Spring 容器中匹配的候选 Bean 数目必须有且仅有一个。当找不到一个匹配的 Bean 时,Spring 容器将抛出 BeanCreationException 异常,并指出必须至少拥有一个匹配的 Bean。
当不能确定 Spring 容器中一定拥有某个类的 Bean 时,可以在需要自动注入该类 Bean 的地方可以使用 @Autowired(required = false),这等于告诉 Spring: 在找不到匹配 Bean 时也不报错
@Autowired注解注入map、list与@Qualifier在新窗口打开
1 | "name")//表示这是一个配置信息类,可以给这个配置类也起一个名称 ( |
导入Config1配置类里实例化的bean
1 |
|
@Order(1),值越小优先级超高,越先运行
如果注解指定的条件成立,则触发指定行为,条件注解列表:
@ConditionalOnRepositoryType (org.springframework.boot.autoconfigure.data)
@ConditionalOnDefaultWebSecurity (org.springframework.boot.autoconfigure.security)
@ConditionalOnSingleCandidate (org.springframework.boot.autoconfigure.condition)
@ConditionalOnWebApplication (org.springframework.boot.autoconfigure.condition)
@ConditionalOnWarDeployment (org.springframework.boot.autoconfigure.condition)
@ConditionalOnJndi (org.springframework.boot.autoconfigure.condition)
@ConditionalOnResource (org.springframework.boot.autoconfigure.condition)
@ConditionalOnExpression (org.springframework.boot.autoconfigure.condition)
@ConditionalOnClass (org.springframework.boot.autoconfigure.condition)
@ConditionalOnEnabledResourceChain (org.springframework.boot.autoconfigure.web)
@ConditionalOnMissingClass (org.springframework.boot.autoconfigure.condition)
@ConditionalOnNotWebApplication (org.springframework.boot.autoconfigure.condition)
@ConditionalOnProperty (org.springframework.boot.autoconfigure.condition)
@ConditionalOnCloudPlatform (org.springframework.boot.autoconfigure.condition)
@ConditionalOnBean (org.springframework.boot.autoconfigure.condition)
@ConditionalOnMissingBean (org.springframework.boot.autoconfigure.condition)
@ConditionalOnMissingFilterBean (org.springframework.boot.autoconfigure.web.servlet)
@Profile (org.springframework.context.annotation)
@ConditionalOnInitializedRestarter (org.springframework.boot.devtools.restart)
@ConditionalOnGraphQlSchema (org.springframework.boot.autoconfigure.graphql)
@ConditionalOnJava (org.springframework.boot.autoconfigure.condition)
如果类路径中不存在这个类,则触发指定行为
如果容器中存在这个Bean(组件),则触发指定行为,@ConditionalOnBean(value=组件类型,name=组件名字):判断容器中是否有这个类型的组件,并且名字是指定的值
如果容器中不存在这个Bean(组件),则触发指定行为
场景:
如果存在
FastsqlException
这个类,给容器中放一个Cat
组件,名cat01,否则,就给容器中放一个
Dog
组件,名dog01如果系统中有
dog01
这个组件,就给容器中放一个 User组件,名zhangsan否则,就放一个User,名叫lisi
这个注解能够控制某个 @Configuration 是否生效。具体操作是通过其两个属性name以及havingValue来实现的,其中name用来从application.properties中读取某个属性值,如果该值为空,则返回false;如果值不为空,则将该值与havingValue指定的值进行比较,如果一样则返回true;否则返回false。如果返回值为false,则该configuration不生效;为true则生效。
https://blog.csdn.net/dalangzhonghangxing/article/details/78420057
如果类路径中存在这个类,则触发指定行为;
该注解的参数对应的类必须存在,否则不解析该注解修饰的配置类;
1 |
|
1 | @ConditionalOnMisssingClass({ApplicationManager.class}) |
如果存在它修饰的类的bean,则不需要再创建这个bean;
1 | @ConditionOnMissingBean(name = "example") |
表示如果name为“example”的bean存在,该注解修饰的代码块不执行。
1 |
|
开关为true的时候才实例化bean
声明组件的属性和配置文件哪些前缀开始项进行绑定
快速注册注解:
这个一点倒是很重要,SpringBoot默认扫描当前项目主程序包及其子包,再加上自动配置类,那么属性类是不会扫描到的,此时就算加上@Component注解也是没有任何用处,所以一般是在属性类上面使用 @ConfigurationProperties注解,而在相应的自动配置类上面使用@EnableConfigurationProperties注解,从而让属性类绑定生效
将容器中任意组件(Bean)的属性值和配置文件的配置项的值进行绑定
1、给容器中注册组件(@Component、@Bean)
2、使用 @ConfigurationProperties 声明组件和配置文件的哪些配置项进行绑定
开启缓存注解的支持
@Cacheable
注解表示方法的结果应该被缓存起来,下次调用该方法时,如果参数和之前相同,则返回缓存结果。
1 |
|
@CachePut
注解表示方法的结果应该被缓存起来,下次调用该方法时,不会返回缓存结果,而是重新计算结果并缓存起来。
1 |
|
@CacheEvict
注解表示方法执行后从缓存中删除指定项。
1 |
|
@CacheConfig是结合@Cacheable使用的来设置过期时间的
@Caching 注解可以在一个方法或者类上同时指定多个Spring Cache相关的注解。
其拥有三个属性:cacheable、put 和 evict,分别用于指定@Cacheable、@CachePut 和 @CacheEvict。对于一个数据变动,更新多个缓存的场景,可以通过 @Caching 来实现:
1 | @Caching(cacheable = @Cacheable(cacheNames = "caching", key = "#age"), evict = @CacheEvict(cacheNames = "t4", key = "#age")) |
参考:
https://www.cnblogs.com/ziyue7575/p/c925cfe466df01c1d352f37da8823946.html
https://pdai.tech/md/spring/springboot/springboot-x-hello-anno.html
]]>1 | Restricted禁止的 |
3.1 会提示输入参数:RemoteSigned,然后回车
3.2 或者直接输入 set-executionpolicy remotesigned 回车
这样就能使用了;
]]> 举个例子,一个消息M发送到了消息中间件,消息投递到了消费程序A,A接受到了消息,然后进行消费,但在消费到一半的时候程序重启了,这时候这个消息并没有标记为消费成功,这个消息还会继续投递给这个消费者,直到其消费成功了,消息中间件才会停止投递。
然而这种可靠的特性导致,消息可能被多次地投递。举个例子,还是刚刚这个例子,程序A接受到这个消息M并完成消费逻辑之后,正想通知消息中间件“我已经消费成功了”的时候,程序就重启了,那么对于消息中间件来说,这个消息并没有成功消费过,所以他还会继续投递。这时候对于应用程序A来说,看起来就是这个消息明明消费成功了,但是消息中间件还在重复投递。
这在RockectMQ的场景来看,就是同一个messageId的消息重复投递下来了。
基于消息的投递可靠(消息不丢)是优先级更高的,所以消息不重的任务就会转移到应用程序自我实现,这也是为什么RocketMQ的文档里强调的,消费逻辑需要自我实现幂等。背后的逻辑其实就是:不丢和不重是矛盾的(在分布式场景下),但消息重复是有解决方案的,而消息丢失是很麻烦的。
例如:假设我们业务的消息消费逻辑是:插入某张订单表的数据,然后更新库存:
1 | insert into t_order values ..... update t_inv set count = count-1 where good_id = 'good123'; |
要实现消息的幂等,我们可能会采取这样的方案:
1 | select * from t_order where order_no = 'order123' if(order != null) { return ;//消息重复,直接返回 } |
这对于很多情况下,的确能起到不错的效果,但是在并发场景下,还是会有问题。
假设这个消费的所有代码加起来需要1秒,有重复的消息在这1秒内(假设100毫秒)内到达(例如生产者快速重发,Broker重启等),那么很可能,上面去重代码里面会发现,数据依然是空的(因为上一条消息还没消费完,还没成功更新订单状态),
那么就会穿透掉检查的挡板,最后导致重复的消息消费逻辑进入到非幂等安全的业务代码中,从而引发重复消费的问题(如主键冲突抛出异常、库存被重复扣减而没释放等)
要解决上面并发场景下的消息幂等问题,一个可取的方案是开启事务把select 改成 select for update语句,把记录进行锁定。
1 | select * from t_order where order_no = 'THIS_ORDER_NO' for update //开启事务 if(order.status != null) { return ;//消息重复,直接返回 } |
但这样消费的逻辑会因为引入了事务包裹而导致整个消息消费可能变长,并发度下降。
当然还有其他更高级的解决方案,例如更新订单状态采取乐观锁,更新失败则消息重新消费之类的。但这需要针对具体业务场景做更复杂和细致的代码开发、库表设计,不在本文讨论的范围。
但无论是select for update, 还是乐观锁这种解决方案,实际上都是基于业务表本身做去重,这无疑增加了业务开发的复杂度, 一个业务系统里面很大部分的请求处理都是依赖MQ的,如果每个消费逻辑本身都需要基于业务本身而做去重/幂等的开发的话,这是繁琐的工作量。本文希望探索出一个通用的消息幂等处理的方法,从而抽象出一定的工具类用以适用各个业务场景。
在消息中间件里,有一个投递语义的概念,而这个语义里有一个叫”Exactly Once”,即消息肯定会被成功消费,并且只会被消费一次。以下是阿里云里对Exactly Once的解释:
Exactly-Once 是指发送到消息系统的消息只能被消费端处理且仅处理一次,即使生产端重试消息发送导致某消息重复投递,该消息在消费端也只被消费一次。
在我们业务消息幂等处理的领域内,可以认为业务消息的代码肯定会被执行,并且只被执行一次,那么我们可以认为是Exactly Once。
但这在分布式的场景下想找一个通用的方案几乎是不可能的。不过如果是针对基于数据库事务的消费逻辑,实际上是可行的。
假设我们业务的消息消费逻辑是:更新MySQL数据库的某张订单表的状态:
1 | update t_order set status = 'SUCCESS' where order_no= 'order123'; |
要实现Exaclty Once即这个消息只被消费一次(并且肯定要保证能消费一次),我们可以这样做:在这个数据库中增加一个消息消费记录表,把消息插入到这个表,并且把原来的订单更新和这个插入的动作放到同一个事务中一起提交,就能保证消息只会被消费一遍了。
说明:
事实上,阿里云ONS的EXACTLY-ONCE语义的实现上,就是类似这个方案基于数据库的事务特性实现的。更多详情可参考:https://help.aliyun.com/document_detail/102777.html
基于这种方式,的确这是有能力拓展到不同的应用场景,因为他的实现方案与具体业务本身无关——而是依赖一个消息表。
但是这里有它的局限性
注:业务上,消息表的设计不应该以消息ID作为标识,而应该以业务的业务主键作为标识更为合理,以应对生产者的重发。阿里云上的消息去重只是RocketMQ的messageId,在生产者因为某些原因手动重发(例如上游针对一个交易重复请求了)的场景下起不到去重/幂等的效果(因消息id不同)。
如上所述,这种方式Exactly Once语义的实现,实际上有很多局限性,这种局限性使得这个方案基本不具备广泛应用的价值。并且由于基于事务,可能导致锁表时间过长等性能问题。
例如我们以一个比较常见的一个订单申请的消息来举例,可能有以下几步(以下统称为步骤X):
这种情况下,我们如果采取消息表+本地事务的实现方式,消息消费过程中很多子过程是不支持回滚的,也就是说就算我们加了事务,实际上这背后的操作并不是原子性的。怎么说呢,就是说有可能第一条小在经历了第二步锁库存的时候,服务重启了,这时候实际上库存是已经在另外的服务里被锁定了,这并不能被回滚。当然消息还会再次投递下来,要保证消息能至少消费一遍,换句话说,锁库存的这个RPC接口本身依旧要支持“幂等”。
再者,如果在这个比较耗时的长链条场景下加入事务的包裹,将大大的降低系统的并发。所以通常情况下,我们处理这种场景的消息去重的方法还是会使用一开始说的业务自己实现去重逻辑的方式,如前面加select for update,或者使用乐观锁。
那我们有没有方法抽取出一个公共的解决方案,能兼顾去重、通用、高性能呢?
其中一个思路是把上面的几步,拆解成几个不同的子消息,例如:
注:上述步骤需要保证本地事务和消息是一个事务的(至少是最终一致性的),这其中涉及到分布式事务消息相关的话题,不在本文论述。
可以看到这样的处理方法会使得每一步的操作都比较原子,而原子则意味着是小事务,小事务则意味着使用消息表+事务的方案显得可行。
然而,这太复杂了!这把一个本来连续的代码逻辑割裂成多个系统多次消息交互!那还不如业务代码层面上加锁实现呢。
上面消息表+本地事务的方案之所以有其局限性和并发的短板,究其根本是因为它依赖于关系型数据库的事务,且必须要把事务包裹于整个消息消费的环节。
如果我们能不依赖事务而实现消息的去重,那么方案就能推广到更复杂的场景例如:RPC、跨库等。
例如,我们依旧使用消息表,但是不依赖事务,而是针对消息表增加消费状态,是否可以解决问题呢?
以上是去事务化后的消息幂等方案的流程,可以看到,此方案是无事务的,而是针对消息表本身做了状态的区分:消费中、消费完成。只有消费完成的消息才会被幂等处理掉。而对于已有消费中的消息,后面重复的消息会触发延迟消费(在RocketMQ的场景下即发送到RETRY TOPIC),之所以触发延迟消费是为了控制并发场景下,第二条消息在第一条消息没完成的过程中,去控制消息不丢(如果直接幂等,那么会丢失消息(同一个消息id的话),因为上一条消息如果没有消费完成的时候,第二条消息你已经告诉broker成功了,那么第一条消息这时候失败broker也不会重新投递了)
上面的流程不再细说,后文有github源码的地址,读者可以参考源码的实现,这里我们回头看看我们一开始想解决的问题是否解决了:
关于第一个问题已经很明显已经解决了,在此就不讨论了。
关于第二个问题是如何解决的?主要是依靠插入消息表的这个动作做控制的,假设我们用MySQL作为消息表的存储媒介(设置消息的唯一ID为主键),那么插入的动作只有一条消息会成功,后面的消息插入会由于主键冲突而失败,走向延迟消费的分支,然后后面延迟消费的时候就会变成上面第一个场景的问题。
关于第三个问题,只要我们设计去重的消息键让其支持业务的主键(例如订单号、请求流水号等),而不仅仅是messageId即可。所以也不是问题。
如果细心的读者可能会发现这里实际上是有逻辑漏洞的,问题出在上面聊到的个三问题中的第2个问题(并发场景),在并发场景下我们依赖于消息状态是做并发控制使得第2条消息重复的消息会不断延迟消费(重试)。但如果这时候第1条消息也由于一些异常原因(例如机器重启了、外部异常导致消费失败)没有成功消费成功呢?也就是说这时候延迟消费实际上每次下来看到的都是消费中的状态,最后消费就会被视为消费失败而被投递到死信Topic中(RocketMQ默认可以重复消费16次)。
有这种顾虑是正确的!对于此,我们解决的方法是,插入的消息表必须要带一个最长消费过期时间,例如10分钟,意思是如果一个消息处于消费中超过10分钟,就需要从消息表中删除(需要程序自行实现)。所以最后这个消息的流程会是这样的:
我们这个方案实际上没有事务的,只需要一个存储的中心媒介,那么自然我们可以选择更灵活的存储媒介,例如Redis。使用Redis有两个好处:
当然Redis存储的数据可靠性、一致性等方面是不如MySQL的,需要用户自己取舍。
以上方案针对RocketMQ的Java实现已经开源放到Github中,具体的使用文档可以参考https://github.com/Jaskey/RocketMQDedupListener ,
以下仅贴一个Readme中利用Redis去重的使用样例,用以意业务中如果使用此工具加入消息去重幂等的是多么简单:
1 | //利用Redis做幂等表 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TEST-APP1"); consumer.subscribe("TEST-TOPIC", "*"); String appName = consumer.getConsumerGroup();// 大部分情况下可直接使用consumer group名 StringRedisTemplate stringRedisTemplate = null;// 这里省略获取StringRedisTemplate的过程 DedupConfig dedupConfig = DedupConfig.enableDedupConsumeConfig(appName, stringRedisTemplate); DedupConcurrentListener messageListener = new SampleListener(dedupConfig); consumer.registerMessageListener(messageListener); consumer.start(); |
以上代码大部分是原始RocketMQ的必须代码,唯一需要修改的仅仅是创建一个DedupConcurrentListener
示例,在这个示例中指明你的消费逻辑和去重的业务键(默认是messageId)。
更多使用详情请参考Github上的说明。
实现到这里,似乎方案挺完美的,所有的消息都能快速的接入去重,且与具体业务实现也完全解耦。那么这样是否就完美的完成去重的所有任务呢?
很可惜,其实不是的。原因很简单:因为要保证消息至少被成功消费一遍,那么消息就有机会消费到一半的时候失败触发消息重试的可能。还是以上面的订单流程X:
- 检查库存(RPC)
- 锁库存(RPC)
- 开启事务,插入订单表(MySQL)
- 调用某些其他下游服务(RPC)
- 更新订单状态
- commit 事务(MySQL)
当消息消费到步骤3的时候,我们假设MySQL异常导致失败了,触发消息重试。因为在重试前我们会删除幂等表的记录,所以消息重试的时候就会重新进入消费代码,那么步骤1和步骤2就会重新再执行一遍。如果步骤2本身不是幂等的,那么这个业务消息消费依旧没有做好完整的幂等处理。
那么既然这个并不能完整的完成消息幂等,还有什么价值呢?价值可就大了!虽然这不是解决消息幂等的银弹(事实上,软件工程领域里基本没有银弹),但是他能以便捷的手段解决:
1.各种由于Broker、负载均衡等原因导致的消息重投递的重复问题
2.各种上游生产者导致的业务级别消息重复问题
3.重复消息并发消费的控制窗口问题,就算重复,重复也不可能同一时间进入消费逻辑
也就是说,使用这个方法能保证正常的消费逻辑场景下(无异常,无异常退出),消息的幂等工作全部都能解决,无论是业务重复,还是rocketmq特性带来的重复。
事实上,这已经能解决99%的消息重复问题了,毕竟异常的场景肯定是少数的。那么如果希望异常场景下也能处理好幂等的问题,可以做以下工作降低问题率:
消息的存储是一直存在于CommitLog中的。而由于CommitLog是以文件为单位(而非消息)存在的,CommitLog的设计是只允许顺序写的,且每个消息大小不定长,所以这决定了消息文件几乎不可能按照消息为单位删除(否则性能会极具下降,逻辑也非常复杂)。所以消息被消费了,消息所占据的物理空间并不会立刻被回收。
但消息既然一直没有删除,那RocketMQ怎么知道应该投递过的消息就不再投递?——答案是客户端自身维护——客户端拉取完消息之后,在响应体中,broker会返回下一次应该拉取的位置,PushConsumer通过这一个位置,更新自己下一次的pull请求。这样就保证了正常情况下,消息只会被投递一次。
那消息文件到底删不删,什么时候删?
消息存储在CommitLog之后,的确是会被清理的,但是这个清理只会在以下任一条件成立才会批量删除消息文件(CommitLog):
注:若磁盘空间达到危险水位线(默认90%),出于保护自身的目的,broker会拒绝写入服务。
消息的物理文件一直存在,消费逻辑只是听客户端的决定而搜索出对应消息进行,这样做,笔者认为,有以下几个好处:
注:在消息清理的时候,由于消息文件默认是1GB,所以在清理的时候其实是在删除一个大文件操作,这对于IO的压力是非常大的,这时候如果有消息写入,写入的耗时会明显变高。这个现象可以在凌晨4点(默认删时间时点)后的附近观察得到。
RocketMQ官方建议Linux下文件系统改为Ext4,对于文件删除操作相比Ext3有非常明显的提升。
由于消息本身是没有过期的概念,只有文件才有过期的概念。那么对于很多业务场景——一个消息如果太老,是无需要被消费的,是不合适的。
这种需要跳过历史消息的场景,在RocketMQ要怎么实现呢?
对于一个全新的消费组,PushConsumer默认就是跳过以前的消息而从最尾开始消费的,解析请参看RocketMQ——消息ACK机制及消费进度管理相关章节。
但对于已存在的消费组,RocketMQ没有内置的跳过历史消息的实现,但有以下手段可以解决:
自身的消费代码按照日期过滤,太老的消息直接过滤。如:
1 |
|
自身的消费代码代码判断消息的offset和MAX_OFFSET相差很远,认为是积压了很多,直接return CONSUME_SUCCESS过滤。
1 |
|
消费者启动前,先调整该消费组的消费进度,再开始消费。可以人工使用控制台命令resetOffsetByTime把消费进度调整到后面,再启动消费。
原理同3,但使用代码来控制。代码中调用内部的运维接口,具体代码实例祥见ResetOffsetByTimeCommand.java
欢迎来到 Nacos 的世界!
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
服务(Service)是 Nacos 世界的一等公民。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理:
Kubernetes Service
gRPC & Dubbo RPC Service
Spring Cloud RESTful Service
Nacos 的关键特性包括:
这个快速开始手册是帮忙您快速在您的电脑上,下载、安装并使用 Nacos。
您可以在Nacos的release notes及博客中找到每个版本支持的功能的介绍,当前推荐的稳定版本为1.4.2或2.0.1。
Nacos 依赖 Java 环境来运行。如果您是从代码开始构建并运行Nacos,还需要为此配置 Maven环境,请确保是在以下版本环境中安装使用:
您可以从 最新稳定版本 下载 nacos-server-$version.zip
包。
解压nacos-server-1.4.2.zip
启动命令(standalone代表着单机模式运行,非集群模式):
1 | $ cd bin/ |
在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:
1 | #*************** Config Module Related Configurations ***************# |
配置好数据库然后重启nacos如下:
http://192.168.2.159:8848/nacos/index.html
输入用户名 nacos 密码 nacos
这样就启动配置成功!
]]>1 | mysql> show engines; |
InnoDB是MySQL的默认事务型引擎,也是最重要、使用最广泛的存储引擎。它被设计 用来处理大量的短期(short-lived)事务,短期事务大部分情况是正常提交的,很少会被回滚。InnoDB的性能和自动崩溃恢复特性,使得它在非事务型存储的需求中也很流 行。除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎。 □D>如果要学习存储引擎,InnoDB也是一个非常好的值得花最多的时间去深入学习的对象, 收益肯定比将时间平均花在每个存储引擎的学习上要高得多。
InnoDB有着复杂的发布历史,了解一下这段历史对于理解InnoDB很有帮助。2008年, 发布了所谓的InnoDB plugin,适用于MySQL 5.1版本,但这是Oracle创建的下一代 InnoDB引擎,其拥有者是InnoDB而不是MySQL。这基于很多原因,这些原因如果要一一道来,恐怕得喝掉好几桶啤酒。MySQL默认还是选择了集成旧的InnoDB引擎。当 然用户可以自行选择使用新的性能更好、扩展性更佳的InnoDB plugin来覆盖旧的版本。 直到最后,在Oracle收购了 Sun公司后发布的MySQL 5.5中才彻底使用InnoDB plugin 替代了旧版本的InnoDB (是的,这也意味着InnoDB plugin已经是原生编译了,而不是编译成一个插件,但名字已经约定俗成很难更改)。
这个现代的InnoDB版本,也就是MySQL 5.1中所谓的InnoDB plugin,支持一些新特性, 诸如利用排序创建索引(building index by sorting)、删除或者增加索引时不需要复制全表数据、新的支持压缩的存储格式、新的大型列值如BLOB的存储方式,以及文件格式管 理等。很多用户在MySQL 5.1中没有使用InnoDB plugin,或许是因为他们没有注意到有这个区别。所以如果你使用的是MySQL 5.1, 一定要使用InnoDB plugin,真的比旧版本的InnoDB要好很多。
InnoDB是一个很重要的存储引擎,很多个人和公司都对其贡献代码,而不仅仅是 Oracle公司的开发团队。一些重要的贡献者包括Google、Yasufumi Kinoshita、Percona,、Facebook等,他们的一些改进被直接移植到官方版本,也有一些由InnoDB团队重新实现。 在过去的几年间,InnoDB的改进速度大大加快,主要的改进集中在可测量性、可扩展性、 可配置化、性能、各种新特性和对Windows的支持等方面。MySQL 5.6实验室预览版 和里程碑版也包含了一系列重要的InnoDB新特性。
为改善InnoDB的性能,Oracle投入了大量的资源,并做了很多卓有成效的工作(外部贡献者对此也提供了很大的帮助)。在本书的第二版中,我们注意到在超过四核CPU的系统中InnoDB表现不佳,而现在已经可以很好地扩展至24核的系统,甚至在某些场景, 32核或者更多核的系统中也表现良好。很多改进将在即将发布的MySQL 5.6中引入, 当然也还有机会做更进一步的改善。
InnoDB的数据存储在表空间(tablespace)中,表空间是由InnoDB管理的一个黑盒子, 由一系列的数据文件组成。在MySQL 4.1以后的版本中,InnoDB可以将每个表的数据 和索引存放在单独的文件中。InnoDB也可以使用裸设备作为表空间的存储介质,但现代的文件系统使得裸设备不再是必要的选择。
InnoDB采用MVCC来支持高并发,并且实现了四个标准的隔离级别。其默认级别是 REPEATABLE READ (可重复读),并且通过间隙锁(next-key locking)策略防止幻读的出现。 间隙锁使得InnoDB不仅仅锁定査询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。 -
InnoDB表是基于聚簇索引建立的,我们会在后面的章节详细讨论聚簇索引。IimoDB的 索引结构和MySQL的其他存储引擎有很大的不同,聚簇索引对主键査询有很髙的性能。 不过它的二级索引(secondary index,非主键索引)中必须包含主键列,所以如果主键 列很大的话,其他的所有索引都会很大。因此,若表上的索引较多的话,主键应当尽可 能的小。InnoDB的存储格式是平台独立的,也就是说可以将数据和索引文件从Intel平 台复制到PowerPC或者Sun SPARC平台。
InnoDB内部做了很多优化,包括从磁盘读取数据时釆用的可预测性预读,能够自动在 内存中创建hash索引以加速读操作的自适应哈希索引(adaptive hash index),以及能够 加速插入操作的插入缓冲区(insert buffer)等。本书后面将更详细地讨论这些内容。
InnoDB的行为是非常复杂的,不容易理解。如果使用了 InnoDB引擎,笔者强烈建议阅 读官方手册中的”InnoDB事务模型和锁” 一节。如果应用程序基于InnoDB构建,则事 先了解一下InnoDB的MVCC架构带来的一些微妙和细节之处是非常有必要的。存储引 擎要为所有用户甚至包括修改数据的用户维持一致性的视图,是非常复杂的工作。
作为事务型的存储引擎,InnoDB通过一些机制和工具支持真正的热备份,Oracle提供 的MySQL Enterprise Backup. Percona提供的开源的XtraBackup都可以做到这一点。 MySQL的其他存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写入, 而在读写混合场景中,停止写入可能也意味着停止读取。
在MySQL 5.1及之前的版本,MylSAM是默认的存储引擎。MylSAM提供了大量的特 性,包括全文索引、压缩、空间函数(GIS)等,但MylSAM不支持事务和行级锁,而且有一个毫无疑问的缺陷就是崩溃后无法安全恢复。正是由于MylSAM引擎的缘故,即 使MySQL支持事务已经很长时间了,在很多人的概念中MySQL还是非事务型的数据 库。尽管MylSAM引擎不支持事务、不支持崩溃后的安全恢复,但它绝不是一无是处的。对于只读的数据,或者表比较小、可以忍受修复(repair)操作,则依然可以继续使 用MylSAM (但请不要默认使用MylSAM,而是应当默认使用InnoDB)。
MylSAM会将表存储在两个文件中:数据文件和索引文件,分别以.MYD和.MYI为扩展名。MylSAM表可以包含动态或者静态(长度固定)行。MySQL会根据表的定义来 决定采用何种行格式。MylSAM表可以存储的行记录数,一般受限于可用的磁盘空间, 或者操作系统中单个文件的最大尺寸。
在MySQL 5.0中,MylSAM表如果是变长行,则默认配置只能处理256TB的数据,因 .为指向数据记录的指针长度是6个字节。而在更早的版本中,指针长度默认是4字节,所以只能处理4GB的数据。而所有的MySQL版本都支持8字节的指针。要改变 MylSAM表指针的长度(调高或者调低),可以通过修改表的MAX_R0WS和AVG_R0W_ LENGTH选项的值来实现,两者相乘就是表可能达到的最大大小。修改这两个参数会导致 重建整个表和表的所有索引,这可能需要很长的时间才能完成。
作为MySQL最早的存储引擎之一,MylSAM有一些已经开发出来很多年的特性,可以 满足用户的实际需求。
MylSAM对整张表加锁,而不是针对行。读取时会对需要读到的所有表加共享锁, 写入时则对表加排他锁。但是在表有读取査询的同时,也可以往表中插入新的记录 (这被称为并发插入,CONCURRENT INSERT) o
对于MylSAM表,MySQL可以手工或者自动执行检査和修复操作,但这里说的修 复和事务.恢复以及崩溃恢复是不同的概念。执行表的修复可能导致一些数据丢失, 而且修复操作是非常慢的。可以通过CHECK TABLE mytable检査表的错误,如果有 错误可以通过执行REPAIR TABLE mytable进行修复。另外,如果MySQL服务器已 经关闭,也可以通过myisamchk命令行工具进行检査和修复操作。
对于MylSAM表,即使是BLOB和TEXT等长字段,也可以基于其前500个字符创建
索引。MylSAM也支持全文索引,这是一种基于分词创建的索引,可以支持复杂的 査询。
创建MylSAM表的时候,如果指定了 DELAY_KEY_WRITE选项,在每次修改执行完成 时,不会立刻将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区(in.memory key buffer),只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入到磁 盘。这种方式可以极大地提升写入性能,但是在数据库或者主机崩溃时会造成索引 损坏,需要执行修复操作。延迟更新索引键的特性,可以在全局设置,也可以为单 个表设置。
如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表或许适合釆用- MylSAM压缩表。
可以使用myisampack对MylSAM表进行压缩(也叫打包pack)o压缩表是不能进行修 改的(除非先将表解除压缩,修改数据,然后再次压缩)。压缩表可以极大地减少磁盘 空间占用,因此也可以减少磁盘I/O,从而提升査询性能。压缩表也支持索引,但索引 也是只读的。
以现在的硬件能力,对大多数应用场景,读取压缩表数据时的解压带来的开销影响并不 大,而减少I/O带来的好处则要大得多。压缩时表中的记录是独立压缩的,所以读取单 行的时候不需要去解压整个表(甚至也不解压行所在的整个页面)。
MylSAM引擎设计简单,数据以紧密格式存储,所以在某些场景下的性能很好。 MylSAM有一些服务器级别的性能扩展限制,比如对索引键缓冲区(key cache)的 Mutex锁,MariaDB基于段(segment)的索引键缓冲区机制来避免该问题。但MylSAM 最典型的性能问题还是表锁的问题,如果你发现所有的査询都长期处于“Locked”状态, 那么毫无疑问表锁就是罪魁祸首。
MySQL还有一些有特殊用途的存储引擎。在新版本中,有些可能因为一些原因已经不 再支持;另外还有些会继续支持,但是需要明确地启用后才能使用。
Archive存储引擎只支持INSERT和SELECT操作,在MySQL 5.1之前也不支持索引。
Archive引擎会缓存所有的写并利用zlib对插入的行进行压缩,所以比MylSAM表的磁 盘I/O更少。但是每次SELECT査询都需要执行全表扫描。所以Archive表适合日志和 数据釆集类应用,这类应用做数据分析时往往需要全表扫描。或者在一些需要更快速的 INSERT操作的场合下也可以使用。
Archive引擎支持行级锁和专用的缓冲区,所以可以实现高并发的插入。在一个査询开 始直到返回表中存在的所有行数之前,Archive引擎会阻止其他的SELECT执行,以实现 一致性读。另外,也实现了批量插入在完成之前对读操作是不可见的。这种机制模仿了 事务和MVCC的一些特性,但Archive引擎不是一个事务型的引擎,而是一个针对高速 插入和压缩做了优化的简单引擎。
Blackhole引擎没有实现任何的存储机制,它会丢弃所有插入的数据,不做任何保存。但 是服务器会记录Blackhole表的日志,所以可以用于复制数据到备库,或者只是简单地 记录到日志。这种特殊的存储引擎可以在一些特殊的复制架构和日志审核时发挥作用。 但这种应用方式我们碰到过很多问题,因此并不推荐。
CSV引擎可以将普通的CSV文件(逗号分割值的文件)作为MySQL的表来处理,但 这种表不支持索引。CSV引擎可以在数据库运行时拷入或者拷出文件。可以将Excel 等电子表格软件中的数据存储为CSV文件,然后复制到MySQL数据目录下,就能在 MySQL中打开使用。同样,如果将数据写入到一个CSV引擎表,其他的外部程序也能 立即从表的数据文件中读取csv格式的数据。因此CSV引擎可以作为一种数据交换的 机制,非常有用。
Federated引擎是访问其他MySQL服务器的一个代理,它会创建一个到远程MySQL服 务器的客户端连接,并将査询传输到远程服务器执行,然后提取或者发送需要的数据。 最初设计该存储引擎是为了和企业级数据库如Microsoft SQL Server和Oracle的类似特 性竞争的,可以说更多的是一种市场行为。尽管该引擎看起来提供了一种很好的跨服务 器的灵活性,但也经常带来问题,因此默认是禁用的。MariaDB使用了它的一个后续改 进版本,叫做FederatedXo
如果需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没有关系,那么 使用Memory表(以前也叫做HEAP表)是非常有用的。Memory表至少比MylSAM表 要快一个数量级,因为所有的数据都保存在内存中,不需要进行磁盘I/O。Memory表的 结构在重启以后还会保留,但数据会丢失。
Memroy表在很多场景可以发挥好的作用:
用于査找(lookup)或者映射(mapping)表,例如将邮编和州名映射的表。
用于缓存周期性聚合数据(periodically aggregated data)的结果。
用于保存数据分析中产生的中间数据。
Memory表支持Hash索引,因此査找操作非常快。虽然Memory表的速度非常快,但还 是无法取代传统的基于磁盘的表。Memroy表是表级锁,因此并发写入的性能较低。它 不支持BLOB或TEXT类型的列,并且每行的长度是固定的,所以即使指定了 VARCHAR列, 实际存储时也会转换成CHAR,这可能导致部分内存的浪费(其中一些限制在Percona版 本已经解决)。
如果MySQL在执行査询的过程中需要使用临时表来保存中间结果,内部使用的临时表 就是Memory表。如果中间结果太大超出了 Memory表的限制,或者含有BLOB或TEXT 字段,则临时表会转换成MylSAM表。在后续的章节还会继续讨论该问题。
人们经常混淆Memory表和临时表。临时表是指使用CREATE TEMPORARY TABLE语句 创建的表,它可以使用任何存储引擎,因此和Memory表不是一回事。临时表只在 单个连接中可见,当连接断开时,临时表也将不复存在。
Merge引擎是MylSAM引擎的一个变种。Merge表是由多个MylSAM表合并而来的虚 拟表。如果将MySQL用于日志或者数据仓库类应用,该引擎可以发挥作用。但是引入 分区功能后,该引擎已经被放弃(参考第7章)。
2003年,当时的MySQL AB公司从索尼爱立信公司收购了 NDB数据库,然后开发了 NDB集群存储引擎,作为SQL和NDB原生协议之间的接口。MySQL服务器、NDB集 群存储引擎,以及分布式的、share.nothing的、容灾的、高可用的NDB数据库的组合, 被称为MySQL集群(MySQL Cluster)
OLTP类引擎等不多介绍了。
]]>一步一步推导出 Mysql 索引的底层数据结构。
Mysql 作为互联网中非常热门的数据库,其底层的存储引擎和数据检索引擎的设计非常重要,尤其是 Mysql 数据的存储形式以及索引的设计,决定了 Mysql 整体的数据检索性能。
我们知道,索引的作用是做数据的快速检索,而快速检索的实现的本质是数据结构。通过不同数据结构的选择,实现各种数据快速检索。在数据库中,高效的查找算法是非常重要的,因为数据库中存储了大量数据,一个高效的索引能节省巨大的时间。比如下面这个数据表,如果 Mysql 没有实现索引算法,那么查找 id=7 这个数据,那么只能采取暴力顺序遍历查找,找到 id=7 这个数据需要比较 7 次,如果这个表存储的是 1000W 个数据,查找 id=1000W 这个数据那就要比较 1000W 次,这种速度是不能接受的。
哈希表是做数据快速检索的有效利器。
哈希算法:也叫散列算法,就是把任意值(key)通过哈希函数变换为固定长度的 key 地址,通过这个地址进行具体数据的数据结构。
考虑这个数据库表 user,表中一共有 7 个数据,我们需要检索 id=7 的数据,SQL 语法是:
1 | select \* from user where id=7; |
哈希算法首先计算存储 id=7 的数据的物理地址 addr=hash(7)=4231,而 4231 映射的物理地址是 0x77,0x77 就是 id=7 存储的额数据的物理地址,通过该独立地址可以找到对应 user_name=’g’这个数据。这就是哈希算法快速检索数据的计算过程。
但是哈希算法有个数据碰撞的问题,也就是哈希函数可能对不同的 key 会计算出同一个结果,比如 hash(7)可能跟 hash(199)计算出来的结果一样,也就是不同的 key 映射到同一个结果了,这就是碰撞问题。解决碰撞问题的一个常见处理方式就是链地址法,即用链表把碰撞的数据接连起来。计算哈希值之后,还需要检查该哈希值是否存在碰撞数据链表,有则一直遍历到链表尾,直达找到真正的 key 对应的数据为止。
从算法时间复杂度分析来看,哈希算法时间复杂度为 O(1),检索速度非常快。比如查找 id=7 的数据,哈希索引只需要计算一次就可以获取到对应的数据,检索速度非常快。但是 Mysql 并没有采取哈希作为其底层算法,这是为什么呢?
因为考虑到数据检索有一个常用手段就是范围查找,比如以下这个 SQL 语句:
1 | select \* from user where id \>3; |
针对以上这个语句,我们希望做的是找出 id>3 的数据,这是很典型的范围查找。如果使用哈希算法实现的索引,范围查找怎么做呢?一个简单的思路就是一次把所有数据找出来加载到内存,然后再在内存里筛选筛选目标范围内的数据。但是这个范围查找的方法也太笨重了,没有一点效率而言。
所以,使用哈希算法实现的索引虽然可以做到快速检索数据,但是没办法做数据高效范围查找,因此哈希索引是不适合作为 Mysql 的底层索引的数据结构。
二叉查找树是一种支持数据快速查找的数据结构,如图下所示:
二叉查找树的时间复杂度是 O(lgn),比如针对上面这个二叉树结构,我们需要计算比较 3 次就可以检索到 id=7 的数据,相对于直接遍历查询省了一半的时间,从检索效率上看来是能做到高速检索的。此外二叉树的结构能不能解决哈希索引不能提供的范围查找功能呢?
答案是可以的。观察上面的图,二叉树的叶子节点都是按序排列的,从左到右依次升序排列,如果我们需要找 id>5 的数据,那我们取出节点为 6 的节点以及其右子树就可以了,范围查找也算是比较容易实现。
但是普通的二叉查找树有个致命缺点:极端情况下会退化为线性链表,二分查找也会退化为遍历查找,时间复杂退化为 O(N),检索性能急剧下降。比如以下这个情况,二叉树已经极度不平衡了,已经退化为链表了,检索速度大大降低。此时检索 id=7 的数据的所需要计算的次数已经变为 7 了。
在数据库中,数据的自增是一个很常见的形式,比如一个表的主键是 id,而主键一般默认都是自增的,如果采取二叉树这种数据结构作为索引,那上面介绍到的不平衡状态导致的线性查找的问题必然出现。因此,简单的二叉查找树存在不平衡导致的检索性能降低的问题,是不能直接用于实现 Mysql 底层索引的。
二叉查找树存在不平衡问题,因此学者提出通过树节点的自动旋转和调整,让二叉树始终保持基本平衡的状态,就能保持二叉查找树的最佳查找性能了。基于这种思路的自调整平衡状态的二叉树有 AVL 树和红黑树。
首先简单介绍红黑树,这是一颗会自动调整树形态的树结构,比如当二叉树处于一个不平衡状态时,红黑树就会自动左旋右旋节点以及节点变色,调整树的形态,使其保持基本的平衡状态(时间复杂度为 O(logn)),也就保证了查找效率不会明显减低。比如从 1 到 7 升序插入数据节点,如果是普通的二叉查找树则会退化成链表,但是红黑树则会不断调整树的形态,使其保持基本平衡状态,如下图所示。下面这个红黑树下查找 id=7 的所要比较的节点数为 4,依然保持二叉树不错的查找效率。
红黑树拥有不错的平均查找效率,也不存在极端的 O(n)情况,那红黑树作为 Mysql 底层索引实现是否可以呢?其实红黑树也存在一些问题,观察下面这个例子。
红黑树顺序插入 1~7 个节点,查找 id=7 时需要计算的节点数为 4。
红黑树顺序插入 1~16 个节点,查找 id=16 需要比较的节点数为 6 次。观察一下这个树的形态,是不是当数据是顺序插入时,树的形态一直处于“右倾”的趋势呢?从根本上上看,红黑树并没有完全解决二叉查找树虽然这个“右倾”趋势远没有二叉查找树退化为线性链表那么夸张,但是数据库中的基本主键自增操作,主键一般都是数百万数千万的,如果红黑树存在这种问题,对于查找性能而言也是巨大的消耗,我们数据库不可能忍受这种无意义的等待的。
现在考虑另一种更为严格的自平衡二叉树 AVL 树。因为 AVL 树是个绝对平衡的二叉树,因此他在调整二叉树的形态上消耗的性能会更多。
AVL 树顺序插入 1~7 个节点,查找 id=7 所要比较节点的次数为 3。
AVL 树顺序插入 1~16 个节点,查找 id=16 需要比较的节点数为 4。从查找效率而言,AVL 树查找的速度要高于红黑树的查找效率(AVL 树是 4 次比较,红黑树是 6 次比较)。从树的形态看来,AVL 树不存在红黑树的“右倾”问题。也就是说,大量的顺序插入不会导致查询性能的降低,这从根本上解决了红黑树的问题。
总结一下 AVL 树的优点:
看起来 AVL 树作为数据查找的数据结构确实很不错,但是 AVL 树并不适合做 Mysql 数据库的索引数据结构,因为考虑一下这个问题:
数据库查询数据的瓶颈在于磁盘 IO,如果使用的是 AVL 树,我们每一个树节点只存储了一个数据,我们一次磁盘 IO 只能取出来一个节点上的数据加载到内存里,那比如查询 id=7 这个数据我们就要进行磁盘 IO 三次,这是多么消耗时间的。所以我们设计数据库索引时需要首先考虑怎么尽可能减少磁盘 IO 的次数。
磁盘 IO 有个有个特点,就是从磁盘读取 1B 数据和 1KB 数据所消耗的时间是基本一样的,我们就可以根据这个思路,我们可以在一个树节点上尽可能多地存储数据,一次磁盘 IO 就多加载点数据到内存,这就是 B 树,B+树的的设计原理了。
下面这个 B 树,每个节点限制最多存储两个 key,一个节点如果超过两个 key 就会自动分裂。比如下面这个存储了 7 个数据 B 树,只需要查询两个节点就可以知道 id=7 这数据的具体位置,也就是两次磁盘 IO 就可以查询到指定数据,优于 AVL 树。
下面是一个存储了 16 个数据的 B 树,同样每个节点最多存储 2 个 key,查询 id=16 这个数据需要查询比较 4 个节点,也就是经过 4 次磁盘 IO。看起来查询性能与 AVL 树一样。
但是考虑到磁盘 IO 读一个数据和读 100 个数据消耗的时间基本一致,那我们的优化思路就可以改为:尽可能在一次磁盘 IO 中多读一点数据到内存。这个直接反映到树的结构就是,每个节点能存储的 key 可以适当增加。
当我们把单个节点限制的 key 个数设置为 6 之后,一个存储了 7 个数据的 B 树,查询 id=7 这个数据所要进行的磁盘 IO 为 2 次。
一个存储了 16 个数据的 B 树,查询 id=7 这个数据所要进行的磁盘 IO 为 2 次。相对于 AVL 树而言磁盘 IO 次数降低为一半。
所以数据库索引数据结构的选型而言,B 树是一个很不错的选择。总结来说,B 树用作数据库索引有以下优点:
B 树和 B+树有什么不同呢?
第一,B 树一个节点里存的是数据,而 B+树存储的是索引(地址),所以 B 树里一个节点存不了很多个数据,但是 B+树一个节点能存很多索引,B+树叶子节点存所有的数据。
第二,B+树的叶子节点是数据阶段用了一个链表串联起来,便于范围查找。
通过 B 树和 B+树的对比我们看出,B+树节点存储的是索引,在单个节点存储容量有限的情况下,单节点也能存储大量索引,使得整个 B+树高度降低,减少了磁盘 IO。其次,B+树的叶子节点是真正数据存储的地方,叶子节点用了链表连接起来,这个链表本身就是有序的,在数据范围查找时,更具备效率。因此 Mysql 的索引用的就是 B+树,B+树在查找效率、范围查找中都有着非常不错的性能。
Mysql 底层数据引擎以插件形式设计,最常见的是 Innodb 引擎和 Myisam 引擎,用户可以根据个人需求选择不同的引擎作为 Mysql 数据表的底层引擎。我们刚分析了,B+树作为 Mysql 的索引的数据结构非常合适,但是数据和索引到底怎么组织起来也是需要一番设计,设计理念的不同也导致了 Innodb 和 Myisam 的出现,各自呈现独特的性能。
MyISAM 虽然数据查找性能极佳,但是不支持事务处理。Innodb 最大的特色就是支持了 ACID 兼容的事务功能,而且他支持行级锁。Mysql 建立表的时候就可以指定引擎,比如下面的例子,就是分别指定了 Myisam 和 Innodb 作为 user 表和 user2 表的数据引擎。
执行这两个指令后,系统出现了以下的文件,说明这两个引擎数据和索引的组织方式是不一样的。
Innodb 创建表后生成的文件有:
Myisam 创建表后生成的文件有
从生成的文件看来,这两个引擎底层数据和索引的组织方式并不一样,MyISAM 引擎把数据和索引分开了,一人一个文件,这叫做非聚集索引方式;Innodb 引擎把数据和索引放在同一个文件里了,这叫做聚集索引方式。下面将从底层实现角度分析这两个引擎是怎么依靠 B+树这个数据结构来组织引擎实现的。
MyISAM 用的是非聚集索引方式,即数据和索引落在不同的两个文件上。MyISAM 在建表时以主键作为 KEY 来建立主索引 B+树,树的叶子节点存的是对应数据的物理地址。我们拿到这个物理地址后,就可以到 MyISAM 数据文件中直接定位到具体的数据记录了。
当我们为某个字段添加索引时,我们同样会生成对应字段的索引树,该字段的索引树的叶子节点同样是记录了对应数据的物理地址,然后也是拿着这个物理地址去数据文件里定位到具体的数据记录。
InnoDB 是聚集索引方式,因此数据和索引都存储在同一个文件里。首先 InnoDB 会根据主键 ID 作为 KEY 建立索引 B+树,如左下图所示,而 B+树的叶子节点存储的是主键 ID 对应的数据,比如在执行 select * from user_info where id=15 这个语句时,InnoDB 就会查询这颗主键 ID 索引 B+树,找到对应的 user_name=’Bob’。
这是建表的时候 InnoDB 就会自动建立好主键 ID 索引树,这也是为什么 Mysql 在建表时要求必须指定主键的原因。当我们为表里某个字段加索引时 InnoDB 会怎么建立索引树呢?比如我们要给 user_name 这个字段加索引,那么 InnoDB 就会建立 user_name 索引 B+树,节点里存的是 user_name 这个 KEY,叶子节点存储的数据的是主键 KEY。注意,叶子存储的是主键 KEY!拿到主键 KEY 后,InnoDB 才会去主键索引树里根据刚在 user_name 索引树找到的主键 KEY 查找到对应的数据。
问题来了,为什么 InnoDB 只在主键索引树的叶子节点存储了具体数据,但是其他索引树却不存具体数据呢,而要多此一举先找到主键,再在主键索引树找到对应的数据呢?
其实很简单,因为 InnoDB 需要节省存储空间。一个表里可能有很多个索引,InnoDB 都会给每个加了索引的字段生成索引树,如果每个字段的索引树都存储了具体数据,那么这个表的索引数据文件就变得非常巨大(数据极度冗余了)。从节约磁盘空间的角度来说,真的没有必要每个字段索引树都存具体数据,通过这种看似“多此一举”的步骤,在牺牲较少查询的性能下节省了巨大的磁盘空间,这是非常有值得的。
在进行 InnoDB 和 MyISAM 特点对比时谈到,MyISAM 查询性能更好,从上面索引文件数据文件的设计来看也可以看出原因:MyISAM 直接找到物理地址后就可以直接定位到数据记录,但是 InnoDB 查询到叶子节点后,还需要再查询一次主键索引树,才可以定位到具体数据。等于 MyISAM 一步就查到了数据,但是 InnoDB 要两步,那当然 MyISAM 查询性能更高。
本文首先探讨了哪种数据结构更适合作为 Mysql 底层索引的实现,然后再介绍了 Mysql 两种经典数据引擎 MyISAM 和 InnoDB 的底层实现。最后再总结一下什么时候需要给你的表里的字段加索引吧:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10
union all
select id from t where num=20
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
select id from t where name like ‘%abc%’ 和 select id from t where name like ‘%abc’
只有like abc% 索引才有效,若要提高效率,可以考虑全文检索
select id from t where num=@num
可以改为强制查询使用索引:
select id from t with(index(索引名)) where num=@num
select id from t where num/2=100
应改为:
select id from t where num=100*2
select id from t where substring(name,1,3)=’abc’–name以abc开头的id
select id from t where datediff(day,createdate,’2005-11-30’)=0–‘2005-11-30’生成的id
应改为:
select id from t where name like ‘abc%’
select id from t where createdate>=’2005-11-30’ and createdate<’2005-12-1’
不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
对于复合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。
例如索引是key index (a,b,c)。 可以支持a | a,b| a,b,c 3种组合进行查找,但不支持 b,c进行查找 .当最左侧字段是常量引用时,索引就十分有效
select col1,col2 into #t from t where 1=0
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
create table #t(…)
select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)
索引并不是时时都会生效的,比如以下几种情况,将导致索引失效:
注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
对于多列索引,不是使用的第一部分,则不会使用索引
like查询是以%开头,索引无效;当like前缀没有%,后缀有%时,索引有效。
此外,查看索引的使用情况
show status like ‘Handler_read%’;
大家可以注意:
handler_read_key:这个值越高越好,越高表示使用索引查询到的次数
handler_read_rnd_next:这个值越高,说明查询低效
1) 没有查询条件,或者查询条件没有建立索引
2) 在查询条件上没有使用引导列
3) 查询的数量是大表的大部分,应该是30%以上。
4) 索引本身失效
5) 查询条件使用函数在索引列上,或者对索引列进行运算,运算包括(+,-,*,/,! 等)
错误的例子:
select * from test where id-1=9;
正确的例子:
select * from test where id=10;
6) 对小表查询
7) 提示不使用索引
8) 统计数据不真实
9) CBO计算走索引花费过大的情况。其实也包含了上面的情况,这里指的是表占有的block要比索引小。
10) 隐式转换导致索引失效.这一点应当引起重视.也是开发中经常会犯的错误. 由于表的字段tu_mdn定义为varchar2(20),但在查询时把该字段作为number类型以where条件传给Oracle,这样会导致索引失效. .
错误的例子:
select * from test where tu_mdn=13333333333;
正确的例子:
select * from test where tu_mdn=’13333333333’;
12) 1,<> 2,单独的>,
13) like “%_” 百分号在前.
14) 表没分析.
15) 单独引用复合索引里非第一位置的索引列.
16) 字符型字段为数字时在where条件里不添加引号.
17) 对索引列进行运算.需要建立函数索引.
18) not in ,not exist.
19) 当变量采用的是times变量,而表的字段采用的是date变量时.或相反情况。
20) B-tree索引 is null不会走,is not null会走,位图索引 is null,is not null 都会走
21) 联合索引 is not null 只要在建立的索引列(不分先后)都会走, in null时 必须要和建立索引第一列一起使用,当建立索引第一位置条件是is null 时,其他建立索引的列可以是is null(但必须在所有列 都满足is null的时候),或者=一个值; 当建立索引的第一位置是=一个值时,其他索引列可以是任何情况(包括is null =一个值),以上两种情况索引都会走。其他情况不会走。
]]>目前大多数索引都是采用B-树来存储,其包含组件有:
哈希索引也称为散列索引或 HASH 索引。MySQL 目前仅有 MEMORY 存储引擎和 HEAP 存储引擎支持这类索引。其中,MEMORY 存储引擎可以支持 B-树索引和 HASH 索引,且将 HASH 当成默认索引。
哈希索引的最大特点是访问速度快,但也存在下面的一些缺点:
普通索引是 MySQL 中最基本的索引类型,它没有任何限制,唯一任务就是加快系统对数据的访问速度。允许重复值和空值。
关键字是 INDEX 或 KEY。
唯一索引列的值必须唯一,允许有空值。如果是组合索引,则列值的组合必须唯一。
关键字是 UNIQUE。
主键索引是一种特殊的唯一索引,不允许值重复或者值为空。
关键字是 PRIMARY KEY。
空间索引是对空间数据类型的字段建立的索引,不允许空值,只能在存储引擎为 MyISAM 的表中创建。
关键字是 SPATIAL。
全文索引主要用来查找文本中的关键字,只能在 CHAR、VARCHAR 或 TEXT 类型的列上创建。只有 MyISAM 存储引擎支持,允许重复值和空值。
关键字是 FULLTEXT。
单列索引可以是普通索引,也可以是唯一性索引,还可以是全文索引。只要保证该索引只对应一个字段即可。
组合索引也称为复合索引或多列索引。相对于单列索引来说,组合索引是将原表的多个列共同组成一个索引。
查询时,字段顺序需与索引顺序一致;LIKE时,首字符不能是 ‘%’,否则会影响索引使用。
]]>表示普通索引,大多数情况下都可以使用
表示唯一的,不允许重复的索引,如果该字段信息保证不会重复例如身份证号用作索引时,可设置为unique
约束唯一标识数据库表中的每一条记录,即在单表中不能用每条记录是唯一的(例如身份证就是唯一的),Unique(要求列唯一)和Primary Key(primary key = unique + not null 列唯一)约束均为列或列集合中提供了唯一性的保证,Primary Key是拥有自动定义的Unique约束,但是每个表中可以有多个Unique约束,但是只能有一个Primary Key约束。
mysql中创建Unique约束
表示全文收索,在检索长文本的时候,效果最好,短文本建议使用Index,但是在检索的时候数据量比较大的时候,现将数据放入一个没有全局索引的表中,然后在用Create Index创建的Full Text索引,要比先为一张表建立Full Text然后在写入数据要快的很多
FULLTEXT 用于搜索很长一篇文章的时候,效果最好。用在比较短的文本,如果就一两行字的,普通的 INDEX 也可以。
空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MYISAM的表中创建
1、BTREE(B树(可以是多叉树)) {主流使用}
2、HASH(key,value) 这种方式对范围查询支持得不是很好
hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。
可 能很多人又有疑问了,既然 Hash 索引的效率要比 B-Tree 高很多,为什么大家不都用 Hash 索引而还要使用 B-Tree 索引呢?任何事物都是有两面性的,Hash 索引也一样,虽然 Hash 索引效率高,但是 Hash 索引本身由于其特殊性也带来了很多限制和弊端,主要有以下这些。
(1)Hash 索引仅仅能满足”=”,”IN”和”<=>”查询,不能使用范围查询。
由于 Hash 索引比较的是进行 Hash 运算之后的 Hash 值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的 Hash 算法处理之后的 Hash 值的大小关系,并不能保证和Hash运算前完全一样。
(2)Hash 索引无法被用来避免数据的排序操作。
由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash 值,而且Hash值的大小关系并不一定和 Hash 运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算;
(3)Hash 索引不能利用部分索引键查询。
对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并后再一起计算 Hash 值,而不是单独计算 Hash 值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用。
(4)Hash 索引在任何时候都不能避免表扫描。
前面已经知道,Hash 索引是将索引键通过 Hash 运算之后,将 Hash运算结果的 Hash 值和所对应的行指针信息存放于一个 Hash 表中,由于不同索引键存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash 索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。
(5)Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。
对于选择性比较低的索引键,如果创建 Hash 索引,那么将会存在大量记录指针信息存于同一个 Hash 值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。
为了使索引的使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型的索引,有7大原则:
MySQL目前主要有以下几种索引类型:
1.普通索引
2.唯一索引
3.主键索引
4.组合索引
5.全文索引
1 | CREATE TABLE table_name[col_name data type] |
1.普通索引
是最基本的索引,它没有任何限制。它有以下几种创建方式:
(1)直接创建索引
1 | CREATE INDEX index_name ON table(column(length)) |
(2)修改表结构的方式添加索引
1 | ALTER TABLE table_name ADD INDEX index_name ON (column(length)) |
(3)创建表的时候同时创建索引
1 | CREATE TABLE `table` ( |
(4)删除索引
1 | DROP INDEX index_name ON table |
2.唯一索引
与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。它有以下几种创建方式:
(1)创建唯一索引
1 | CREATE UNIQUE INDEX indexName ON table(column(length)) |
(2)修改表结构
1 | ALTER TABLE table_name ADD UNIQUE indexName ON (column(length)) |
(3)创建表的时候直接指定
1 | CREATE TABLE `table` ( |
3.主键索引
是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引:
1 | CREATE TABLE `table` ( |
4.组合索引
指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合
1 | ALTER TABLE `table` ADD INDEX name_city_age (name,city,age); |
5.全文索引
主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。fulltext索引配合match against操作使用,而不是一般的where语句加like。它可以在create table,alter table ,create index使用,不过目前只有char、varchar,text 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用CREATE index创建fulltext索引,要比先为一张表建立fulltext然后再将数据写入的速度快很多。
(1)创建表的适合添加全文索引
1 | CREATE TABLE `table` ( |
(2)修改表结构添加全文索引
1 | ALTER TABLE article ADD FULLTEXT index_content(content) |
(3)直接创建索引
1 | CREATE FULLTEXT INDEX index_content ON article(content) |
使用索引时,有以下一些技巧和注意事项:
1 | SELECT * FROM table_name WHERE YEAR(column_name)<2017; |
要理解MySQL中索引是如何工作的,最简单的方法就是去看看一本书的“索引”部分: 如果想在一本书中找到某个特定主题,一般会先看书的“索引”,找到对应的页码。
在MySQL中,存储引擎用类似的方法使用索引,其先在索引中找到对应值,然后根据 匹配的索引记录找到对应的数据行。假如要运行下面的査询:
mysql> SELECT first_ame FROM sakila.actor WHERE actor_id = 5;
如果在actor_id列上建有索引,则MySQL将使用该索引找到actor_id为5的行,也 就是说,MySQL先在索引上按值进行査找,然后返回所有包含该值的数据行。
索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因 为MySQL只能高效地使用索引的最左前缀列。创建一个包含两个列的索引,和创建两 个只包含一列的索引是大不相同的,下面将详细介绍。
简而言之:是的,仍然需要理解索引,即使是使用对象关系映射(ORM)工具。
(ORM)工具能够生产符合逻辑的、合法的查询(多数时候),除非只是生成非常基本 的查询(例如仅是根据主键查询),否则它很难生成适合索引的查询。无论是多么 复杂的(ORM)工具,在精妙和复杂的索引面前都是“浮云”。读完本章后面的内容 以后,你就会同意这个观点的!很多时候,即使是查询优化技术专家也很难兼顾到 各种情况,更别说(ORM) 了。
]]>user表分表分为2个表,2个库
pom文件引入如下相关依赖:
1 |
|
1 | package cn.cicoding.controller; |
1 | package cn.cicoding.controller; |
1 | package cn.cicoding.service; |
1 | package cn.cicoding.service; |
1 | package cn.cicoding.service; |
1 | package cn.cicoding.service; |
1 | package cn.cicoding.repository; |
1 | package cn.cicoding.repository; |
1 |
|
1 |
|
1 | package cn.cicoding.model; |
到这我们完成了基本的代码编写,由于sharding-jdbc是jar包,我们来看主要的配置信息
1 | # Spring Boot版 Sharding JDBC 垂直拆分(不同的表在不同的库中)+ 读写分离 |
1 | package cn.cicoding; |
启动启动类,访问http://localhost:8084/add
分别进入不同的库!
到此我们就实现了sharding-jdbc主从读写分离实现,更多配置请参考此处!
]]>把bean放入到Spring的Ioc容器叫做装配,那么在装配Bean的时候,我们首先要知道哪些类需要被装配,实现这一方式的途径总体上说分为两种,一种是传统的xml方式,另一种则是注解方式。下面介绍下通过注解来实现装配。
启动原理
我们发现任何一个springboot的项目都会有如下一个启动类:
1 |
|
为了了解SpringBoot原理,我们直接从Annotation 入 手,看看@SpringBootApplication里面,做了什么? 打开SpringBootApplication这个注解,可以看到它实际上 是一个复合注解
1 | (ElementType.TYPE) |
SpringBootApplication本质上是由3个注解组成,分别是
我们可以直接用这三个注解也可以启动 springboot 应用, 只是每次配置三个注解比较繁琐,所以直接用一个复合注 解更方便些。 然后仔细观察者三个注解,除了EnableAutoConfiguration 可能稍微陌生一点,其他两个注解使用得都很多 。
Configuration这个注解大家应该有用过,它是JavaConfig形式的基于Spring IOC容器的配置类使用的一种注解。因为 SpringBoot 本质上就是一个 spring 应用,所以通过这个注解来加载IOC容器的配置是很正常的。所以在启动类 里面标注了@Configuration,意味着它其实也是一个 IoC 容器的配置类。
传统意义上的 spring 应用都是基于 xml 形式来配置 bean 的依赖关系。然后通过spring容器在启动的时候,把bean进行初始化并且,如果bean之间存在依赖关系,则分析这些已经在IoC容器中的bean根据依赖关系进行组装。 直到 Java5 中,引入了 Annotations 这个特性,Spring 框架也紧随大流并且推出了基于 Java 代码和Annotation元信息的依赖关系绑定描述的方式,也就是JavaConfig。
从spring3开始,spring就支持了两种bean的配置方式, 一种是基于xml文件方式、另一种就是JavaConfig 。
任何一个标注了@Configuration 的 Java 类定义都是一个 JavaConfig 配置类。而在这个配置类中,任何标注了 @Bean 的方法,它的返回值都会作为 Bean 定义注册到 Spring的IOC容器,方法名默认成为这个bean的id
@ComponentScan这个注解是大家接触得最多的了,相当于 xml 配置文件中的<context:component-scan />。 它的主要作用就是扫描指定路径下的标识了需要装配的类,自动装配到spring的Ioc容器中。
标识需要装配类的形式主要是:@Component、@Repository、@Service、@Controller这类的注解标识的类;
ComponentScan 默认会扫描当前package 下的的所有加了相关注解标识的类到IoC容器中。
Enable 并不是新鲜玩意
在 spring3.1 版本中,提供了一系列的@Enable 开 头的注解,Enable主机应该是在JavaConfig框架上更进一步的完善,是的用户在使用spring相关的框架是,避免配置大量的代码从而降低使用的难度 。
比如@EnableScheduling、@EnableCaching、@EnableWebMvc等,@EnableAutoConfiguration的理念和做事方式其实一脉相承,简单概括一下就是,借助@Import的支持,收集和注册特定场景相关的bean定义。
而@EnableAutoConfiguration也是借助@Import的帮助,将所有符合条件的@Configuration 配置都加载到当前SpringBoot创建并使用的IoC容器中。仅此而已!
@EnableAutoConfiguration会根据类路径中的jar依赖为项目进行自动配置,如:添加了spring-boot-starter-web依赖,会自动添加Tomcat和Spring MVC的依赖,Spring Boot会对Tomcat和Spring MVC进行自动配置。
@EnableAutoConfiguration作为一个复合Annotation,其自身定义关键信息如下:
1 | (ElementType.TYPE) |
@Import(AutoConfigurationImportSelector.class)从名字来看,可以猜到它是基于ImportSelector来实现基于动态bean的加载功能。要知道Springboot @Enable*注解的工作原理ImportSelector接口selectImports返回的数组(类的全类名)都会被纳入到 spring容器中。 那么可以猜想到这里的实现原理也一定是一样的,定位到 AutoConfigurationImportSelector这个类中的 selectImports方法 。
本质上来说,其实EnableAutoConfiguration会帮助 springboot应用把所有符合@Configuration配置都加载到当前SpringBoot创建的IoC容器,而这里面借助了Spring框架提供的一个工具类SpringFactoriesLoader的支持。以及用到了Spring提供的条件注解 @Conditional,选择性的针对需要加载的bean进行条件过滤
AutoConfigurationImportSelector
我们来看下AutoConfigurationImportSelector源码下的selectImports方法
1 |
|
AutoConfigurationMetadataLoader 的源码:
1 | final class AutoConfigurationMetadataLoader { |
上述selectImports方法就返回了需要springboot自动装配的一些bean,通过String[]的形式返回需要装配的bean的name,但是这个方法的在真正返回需要装配的bean的name之前,还做了很多操作。做了些动态过滤的操作。
第一步是通过loadMetadata加载当前classpath下的spring-autoconfigure-metadata.properties文件,这个文件里面配置了所有动态加载的条件。第二步是通过getAutoConfigurationEntry获取需要动态加载的class,这一步具体源码如下:
1 | protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, |
getCandidateConfigurations这个详细源码如下:
1 | protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { |
Spring 工厂加载机制,即 Spring Factories Loader,核心逻辑是使用 SpringFactoriesLoader加载由用户实现的类,并配置在约定好的META-INF/spring.factories 路径下,该机制可以为框架上下文动态的增加扩展。
该机制类似于 Java SPI,给用户提供可扩展的钩子,从而达到对框架的自定义扩展功能。
这里SpringFactoriesLoader 的作用就是从classpath/META-INF/spring.factories文件中,根据key来 加载对应的类到spring IoC容器中。
可以看出这里就是加载当前classpath下的所有的spring.factories文件中的内容。下面就是spring.factories中EnableAutoConfiguration的配置。这些如果没有AutoConfigurationImportSelector的过滤操作,这里所有配置的值,都会在getCandidateConfigurations方法中会返回给IOC容器,springboot会自动装载这些类。
1 | # Auto Configure |
SpringBoot项目启动时,就是加载上述XXAutoConfiguration从而实现自动装配。
@EnableAutoConfiguration作用就是从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。这些功能配置类要生效的话,会去classpath中找是否有该类的依赖类(也就是pom.xml必须有对应功能的jar包才行)并且配置类里面注入了默认属性值类,功能类可以引用并赋默认值。生成功能类的原则是自定义优先,没有自定义时才会使用自动装配类。
]]>我们知道SpringBoot的自动装配的秘密在org.springframework.boot.autoconfigure
包下的spring.factories
文件中,而嵌入Tomcat的原理就在这个文件中加载的一个配置类:org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
1 |
|
首先看一下上方的几个注解
@AutoConfigureOrder
这个注解是决定配置类的加载顺序的,当注解里的值越小越先加载,而Ordered.HIGHEST_PRECEDENCE
的值是Integer.MIN_VALUE
也就是说这个类肯定是最先加载的那一批@ConditionalOnXXX
在之前的文章中已经无数次提到了,就不再阐述了@EnableConfigurationProperties
开启ServerProperties
类的属性值配置。而这个类里面包含的就是Web服务的配置1 | "server", ignoreUnknownFields = true) (prefix = |
这个类的代码太多了,这里就不一一贴出来了,我们平常在application.properties
中配置的server.xxx就是这个类中属性
@Import
引入了4个类,看都是什么吧BeanPostProcessorsRegistrar
1 | public static class BeanPostProcessorsRegistrar |
这个类注册了两个bean:WebServerFactoryCustomizerBeanPostProcessor
和ErrorPageRegistrarBeanPostProcessor
关于这两个bean的作用稍后再详细介绍
EmbeddedTomcat
1 |
|
这个类会在存在Tomcat相关jar包时添加一个TomcatServletWebServerFactory
bean
其他两个相信大家都知道怎么回事了
ServletWebServerFactoryCustomizer
和TomcatServletWebServerFactoryCustomizer
现在前期准备工作已经做好了,看一下这个Tomcat是如何启动的吧
启动入口在ServletWebServerApplicationContext
中的onRefresh
方法
1 | protected void onRefresh() { |
Tomcat的启动就在createWebServer
方法里面了
1 | private void createWebServer() { |
首先看一下getWebServerFactory
1 | protected ServletWebServerFactory getWebServerFactory() { |
准备环境里注册的bean现在出来一个了。注意,上方还注册了一个后置处理器EmbeddedServletContainerCustomizerBeanPostProcessor
,获取beantomcatServletWebServerFactory
的时候就会执行后置处理器的postProcessBeforeInitialization
方法
1 | public Object postProcessBeforeInitialization(Object bean, String beanName) |
这个处理器的作用是获得所有定制器,然后执行定制器的方法
这个时候就可以启动Tomcat了
1 | public WebServer getWebServer(ServletContextInitializer... initializers) { |
spring.factories
文件,当我们使用的时候只需要引入如下依赖1 | <dependency> |
然后在org.springframework.boot.spring-boot-actuator-autoconfigure
包下去就可以找到这个文件
查看这个文件发现引入了很多的配置类,这里先关注一下XXXHealthIndicatorAutoConfiguration
系列的类,这里咱们拿第一个RabbitHealthIndicatorAutoConfiguration
为例来解析一下。看名字就知道这个是RabbitMQ的健康检查的自动配置类
1 |
|
按照以往的惯例,先解析注解
@ConditionalOnXXX
系列又出现了,前两个就是说如果当前存在RabbitTemplate
这个bean也就是说我们的项目中使用到了RabbitMQ才能进行下去@ConditionalOnEnabledHealthIndicator
这个注解很明显是SpringBoot actuator自定义的注解,看一下吧1 | (OnEnabledHealthIndicatorCondition.class) |
上方的入口方法是SpringBootCondition
类的matches
方法,getMatchOutcome
这个方法则是子类OnEndpointElementCondition
的,这个方法首先会去环境变量中查找是否存在management.health.rabbit.enabled
属性,如果没有的话则去查找management.health.defaults.enabled
属性,如果这个属性还没有的话则设置默认值为true
当这里返回true时整个RabbitHealthIndicatorAutoConfiguration
类的自动配置才能继续下去
@AutoConfigureBefore
既然这样那就先看看类HealthIndicatorAutoConfiguration
都是干了啥再回来吧1 |
|
首先这个类引入了配置文件HealthIndicatorProperties
这个配置类是系统状态相关的配置
1 | "management.health.status") (prefix = |
接着就是注册了2个beanApplicationHealthIndicator
和OrderedHealthAggregator
这两个bean的作用稍后再说,现在回到RabbitHealthIndicatorAutoConfiguration
类
@AutoConfigureAfter
这个对整体逻辑没影响,暂且不提HealthIndicator
这个bean的创建逻辑是在父类中的1 | public abstract class CompositeHealthIndicatorConfiguration<H extends HealthIndicator, S> { |
HealthAggregator
,这个对象就是刚才注册的OrderedHealthAggregator
createHealthIndicator
方法执行逻辑为:如果传入的beans的size 为1,则调用createHealthIndicator
创建HealthIndicator
否则创建CompositeHealthIndicator
,遍历传入的beans,依次创建HealthIndicator
,加入到CompositeHealthIndicator
中createHealthIndicator
的执行逻辑为:获得CompositeHealthIndicatorConfiguration
中的泛型参数根据泛型参数H对应的class和S对应的class,在H对应的class中找到声明了参数为S类型的构造器进行实例化RabbitHealthIndicator
HealthIndicator
接口,由此可以猜测RabbitHealthIndicator
应该也是这样做的。观察这个类的继承关系可以发现这个类继承了一个实现实现此接口的类AbstractHealthIndicator
,而RabbitMQ的监控检查流程则如下代码所示1 | //这个方法是AbstractHealthIndicator的 |
上方一系列的操作之后,其实就是搞出了一个RabbitMQ的HealthIndicator
实现类,而负责检查RabbitMQ健康不健康也是这个类来负责的。由此我们可以想象到如果当前环境存在MySQL、Redis、ES等情况应该也是这么个操作
那么接下来无非就是当有调用方访问如下地址时,分别调用整个系统的所有的HealthIndicator
的实现类的health
方法即可了
1 | http://ip:port/actuator/health |
HealthEndpointAutoConfiguration
上边说的这个操作过程就在类HealthEndpointAutoConfiguration
中,这个配置类同样也是在spring.factories
文件中引入的
1 |
|
这里重点的地方在于引入的HealthEndpointConfiguration
这个类
1 |
|
这个类只是构建了一个类HealthEndpoint
,这个类我们可以理解为一个SpringMVC的Controller,也就是处理如下请求的
1 | http://ip:port/actuator/health |
那么首先看一下它的构造方法传入的是个啥对象吧
1 | public static HealthIndicator get(ApplicationContext applicationContext) { |
跟我们想象中的一样,就是通过Spring容器获取所有的HealthIndicator
接口的实现类,我这里只有几个默认的和RabbitMQ
然后都放入了其中一个聚合的实现类CompositeHealthIndicator
中
既然HealthEndpoint
构建好了,那么只剩下最后一步处理请求了
1 | "health") (id = |
刚刚我们知道,这个类是通过CompositeHealthIndicator
构建的,所以health
方法的实现就在这个类中
1 | public Health health() { |
至此SpringBoot的健康检查实现原理全部解析完成
]]>SpringApplication
这个类1 |
|
点击run
方法一路跟踪下来,发现首先做的是实例化SpringApplication
对象实例
1 | public static ConfigurableApplicationContext run(Class<?> primarySource, |
deduceWebApplicationType
方法1 | private WebApplicationType deduceWebApplicationType() { |
大抵意思就是根据当前项目中是否存在上方的几个类来推断出当前的web环境,这里因为SpringBoot默认使用的web框架是SpringMVC,所以最后返回结果为WebApplicationType.SERVLET
ApplicationContextInitializer
和ApplicationListener
的实现类1 | private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) { |
可以看到主要还是用的SpringFactoriesLoader
这个类去加载这两个接口的实现类,加载到类以后使用反射的方式构造出这些类的实例,然后根据这些实现类上的Order
注解的值进行排序
关于这些实现类的具体作用请关注后续的文章
mainApplicationClass
我们详细描述了SpringApplication对象实例的创建过程,本篇文章继续看run
方法的执行逻辑吧
1 | public ConfigurableApplicationContext run(String... args) { |
StopWatch
来记录开始时间java.awt.headless
环境变量,在网上了解了一下这个变量的相关信息Headless模式是系统的一种配置模式。在系统可能缺少显示设备、键盘或鼠标这些外设的情况下可以使用该模式
个人理解为是一些图形相关的组件能否使用的开关,欢迎各位大佬指正
SpringApplication
实例时加载的SpringApplicationRunListener
,调用它们的started
方法这里构造时仅仅加载了一个EventPublishingRunListener
类,所以咱们就来解析一下这个东东
1 | public void starting() { |
可以看到这里调用了SimpleApplicationEventMulticaster
类的multicastEvent
方法并且传入了ApplicationStartingEvent
对象,看名字就知道了这个是SpringBoot启动事件
1 | public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { |
其中获取监听器使用的是getApplicationListeners
方法,这个方法中主要就是从最启动时获取的所有监听器和这个事件做了下匹配,返回通过匹配的监听器集合
接着就是看是否设置线程池参数,如果有线程池则使用线程池的线程进行操作,否则将同步调用监听器
把所有的命令行启动参数封装成ConfigurableEnvironment
对象
准备运行时环境
1 | private ConfigurableEnvironment prepareEnvironment( |
getOrCreateEnvironment
方法名就很直观,有就直接获取,没有就新建
1 | private ConfigurableEnvironment getOrCreateEnvironment() { |
上篇文章中说过了,咱们是Servlet环境,所以当前方法是返回一个StandardServletEnvironment
对象,这个对象的构造过程中调用了customizePropertySources
方法(它父类的父类调用的)
1 | protected void customizePropertySources(MutablePropertySources propertySources) { |
可以看出StandardServletEnvironment
往propertySources
中添加了两个StubPropertySource
对象,而它的父类添加了一个包含java系统属性和一个操作系统环境变量的对象
configureEnvironment
1 | protected void configureEnvironment(ConfigurableEnvironment environment, |
分别看一下两个方法
1 | protected void configurePropertySources(ConfigurableEnvironment environment, |
这里就体现出了这个命令行参数比应用配置文件的优先级高的情况了
从PropertySources中查找spring.profiles.active属性,存在则将其值添加activeProfiles集合中
1 | protected void configureProfiles(ConfigurableEnvironment environment, String[] args) { |
EnvirongmentPreparedEvent
事件1 | protected void bindToSpringApplication(ConfigurableEnvironment environment) { |
如果web环境变更为NONE则将StandardServletEnvironment
转换为StandardEnvironment
ConfigurationPropertySources.attach(environment)
1 | public static void attach(Environment environment) { |
最终这个sources
对象的第一个位置放的是它自己,循环引用,这个具体的含义
1 | public ConfigurableApplicationContext run(String... args) { |
spring.beaninfo.ignore
1 | private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) { |
但是这个属性的作用还真不知道。。
打印banner
根据当前环境创建ApplicationContext
1 | protected ConfigurableApplicationContext createApplicationContext() { |
基于咱们的Servlet环境,所以创建的ApplicationContext为AnnotationConfigServletWebServerApplicationContext
SpringBootExceptionReporter
,这个类里包含了SpringBoot启动失败后异常处理相关的组件1 | private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) { |
1 | private void prepareContext(ConfigurableApplicationContext context, |
1 | public void setEnvironment(ConfigurableEnvironment environment) { |
1 | protected void postProcessApplicationContext(ConfigurableApplicationContext context) { |
这一块默认beanNameGenerator
和resourceLoader
都是空的,只有当我们自定义这两个对象时才会把容器内的bean替换
ApplicationContextInitializer
的initialize
方法1 | protected void applyInitializers(ConfigurableApplicationContext context) { |
listeners.contextPrepared(context)
这是个空方法,没有实现,一个Spring的扩展点springApplicationArguments
1 | public void contextLoaded(ConfigurableApplicationContext context) { |
这里不仅发布了ApplicationPreparedEvent
事件,还往实现了ApplicationContextAware
接口的监听器中注入了context容器
BeanDefinitionLoader
对象1 | protected void load(ApplicationContext context, Object[] sources) { |
refreshContext
AbstractApplicationContext
类的refresh
方法,由于篇幅过长这里就不展开了,感兴趣的同学可以参考这篇文章:基于注解的SpringIOC源码解析1 | public void refresh() throws BeansException, IllegalStateException { |
afterRefresh
这里没有任何实现,Spring留给我们的扩展点
停止之前启动的计时装置,然后发送ApplicationStartedEvent
事件
调用系统中ApplicationRunner
以及CommandLineRunner
接口的实现类,关于这两个接口的使用可以参考我的这篇文章:Java项目启动时执行指定方法的几种方式
1 | private void callRunners(ApplicationContext context, ApplicationArguments args) { |
异常处理
发送ApplicationReadyEvent
事件
很多时候我们都会碰到需要在程序启动时去执行的方法,比如说去读取某个配置,预加载缓存,定时任务的初始化等。这里给出几种解决方案供大家参考。
这个注解呢,可以在Spring加载这个类的时候执行一次。来看一下下方代码。
1 |
|
上方就是@PostConstruct注解的使用方法了,同时也表示了此类被加载时的执行顺序。
使用CommandLineRunner接口类似于Main方法启动,可以接受一个字符串数组的命令行参数,来看一下实现
1 |
|
此种方式与实现CommandLineRunner接口的区别就是他的参数是ApplicationArguments
1 | 1) (value = |
我们可以看到,此类相比较于第二种方式还增加一个@Order注解,这个注解其实第二种方式也是能加的。
它的作用就是控制类的加载顺序,这个顺序是从小到大的。比如说启动时先去加载Order的value等于1的类,然后去加载等于2的类。
]]>