分類
博客园cnblogs

【一起学源码-微服务】Nexflix Eureka 源码七:通过单元测试来Debug Eureka注册过程

前言

上一讲eureka client是如何注册的,一直跟到源码发送http请求为止,当时看eureka client注册时如此费尽,光是找一个regiter的地方就找了半天,那么client端发送了http请求给server端,server端是如何处理的呢?

带着这么一个疑问 就开始今天源码的解读了。

如若转载 请标明来源:一枝花算不算浪漫

源码解读

从何读起?

上一讲我们知道,跟进client注册 一直到 AbstractJersey2EurekaHttpClient.register方法,这里先看下其中的源码:

public EurekaHttpResponse<Void> register(InstanceInfo info) {
        String urlPath = "apps/" + info.getAppName();
        Response response = null;
        try {
            // 发送请求,类似于:http://localhost:8080/v2/apps/ServiceA
            // 发送的是post请求,服务实例的对象被打成了一个json发送,包括自己的主机、ip、端口号
            // eureka server 就知道了这个ServiceA这个服务,有一个服务实例,比如是在192.168.31.109、host-01、8761端口
            Builder resourceBuilder = jerseyClient.target(serviceUrl).path(urlPath).request();
            addExtraProperties(resourceBuilder);
            addExtraHeaders(resourceBuilder);
            response = resourceBuilder
                    .accept(MediaType.APPLICATION_JSON)
                    .acceptEncoding("gzip")
                    .post(Entity.json(info));
            return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Jersey2 HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                        response == null ? "N/A" : response.getStatus());
            }
            if (response != null) {
                response.close();
            }
        }
    }

那这种情况我们肯定可以猜测,server端应该有个controller来接收此http请求,然后默默的去做一些注册的逻辑。

紧接着我们从/apps/这个关键词入手,进行全局搜索:

全局搜索结果如下,这里可以看到很多test 调用,这里框起来的一个是不是类似于我们controller接口的调用呢?直接点进去查看,然后一步步跟进。

源码分析

接着上面说的,跟进ApplicationResource这个类,可以找到如下方法:

@Path("{appId}")
public ApplicationResource getApplicationResource(
        @PathParam("version") String version,
        @PathParam("appId") String appId) {
    CurrentRequestVersion.set(Version.toEnum(version));
    return new ApplicationResource(appId, serverConfig, registry);
}

这个appId可以理解为我们之前传递的appName,紧接着这里是直接构造了一个ApplicationResource实例,接着跟进代码,进入ApplicationResource中我们可以看到很多@GET@POST 等restful接口,还记得上面我们register方法中,发送的http请求用的就是POST方法,所以我们这里直接看@POST请求

@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
                            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
    logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
    // validate that the instanceinfo contains all the necessary required fields
    if (isBlank(info.getId())) {
        return Response.status(400).entity("Missing instanceId").build();
    } else if (isBlank(info.getHostName())) {
        return Response.status(400).entity("Missing hostname").build();
    } else if (isBlank(info.getIPAddr())) {
        return Response.status(400).entity("Missing ip address").build();
    } else if (isBlank(info.getAppName())) {
        return Response.status(400).entity("Missing appName").build();
    } else if (!appName.equals(info.getAppName())) {
        return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build();
    } else if (info.getDataCenterInfo() == null) {
        return Response.status(400).entity("Missing dataCenterInfo").build();
    } else if (info.getDataCenterInfo().getName() == null) {
        return Response.status(400).entity("Missing dataCenterInfo Name").build();
    }

    // handle cases where clients may be registering with bad DataCenterInfo with missing data
    DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
    if (dataCenterInfo instanceof UniqueIdentifier) {
        String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
        if (isBlank(dataCenterInfoId)) {
            boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
            if (experimental) {
                String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                return Response.status(400).entity(entity).build();
            } else if (dataCenterInfo instanceof AmazonInfo) {
                AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
                String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
                if (effectiveId == null) {
                    amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
                }
            } else {
                logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
            }
        }
    }

    registry.register(info, "true".equals(isReplication));
    return Response.status(204).build();  // 204 to be backwards compatible
}

由于代码不是很长,这里都给截取出来了。其实这里做的事情就很简单了。

  1. 做一些常规的chek,检查注册实例InstanceInfo的一些基本信息
  2. DataCenter的相关操作,这里还涉及到亚马逊云,我们直接跳过
  3. registry.register(info, "true".equals(isReplication)); 这里才是核心的注册,我们继续往下
public void register(final InstanceInfo info, final boolean isReplication) {
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
    super.register(info, leaseDuration, isReplication);
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    try {
        read.lock();
        Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
        REGISTER.increment(isReplication);
        if (gMap == null) {
            final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
            gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
            if (gMap == null) {
                gMap = gNewMap;
            }
        }
        Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
        // Retain the last dirty timestamp without overwriting it, if there is already a lease
        if (existingLease != null && (existingLease.getHolder() != null)) {
            Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
            Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
            logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);

            // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
            // InstanceInfo instead of the server local copy.
            if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
                        " than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
                registrant = existingLease.getHolder();
            }
        } else {
            // The lease does not exist and hence it is a new registration
            synchronized (lock) {
                if (this.expectedNumberOfRenewsPerMin > 0) {
                    // Since the client wants to cancel it, reduce the threshold
                    // (1
                    // for 30 seconds, 2 for a minute)
                    this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
                    this.numberOfRenewsPerMinThreshold =
                            (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
                }
            }
            logger.debug("No previous lease information found; it is new registration");
        }
        Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
        if (existingLease != null) {
            lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
        }
        gMap.put(registrant.getId(), lease);
        synchronized (recentRegisteredQueue) {
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
        }
        // This is where the initial state transfer of overridden status happens
        if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
            logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
                            + "overrides", registrant.getOverriddenStatus(), registrant.getId());
            if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                logger.info("Not found overridden id {} and hence adding it", registrant.getId());
                overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
            }
        }
        InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
        if (overriddenStatusFromMap != null) {
            logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
            registrant.setOverriddenStatus(overriddenStatusFromMap);
        }

        // Set the status based on the overridden status rules
        InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
        registrant.setStatusWithoutDirty(overriddenInstanceStatus);

        // If the lease is registered with UP status, set lease service up timestamp
        if (InstanceStatus.UP.equals(registrant.getStatus())) {
            lease.serviceUp();
        }
        registrant.setActionType(ActionType.ADDED);
        recentlyChangedQueue.add(new RecentlyChangedItem(lease));
        registrant.setLastUpdatedTimestamp();
        invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
        logger.info("Registered instance {}/{} with status {} (replication={})",
                registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
    } finally {
        read.unlock();
    }
}

到了这里东西就有点多了,我们慢慢梳理。

  1. reda.lock() 这里使用的是读锁,方便多个服务实例同时来注册
  2. 这里关键信息是registry的数据结构,同时这也是保存注册实例的对象。
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

ConcurrentHashMap的key是appName
第二层Map的key是appId,所以数据结构格式类似于:

{
    “ServiceA”: {
        “001”: Lease<InstanceInfo>,
        “002”: Lease<InstanceInfo>,
        “003”: Lease<InstanceInfo>
    },
    “ServiceB”: {
        “001”: Lease<InstanceInfo>
    }
}
  1. 这里面还有两个队列recentRegisteredQueuerecentlyChangedQueue,其中registerQueue默认保存最近1000条注册的实例信息。
  2. 后面就是一些状态设置之类的操作

注册表使用场景

我们注册完成之后,打开eureka 后台配置页面,可以看到自己的实例已经在页面上了,那么这个东东是如何展示的呢?

我们都知道eureka-resources模块下有很多jsp信息,点开status.jsp查看一下:

这里用到了 serverContext.getRegistry().getSortedApplications(), 然后在通过获取的Applicaiton 去执行app.getInstances()等到了所有大的服务实例信息。

这里我们还需要回头看下EurekaBootStrap中的代码,看看Application是如何来的。

PeerAwareInstanceRegistryImpl.javagetSortedApplications()一直跟到 AbstractInstanceRegistry.javagetApplicationsFromMultipleRegions(),如下图所示:

看到这里是不是就真相大白了?
这里再总结一下:

在jsp代码中,拿到了EurekaServerContext,所以之前为什么要将这个东东放到一个Holder里面去,就是随时都要从这个里面去获取一些数据

然后会从EurekaServerContext,获取到注册表,PeerAwareInstanceRegistry,注册表,从里面获取所有的服务信息,从底层的map数据结构中,获取所有的服务注册的信息,遍历,封装到一个叫Application的东西里去,一个Application就代表了一个服务,里面包含很多个服务实例。

Eureka的服务注册流程图

申明

本文章首发自本人博客:https://www.cnblogs.com/wang-meng 和公众号:壹枝花算不算浪漫,如若转载请标明来源!

感兴趣的小伙伴可关注个人公众号:壹枝花算不算浪漫

分類
博客园cnblogs

驱动领域DDD的微服务设计和开发实战

你是否还在为微服务应该拆多小而争论不休?到底如何才能设计出收放自如的微服务?怎样才能保证业务领域模型与代码模型的一致性?或许本文能帮你找到答案。 本文是基于 DDD 的微服务设计和开发实战篇,通过借鉴领域驱动设计思想,指导微服务项目团队进行设计和开发(理论篇详见《当中台遇上 DDD,我们该如何设计微服务?》)。本文包括三部分内容:第一部分讲述领域驱动设计基本知识,包括:分层架构、服务视图、数据视图和领域事件发布和订阅等;第二部分讲述微服务设计方法、过程、模板、代码目录、设计原则等内容;最后部分以一个项目为例讲述基于 DDD 的微服务设计过程。

目标

本文采用 DDD(领域驱动设计)作为微服务设计指导思想,通过事件风暴建立领域模型,合理划分领域逻辑和物理边界,建立领域对象及服务矩阵和服务架构图,定义符合 DDD 分层架构思想的代码结构模型,保证业务模型与代码模型的一致性。通过上述设计思想、方法和过程,指导团队按照 DDD 设计思想完成微服务设计和开发。 通过领域模型和 DDD 的分层思想,屏蔽外部变化对领域逻辑的影响,确保交付的软件产品是边界清晰的微服务,而不是内部边界依然混乱的小单体。在需求和设计变化时,可以轻松的完成微服务的开发、拆分和组合,确保微服务不易受外部变化的影响,并稳定运行。

适用范围

本文适用于按照 DDD 设计方法进行微服务设计和开发的项目及相关人员。

DDD 分层架构视图

DDD 分层架构包括:展现层、应用层、领域层和基础层。

DDD 分层架构各层职能如下:

  • 展现层
    展现层负责向用户显示信息和解释用户指令。
  • 应用层
    应用层是很薄的一层,主要面向用户用例操作,协调和指挥领域对象来完成业务逻辑。应用层也是与其他系统的应用层进行交互的必要渠道。应用层服务尽量简单,它不包含业务规则或知识,只为下一层的领域对象协调任务,使它们互相协作。应用层还可进行安全认证、权限校验、分布式和持久化事务控制或向外部应用发送基于事件的消息等。
  • 领域层
    领域层是软件的核心所在,它实现全部业务逻辑并且通过各种校验手段保证业务正确性。它包含业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。它负责表达业务概念、业务状态以及业务规则,具体表现形式就是领域模型。
  • 基础层
    基础层为各层提供通用的技术能力,包括:为应用层传递消息、提供 API 管理,为领域层提供数据库持久化机制等。它还能通过技术框架来支持各层之间的交互。

服务视图

微服务内的服务视图

微服务内有 Facade 接口、应用服务、领域服务和基础服务,各层服务协同配合,为外部提供服务。

1、接口服务

接口服务位于用户接口层,用于处理用户发送的 Restful 请求和解析用户输入的配置文件等,并将信息传递给应用层。

2、应用服务

应用服务位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装。 应用层的服务包括应用服务和领域事件相关服务。 应用服务可对微服务内的领域服务以及微服务外的应用服务进行组合和编排,或者对基础层如文件、缓存等数据直接操作形成应用服务,对外提供粗粒度的服务。 领域事件服务包括两类:领域事件的发布和订阅。通过事件总线和消息队列实现异步数据传输,实现微服务之间的解耦。

3、领域服务

领域服务位于领域层,为完成领域中跨实体或值对象的操作转换而封装的服务,领域服务以与实体和值对象相同的方式参与实施过程。 领域服务对同一个实体的一个或多个方法进行组合和封装,或对多个不同实体的操作进行组合或编排,对外暴露成领域服务。领域服务封装了核心的业务逻辑。实体自身的行为在实体类内部实现,向上封装成领域服务暴露。 为隐藏领域层的业务逻辑实现,所有领域方法和服务等均须通过领域服务对外暴露。 为实现微服务内聚合之间的解耦,原则上禁止跨聚合的领域服务调用和跨聚合的数据相互关联。

4、基础服务

基础服务位于基础层。为各层提供资源服务(如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务逻辑的影响。 基础服务主要为仓储服务,通过依赖反转的方式为各层提供基础资源服务,领域服务和应用服务调用仓储服务接口,利用仓储实现持久化数据对象或直接访问基础资源。

微服务外的服务视图

1. 前端应用与微服务

微服务中的应用服务通过用户接口层组装和数据转换后,发布在 API 网关,为前端应用提供数据展示服务。

2. 微服务与外部应用

跨微服务数据处理时,对实时性要求高的场景,可选择直接调用应用服务的方式(新增和修改类型操作需关注事务一致性)。对实时性要求不高的场景,可选择异步化的领域事件驱动机制(最终数据一致性)。

数据视图

DDD 分层架构中数据对象转换的过程如下图。

数据视图应用服务通过数据传输对象(DTO)完成外部数据交换。领域层通过领域对象(DO)作为领域实体和值对象的数据和行为载体。基础层利用持久化对象(PO)完成数据库的交换。 DTO 与 VO 通过 Restful 协议实现 JSON 格式和对象转换。 前端应用与应用层之间 DTO 与 DO 的转换发生在用户接口层。如微服务内应用服务需调用外部微服务的应用服务,则 DTO 的组装和 DTO 与 DO 的转换发生在应用层。 领域层 DO 与 PO 的转换发生在基础层。

领域事件和事件总线

领域事件是领域模型中非常重要的部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,有助于形成完整的业务闭环。领域事件主要用于解耦微服务,各个微服务之间不再是强一致性,而是基于事件的最终一致性。

微服务内的领域事件

微服务内的领域事件可以通过事件总线或利用应用服务实现不同聚合之间的业务协同。当微服务内发生领域事件时,由于大部分事件的集成发生在同一个线程内,不一定需要引入消息中间件。但一个事件如果同时更新多个聚合数据,按照 DDD“一个事务只更新一个聚合根”的原则,可以考虑引入消息中间件,通过异步化的方式,对微服务内不同的聚合根采用不同的事务。

微服务之间的领域事件

微服务之间的数据交互方式通常有两种:应用服务调用和领域事件驱动机制。 领域事件驱动机制更多的用于不同微服务之间的集成,实现微服务之间的解耦。事件库(表)可以用于微服务之间的数据对账,在应用、网络等出现问题后,可以实现源和目的端的数据比对,在数据暂时不一致的情况下仍可根据这些数据完成后续业务处理流程,保证微服务之间数据的最终一致性。 应用服务调用方式通常应用于实时性要求高的业务场景,但一旦涉及到跨微服务的数据修改,将会增加分布式事务控制成本,影响系统性能,微服务之间的耦合度也会变高。

事件总线

事件总线位于基础层,为应用层和领域层服务提供事件消息接收和分发等服务。其大致流程如下: 1、服务触发并发布事件。 2、事件总线事件分发。

  • 如果是微服务内的订阅者(微服务内的其它聚合),则直接分发到指定订阅者。
  • 如果是微服务外的订阅者,则事件消息先保存到事件库(表)并异步发送到消息中间件。
  • 如果同时存在微服务内和外订阅者,则分发到内部订阅者,并将事件消息保存到事件库(表)并异步发送到消息中间件。为了保证事务的一致性,事件表可以共享业务数据库。也可以采用多个微服务共享事件库的方式。当业务操作和事件发布操作跨数据库时,须保证业务操作和事件发布操作数据的强一致性。

事件数据持久化

事件数据的持久化存储可以有两种方案,在项目实施过程中根据具体场景选择最佳方案。

  • 事件数据保存到微服务所在业务数据库的事件表中,利用本地事务保证业务操作和事件发布操作的强一致性。
  • 事件数据保存到多个微服务共享的事件库中。需要注意的一点是:这时业务操作和事件发布操作会跨数据库操作,须保证事务的强一致性(如分布式事务机制)。

事件数据的持久化可以保证数据的完整性,基于这些数据可以完成跨微服务数据的一致性比对。

微服务设计方法

事件风暴

本阶段主要完成领域模型设计。 基于 DDD 的微服务设计通常采用事件风暴方法。通过事件风暴完成领域模型设计,划分出微服务逻辑边界和物理边界,定义领域模型中的领域对象,指导微服务设计和开发。事件风暴通常包括产品愿景、场景分析、领域建模、微服务设计和拆分等过程。本文不对事件风暴详细方法做深入描述,如感兴趣可查阅相关资料。

  • 1、产品愿景

产品愿景是对产品的顶层价值设计,对产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。建议参与角色:业务需求方、产品经理和开发组长。

  • 2、场景分析

场景分析是从用户视角出发,探索业务领域中的典型场景,产出领域中需要支撑的场景分类、用例操作以及不同子域之间的依赖关系,用以支撑领域建模。 建议参与角色:产品经理、需求分析人员、架构师、开发组长和测试组长。

  • 3、领域建模

领域建模是通过对业务和问题域进行分析,建立领域模型,向上通过限界上下文指导微服务边界设计,向下通过聚合指导实体的对象设计。 建议参与角色:领域专家、产品经理、需求分析人员、架构师、开发组长和测试组长。

  • 4、微服务拆分和设计

结合业务限界上下文与技术因素,对服务的粒度、分层、边界划分、依赖关系和集成关系进行梳理,完成微服务拆分和设计。 微服务设计应综合考虑业务职责单一、敏态与稳态业务分离、非功能性需求(如弹性伸缩要求、安全性等要求)、团队组织和沟通效率、软件包大小以及技术异构等因素。 建议参与角色:产品经理、需求分析人员、架构师、开发组长和测试组长。

领域对象及服务矩阵和代码模型设计

本阶段完成领域对象及服务矩阵文档以及微服务代码模型设计。

  • 1、领域对象及服务矩阵

根据事件风暴过程领域对象和关系,对产出的限界上下文、聚合、实体、值对象、仓储、事件、应用服务、领域服务等领域对象以及各对象之间的依赖关系进行梳理,确定各对象在分层架构中的位置和依赖关系,建立领域对象分层架构视图,为每个领域对象建立与代码模型对象的一一映射。 建议参与角色:架构师和开发组长。

  • 2、微服务代码模型

根据领域对象在 DDD 分层架构中所在的层、领域类型、与代码对象的映射关系,定义领域对象在微服务代码模型中的包、类和方法名称等,设计微服务工程的代码层级和代码结构,明确各层间的调用关系。 建议参与角色:架构师和开发组长。

领域对象及服务矩阵样例说明

领域对象及服务矩阵主要用来记录事件风暴和微服务设计过程中产出的领域对象属性,如:各领域对象在 DDD 分层架构中的位置、属性、依赖关系以及与代码对象的映射关系等。通过建立领域对象与代码对象的映射关系,可指导软件开发人员准确无误的按照设计文档完成微服务开发。 以下为领域对象及服务矩阵样例(部分数据,仅供参考)。

各栏说明如下: * 层:

定义领域对象位于 DDD 分层架构中的哪一层。如:接口层、应用层、领域层以及基础层等。

  • 聚合:

在事件风暴过程中将关联紧密的实体和值对象等组合形成聚合。本栏说明聚合名称。 * 领域对象名称:

领域模型中领域对象的具体名称。如:“请假审批已通过”是类型为“事件”的领域对象;“请假单”是领域类型为“实体”的领域对象。

  • 领域类型:

在领域模型中根据 DDD 知识域定义的领域对象的类型,如:限界上下文、聚合、聚合根(实体)、实体、值对象、事件、命令、应用服务、领域服务和仓储服务等。 依赖对象名称:根据业务对象依赖或分层调用依赖关系建立的领域对象的依赖关系(如服务调用依赖、关联对象聚合等)。本栏说明领域对象需依赖的其他领域对象,如上层服务在组合和编排过程中对下层服务的调用依赖、实体之间或者实体与值对象在聚合内的依赖等。

  • 包名:

代码模型中的包名,本栏说明领域对象所在的软件包。

  • 类名:

代码模型中的类名,本栏说明领域对象的类名。

  • 方法名:

代码模型中的方法名,本栏说明领域对象实现或操作的方法名。

微服务代码结构模型

微服务代码模型最终结果来源于领域对象及服务矩阵。在代码模型设计时须建立领域对象和代码对象的一一映射,保证业务模型与代码模型的一致性,即使不熟悉业务的开发人员或者不熟悉代码的业务人员也可以很快定位到代码位置。

微服务代码总目录

基于 DDD 的代码模型包括 interfaces、application、domain 和 infrastructure 四个目录。

  • Interfaces(用户接口层):

本目录主要存放用户接口层代码。前端应用通过本层向应用服务获取展现所需的数据。本层主要用于处理用户发送的 Restful 请求和解析用户输入的配置文件等,并将信息传递给 Application 层。主要代码形态是数据组装以及 Facade 接口等。

  • Application(应用层):

本目录主要存放应用层代码。应用服务代码基于微服务内的领域服务或微服务外的应用服务完成服务编排和组合。为用户接口层提供各种应用数据展现支持。主要代码形态是应用服务和领域事件等。

  • Domain(领域层):

本目录主要存放领域层代码。本层代码主要实现核心领域逻辑,其主要代码形态是实体类方法和领域服务等。

  • Infrastructure(基础层):

本目录存放基础层代码,为其它各层提供通用技术能力、三方软件包、配置和基础资源服务等。

用户接口层代码模型

用户接口层代码模型目录包括:assembler、dto 和 facade。

  • Assembler:实现 DTO 与领域对象之间的相互转换和数据交换。理论上 Assembler 总是与 DTO 一同被使用。

  • Dto:数据传输的载体,内部不存在任何业务逻辑,通过 DTO 把内部的领域对象与外界隔离。

  • Facade:提供较粗粒度的调用接口,将用户请求委派给一个或多个应用服务进行处理。

应用层代码模型

应用层代码模型目录包括:event 和 service。

  • Event(事件):事件目录包括两个子目录:publish 和 subscribe。publish 目录主要存放微服务内领域事件发布相关代码。subscribe 目录主要存放微服务内聚合之间或外部微服务领域事件订阅处理相关代码。为了实现领域事件的统一管理,微服务内所有领域事件(包括应用层和领域层事件)的发布和订阅处理都统一放在应用层。

  • Service(应用服务):这里的服务是应用服务。应用服务对多个领域服务或外部应用服务进行封装、编排和组合,对外提供粗粒度的服务。

领域层代码模型

微服务领域层包括一个或多个聚合代码包。标准的聚合代码模型包括:entity、repository 和 service 三个子目录。

  • Aggregate(聚合):聚合代码包的根目录,实际项目中以实际业务属性的名称来命名。聚合定义了领域对象之间的关系和边界,实现领域模型的内聚。
  • Entity(实体):存放实体(含聚合根、实体和值对象)相关代码。同一实体所有相关的代码(含对同一实体类多个对象操作的方法,如对多个对象的 count 等)都放在一个实体类中。
  • Service(领域服务):存放对多个不同实体对象操作的领域服务代码。这部分代码以领域服务的形式存在,在设计时一个领域服务对应一个类。
  • Repository(仓储):存放聚合对应的查询或持久化领域对象的代码,通常包括仓储接口和仓储实现方法。为了方便聚合的拆分和组合,我们设定一个原则:一个聚合对应一个仓储。
  • 特别说明:按照 DDD 分层原则,仓储实现本应属于基础层代码,但为了微服务代码拆分和重组的便利性,我们把聚合的仓储实现代码放到了领域层对应的聚合代码包内。如果需求或者设计发生变化导致聚合需要拆分或重新组合时,我们可以聚合代码包为单位,轻松实现微服务聚合的拆分和组合。

基础层代码模型

基础层代码模型包括:config 和 util 两个子目录。

  • Config:主要存放配置相关代码。
  • Util:主要存放平台、开发框架、消息、数据库、缓存、文件、总线、网关、第三方类库、通用算法等基础代码,可为不同的资源类别建立不同的子目录。

微服务总目录结构

微服务总目录结构如下:

微服务设计原则

微服务设计原则中如高内聚低耦合、复用、单一职责等原则在此就不赘述了,这里主要强调以下几条:

  • 第一条:“要领域驱动设计,而不是数据驱动设计,也不是界面驱动设计”。

微服务设计首先应建立领域模型,确定逻辑和物理边界后,然后才进行微服务边界拆分,而不是一上来就定义数据库表结构,也不是界面需要什么,就去调整领域逻辑代码。 领域模型和领域服务应具有高度通用性,通过接口层和应用层屏蔽外部变化对业务逻辑的影响,保证核心业务功能的稳定性。

  • 第二条:“要边界清晰的微服务,而不是泥球小单体”。

微服务完成开发后其功能和代码也不是一成不变的。随着需求或设计变化,微服务内的代码也会分分合合。逻辑边界清晰的微服务,可快速实现微服务代码的拆分和组合。DDD 思想中的逻辑边界和分层设计也是为微服务各种可能的分分合合做准备的。 微服务内聚合与聚合之间的领域服务以及数据原则上禁止相互产生依赖。如有必要可通过上层的应用服务编排或者事件驱动机制实现聚合之间的解耦,以利于聚合之间的组合和拆分。

  • 第三条:“要职能清晰的分层,而不是什么都放的大箩筐”。

分层架构中各层职能定位清晰,且都只能与其下方的层发生依赖,也就是说只能从外层调用内层服务,内层服务通过封装、组合或编排对外逐层暴露,服务粒度由细到粗。 应用层负责服务的编排和组合,领域层负责领域业务逻辑的实现,基础层为各层提供资源服务。

  • 第四条:“要做自己能 hold 住的微服务,而不是过度拆分的微服务”

微服务的过度拆分必然会带来软件维护成本的上升,如:集成成本、运维成本以及监控和定位问题的成本。企业转型过程中很难短时间内提升这些能力,如果项目团队不具备这些能力,将很难 hold 住这些过细的微服务。而如果我们在微服务设计之初就已经定义好了微服务内的逻辑边界,项目初期我们可以尽可能少的拆分出过细的微服务,随着技术的积累和时间的推移,当我们具有这些能力后,由于微服务内有清晰的逻辑边界,这时就可以随时根据需要轻松的拆分或组合出新的微服务。

不同场景的微服务设计

微服务的设计先从领域建模开始,领域模型是微服务设计的核心,微服务是领域建模的结果。在微服务设计之前,请先判断你的业务是否聚焦在领域和领域逻辑。 实际在做系统设计时我们可能面临各种不同的情形,如从传统单体拆分为多个微服务,也可能是一个全新领域的微服务设计(如创业中的应用),抑或是将一个单体中面临问题或性能瓶颈的模块拆分为微服务而其余功能仍为单体的情况。 下面分几类不同场景说明如何进行微服务和领域模型设计。

新建系统的微服务设计

新建系统会遇到复杂和简单领域两种场景,两者的领域建模过程也会有所差别。

1、简单领域的建模

对于简单的业务领域,一个领域可能就是一个小的子域。领域建模过程相对简单,根据事件风暴可以分解出事件、命令、实体、聚合和限界上下文等,根据领域模型和微服务拆分原则设计出微服务即可。

2、复杂领域的建模

对于复杂的业务领域,领域可能还需要拆分为子域,甚至子域还会进一步拆分,如:保险领域可以拆分为承保、理赔、收付费和再保等子域,承保子域还可以再拆分为投保、保单管理等子子域。对于这种复杂的领域模型,是无法通过一个事件风暴完成领域建模的,即使能完成,其工程量也是非常浩大,效果也不一定好。 对于这种复杂的领域,我们可以分三阶段来完成领域模型和微服务设计。

  • 拆分子域建立领域模型:根据业务特点考虑流程节点或功能模块等边界因素(微服务最终的拆分结果很多时候跟这些边界因素有一定的相关性),按领域逐级分解为大小合适的子域,针对子域进行事件风暴,记录领域对象、聚合和限界上下文,初步确定各级子域的领域模型。

  • 领域模型微调:梳理领域内所有子域的领域模型,对各子域模型进行微调,这个过程重点考虑不同限界上下文内聚合的重新组合,同步需要考虑子域、限界上下文以及聚合之间的边界、服务以及事件之间的依赖关系,确定最终的领域模型。

  • 微服务设计和拆分:根据领域模型的限界上下文和微服务的拆分原则,完成微服务的拆分和设计。

单体遗留系统的微服务设计

如果一个单体遗留系统,只是将面临问题或性能瓶颈的模块拆分为微服务,而其余功能仍为单体。我们只需要将这些特定功能领域理解为一个简单的子领域,按照简单领域建模方式进行领域模型的设计即可。但在新微服务设计中需要考虑新老系统之间的服务协议,必要时引入防腐层。

特别说明

虽然有些业务领域在事件风暴后发现无法建立领域模型,如数据处理或分析类场景,但本文所述的分层架构模型、服务之间规约和代码目录结构在微服务设计和开发中仍然是通用的。

基于 DDD 的微服务设计和开发实例

为了更好的理解 DDD 的设计思想和过程,我们用一个场景简单但基本涵盖 DDD 设计思想的项目来说明微服务设计和开发过程。

项目基本信息

项目主要目标是实现在线请假和考勤管理。基本功能包括:请假、考勤以及人员管理等。

  • 请假:请假人填写请假单提交审批,根据请假人身份和请假天数进行校验,根据审批规则逐级递交审批,核批通过则完成审批。
  • 考勤:根据考勤规则,剔除请假数据后,对员工考勤数据进行校验,输出考勤统计表。
  • 人员管理:维护人员基本信息和上下级关系。 ……

设计和实施步骤

步骤一:事件风暴

由于项目目标基本明确,我们在事件风暴过程中裁剪了产品愿景,直接从用户旅程和场景分析开始。

  • 1、场景分析:场景分析是一个发散的过程。根据不同角色的旅程和场景分析,尽可能全面的梳理从前端操作到后端业务逻辑发生的所有操作、命令、领域事件以及外部依赖关系等信息(如下图),如:请假人员会执行创建请假信息操作命令,审批人员会执行审批操作,请假审批通过后会产生领域事件,通知邮件系统反馈请假人员结果,并将请假数据发送到考勤以便核销等。在记录这些领域对象的同时,我们也会标记各对象在 DDD 中的层和对象类型等属性,如:应用服务、领域服务、事件和命令等类型。

  • 2、领域建模:领域建模是一个收敛的过程。这个收敛过程分三步:第一步根据场景分析中的操作集合定义领域实体;第二步根据领域实体业务关联性,定义聚合;第三步根据业务及语义边界等因素,定义限界上下文。

    • 定义领域实体:在场景分析过程中梳理完操作、命令、领域事件以及外部依赖关系等领域对象后。分析这些操作应由什么实体发起或产生,从而定义领域实体对象,并将这些操作与实体进行关联。 在请假场景中,经分析需要有请假单实体对象,请假单实体有创建请假信息以及修改请假信息等操作。
    • 定义聚合:将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。经分析项目最终形成三个聚合:人员管理、请假和考勤。在请假聚合中有请假单、审批轨迹、审批规则等实体,其中请假单是聚合根,审批轨迹是请假单的值对象,审批规则是辅助实体。
    • 聚合内须保证业务操作的事务性,高度内聚的实体对象可自包含完成本领域功能。聚合是可拆分为微服务的最小单元。在同一限界上下文内多个聚合可以组合为一个微服务。如有必要,也可以将某一个聚合独立为微服务。
    • 定义限界上下文:根据领域及语义边界等因素确定限界上下文,将同一个语义环境下的一个或者多个聚合放在一个限界上下文内。由于人员管理与请假聚合两者业务关联紧密,共同完成人员请假功能,两者一起构成请假限界上下文,考勤聚合则单独形成考勤限界上下文。
  • 3、微服务设计和拆分:理论上一个限界上下文可以设计为一个微服务,但还需要综合考虑多种外部因素,如:职责单一性、性能差异、版本发布频率、团队沟通效率和技术异构等要素。

由于本项目微服务设计受技术以及团队等因素影响相对较小,主要考虑职责单一性,因此根据限界上下文直接拆分为请假和考勤两个微服务。其中请假微服务包含人员和请假两个聚合,考勤微服务只包含考勤聚合。

步骤二、领域对象及服务矩阵

将事件风暴中产出的领域对象按照各自所在的微服务进行分类,定义每个领域对象在微服务中的层、领域类型和依赖的领域对象等。

这个步骤最关键的工作是确定实体、方法、服务等领域对象在微服务分层架构中的位置以及各对象之间的依赖关系,形成服务矩阵(如下表)。这个过程也将在事件风暴数据的基础上,进一步细化领域对象以及它们之间关系,并补充事件风暴中可能遗漏的细节。

确定完各领域对象的属性后,按照代码模型设计各个领域对象在代码模型中的代码对象(包括代码对象所在的:包名、类名和方法名),建立领域对象与代码对象的一一映射关系。根据这种映射关系,相关人员可快速定位到业务逻辑所在的代码位置。

步骤三:领域模型及服务架构

根据领域模型中领域对象属性以及服务矩阵,画出领域对象及服务架构视图(如下图)。这个视图可以作为标准的 DDD 分层领域服务架构视图模型,应用在不同的领域模型中。这个模型可以清晰的体现微服务内实体、聚合之间的关系,各层服务之间的依赖关系以及应用层服务组合和编排的关系,微服务之间的服务调用以及事件驱动的前后处理逻辑关系。 在这个阶段,前端的设计也可以同步进行,在这里我们用到了微前端的设计理念,为请假和考勤微服务分别设计了请假和考勤微前端,基于微前端和微服务,形成从前端到后端的业务逻辑自包含组件。两个微前端之上有一个集成主页面,可根据页面流动态加载请假和考勤的微前端页面。

步骤四:代码模型设计

根据 DDD 的代码结构模型和各领域对象在所在的包、类和方法,定义出请假微服务的代码结构模型。应用层代码结构包括:应用服务以及事件发布相关代码(如下图)。

领域层代码结构包括一个或多个聚合的实体类以及领域服务相关代码(如下图)。在本项目中请假微服务领域层包含了请假和人员两个聚合。

领域模型中的一个聚合对应一个聚合代码包,如:人员和请假领域逻辑代码都放在各自的聚合代码包中,如随着业务发展,人员管理功能需要从请假微服务中拆分出来,我们只需要将人员聚合代码包稍加改造并独立部署即可快速发布为人员管理微服务。

步骤五:详细设计

在完成领域模型和代码模型设计后,我们就可以开始详细设计了,详细设计主要结合具体的业务功能来开展,主要工作包括:系统界面、数据库表以及字段、服务参数规约及功能等。

步骤六:代码开发

软件开发人员只需要按照设计文档和功能要求,找到业务功能对应的代码位置,完成代码开发和服务编排即可。

步骤七:测试和发布

完成代码开发后,由开发人员编写单元测试用例,基于挡板模拟依赖对象完成跨服务的测试。单元测试完成后,在团队内可进一步完成微服务与相应微前端的集成和测试,形成请假和考勤两个业务组件。前端主页面完成请假和考勤微前端页面集成和页面流及组件基础数据配置,主页面可以按照页面流程动态加载请假和考勤微前端页面。最终部署的软件包包括:请假和考勤两个微服务,请假和考勤两个微前端,一个主页面共计五个。这五个部署包独立开发、独立运行和独立部署。

技术组件说明

主页面和微前端采用:Vue(前端框架),ElementUI(UI 框架 -PC),VUX(UI 框架 – 移动端) 和 MPVUE(UI 框架 – 小程序) 等。微服务开发采用:Spring Cloud、Kafka、Redis 等。数据库采用:PostgreSQL。

附录一:DDD 名词和术语

      • Event Storming(事件风暴):事件风暴是一项团队活动,旨在通过领域事件识别出聚合根,进而划分微服务的限界上下文。在活动中,团队先通过头脑风暴的形式罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对于每一个事件,标注出导致该事件的命令(Command),再然后为每个事件标注出命令发起方的角色,命令可以是用户发起,也可以是第三方系统调用或者是定时器触发等。最后对事件进行分类整理出聚合根以及限界上下文。
      • Entity(实体):每个实体是唯一的,并且可以相当长的一段时间内持续地变化。我们可以对实体做多次修改,故一个实体对象可能和它先前的状态大不相同。但是,由于它们拥有相同的身份标识,他们依然是同一个实体。例如一件商品在电商商品上下文中是一个实体,通过商品中台唯一的商品 id 来标示这个实体。
      • ValueObject(值对象):值对象用于度量和描述事物,当你只关心某个对象的属性时,该对象便可作为一个值对象。实体与值对象的区别在于唯一的身份标识和可变性。当一个对象用于描述一个事物,但是又没有唯一标示,那么它就是一个值对象。例如商品中的商品类别,类别就没有一个唯一标识,通过图书、服装等这些值就能明确表示这个商品类别。
      • Aggregate(聚合):聚合是实体的升级,是由一组与生俱来就密切相关实体和值对象组合而成的,整个组合的最上层实体就是聚合。
      • Bounded Context(限界上下文):用来封装通用语言和领域对象,为领域提供上下文语境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。使团队所有成员能够明确地知道什么必须保持一致,什么必须独立开发。
分類
博客园cnblogs

深入学习CSS中如何使用定位

CSS中定位介绍

  • position属性在英文单词中表示位置的意思,在CSS中主要作用设置元素的定位。
  • CSS中一共有3种定位如下:
属性值 描述
fixed 设置固定定位。
relative 设置相对定位。
absolute 设置绝对定位。

固定定位实践

  • 在实践固定定位之前我们先看看代码结构是什么样子的呢。
  • 代码块

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>定位</title>
  <style>
     .box{
      
       width: 100px;
       height: 100px;
       background-color: red;
       margin: 0;
       padding: 0;
     }
     div{
       width: 200px;
       height: 200px;
       background-color:springgreen;
        margin: 0;
        padding: 0;
     }
  </style>
</head>

<body>
   <h1 class="box"></h1>
   <div></div>
</body>

</html>
  • 结果图

  • 现在笔者将h1元素设置为固定定位,看看和上面的结构实践有什么区别,然后我们在分析一些固定定位的特点。

  • 代码块

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>定位</title>
  <style>
     .box{
      position:fixed;
       width: 100px;
       height: 100px;
       background-color: red;
       margin: 0;
       padding: 0;
     }
     div{
       width: 200px;
       height: 200px;
       background-color:springgreen;
        margin: 0;
        padding: 0;
     }
  </style>
</head>

<body>
   <h1 class="box"></h1>
   <div></div>
</body>

</html>
  • 结果图

  • 固定定位特点分析如下:
  • 固定定位,它是相对于浏览器窗口进行设置定位,不管页面如果滚动,固定定位的元素位置不会受到任何影响。
  • 固定定位的元素特点:它已经脱离了标准文档流。
  • 固定定位的元素特点:它的层级比标准文档流的元素要高,所以我们给h1标签设置了固定定位会压盖到div标签。
  • 固定定位的元素特点:h1标签在div标签之上,所以固定定位的元素已经不再占用任何空间。

相对定位实践

  • 在实践相对定位之前我们先看看代码结构是什么样子的呢。
  • 代码块

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>定位</title>
  <style>
     .box{
      width: 400px;
      height: 300px;
      border:  1px solid darkorange;
      
     }
    .box div{
       width: 100px;
       height: 100px;
     }
     .div1{
       background-color: red;
     }
     .div2{
       background-color: slateblue;
     }
     .div3{
       background-color: springgreen;
     }
  </style>
</head>

<body>
   <div class="box">
     <div  class="div1"></div>
     <div class="div2"></div>
     <div class="div3"></div>
   </div>
</body>

</html>
  • 结果图

  • 现在笔者将class属性值为.div2元素设置为相对定位,看看和上面的结构实践有什么区别,然后我们在分析一些相对定位的特点。

  • 代码块

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>定位</title>
  <style>
     .box{
      width: 400px;
      height: 300px;
      border:  1px solid darkorange;
      
     }
    .box div{
       width: 100px;
       height: 100px;
     }
     .div1{
       background-color: red;
     }
     .div2{
       background-color: slateblue;
       position: relative;
     }
     .div3{
       background-color: springgreen;
     }
  </style>
</head>

<body>
   <div class="box">
     <div  class="div1"></div>
     <div class="div2"></div>
     <div class="div3"></div>
   </div>
</body>

</html>
  • 结果图

  • 注意:在我们没有给相对定位设置坐标位置,它是不会有任何移动的。

  • 笔者给class属性值为div2元素设置定位坐标实践。
  • 代码块

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>定位</title>
  <style>
     .box{
      width: 400px;
      height: 300px;
      border:  1px solid darkorange;
      
     }
    .box div{
       width: 100px;
       height: 100px;
     }
     .div1{
       background-color: red;
     }
     .div2{
       background-color: slateblue;
       position: relative;
       left: 50px;
       top: 50px;
     }
     .div3{
       background-color: springgreen;
     }
  </style>
</head>

<body>
   <div class="box">
     <div  class="div1"></div>
     <div class="div2"></div>
     <div class="div3"></div>
   </div>
</body>

</html>
  • 结果图

  • 相对定位特点分析如下:
  • 相对定位的元素它没有脱离标准文档流。
  • 相对定位的元素如果没有设置坐标它会在原地位置。
  • 相对定位的元素设置了坐标位置,它会根据原来的位置开始计算移动的位置。
  • 相对定位的元素它比标准文档流的元素层级要高,会覆盖标准文档流中的元素。
  • 相对定位的元素它可以设置为负数。

绝对定位实践

  • 在实践绝对定位之前我们先看看代码结构是什么样子的呢。
  • 代码块

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>定位</title>
  <style>
     .box{
      width: 400px;
      height: 300px;
      border:  1px solid darkorange;
      
     }
    .box div{
       width: 100px;
       height: 100px;
     }
     .div1{
       background-color: red;
     }
     .div2{
       background-color: slateblue;
     }
     .div3{
       background-color: springgreen;
     }
  </style>
</head>

<body>
   <div class="box">
     <div  class="div1"></div>
     <div class="div2"></div>
     <div class="div3"></div>
   </div>
</body>

</html>
  • 结果图

  • 现在笔者将class属性值为.div2元素设置为绝对定位,看看和上面的结构实践有什么区别,然后我们在分析一些绝对定位的特点。

  • 代码块

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>定位</title>
  <style>
     .box{
      width: 400px;
      height: 300px;
      border:  1px solid darkorange;
      
     }
    .box div{
       width: 100px;
       height: 100px;
     }
     .div1{
       background-color: red;
     }
     .div2{
       background-color: slateblue;
       position:absolute;
     }
     .div3{
       background-color: springgreen;
     }
  </style>
</head>

<body>
   <div class="box">
     <div  class="div1"></div>
     <div class="div2"></div>
     <div class="div3"></div>
   </div>
</body>

</html>
  • 结果图

  • 注意:绝对定位已经脱离了标准文档流。

  • 笔者给class属性值为div2元素设置定位坐标实践,为了让读者有一个直观的印象我给最外层的div元素设置了居中对齐。
  • 代码块

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>定位</title>
  <style>
     .box{
      width: 400px;
      height: 300px;
      border:  1px solid darkorange;
      margin: 0px auto;
      
     }
    .box div{
       width: 100px;
       height: 100px;
     }
     .div1{
       background-color: red;
     }
     .div2{
       background-color: slateblue;
       position:absolute;
       left:0px ;
     }
     .div3{
       background-color: springgreen;
     }
  </style>
</head>

<body>
   <div class="box">
     <div  class="div1"></div>
     <div class="div2"></div>
     <div class="div3"></div>
   </div>
</body>

</html>
  • 结果图

  • 注意:绝对定位元素为什么会出现在浏览器左边缘呢,绝对定位移动原理:绝对定位的元素它会寻找父元素是否有定位,如果有定位它会根据父元素进行定位,如果父元素没有设置定位,它会在找父元素的父元素是否有定位,以此类推直到body元素就停止了,因为body元素就是浏览器的位置,说了这么多笔者给新学者一个直观的印象,那咱们就实践见真招。

  • 代码块

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>定位</title>
  <style>
     .box{
      width: 400px;
      height: 300px;
      border:  1px solid darkorange;
      margin: 0px auto;
      position: relative;
      
     }
    .box div{
       width: 100px;
       height: 100px;
     }
     .div1{
       background-color: red;
     }
     .div2{
       background-color: slateblue;
       position:absolute;
       right:0px ;
     }
     .div3{
       background-color: springgreen;
     }
  </style>
</head>

<body>
   <div class="box">
     <div  class="div1"></div>
     <div class="div2"></div>
     <div class="div3"></div>
   </div>
</body>

</html>
  • 结果图

  • 注意:现在笔者给绝对定位坐标更换成了向右定位,父元素设置了一个相对定位,在这里就不多进行实践了,如果定位的父元素的父元素也就是爷爷的元素,父元素和爷爷元素同时都设置了定位,该元素会根据父元素决定定位而不是爷爷元素。

  • 绝对定位特点分析如下:
  • 绝对定位元素它已经脱离了标准文档流。
  • 绝对定位元素它会覆盖掉标准文档流的元素。
  • 绝对定位元素它已经不再占用任何空间了。
  • 绝对定位元素它根据父元素之祖先元素之间是否有定位,如果有根据最近元素进行设置定位的位置,如果没有根据body元素进行定位。
  • 绝对定位元素的父元素可以是用任何定位包括绝对定位,笔者建议是用相对定位,一般相对定位是配合着绝对定位使用的。
分類
博客园cnblogs

封装axios

前言

axios 是一个轻量的HTTP客户端,它基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js 端。自Vue2.0起,尤大大(Vue作者尤雨溪)宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue 开发者的首选。
(如果你还不熟悉 axios,可以在这里查看它的API)。

axios 的API很友好,你完全可以很轻松地在项目中直接使用。不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都就地写一遍,得疯!这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。

为了提高我们的代码质量,我们应该在项目中二次封装一下 axios 再使用。

那么,怎么封装 axios 呢?

原来的样子

封装前,先来看下,不封装的情况下,一个实际项目中axios请求的样子。大概是长这样:

axios('http://localhost:3000/data', {
  method: 'GET',
  timeout: 1000,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'xxx',
  },
  transformRequest: [function (data, headers) {
    return data;
  }],
  // 其他请求配置...
})
.then((data) => {
  // todo: 真正业务逻辑代码
  console.log(data);
}, (err) => {
  if (err.response.status === 401) {
  // handle authorization error
  }
  if (err.response.status === 403) {
  // handle server forbidden error
  }
  // 其他错误处理.....
  console.log(err);
});

可以看到在这段代码中,页面代码逻辑只在第15行处,上方的一大块请求配置代码和下方一大块响应错误处理代码,几乎跟页面功能没有关系,而且每个请求中这些内容都差不多,甚至有的部分完全一样。想象一下,每发一次请求都来这么一下,十几个请求一写,会是什么盛况?

封装步骤

封装的本质就是在待封装的内容外面添加各种东西,然后把它们作为一个新的整体呈现给使用者,以达到扩展和易用的目的。

封装axios要做的事情,就是把所有HTTP请求共用的配置,事先都在axios上配置好,预留好必要的参数和接口,然后把它作为新的axios返回。

接下来我们借助一个demo实现一个具有良好扩展性的axios封装。

demo目录结构如下(由Vue-cli 3.0 生成):

|--public/
|--mock/
|   |--db.json  # 我新建的接口模拟数据
|--src/
|   |--assets/
|   |--components/
|   |--router/
|   |--store/
|   |--views/
|       |--Home.Vue
|   |--App.vue
|   |--main.js
|   |--theme.styl
|--package.json
|...

封装目标

我希望在 Home 页,发起 axios 请求时就像调用一个只有少量参数的方法一样简单,这样我就可以专注业务代码了。

1. 将 axios 封装到一个独立的文件

  • 在src下创建 utils/http.js 文件
cd src
mkdir utils
touch http.js
  • 引入 axios
// src/utils/http.js

import axios from 'axios';
  • 创建一个类
    你也可以用函数来封装,我只是觉得类更语义化而已。
//src/utils/http.js

//...
class NewAxios {

}
  • 给不同环境配置不同请求地址
    根据 process.env.NODE_ENV 配置不同的 baseURL,使项目只需执行相应打包命令,就可以在不同环境中自动切换请求主机地址。
// src/utils/http.js

//...
const getBaseUrl = (env) => {
  let base = {
    production: '/',
    development: 'http://localhost:3000',
    test: 'http://localhost:3001',
  }[env];
  if (!base) {
    base = '/';
  }
  return base;
};

class NewAxios {
  constructor() {
    this.baseURL = getBaseUrl(process.env.NODE_ENV);
  }
}
  • 配置超时时间
    timeout属性,我一般设置10秒。
// src/utils/http.js

//...
class NewAxios {
  constructor() {
    //...
    this.timeout = 10000;
  }
}
  • 配置允许携带凭证
    widthCredentials属性设为true。
// src/utils/http.js

//...
class NewAxios {
  constructor() {
    //...
    this.withCredentials = true;
  }
}
  • 给这个类创建实例上的方法request
    request 方法里,创建新的axios实例,接收请求配置参数,处理参数,添加配置,返回axios实例的请求结果(一个promise对象)。
    你也可以不创建,直接使用默认导出的axios实例,然后把所有配置都放到它上面,不过这样一来整个项目就会共用一个axios实例。虽然大部分项目下这样够用没问题,但是有的项目中不同服务地址的请求和响应结构可能完全不同,这个时候共用一个实例就没办法支持了。所以为了封装可以更通用,更具灵活性,我会使用axios的create方法,使每次发请求都是新的axios实例。
// src/utils/http.js

//...
class NewAxios {
  //...
  request(options) {
    // 每次请求都会创建新的axios实例。
    const instance = axios.create();
    const config = { // 将用户传过来的参数与公共配置合并。
      ...options,
      baseURL: this.baseURL,
      timeout: this.timeout,
      withCredentials: this.withCredentials,
    };
    // 配置拦截器,支持根据不同url配置不同的拦截器。
    this.setInterceptors(instance, options.url);
    return instance(config); // 返回axios实例的执行结果
  }
}

因为拦截器配置内容比较多,所以封装成一个内部函数了。

  • 配置请求拦截器
    在发送请求前对请求参数做的所有修改都在这里统一配置。比如统一添加token凭证、统一设置语言、统一设置内容类型、指定数据格式等等。做完后记得返回这个配置,否则整个请求不会进行。
    我这里就配置一个token。
// src/utils/http.js

//...
class NewAxios {
  //...
  // 这里的url可供你针对需要特殊处理的接口路径设置不同拦截器。
  setInterceptors = (instance, url) => { 
    instance.interceptors.request.use((config) => { // 请求拦截器
      // 配置token
      config.headers.AuthorizationToken = localStorage.getItem('AuthorizationToken') || '';
      return config;
    }, err => Promise.reject(err));
  }
  //...
}
  • 配置响应拦截器
    在请求的thencatch处理前对响应数据进行一轮预先处理。比如过滤响应数据,更多的,是在这里对各种响应错误码进行统一错误处理,还有断网处理等等。
    我这里就判断一下403和断网。
// src/utils/http.js

//...
class NewAxios {
  //...
  setInterceptors = (instance, url) => {
    //...
    instance.interceptors.response.use((response) => { // 响应拦截器
      // todo: 想根据业务需要,对响应结果预先处理的,都放在这里
      console.log();
      return response;
    }, (err) => {
      if (err.response) { // 响应错误码处理
        switch (err.response.status) {
          case '403':
            // todo: handler server forbidden error
            break;
            // todo: handler other status code
          default:
            break;
        }
        return Promise.reject(err.response);
      }
      if (!window.navigator.online) { // 断网处理
        // todo: jump to offline page
        return -1;
      }
      return Promise.reject(err);
    });
  }
  //...
}

另外,在拦截器里,还适合放置loading等缓冲效果:在请求拦截器里显示loading,在响应拦截器里移除loading。这样所有请求就都有了一个统一的loading效果。

  • 默认导出新的实例
// src/utils/http.js

//...
export default new NewAxios();

最后完整的代码如下:

// src/utils/http.js

import axios from 'axios';

const getBaseUrl = (env) => {
  let base = {
    production: '/',
    development: 'http://localhost:3000',
    test: 'http://localhost:3001',
  }[env];
  if (!base) {
    base = '/';
  }
  return base;
};

class NewAxios {
  constructor() {
    this.baseURL = getBaseUrl(process.env.NODE_ENV);
    this.timeout = 10000;
    this.withCredentials = true;
  }

  setInterceptors = (instance, url) => {
    instance.interceptors.request.use((config) => {
      // 在这里添加loading
      // 配置token
      config.headers.AuthorizationToken = localStorage.getItem('AuthorizationToken') || '';
      return config;
    }, err => Promise.reject(err));

    instance.interceptors.response.use((response) => {
      // 在这里移除loading
      // todo: 想根据业务需要,对响应结果预先处理的,都放在这里
      return response;
    }, (err) => {
      if (err.response) { // 响应错误码处理
        switch (err.response.status) {
          case '403':
            // todo: handler server forbidden error
            break;
            // todo: handler other status code
          default:
            break;
        }
        return Promise.reject(err.response);
      }
      if (!window.navigator.online) { // 断网处理
        // todo: jump to offline page
        return -1;
      }
      return Promise.reject(err);
    });
  }

  request(options) {
    // 每次请求都会创建新的axios实例。
    const instance = axios.create();
    const config = { // 将用户传过来的参数与公共配置合并。
      ...options,
      baseURL: this.baseURL,
      timeout: this.timeout,
      withCredentials: this.withCredentials,
    };
    // 配置拦截器,支持根据不同url配置不同的拦截器。
    this.setInterceptors(instance, options.url);
    return instance(config); // 返回axios实例的执行结果
  }
}

export default new NewAxios();

现在 axios 封装算是完成了80%。我们还需要再进一步把axios和接口结合再封装一层,才能达到我在一开始定的封装目标。

2. 使用新的 axios 封装API

  • 在 src 目录下新建 api 文件夹。把所有涉及HTTP请求的接口统一集中到这个目录来管理。
  • 新建 home.js。我们需要把接口根据一定规则分好类,一类接口对应一个js文件。这个分类可以是按页面来划分,或者按模块等等。为了演示更直观,我这里就按页面来划分了。实际根据自己的需求来定。
  • 使用新的 axios 封装API(固定url的值,合并用户传过来的参数),然后命名导出这些函数。
// src/api/home.js 

import axios from '@/utils/http';
export const fetchData = options => axios.request({
  ...options,
  url: '/data',
});
export default {};
  • 在 api 目录下新建 index.js,把其他文件的接口都在这个文件里汇总导出。
// src/api/index.js

export * from './home';

这层封装将我们的新的axios封装到了更简洁更语义化的接口方法中。

现在我们的目录结构长这样:

|--public/
|--mock/
|   |--db.json  # 接口模拟数据
|--src/
|   |--api/     # 所有的接口都集中在这个目录下
|       |--home.js  # Home页面里涉及到的接口封装在这里
|       |--index.js # 项目中所有接口调用的入口
|   |--assets/
|   |--components/
|   |--router/
|   |--store/
|   |--utils/
|       |--http.js  # axios封装在这里
|   |--views/
|       |--Home.Vue
|   |--App.vue
|   |--main.js
|   |--theme.styl
|--package.json
|...

使用封装后的axios

现在我们要发HTTP请求时,只需引入 api 下的 index.js 文件就可以调用任何接口了,并且用的是封装后的 axios

// src/views/Home.vue

<template>
  <div class="home">
    <h1>This is home page</h1>
  </div>
</template>

<script>
// @ is an alias to /src
import { fetchData } from '@/api/index';

export default {
  name: 'home',
  mounted() {
    fetchData()  // axios请求在这里
      .then((data) => {
        console.log(data);
      })
      .catch((err) => {
        console.log(err);
      });
  },
};
</script>

axios请求被封装在fetchData函数里,页面请求压根不需要出现任何axios API,悄无声息地发起请求获取响应,就像在调用一个简单的 Promise 函数一样轻松。并且在页面中只需专注处理业务功能,不用被其他事物干扰。

运行

运行 npm run serve 启动项目,执行 npm run mock 启动服务mock接口。

现在打开 localhost:8080 可以看到home页面。打开浏览器控制台,可以看到打印的请求响应结果:

简洁,优雅。

总结

  1. 封装思想是前端技术中很有用的思想,简单的axios及接口封装,就可以让我们可以领略到它的魅力。
  2. 封装 axios 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案。
  3. BTW:以上封装给大家提供了一个封装好的axios和api框架,经过以上过程封装好的 axios,可以不局限于 Vue,React 项目同样可以拿去使用,它适用任何前端项目。

本文的代码可以在这里获取:https://github.com/yc111/wrap-axios

欢迎交流~

欢迎转载,转载请注明出处:
https://champyin.com/2019/12/23/%E5%B0%81%E8%A3%85axios/

分類
博客园cnblogs

C#反射与特性(一):反射基础

目录

C#反射与特性(一):反射基础

1. 说明

1.1 关于反射、特性

在 《C# 7.0 本质论》中,关于这方面的知识在 《第十八章 反射、特性和动态编程》;在《C# 7.0 核心技术指南》中,这部分内容在《第19章 反射和元数据》。

[图片来自 《C# 7.0 本质论》]

在这里我们可以获得一些关联性很大的技术:反射、特性、元数据;

元数据:C# 编写的程序编译成一个程序集,程序集会包含元数据、编译代码和资源。
元数据包含内容:

  • 程序或类库中每一个类型的描述;
  • 清单信息,包括与程序本身有关的数据,以及它依赖的库;
  • 在代码中嵌入的自定义特性,提供与特性所修饰的构造有关的额外信息。

反射:在运行时检查并使用元数据和编译代码的操作称为反射。

一个程序集包含的内容:

[图片来自 《C# 7.0 核心技术指南》]

2. 程序集操作

C# 编译成的代码会生成到 .dll 或 .exe 文件中,我们可以通过 Assembly 类,手动加载 程序集文件,实现各种操作。

Assembly 类在 System.Reflection 命名空间中。

《C# 7.0 核心技术指南》中,列出类 Assembly 类常用的属性和方法:

接下来我们将通过代码操作,了解 Assembly 的使用方法。

创建一个控制台项目,并设置程序集描述信息。

2.1 获取 程序集对象(Assembly)

微软官方文档建议使用的加载程序集的方式:

  • 加载程序集的建议方法是使用 Load 方法,该方法标识要由其显示名称(例如 “b77a5c561934e089,Version = 2.0.0.0,Culture = 中立,PublicKeyToken =”)加载的程序集。 该程序集的搜索遵循运行时如何定位程序集中所述的规则。
  • 利用 ReflectionOnlyLoadReflectionOnlyLoadFrom 方法,你可以加载用于反射的程序集,但不能加载用于执行的程序集。 例如,可通过在32位平台上运行的代码来检查面向64位平台的程序集。
  • 对于程序集必须按路径标识的罕见方案,会提供 LoadFileLoadFrom 方法。

一般获取程序集有三种方式:

  • Assembly.Load()
  • Assembly.LoadFrom()
  • Assembly.LoadFile()

以下方法可以获取到当前程序引用到的程序集:

AppDomain.CurrentDomain.GetAssemblies();

输出

System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e

ConsoleApp4, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

System.Runtime.Extensions, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

System.Console, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

2.1.1 运行时获取程序集

通过正在运行的类型、函数等形式,去获取程序集。

Assembly 类:
        public static Assembly? GetAssembly(Type type);
    
        public static Assembly GetCallingAssembly();
    
        public static Assembly? GetEntryAssembly();
    
        public static Assembly GetExecutingAssembly();
        
Type 类:
        {type}.Assembly

解析说明:

位置 函数 说明
Assembly GetAssembly(Type) 获取在其中定义指定类型的当前加载的程序集
Assembly GetCallingAssembly() 返回方法(该方法调用当前正在执行的方法)的 Assembly
Assembly GetEntryAssembly() 获取默认应用程序域中的进程可执行文件。 在其他的应用程序域中,这是由 ExecuteAssembly(String)执行的第一个可执行文件
Assembly GetExecutingAssembly() 获取包含当前执行的代码的程序集
Type Assembly 返回一个类型所在的程序集

2.1.2 使用方法

            Assembly assem = typeof(Console).Assembly;
            Assembly ass = Assembly.GetExecutingAssembly();

2.1.3 从文件加载程序集

函数 说明
LoadFrom(String) 已知程序集的文件名或路径,加载程序集
LoadFrom(String, Byte[], AssemblyHashAlgorithm) 通过给定程序集文件名或路径、哈希值及哈希算法来加载程序集
LoadFrom(String, Evidence) 在给定程序集的文件名或路径并提供安全证据的情况下,加载程序集
LoadFrom(String, Evidence, Byte[], AssemblyHashAlgorithm) 通过给定程序集文件名或路径、安全证据、哈希值及哈希算法来加载程序集

2.1.4 使用方法

Assembly ass = Assembly.LoadFrom(@"X:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.0.0\ref\netcoreapp3.0\System.Console.dll");

另外还有更多中加载程序集的方法,这些方法很偏僻,没必要列出来(因为我不会)。

2.2 Assembly 使用

获得 Assembly 对象后,就可以进行一系列的骚操作。

常用的 Assembly 函数可以查看图三。

先设置两个 Assembly 对象

            Assembly assemA = typeof(Console).Assembly;
            Assembly assemB = Assembly.GetExecutingAssembly();

2.2.1 获取程序集完全限定名称

            Console.WriteLine("程序集完全限定名");
            Console.WriteLine(assemA.FullName);
            Console.WriteLine(assemB.FullName);
程序集完全限定名
System.Console, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
ConsoleApp4, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

里面有个 PublicKeyToken 属性,前面我们介绍了 Assembly 获取程序集的方式,通过 PublicKeyToken ,我们也可以使用 Load 来加载程序集。

但是你可以看到上面的输出, System.Console 有 PublicKeyToken 值,但是自己创建的项目 ConsoleApp4 没有。

2.2.2 AssemblyName

AssmblyName 是用来完整描述程序集的类型。

AssmblyName 是用来获取 程序集 各种信息的类,本身不具有操作功能,仅用于获取程序集的元数据信息。

AssmblyName 实例可以使用 Assembly 的 GetName() 方法获取。

属性 说明
CodeBase 获取或设置程序集的 URL 位置。
ContentType 获取或设置指示程序集包含的内容类型的值。
CultureInfo 获取或设置程序集支持的区域性。
CultureName 获取或设置与此程序集关联的区域性名称。
EscapedCodeBase 获取 URI,包括表示基本代码的转义符。
Flags 获取或设置该程序集的属性。
FullName 获取程序集的全名(也称为显示名称)。
HashAlgorithm 获取或设置程序集清单使用的哈希算法。
KeyPair 获取或设置用于为程序集创建强名称签名的加密公钥/私钥对。
Name 获取或设置程序集的简单名称。 这通常(但不一定)是程序集的清单文件的文件名,不包括其扩展名。
ProcessorArchitecture 获取或设置一个值,该值标识可执行文件的目标平台的处理器和每字位数。
Version 获取或设置程序集的主版本号、次版本号、内部版本号和修订号。
VersionCompatibility 获取或设置与程序集同其他程序集的兼容性相关的信息。
            AssemblyName assemNameA = assemA.GetName();
            AssemblyName assemNameB = assemB.GetName();

            Console.WriteLine("程序集名称: {0}", assemNameA.Name);
            Console.WriteLine("程序集名称: {0}", assemNameB.Name);

            // 版本
            Console.WriteLine("\nVersion: {0}.{1}",
                assemNameA.Version.Major, assemNameA.Version.Minor);
            Console.WriteLine("Version: {0}.{1}",
    assemNameB.Version.Major, assemNameB.Version.Minor);

            // 程序集的物理文件位置
            Console.WriteLine("\nAssembly CodeBase:{0}", assemA.CodeBase);
            Console.WriteLine("\nAssembly CodeBase:{0}", assemB.CodeBase);

输出信息

程序集名称: System.Console
程序集名称: ConsoleApp4

Version: 4.1
Version: 1.0

Assembly CodeBase:file:///x:/Program Files/dotnet/shared/Microsoft.NETCore.App/3.0.1/System.Console.dll

Assembly CodeBase:file:///X:/Users/whuanle/source/repos/ConsoleApp4/ConsoleApp4/bin/Debug/netcoreapp3.0/ConsoleApp4.dll

除了 GetName(),Assembly 类还提供了许多与成员的有关程序集的信息。 例如:

2.3 获取程序集的方式

上面说到,加载程序集的方式一般使用三种方法:

  • Assembly.Load()
  • Assembly.LoadFrom()
  • Assembly.LoadFile()

上面已经演示运行时获取和 LoadFrom 两种获取方式。

下面来继续介绍 Assembly.Load()Assembly.LoadFile()

2.3.1 Assembly.Load()

Assembly.Load() 以强类型的方式去加载程序集,

强名称和程序集签名 指的是 程序集具有唯一的和不可更改的标识。

何以为强类型?通过在清单中添加如下的两种元数据实现:

  • 属于该程序集作者的唯一编号;

  • 程序集签名后的散列值,以证实该程序集是由持有其唯一编号的作者生成;

关于这部分内容可以参考 《C# 7.0 核心技术指南》的《18.2 强名称和程序集签名》部分,这里不再赘述。

Assembly.Load() 加载程序集,同时可以自动加载程序集引用到的其它程序集,并且不会造成重复加载问题。

使用示例:

            Assembly assemA = Assembly.Load("System.Console");
            Assembly assemB = Assembly.Load("ConsoleApp4");
            Assembly assemC = Assembly.Load("System.Console, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");

参考:Assembly.Load 详解(c#)

地址:https://www.cnblogs.com/weifeng123/p/8855629.html

参考:深入了解C#反射中Assembly.Load()、Assembly.LoadFrom()、Assembly.LoadF ile ()方法

地址:https://blog.csdn.net/xuchen_wang/article/details/92773260

2.3.2 Assembly.LoadFile()

Assembly.LoadFile()Assembly.LoadFrom 的使用方法一致。

区别: Assembly.LoadFile()只会加载指定的一个程序集; Assembly.LoadFrom 会加载一个程序集,然后自动加载此程序集依赖的其它程序集。

分類
博客园cnblogs

c++ 知道旋转前后矩阵向量值 求旋转矩阵c++/c#代码 知道两个向量求他们的旋转矩阵

 

原文作者:aircraft

原文链接:https://www.cnblogs.com/DOMLX/p/12115244.html

 

知道旋转前后矩阵向量值 如何去求旋转矩阵R 的c++/c#代码???

 

因为需要用到矩阵处理库所以需要先配置

一、Eigen库的配置(VS2017)

      1. Eigen库下载: http://eigen.tuxfamily.org/index.php?title=Main_Page
        下载文件并解压:

 

 

然后在自己的VS工程属性中的这个附加包含进去

 

 

 注意看清楚了 是D:\Dependencies\eigen-eigen\eigen-eigen;      前面部分是你们自己的路径 后面的这个eigen-eigen\eigen-eigen; 代表的意思解压是点击进去选择里面那个名字跟外面一样的

 

二、实现代码

c++代码:

#include <cmath>
#include <iostream>
#include "Eigen/Dense"  
#include "Eigen/LU"  
#include "Eigen/Core"  
#define PI 3.1415926

//计算旋转角
double calculateAngle(const Eigen::Vector3d &vectorBefore, const Eigen::Vector3d &vectorAfter)
{
    double ab, a1, b1, cosr;
    ab = vectorBefore.x()*vectorAfter.x() + vectorBefore.y()*vectorAfter.y() + vectorBefore.z()*vectorAfter.z();
    a1 = sqrt(vectorBefore.x()*vectorBefore.x() + vectorBefore.y()*vectorBefore.y() + vectorBefore.z()*vectorBefore.z());
    b1 = sqrt(vectorAfter.x()*vectorAfter.x() + vectorAfter.y()*vectorAfter.y() + vectorAfter.z()*vectorAfter.z());
    cosr = ab / a1 / b1;
    return (acos(cosr) * 180 / PI);
}
//计算旋转轴
inline Eigen::Vector3d calculateRotAxis(const Eigen::Vector3d &vectorBefore, const Eigen::Vector3d &vectorAfter)
{
    return Eigen::Vector3d(vectorBefore.y()*vectorAfter.z() - vectorBefore.z()*vectorAfter.y(), \
        vectorBefore.z()*vectorAfter.y() - vectorBefore.x()*vectorAfter.z(), \
        vectorBefore.x()*vectorAfter.y() - vectorBefore.y()*vectorAfter.x());
}
//计算旋转矩阵
void rotationMatrix(const Eigen::Vector3d &vectorBefore, const Eigen::Vector3d &vectorAfter, Eigen::Matrix3d &rotMatrix)
{
    Eigen::Vector3d vector = calculateRotAxis(vectorBefore, vectorAfter);
    double angle = calculateAngle(vectorBefore, vectorAfter);
    Eigen::AngleAxisd rotationVector(angle, vector.normalized());
    Eigen::Matrix3d rotationMatrix = Eigen::Matrix3d::Identity();
    rotMatrix =  rotationVector.toRotationMatrix();//所求旋转矩阵
}

int main()
{
    Eigen::Matrix3d rotMatrix;
    

    Eigen::Vector3d vectorBefore(0,0,1);
    Eigen::Vector3d vectorAfter(1,0,0);
    rotationMatrix(vectorBefore, vectorAfter, rotMatrix);
    std::cout << rotMatrix << std::endl;
    system("pause");
    return 0;
}

 

打印结果:

 

 c#代码:

void Calculation(double[] vectorBefore, double[] vectorAfter)
{
    double[] rotationAxis;
    double rotationAngle;
    double[,] rotationMatrix;
    rotationAxis = CrossProduct(vectorBefore, vectorAfter);
    rotationAngle = Math.Acos(DotProduct(vectorBefore, vectorAfter) / Normalize(vectorBefore) / Normalize(vectorAfter));
    rotationMatrix = RotationMatrix(rotationAngle, rotationAxis);
}

double[] CrossProduct(double[] a, double[] b)
{
    double[] c = new double[3];

    c[0] = a[1] * b[2] - a[2] * b[1];
    c[1] = a[2] * b[0] - a[0] * b[2];
    c[2] = a[0] * b[1] - a[1] * b[0];

    return c;
}

double DotProduct(double[] a, double[] b)
{
    double result;
    result = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];

    return result;
}

double Normalize(double[] v)
{
    double result;

    result = Math.Sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);

    return result;
}

double[,] RotationMatrix(double angle, double[] u)
{
    double norm = Normalize(u);
    double[,] rotatinMatrix = new double[3,3];
    
    u[0] = u[0] / norm;
    u[1] = u[1] / norm;
    u[2] = u[2] / norm;

    rotatinMatrix[0, 0] = Math.Cos(angle) + u[0] * u[0] * (1 - Math.Cos(angle));
    rotatinMatrix[0, 0] = u[0] * u[1] * (1 - Math.Cos(angle) - u[2] * Math.Sin(angle));
    rotatinMatrix[0, 0] = u[1] * Math.Sin(angle) + u[0] * u[2] * (1 - Math.Cos(angle));

    rotatinMatrix[0, 0] = u[2] * Math.Sin(angle) + u[0] * u[1] * (1 - Math.Cos(angle));
    rotatinMatrix[0, 0] = Math.Cos(angle) + u[1] * u[1] * (1 - Math.Cos(angle));
    rotatinMatrix[0, 0] = -u[0] * Math.Sin(angle) + u[1] * u[2] * (1 - Math.Cos(angle));
      
    rotatinMatrix[0, 0] = -u[1] * Math.Sin(angle) + u[0] * u[2] * (1 - Math.Cos(angle));
    rotatinMatrix[0, 0] = u[0] * Math.Sin(angle) + u[1] * u[2] * (1 - Math.Cos(angle));
    rotatinMatrix[0, 0] = Math.Cos(angle) + u[2] * u[2] * (1 - Math.Cos(angle));

    return rotatinMatrix;
}

 

 

三、实现原理

1.旋转角度

已知旋转前向量为P, 旋转后变为Q。由点积定义可知:

可推出P,Q之间的夹角为:

2. 旋转轴

由1中可知,旋转角所在的平面为有P和Q所构成的平面,那么旋转轴必垂直该平面。

假定旋转前向量为a(a1, a2, a3), 旋转后向量为b(b1, b2, b3)。由叉乘定义得:

所以旋转轴c(c1, c2, c3)为:

3.  罗德里格旋转公式(Rodrigues’ rotation formula)

3.1 公式

已知单位向量 , 将它旋转θ角。由罗德里格旋转公式,可知对应的旋转矩阵

其中I是3×3的单位矩阵,

是叉乘中的反对称矩阵r:

3.2 公式证明

假设在坐标系(x, y, z)中,向量v=ax+by+cz,v绕z轴逆时针旋转θ角后得到新的向量v’。

根据2维(x,y)面上的旋转公式可得:

推出:

已知:

将上式带入v’的公式:

  将cz替换掉,可得:

将上式中的叉乘表示为反对称矩阵得:

另外:

最终可以推出:

上式即为罗德里格旋转公式。

 

 

参考博客:https://www.cnblogs.com/xpvincent/archive/2013/02/15/2912836.html 

参考博客里的是c#的实现代码

我是参考完之后改了一个c++的版本出来

 

若有兴趣交流分享技术,可关注本人公众号,里面会不定期的分享各种编程教程,和共享源码,诸如研究分享关于c/c++,python,前端,后端,opencv,halcon,opengl,机器学习深度学习之类有关于基础编程,图像处理和机器视觉开发的知识

分類
博客园cnblogs

一次 RocketMQ 进程自动退出排查经验分享(实战篇)

1、背景

公司一个 RocketMQ 集群由4主4从组成,突然其中3台服务器“竟然”在同一时间下线,其监控显示如下:

依次查看三台机器的监控图形,时间戳几乎完美“吻合”,不可思议吧。

2、故障分析

出现问题,先二话不说,马上重启各服务器,尽快恢复集群,降低对业务的影响,接下来开始对日志进行分析。

Java 进程自动退出(rocketmq 本身就是一个java进程),一种最常见的问题是由于内存溢出或由于内存泄漏导致进程发送Crash等。由于我们的启动参数中未配置-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/jvmdump 这两个参数,不能直接根据 是否生成 dump 文件,那退而求其次去查看其GC日志,将GC日志下载到本地,然后可以使用一个在线gc日志分析工具:https://gceasy.io/ ,将 gc 日志上传后会给出图形化的展示,其图如下:


发现垃圾回收很正常。

既然 Java 进程不是由于内存溢出等问题导致的退出,那又会是什么原因呢?那我们来看一下那个点的broker的日志,其关键日志截图如下:

发现 broker 日志中有打印出 shutdownHook,表示在进程退出之前执行了启动时注册时的退出钩子函数,说明 broker 是正常停止的,并且也不可能是 kill -9 命令,肯定是显示的执行了 shutodown 或 kill 命令,于是立马使用 history 命令 查看历史命令,都未在指定时间执行过该命令,并且切换到 root 命令后,同样使用 history 命令,并未发现端倪。

但我始终相信,肯定是执行了手动执行了 kill 命令导致进程退出的,经过网上查找查,得知可以通过查阅系统日志/var/log/messages 来查看系统命令的调用,于是乎把日志文件下载到本地,开始搜索 kill 关键字,发现如下日志:

发现最近一次 kill 命令是在25号的凌晨1点多,停止 rocketmq 集群,并使用 bin/mqbroker -c conf/broker-b.conf & 进行了重新启动。

这个命令是有问题的,没有使用 nohup ,如果会话失效,该进程就会被退出,为了验证,我们再查一下进程退出时的日志:

发现在故障发生点确实有 Removed 相关的日志。

故障原因基本分析到位了,运维在启动的时候没有使用 nohup 来启动,故马上排查刚启动的集群的方式,重新重启刚启动的 Broker。

RocketMQ优雅重启小建议:

  1. 首先将 broker 的写权限关闭,命令如下:
    bin/mqadmin updateBrokerConfig -b 192.168.x.x:10911 -n 192.168.x.x:9876 -k brokerPermission -v 4

  2. 通过 rocketmq-console 查看该broker的写入TPS,当写入TPS降为0后,再使用 kill pid 关闭 rocketmq 进程。温馨提示:将broker的写权限关闭后,非顺序消息不会立马拒绝,而是需要等客户端路由信息更新后,不会在往该broker上发送消息,故这个过程需要等待。

  3. 启动 rocketmq
    nohup bin/mqbroker -c conf/broker-a.conf /dev/null 2>&1 &

    注意:nohup。

  4. 恢复该节点的写权限
    bin/mqadmin updateBrokerConfig -b 192.168.x.x:10911 -n 192.168.x.x:9876 -k brokerPermission -v 6

本文的故障分析与处理就介绍到这里,本文重点讲解了故障的分析过程以及 RocketMQ Broker 优雅停机的方案。

如果本文对您有所帮助的话,麻烦帮忙点个赞,谢谢。

作者介绍:
丁威,《RocketMQ技术内幕》作者,RocketMQ 社区布道师,公众号:中间件兴趣圈 维护者,目前已陆续发表源码分析Java集合、Java 并发包(JUC)、Netty、Mycat、Dubbo、RocketMQ、Mybatis等源码专栏。欢迎加入我的知识星球,构建一个高质量的技术交流社群。

分類
博客园cnblogs

PRML第一章读书小结

PRML第一章读书小结

    第一章用例子出发,较为简单的引入了概率论、模型、决策、损失、信息论的问题,作为机器学习从业者,读PRML除了巩固已有基础,还受到了很多新的启发,下面将我收到的启发总结如下。

1. 多项式曲线拟合问题

多项式拟合问题作为全书的第一个引例,通过此说明了很多关键的概念。

给定一个训练集,训练集由\(x\)的N次观测组成,记作\(\mathbf{x} \equiv\left(x_{1}, \cdots, x_{N}\right)^{T}\),对应了相应的观测值\(t\),记作\(\mathbf{t} \equiv\left(t_{1}, \cdots, t_{N}\right)^{T}\)它们拥有了一个内在的规律,这个规律是我们想要学习的,但是同时独立的观察会被随机噪声所干扰。我们的目标是利用这个训练集预测输入变量的新值,我们需要隐式地发现内在的函数\(sin(2\pi x)\),由于有限的观察和噪声的,发现这一函数(\(sin(2\pi x)\))很难。

概率论提供了一个框架,用精确的数学形式描述这种不确定性。决策论让我们能够根据合适的标准,利用这种概率的表示,进行最优的预测。

我们经常用多项式函数进行曲线拟合,即\(y(x, \boldsymbol{w})=w_{0}+w_{1} x+w_{2} x^{2}+\ldots+w_{M} x^{M}=\sum_{j=0}^{M} w_{j} x^{j}\),系数的值\(w\)通过拟合训练数据的方式确定,M作为多项式的阶数是模型对比(model comparison)、模型选择(model selection)的重要问题的一个特例。拟合时,我们通过最小化误差函数(error function)的方法实现,一个简单的最小化误差函数如下:
\[ E(\boldsymbol{w})=\frac{1}{2} \sum_{n=1}^{N}\left\{y\left(x_{n}, \boldsymbol{w}\right)-t_{n}\right\}^{2} \]

我们发现过小的M拟合效果非常差,而高阶的M完美拟合了数据,但是曲线剧烈震荡,就表达函数\(sin(2\pi x)\)来说表现很差,这便是过拟合

我们的目标是通过对新数据的预测实现良好的泛化性,于是我们考虑一个额外的训练集,生成方式和之前的训练集完全相同,但是包含的噪声不同,对于每个M的选择,我们可以利用误差函数,或者均方根误差(RMS)衡量:
\[ E_{R M S}=\sqrt{2 E\left(\boldsymbol{w}^{*}\right) / N} \]
N保证了以相同的基础对比不同大小的数据集,平方根保证了\(E_{RMS}\)与目标变量\(t\)使用相同的规模和单位进行度量。

我们发现M的值适中时,均方根误差较小。M特别大的时候,测试误差很大(即过拟合)。进一步思考这个问题,我们发现,对于一个给定的模型复杂度(M给定),数据集规模增加,过拟合问题变得不那么严重,或者说,数据集规模越大,我们能用来拟合数据的模型就越复杂(灵活)。一个粗略的启发是:数据点的数量不应该小于模型的可调节参数的若干倍。我们根据待解决问题的复杂性来选择模型的复杂性,过拟合现象是极大似然的一个通用属性,而通过贝叶斯方法,过拟合问题可以被避免。

目前,我们使用正则化(regularization)技术控制过拟合, 即增加一个惩罚项,使得系数不会达到一个很大的值,如下例是加入平方惩罚项的误差函数:
\[ \tilde{E}(\boldsymbol{w})=\frac{1}{2} \sum_{n=1}^{N}\left\{y\left(x_{n}, \boldsymbol{w}\right)-t_{n}\right\}^{2}+\frac{\lambda}{2}\|\boldsymbol{w}\|^{2} \]
正则化后的进行多项式拟合效果就能达到一个理想的值。

之后,作者在重新考察曲线拟合问题时,提到了最大化似然函数和最小化平方和误差函数,而最大化后验概率等价于最小化正则化的误差函数。

2. 概率论

文章首先通过简单的例子说明了概率论的基本思想,然后表示了概率论的两条基本规则:

加和规则sum rule\(p(X)=\sum_{Y} p(X, Y)\)
乘积规则product rule\(p(X, Y)=p(Y | X) p(X)\)

这两个规则是机器学习全部概率推导的基础。

根据乘积规则,我们得到贝叶斯定理
\[ p(Y | X)=\frac{p(X | Y) p(Y)}{p(X)}=\frac{p(X | Y) p(Y)}{\sum_{Y} p(X | Y) p(Y)} \]
其中,\(p(Y)\)称为先验概率(\(prior\)),即根据先验知识得出的关于变量\(Y\)的分布,\(p(X|Y)\)称为似然函数(\(likelihood\)),\(p(X)\)为变量\(X\)的概率,\(p(Y|X)\)称之为条件概率(给定变量\(X\)的情况下\(Y\)的概率,\(posterior\),后验概率)。

在连续空间中,一个实值变量\(x\)的概率落在区间\((x,x+\delta x)\)的概率由\(p(x)\delta x\)给出(\(\delta x →0\)),那么\(p(x)\)称为\(x\)概率密度(probability density),\(x\)在区间\((a,b)\)的概率由下式给出:
\[ p(x \in(a, b))=\int_{a}^{b} p(x) \mathrm{d} x \]
概率密度是处处大于0且归一化的。

离散变量的期望值(expectation)的定义为:
\[ \mathbb{E}[f]=\sum_{x} p(x) f(x) \]
连续变量的期望值:
\[ \mathbb{E}[f]=\int p(x) f(x) \mathrm{d} x \]
方差(variance)的定义:
\[ \operatorname{var}[f]=\mathbb{E}\left[(f(x)-\mathbb{E}[f(x)])^{2}\right] \]

\[ =\mathbb{E}\left[f(x)^{2}\right]-\mathbb{E}[f(x)]^{2} \]

它度量了\(f(x)\)在均值\(\mathbb{E}[f(x)]\)附近变化性的大小。

协方差(covariance)的定义:
\[ \operatorname{cov}[x, y]=\mathbb{E}_{x, y}[\{x-\mathbb{E}[x]\}\{y-\mathbb{E}[y]\}] \]

\[ =\mathbb{E}_{x, y}[x y]-\mathbb{E}[x] \mathbb{E}[y] \]

它表示在多大程度上\(x\)\(y\)会共同变化,如果独立,协方差为0.

2.1 概率论之贝叶斯学派和频率学派

    频率学派试图从自然的角度出发,试图直接为事件建模,即事件A在独立重复实验中发生的频率趋于极限P,那么这个极限就是事件的概率。

    贝叶斯学派并不试图刻画事件本身,而是从观察者角度。贝叶斯学派并不认为事件本身是随机的,而是从观察者知识不完备这一出发点开始,构造一套贝叶斯概率论的框架下可以对不确定知识作出推断的方法。即不认为事件本身具有某种客观的随机性,而只是观察者不知道事件的结果

    频率学派广泛使用极大似然进行估计,使得似然函数\(p(\mathcal{D} | \boldsymbol{w})\)达到最大。贝叶斯学派广泛使用先验概率。

    补充:根据知乎某大佬所言:频率学派和贝叶斯学派最大差别是产生在对参数空间的认知上。频率学派并不关心参数空间的所有细节,而相信数据都是在某个参数值下产生的,所以频率学派从“那个值最有可能是真实值”出发的。有了极大似然和置信区间。贝叶斯学派关心参数空间的每一个值,我们又没有上帝视角,怎么可能知道哪个值是真的,参数空间的每个值都有可能是真实模型使用的值,只是概率不同。

参考:https://www.zhihu.com/question/20587681

2.2 高斯分布

    高斯分布算是模式识别里面的重点难点,在第一章里面简要介绍了其一些简单性质,总结如下:

一元高斯分布:
\[ \mathcal{N}\left(x | \mu, \sigma^{2}\right)=\frac{1}{\left(2 \pi \sigma^{2}\right)^{\frac{1}{2}}} \exp \left\{-\frac{1}{2 \sigma^{2}}(x-\mu)^{2}\right\} \]

高斯分布满足恒大于0:
\[ \mathcal{N}\left(x | \mu, \sigma^{2}\right)>0 \]
高斯分布是归一化的:
\[ \int_{-\infty}^{\infty} \mathcal{N}\left(x | \mu, \sigma^{2}\right) \mathrm{d} x=1 \]

高斯分布的期望:
\[ \mathbb{E}[x]=\int_{-\infty}^{\infty} \mathcal{N}\left(x | \mu, \sigma^{2}\right) x \mathrm{d} x=\mu \]
二阶矩:
\[ \mathbb{E}\left[x^{2}\right]=\int_{-\infty}^{\infty} \mathcal{N}\left(x | \mu, \sigma^{2}\right) x^{2} \mathrm{d} x=\mu^{2}+\sigma^{2} \]
方差:
\[ \operatorname{var}[x]=\mathbb{E}\left[x^{2}\right]-\mathbb{E}[x]^{2}=\sigma^{2} \]
分布的最大值被称为众数,高斯分布的众数与均值恰好相等。

假定一个观测数据集是独立从高斯分布中抽取(independent and identically distributed, i.i.d.),分布均值\(\mu\)和方差\(\sigma^2\)未知。数据集的概率:
\[ p\left(\mathbf{x} | \mu, \sigma^{2}\right)=\prod_{n=1}^{N} \mathcal{N}\left(x_{n} | \mu, \sigma^{2}\right) \]
当我们把它看做参数的函数的时候,这就是高斯分布的似然函数。之后我们利用极大似然法寻找似然函数取得最大值的参数值。同时书中提到了:给定数据集下最大化概率的参数和给定参数的情况下最大化数据集出现的概率是相关的。

高斯分布的最大似然解:\(\mu_{M L}=\frac{1}{N} \sum_{n=1}^{N} x_{n}\)\(\sigma_{M L}^{2}=\frac{1}{N} \sum_{n=1}^{N}\left(x_{n}-\mu_{M L}\right)^{2}\)

高斯分布的极大似然估计对均值的估计是无偏的,对方差的估计是有偏的(低估)。

3.模型选择

在曲线拟合中,存在一个最优的多项式阶数。实际情况中,我们可能存在多个控制模型复杂度的参数,同时存在过拟合现象,所以我们需要一个验证集。而数据有限,所以需要交叉验证,S-1组进行训练,1组进行评估,运行S次。但是存在一个问题就是训练本身很耗时。

4. 维数灾难

随着维数的变高,我们需要指数级的训练数据。对于高维数据,高斯分布的概率质量集中在薄球壳上。这对我们的模型产生了极大地困难。

5. 决策论

  1. 最小化错误分类率。把每个点分在后验概率最大的类别中,那么我们分类错误的概率就会最小。
  2. 最小化期望损失。损失函数(代价函数)最小。
  3. 拒绝选项。对于低于阈值的概率,拒绝作出识别,拒绝决策带来的损失可以放在损失矩阵中。

6.信息论

随机变量的熵:\(H[x]=-\sum_{x} p(x) \log _{2} p(x)\)

熵是传输一个随机变量状态值所需的比特位的下界。

相对熵:\(\mathrm{KL}(p \| q)=-\int p(\boldsymbol{x}) \ln \left\{\frac{q(\boldsymbol{x})}{p(\boldsymbol{x})}\right\} \mathrm{d} \boldsymbol{x}\)

相对熵也被称之为KL散度,不对称。当且仅当\(p=q\)时,等号成立。

最小化KL散度等价于最大化似然函数(p为真实分布,q为给定分布)。

互信息:
\[ \begin{aligned}I[\boldsymbol{x}, \boldsymbol{y}] & \equiv \mathrm{KL}(p(\boldsymbol{x}, \boldsymbol{y}) \| p(\boldsymbol{x}) p(\boldsymbol{y})) \\&=-\iint p(\boldsymbol{x}, \boldsymbol{y}) \ln \left(\frac{p(\boldsymbol{x}) p(\boldsymbol{y})}{p(\boldsymbol{x}, \boldsymbol{y})}\right) \mathrm{d} \boldsymbol{x} \mathrm{d} \boldsymbol{y}\end{aligned} \]
\(I[\boldsymbol{x}, \boldsymbol{y}] \geq 0\),当且仅当\(x\)\(y\)独立时等号成立。我们可以把互信息看成由于知道\(y\)值而造成的\(x\)的不确定性的减少。

7. 一些小的知识点:

  1. 严格凸函数:每条弦位于函数图像上方,即二阶导数为正
  2. 变分法
  3. 高维空间中,球的大部分体积都聚集在表面附近。
  4. 具体化一个连续变量需要大量比特位。

本文由飞剑客原创,如需转载,请联系私信联系知乎:@AndyChanCD

分類
博客园cnblogs

MySQL性能优化

SQL语句优化-explain分析问题      

Explanin select * from user

会产生如下信息:

id:查询的序列号
select_type:表示查询的类型。
table:输出结果集的表
type:表示表的连接类型
possible_keys:表示查询时,可能使用的索引
key:表示实际使用的索引
key_len:索引字段的长度
ref: 哪个字段或常数与 key 一起被使用
rows:扫描出的行数
Extra:执行情况的描述和说明

select_type列说明

SIMPLE, 表示此查询不包含 UNION 查询或子查询

PRIMARY, 表示此查询是最外层的查询

UNION, 表示此查询是 UNION 的第二或随后的查询

DEPENDENT UNION, UNION 中的第二个或后面的查询语句, 取决于外面的查询

UNION RESULT, UNION 的结果

SUBQUERY, 子查询中的第一个 SELECT

DEPENDENT SUBQUERY: 子查询中的第一个 SELECT, 取决于外面的查询. 即子查询依赖于外层查询的结果.

type列说明

通常来说, 不同的 type 类型的性能关系如下:

ALL < index < range~index_merge < ref < eq_ref < const < system

MySQL性能优化之慢查询

介绍

数据库查询快慢是影响项目性能的一大因素,对于数据库,我们除了要优化 SQL,更重要的是得先找到需要优化的 SQL

MySQL 数据库有一个慢查询日志功能,用来记录查询时间超过某个设定值的SQL,这将极大程度帮助我们快速定位到症结所在,以便对症下药。

MySQL的慢查询日志功能,默认是关闭的,需要手动开启

性能优化思路

  1. 首先需要使用慢查询功能,去获取所有查询时间比较长的SQL语句
  2. 其次使用explain命令去查看有问题的SQL的执行计划
  3. 最后使用show profile [s] 查看有问题的SQL的性能使用情况

开启慢查询功能

查看是否开启慢查询功能

 临时开启慢查询功能

MySQL 执行 SQL 语句设置,但是如果重启 MySQL 的话将失效

set global slow_query_log = ON;

set global long_query_time = 1;

永久开启慢查询功能

修改/etc/my.cnf配置文件,重启 MySQL, 这种永久生效.

[mysqld]

slow_query_log = ON

slow_query_log_file = /var/log/mysql/slow.log

long_query_time = 1

SQL语句优化-show参数 

MySQL客户端连接成功后,通过使用show [session|global] status 命令可以提供服务器状态信息。其中的session来表示当前的连接的统计结果,global来表示自数据库上次启动至今的统计结果。默认是session级别的。默认的情况下,MYSQL的该功能没有打开,需要自己手动启动。

语句使用

show profile show profiles 语句可以展示当前会话中执行语句的资源使用情况.

show profiles :以列表形式显示最近发送到服务器上执行的语句的资源使用情况.显示的记录数由变量:profiling_history_size 控制,默认15条

show profile: 展示最近一条语句执行的详细资源占用信息,默认显示 Status和Duration两列

开启Profile功能

Profile 功能由MySQL会话变量 : profiling控制,默认是OFF关闭状态。

查看是否开启了Profile功能:

select @@profiling;

show variables like ‘%profil%’;

 开启profile功能

set profiling=1; –1是开启、0是关闭

MySQL性能优化细节

  1. 合理的创建及使用索引(考虑数据的增删情况)
  2. 合理的冗余字段(尽量建一些大表,考虑数据库的三范式和业务设计的取舍)
  3. 使用SQL要注意一些细节:select语句中尽量不要使用*、count(*),WHERE语句中尽量不要使用1=1、in语句(建议使用exists)、注意组合索引的创建顺序按照顺序组着查询条件、尽量查询粒度大的SQL放到最左边、尽量建立组合索引
  4. 合理利用慢查询日志、explain执行计划查询、show profile查看SQL执行时的资源使用情况。

MySQL锁 

锁介绍

数据库锁定机制简单来说就是数据库为了保证数据的一致性而使各种共享资源在被并发访问访问变得有序所设计的一种规则。

对于任何一种数据库来说都需要有相应的锁定机制,所以MySQL自然也不能例外。

MySQL数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。

MySQL各存储引擎使用了三种类型(级别)的锁定机制:行级锁定,页级锁定和表级锁定

按照锁的粒度来分:行级锁和表级锁

按照锁的功能来分:共享读锁和排他写锁

行级锁定(row-level)

行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。

虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。

表级锁定(table-level)

和行级锁定相反,表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。

当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。

页级锁定(page-level)

页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。

MySQL这3种锁的特性可大致归纳如下

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
分類
博客园cnblogs

不一样的策略模式(设计模式五)

前言

什么是设计模式?说白了就是套路,是经过历代程序员总结出来的。很多时候我们虽然学会了很多套路,但是啥时候使用,使用哪个合适,我想这才是问题的关键。
知道怎么用不知道什么时候用,这时候看下代码风格也行用的上,策略模式是非常容易通过代码风格使用上的。
策略模式,为什么叫策略模式呢?其实策略模式还有一个别名叫做政策(policy)模式,在古代,对不同的国家呢,实行不同的政策,对A呢,采取税务10%,对B国采取税务20%。
这样根据不同国家政策不同呢,在计算机中就是根据不同对象采取不同的方法,就叫做策略模式咯。但是呢,如果直接采用if else 这种行为呢,人们发现过于臃肿,且复用性极差,那么就形成了这样一种模式去缓解这个问题。
策略模式属于对象行为模式,其实不去看概念也很好记,在运行时针对不同对象,去做出相应的行为。至于为什么不分到结构型或者其他型,只是它更符合行为型,一个类的行为或其算法可以在运行时更改,这些分类其实没有绝对的界限,只是有权威人士对其进行了规划。
那么策略模式的风格是什么,查看正文。

开车出发

public enum PolicyBase
{
    US_Policy,
    DE_Policy,
    FR_Policy
}
public class Strategy
{
    PolicyBase policyBase;

    public Strategy(PolicyBase policyBase) {
        this.policyBase = policyBase;
    }
    public double CalculatePolicy() {
        if (policyBase == PolicyBase.US_Policy)
        {
            return 0.9;
        }
        else if (policyBase == PolicyBase.DE_Policy)
        {
            return 0.8;
        }
        else if (policyBase == PolicyBase.FR_Policy)
        {
            return 0.1;
        }
        return 0;
    }
}

根据不同的国家,采取了不同的政策。这样一看好像没有啥问题啊,根据了不同国家制定了不同税法。
但是呢,在开发代码的时间轴上,也就是未来的角度上存在的极大的问题。比如和英国(GB)合作了,我得改吧?然后又和另外一个国家合作了,恐怕又得来一遍。
这时候有人就纳闷了,还想不改代码?代码的确是要改的,但是不能违法了封闭开发原则。


在红色部分呢,是我们需要继续往下添加代码的地方,也就是我们加一个国家就需要把我们写过的任何一个区域改一遍。
我想这就很糟糕了,牵扯太大,对于发布来说就需要测试整个子模块,我想这代价无法让人接受,这时候策略者模式就出现了。

public enum PolicyBase
{
    US_Policy,
    DE_Policy,
    FR_Policy
}
public interface Policy
{
    double Calculate();
}

public class USPolicy : Policy
{
    public double Calculate()
    {
        throw new NotImplementedException();
    }
}

public class DEPolicy : Policy
{
    public double Calculate()
    {
        throw new NotImplementedException();
    }
}

public class FRPolicy : Policy
{
    public double Calculate()
    {
        throw new NotImplementedException();
    }
}

public class StrategyFactory{
    Policy policy;
    public StrategyFactory(PolicyBase policyBase) {
        switch (policyBase)
        {
            case PolicyBase.US_Policy:
                policy = new USPolicy();
                break;
            case PolicyBase.DE_Policy:
                policy = new DEPolicy();
                break;
            case PolicyBase.FR_Policy:
                policy = new FRPolicy();
                break;
        }
    }
    public Policy GetPolicy() {
        return policy;
    }
}

class SalesOrder
{
    private Policy Policy;

    public SalesOrder(StrategyFactory strategyFactory)
    {
        this.Policy = strategyFactory.GetPolicy();
    }

    public double CalculatePolicy()
    {
        double val = Policy.Calculate();
        return val;
    }

};

上述代码中,通过策略者模式把原来的获取各国的税法比例变成了SalesOrder类,而这个类不再改变,也就是说所以依赖于获取各国税法参数的将依赖于一个稳定的类。
这时候很多纳闷了,如果我需要添加一个英国(GB),依然需要在红色部分就行修改啊,修改的地方如下:


修改的地方一样多,且还要多加一个GBPolicy类,这不是白忙活了吗?
首先我们来看下前后依赖关系图:
使用策略模式前:

使用策略模式后:

这样一看,不仅是没有啥好处,还复杂了。
然而这样一想,我们处理的是解决这个税法问题这个业务上,可以肯定的就是使用策略模式后,我下面红框部分稳定了,也就是在二进制上可以复用,但是上面红色部分倒是有问题了,耦合太大。
但是呢,我们知道上面复杂部分其实就是简单工厂模式,问题就回到了如何优化简单工厂模式了,如果能解决上面红框的问题,那么是可行的。
由于篇幅有限,下一篇总结工厂模式到抽象工厂到反射这个演化。
这时候我们看到了,如果遇到了if else 且以后会增加else if,可以用策略模式,去缓解这个问题,增加代码复用性。
但是稳定的if else 呢是不需要的,比如说星期一到星期日,这种就是稳定的了,本来处于稳定的,那么其改变的价值就不是很大。

uml图

后续补上

总结

策略模式的作用,解决使用不稳定 的if…else 所带来的复杂和难以维护。