# 协议结构
Mysql 双一配置保证数据0丢失
预备知识
请先看mysql如何做到crash后无损恢复数据,了解下mysql是如何数据落地磁盘的。
binlog
事务执行时先把日志写到binlog cache,事务提交,binlog cache就将日志写到文件系统的page cache,这个操作叫做write。 然后等fsync来刷盘,持久化磁盘数据,这个操作叫做fsync。这2个操作是由一个叫做sync_binlog来控制的。
- sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
- sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
- sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
实际业务场景是100-1000,但是对于强数据安全的,可以设置为1,这个1就是双1中的1。
redolog
和前面的binlog差不多,事务执行的时候先把数据写入到redo log buffer里面,事务提交就开始写入到page cache里面,也就是write操作,第三步就是fsync操作,将文件系统缓存的page cache持久化到磁盘里面去。
为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它有三种可能取值:
- 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
- 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;
- 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。
而这个1就是就是双一的最后一个1了
不仅仅如此,其实也没那么简单,比如redo log buffer快满了,怎么办,别的事务没提交怎么办,这些当然都是直接先写入磁盘处理。
后记
Mysql默认就是双一配置。
mysql 如何做到crash后无损恢复数据的
ps
真实的流程没有我说的那么简单,下面的是最基本的情况。
预备知识
- redolog
- binlog
- WAL机制
redolog简介
redolog是个循环日志,其大小固定为4g,存在2个指针来定位其是否已经满了。一个指针是当前写,一个指针是当前checkpoint,其2个指针的顺时针空间就是可写的空间。
这个redolog就是完成mysql突然宕机后,如何无损恢复数据的。
binlog简介
这个是mysql自身的日志,叫做归档日志。和redolog大致3个不同点。
- 其为mysql自带的,redolog是innodb里面的。
- redolog是物理日志,binlog是逻辑日志。
- redolog是循环日志,binlog是增量日志。
这里不对其过多介绍,下一篇文章关于主从的详细介绍,主从就是通过binlog完成的。
WAL机制简介
MySQL里经常说到的 WAL 技术,意思就是数据入库前先写进去日志,再写磁盘里面。
这里采用的是数据安全性最高的双1策略。
其数据更新顺序也保持着其顺序。
下面我来简单介绍下。
1.当要更新id=2这一行数据时,先通过这个表的索引,查询到这行记录所在的数据页。然后判断这个数据页是否在buffer pool(这个是内存)。
2.如果不在内存则需要读到内存(其实也可以不用读入内存,当要更新的这行数据没有唯一索引时,mysql为了提高效率,采用了change buffer(别看有个buffer,但是人家也是持久化到磁盘的)这个东西,将对这行修改的动作记录到change buffer里面,就不用读到内存了)。
3.在buffer pool 内存上修改这行数据(仅仅只是在内存上修改了这行数据,并没有持久化到磁盘里面)。
4.将上面的操作,写入到redolog里面,且将这个操作状态设置为prepare状态,首先写到redologbuffer里面(为了提高效率,mysql做了组提交这个优化,这里不扩展),等到这个事务提交后,redologbuffer然后再写入到文件系统的page cache里面,然后立马调用fsync,将其刷到磁盘。
5.然后写入到binlog里面,先写到binlog cache里面,然后写入page cache,然后调用fsync,写入磁盘。
6.redolog继续写一次,将其状态设置为commit状态。
如此就算完成了一个更新操作。
mysql 如何做到crash后,数据不丢失的
前面说了数据更新操作。
当前buffer pool里面存在大量脏页(就是一些数据页,只在内存里面修改了,没有刷新到磁盘),当系统宕机了,内存里面的数据全部丢失了怎么办?
方法如下:我们需要redolog来完成灾难备份,check point到writ pos这块空间记录的所有的操作步骤派上用场了,根据这些redolog记录从磁盘里面读取所有相关的数据页。然后按照redolog上的操作恢复数据即可,那么我们发现存再内存的数据全部恢复了。
kafka入门006 -日志存储
# 文件结构
前面说过kafka的有序是分区的有序性,一个分区时一个log文件,一个log文件划分为多个日志分段(logsegment)。其中log在物理存储上只以文件夹的形式存储,每个logsegmen对应磁盘上的一个日志文件和2个索引文件。
消息压缩
在kafka中,kafka将多条消息一起压缩。发送到broker之后也是保持压缩状态存储,消费者端从broker获取到的也是压缩的消息,消费者在处理这些消息的时候才会解压消息。
多个消息压缩到一起发送,这个压缩体叫做外层消息,里面有个value保存的则是多个压缩的消息,叫做内层消息。内层消息内部是多个消息的压缩,内部自己维持一个offset,从0开始,同时外层消息也有个offset,这个offset是内层消息最后一个offset的实际位移。
外层消息也有个timestamp。
- 当这个timestamp类型是createTime,那么设置的是内层消息的最大时间戳。
- 当这个类型是LogAppendTime,那么设置的是kafka服务器当前时间戳。
内层消息里面有个时间戳timestamp。
- 当这个外层timestamp类型是createTime,那么内层消息的时间戳设置的是生产者创建消息时的时间戳。
- 当这个外层timestamp类型是createTime,那么内层消息的时间戳会被忽略。
*变长长度
kafka引入Varints变长整形,Varints是使用一个或者多个字节来序列化整数的方法,数值越小,占用字节数越小,Varints每个字节除了最后一个字节外最高位都是1。最后一个字节最高位是0,用于区分是否表示一个整数,除了最高位,其余的位表示数据存储。由于是变长字段,因此存储消息时很灵活。日志索引
前面说了每个日志分段文件对应2个索引文件,但是不保证每个消息都可以在索引文件里面找到索引项的。这个索引文件时每当消息,写入一定量(log.index.interval.bytes默认4kb),偏移量索引文件和时间索引文件会分别增加一个索引项。索引文件都是按顺序递增的,因此我们可以使用二分查找来。
日志文件也会分隔,满足以下条件就会分隔
- 日志文件大小超过 log.segment.bytes,分隔,默认1GB。
- 当前日志分段中最大时间戳和当前系统时间戳差值大于log.roll.ms或者log.roll.hours,第一个优先级最高,但是默认第二个,也就是一周。
- 2个索引文件大小超过log.index.size.max默认10MB时。
- 追加消息偏移量和当前日志最大的偏移量的差值大于Integer.MAX_VALUE。
对于非活跃日志分段,对应的索引文件是只读,对于活跃日志分段,对应的日志分段是可读写,在索引文件切分的时候,则变成只读,同时创建可读写的新的索引文件。其文件会预分配log.index.size.max.bytes的大小空间.只有当索引文件裁剪时才会将文件剪裁到实际大小。
#
zookeeper 入门 001
用处
目的
主要用于搭配kafka进行学习。因为kafka就是使用zookeeper作为协调服务用的。
zookeeper存储模型
Zookeeper的存储结构采用的是层次化的文件结构模型,很像数据结构当中的树,也很像文件系统的目录。树是由节点所组成,Zookeeper的数据存储也同样是基于节点,这里称之为znode。
Znode包含4个信息。
data:Znode存储的数据信息。
ACL:记录Znode的访问权限,哪些ip那些人可以访问本节点。
stat:包含Znode的各种元数据,比如事务id,大小,时间戳等。
Znode特点
- Znode的引用方式是路径引用,类似于文件路径:/a/b。
- znode数据只支持全量读取和写入。
- Znode的API都是互不影响的。
ZNode分类
- 持久。zookeeper集群,client宕机后,重启依旧还在。
- 临时。client宕机,或者一定时间没有发消息,那就消失。
zk session
zk session是zk客户端和zk集群中某一个节点建立的,客户端可以主动关闭session,zk节点如果在这个session所关联的timeout时间内收到客户端消息,zk客户端也会关闭节点,如果zk客户端发现所连接的zk节点出错,会自动和其他zk节点建立连接。
zk Quorun模式
这个模式就是集群模式,包含多个zookeeper节点,其中一个是lead节点,其余的是follower节点。lead节点可以处理读写请求,follower节点只能处理读请求,若是follower节点收到写请求,会将其转发给lead处理。
spring @import注解原理解析
@import
我们可以用这个注解导入bean到容器里面。
3个方式
直接@import
导入的baen
@Data
public class User {
private Integer id;
private String name;
}
导入方法:
@Configuration
@Import(value = { User.class })
public class Config {
}
通过ImportBeanDefinitionRegistrar
还是上面的bean。但是 不是直接导入。
public class UserServiceBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinitionBuilder user = BeanDefinitionBuilder.rootBeanDefinition(User.class);
// 通过registry就可以注入到容器里啦
registry.registerBeanDefinition("user", user.getBeanDefinition());
}
}
注解方法:
@Configuration
@Import(value = { UserServiceBeanDefinitionRegistrar.class })
public class Config {
}
通过ImportSelector
public class UserServiceImportSelect implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] { User.class.getName() };
}
}
注解方法:
@Configuration
@Import(value = { UserServiceImportSelect.class })
public class Config {
}
小结
上述3个方法都可以将bean导入到springbean容器里面。
源码分析
老规矩,我们从springboot的启动开始分析,虽然这直接属于spring那一块。我们从run开始点点点,进入下面方法。
public ConfigurableApplicationContext run(String... args) {
......
refreshContext(context);
......
}
直接关注这个方法,这个方法会调用核心,他是spring容器启动的核心方法。
然后不断点点,进入到这个核心方法。
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
.....
invokeBeanFactoryPostProcessors(beanFactory);
.....
}
这个方法提取出invokeBeanFactoryPostProcessors方法,点进去。
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
......
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
......
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);
......
}
进入这个方法后主要有2个方法需要注意。这个接口BeanFactoryPostProcessor的实现类是这个ConfigurationClassPostProcessor。进入invokeBeanFactoryPostProcessors方法。
private static void invokeBeanFactoryPostProcessors(
Collection<? extends BeanFactoryPostProcessor> postProcessors, ConfigurableListableBeanFactory beanFactory) {
for (BeanFactoryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanFactory(beanFactory);
}
}
调用了postProcessBeanFactory方法。然后点点点,进入processConfigBeanDefinitions方法,这个就是处理@import的方法了。
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
......
parser.parse(candidates);
......
}
进入这个方法,不断进入doProcessConfigurationClass这个方法里面。
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {
......
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), true);
......
}
进去后,第一个方法获取到本项目下所有的bean,然后过滤找到是@Import的bean,再然后在processImports方法里面将其里面的bean注入spring容器进去。
多cookie导致的登录问题
问题如下
本系统登录用户中心后,用户中心返回一个cookie给浏览器,浏览器携带cookie访问我们后台服务,后台服务通过cookie,从redis里面获取到信息后,向用户中心发送rpc请求,从而进行权限校验。问题如下,用户中心修改配置后,账号登录变成单端登录,A登录a账号,B登录a账号,B把A挤了下来。但是A不知道,继续访问后台服务,报错,没有权限,但是浏览器端有了新的cookie。再次登录,用户中心报错。
分析如下
2个方案,用户中心分析这个cookie,选择正确的cookie处理,或者本系统在那个情况下不生成这个cookie。
源码解析
springmvc 请求一个服务,要经过一系列的过滤器链,而我们使用了springsession,那么其中就有一个优先级很高的一个过滤器SessionRepositoryFilter。核心方法为
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
wrappedRequest.commitSession();
}
}
我们注意到,无论如何都要执行的finally代码块。进去观察
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this,
this.response);
}
}
else {
S session = wrappedSession.getSession();
clearRequestedSessionCache();
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
if (!isRequestedSessionIdValid()
|| !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this,
this.response, sessionId);
}
}
}
最后一行代码会进入CookieHttpSessionIdResolver的setSessionId方法中,我们观察这个方法。
@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response,
String sessionId) {
if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
return;
}
request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, sessionId));
}
其会进入DefaultCookieSerializer的writeCookieValue方法里面
@Override
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
StringBuilder sb = new StringBuilder();
sb.append(this.cookieName).append('=');
String value = getValue(cookieValue);
if (value != null && value.length() > 0) {
validateValue(value);
sb.append(value);
}
int maxAge = getMaxAge(cookieValue);
if (maxAge > -1) {
sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
OffsetDateTime expires = (maxAge != 0)
? OffsetDateTime.now().plusSeconds(maxAge)
: Instant.EPOCH.atOffset(ZoneOffset.UTC);
sb.append("; Expires=")
.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
}
String domain = getDomainName(request);
if (domain != null && domain.length() > 0) {
validateDomain(domain);
sb.append("; Domain=").append(domain);
}
String path = getCookiePath(request);
if (path != null && path.length() > 0) {
validatePath(path);
sb.append("; Path=").append(path);
}
if (isSecureCookie(request)) {
sb.append("; Secure");
}
if (this.useHttpOnlyCookie) {
sb.append("; HttpOnly");
}
if (this.sameSite != null) {
sb.append("; SameSite=").append(this.sameSite);
}
response.addHeader("Set-Cookie", sb.toString());
}
找到了,原来在这里会在response里面写入cookie,在这个方法里面只要我们指定了cookie的name和domain和用户中心传给我们的一样,那么就会解决多cookie问题。
从系统启动开始看,加了EnableRedisHttpSession这个注解,会采用spring session,点进去里面有个@import注解,引入了RedisHttpSessionConfiguration这个配置类,这个类里面配置引入了很多bean。同时继承了一个SpringHttpSessionConfiguration这个抽象类,在这个抽象类里面有2个方法和我们强相关
@Autowired(required = false)
public void setCookieSerializer(CookieSerializer cookieSerializer) {
this.cookieSerializer = cookieSerializer;
}
@PostConstruct
public void init() {
CookieSerializer cookieSerializer = (this.cookieSerializer != null)
? this.cookieSerializer
: createDefaultCookieSerializer();
this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}
第一个方法是当容器中存在CookieSerializer实例时,就注入到这个bean中,第二个方法是这个bean执行完构造方法和所有注入后执行的方法,这个方法里面的逻辑如下,如果上下文不存在CookieSerializer这个实例,那么直接new一个。
那么解决方案就出来。我们可以创建一个bean注入到spring容器里面即可
@Configuration
public class CookieConfiguration {
@Value("${cookie.name}")
private String cookieName;
@Value("${cookie.domain}")
private String domain;
@Bean
public DefaultCookieSerializer defaultCookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName(cookieName);
serializer.setDomainName(domain);
return serializer;
}
}
如此即可,返回时写cookie时,name和domain就指定了,就会覆盖之前的cookie,不会出现2个cookie了。问题解决。
什么情况下会写入cookie。
再往上走,会发现
if (!isRequestedSessionIdValid()
|| !sessionId.equals(getRequestedSessionId()))
这个条件下,才会执行写入cookie的情况。我们的上述情况一定在这个里面。主要是第一个条件我们看看,点进去看看
@Override
public boolean isRequestedSessionIdValid() {
if (this.requestedSessionIdValid == null) {
S requestedSession = getRequestedSession();
if (requestedSession != null) {
requestedSession.setLastAccessedTime(Instant.now());
}
return isRequestedSessionIdValid(requestedSession);
}
return this.requestedSessionIdValid;
}
这个方法主要和requestedSessionIdValid这个值相关。我们通过eclipse的call功能,找到了只有这个方法才对这个属性做了修改。
@Override
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
S requestedSession = getRequestedSession();
if (requestedSession != null) {
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
}
........
}
我省略了一部分代码。他会根据requestedSession来判断是否设值进去,因此进入方法getRequestedSession。
private S getRequestedSession() {
if (!this.requestedSessionCached) {
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
.resolveSessionIds(this);
for (String sessionId : sessionIds) {
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
S session = SessionRepositoryFilter.this.sessionRepository
.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
this.requestedSessionCached = true;
}
return this.requestedSession;
}
这里我就不再进入方法内部了,我就直接说这些方法的功能。先获取事先约定好的cookie的value值,然后通过这个value值从redis里面获取对应的hash值,然后组装成session返回给方法,如果不存在,则返回null。最后若是session不存在,那么就会创建一个丢到redis里面去。并且丢到浏览器前端里面。
因此当这个cookie过期,或者cookie在redis里面不存在时就会写入cookie。
springboot 选择日志
明确的概念
目前主流的日志框架是logback,log4j,javalogging,这是目前主流的日志框架,其中出了个适配器,相当于接口,slf4j的作者和logback,log4j是一个人,这个接口可以适配logback,log4j
springboot启动时如何选择日志的呢
我们从springboot启动
预先知识:
- @Import,这个注解的value是Class类型的数组,我们可以直接传递一些类进去,那么这些类就会被导入到当前ioc容器。或者导入,这些类里面加了@Bean方法返回的对象。同时会将实现ImportSelector接口的selectImports方法返回的类的全限定名导入到ioc容器。
流程如下:
1.启动类上面的@SpringBootApplication注解,点进去,再进去EnableAutoConfiguration注解,上面有个import注解,前面说过了,点进去这个AutoConfigurationImportSelector类,看到selectImports方法:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
我们看到第2行代码,点进去
protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
我们点到第五行代码点进去
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
不断深入SpringFactoriesLoader.loadFactoryNames方法
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
其中FACTORIES_RESOURCE_LOCATION= “META-INF/spring.factories”
也就是说从当前jar将前面说的key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的value全部拉出来然后反射实例化后注入到spring ioc容器。这些key都是自动化配置的key。再看看后面的步骤。
2。从SpringApplication.run(WebApplication.class, args)
进入到
*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources,
String[] args) {
return new SpringApplication(primarySources).run(args);
}
这个方法里面创建了SpringApplication对象,进入到其构造方法。
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
在setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));这行代码里面Class类型是ApplicationListener。这个方法前文讲述过了。这里特别提一下其中有个Listener和本文相关,那就是LoggingApplicationListener这个Listener。然后将其注入到SpringApplication的listeners属性里面。前面方法里面创建对象后立马调用了run方法。进去,这个run方法我就不细说了,都知道。
try {
listeners.running(context);
}
这行代码最终是启动listeners的,点进去源码可以看到
@Override
public void running(ConfigurableApplicationContext context) {
context.publishEvent(
new ApplicationReadyEvent(this.application, this.args, context));
}
然后我们进入到LoggingApplicationListener这个类的关键代码处
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartingEvent) {
onApplicationStartingEvent((ApplicationStartingEvent) event);
}
else if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
else if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
.getApplicationContext().getParent() == null) {
onContextClosedEvent();
}
else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}
执行第一个事件。
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
this.loggingSystem.beforeInitialize();
}
进入LoggingSystem的get方法里面。
public static LoggingSystem get(ClassLoader classLoader) {
String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
if (StringUtils.hasLength(loggingSystem)) {
if (NONE.equals(loggingSystem)) {
return new NoOpLoggingSystem();
}
return get(classLoader, loggingSystem);
}
return SYSTEMS.entrySet().stream()
.filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
.map((entry) -> get(classLoader, entry.getValue())).findFirst()
.orElseThrow(() -> new IllegalStateException(
"No suitable logging system located"));
}
核心出来了在这里,从SYSTEMS这个map里面按顺序取值,而这个map的值是下面的。测试了下。顺序取出的顺序是logback,log4j,javalog。按照上面代码的逻辑,如果上下文存在这个日志类,那么springboot就使用哪个类。显然是优先使用logback
private static final Map<String, String> SYSTEMS;
static {
Map<String, String> systems = new LinkedHashMap<>();
systems.put("ch.qos.logback.core.Appender",
"org.springframework.boot.logging.logback.LogbackLoggingSystem");
systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
systems.put("java.util.logging.LogManager",
"org.springframework.boot.logging.java.JavaLoggingSystem");
SYSTEMS = Collections.unmodifiableMap(systems);
}
java 位运算
ps: 在java中,负数的二进制为,正数的二进制的反码+1。
左移( << )
把数字向左移动,比如 <<2,相当于向左移动2位,相当于做了乘法,乘积为2^2。符号位不变。低数位全部用0来补
public static void main(String[] arg) {
System.out.println(5 << 2);
}
输出为20,相当于5*4=20
右移( >> )
把数字向右移动,比如 >>2,相当于向左移动2位,相当于做了除法,除了2^2。符号位不变。正数用0补位,负数用1补位。
public static void main(String[] arg) {
System.out.println(5 >> 2);
}
输出为1
无符号右移( >>> )
ps:没有无符号左移
把数字向右移动,比如 >>2,相当于向左移动2位,相当于做了除法,除了2^2。符号位也变。正数没有啥变化,负数的话,变化蛮大的。因为高数位全部都是0来补充。
位与( & )
将数字转换为2进制后做与运算,每一个数字都要做与运算。2个数进行位与时,将2个数均转换为2进制数,然后按照顺序每一位开始做对应的与运算。均为1才是1,否则,为0.
位或( | )
将数字转换为2进制后做或运算,每一个数字都要做或运算。2个数进行位或时,将2个数均转换为2进制数,然后按照顺序每一位开始做对应的位或运算。均为0才是0,否则为1.
位非( ~ )
将数字转换为2进制后做非运算,每一个数字都要做非运算。对这个数字每一位都要进行非运算,也就是取反。
位异或( ^ )
将数字转换为2进制后做异或运算,每一个数字都要做异或运算。2个数进行异或时,将2个数均转换为2进制数,然后按照顺序每一位开始做对应的异或运算。均为0,或者均为1,才是0,否则为0.
Api网关
什么是网关
网关(英语:Gateway)是转发其他服务器通信数据的服务器,接收从客户端发送来的请求时,它就像自己拥有资源的源服务器一样对请求进行处理。有时客户端可能都不会察觉,自己的通信目标是一个网关。
api网关
API 网关将各系统对外暴露的服务聚合起来,所有要调用这些服务的系统都需要通过 API 网关进行访问,基于这种方式网关可以对 API 进行统一管控,例如:认证、鉴权、流量控制、协议转换、监控等等。
面向 Web 或者移动 App
这类场景,在物理形态上类似前后端分离,前端应用通过 API 调用后端服务,需要网关具有认证、鉴权、缓存、服务编排、监控告警等功能。
面向合作伙伴开放 API
这类场景,主要为了满足业务形态对外开放,与企业外部合作伙伴建立生态圈,此时的 API 网关注重安全认证、权限分级、流量管控、缓存等功能的建设。
企业内部系统互联互通
对于中大型的企业内部往往有几十、甚至上百个系统,尤其是微服务架构的兴起系统数量更是急剧增加。系统之间相互依赖,逐渐形成网状调用关系不便于管理和维护,需要 API 网关进行统一的认证、鉴权、流量管控、超时熔断、监控告警管理,从而提高系统的稳定性、降低重复建设、运维管理等成本。
具备的能力
服务注册和服务接入能力
网关接入和发布核心功能
服务安全
服务管控和治理
ps:https://www.infoq.cn/article/api-gateway-architecture-design
zuul
我司的api网关服务是基于zuul2实现的。为了搭建一个类似的网关服务,我将详细的搭建一个demo,来跑这个服务。
我先根据网上的教程,搭建了一个简单的eureka注册中心,然后搭建了2个简单的server注册到这个注册中心里面,然后搭建一个简单的基于zuul的网关注册到eureka里面,这个api网关是基于serverId实现的,然后启动一个server,路由访问,成功得到数据。实现这个ZuulFilter这个接口做的效果,默认则是org.springframework.cloud.netflix.zuul.filters这个包下的过滤器的实现。
ps:参考这篇文章
http://blog.didispace.com/spring-cloud-source-zuul/
ps(SpringCloud的整体组建包括:Zuul、Ribbon、EureKa、Fein、Hystrix等。其中Zuul就是一个类似APIGateway的组建,Ribbon是类似于Nginx的代理服务器,Eureka用于注册和发现服务,Hystrix可以作为整个架构的断路服务,用于服务降级。Fein可以作为一个Rest服务的提供者,可以供内部服务之间相互调用)