learning, progress, future.

skydh


  • 首页

  • 归档

kafka 服务端基本知识

发表于 2019-11-12

# 协议结构

Mysql 双一配置保证数据0丢失

发表于 2019-11-05

预备知识

  请先看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后无损恢复数据的

发表于 2019-10-31

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 -日志存储

发表于 2019-10-22

# 文件结构
  前面说过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

发表于 2019-10-21

用处

  • 配置管理
  • 分布式锁
  • 组成员管理(hbase)
  • DNS服务

    不适合

  • 存储大量数据,只适合存储协调服务的关键数据

目的

  主要用于搭配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注解原理解析

发表于 2019-10-10

@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导致的登录问题

发表于 2019-10-09

问题如下

  本系统登录用户中心后,用户中心返回一个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 选择日志

发表于 2019-09-23

明确的概念

  目前主流的日志框架是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 位运算

发表于 2019-09-06

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网关

发表于 2019-07-08

什么是网关

  网关(英语: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服务的提供者,可以供内部服务之间相互调用)

123…13

skydh

skydh

126 日志
© 2020 skydh
本站访客数:
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.3