Everybody wants go to heaven, but nobody wants to die.

2019-12-10 0 评论 226 浏览
阅读全文
领域驱动设计DDD之概览 置顶! 有更新!

在从事开发多年之后,你是否会感觉自己只是一个业务CRUD Boy,并认为业务没有多少技术含量。你是否会陷入业务的泥潭中,各种复杂交错的业务规则使得代码开始腐烂开始失控,项目开始变得难以维护,迭代举步维艰。如果你开始意识到这个问题的话,那么我十分推荐你开始学习领域驱动设计面向领域建模的设计方式。

DDD是什么呢?

DDD即Domain Driven Design,翻译成中文的话就是领域驱动设计,首先我们应该先理解这里的领域是什么意思?假设公司内部正在开发一套电商平台,而电商平台中包含了库存、订单、商品等核心业务。这些核心业务逻辑其实呈现的就是电商平台领域。通俗的理解就是一整套体系的业务知识即代表了一个领域。好比在线教育平台,它需要有一套体系的业务,包括招生、线上教学、课程等内容。我们将这些业务抽象出领域模型,而这些领域模型表达了产品经理所阐述的业务需求,我们反复地用这些领域模型与产品经理进行讨论沟通最终确定初步的领域模型。再使用初步的领域模型指导代码设计开发。

简而言之就是:

业务知识 --> 领域模型 --> 项目设计与代码开发

不同的电商平台的核心业务逻辑大都是相似的,这部分领域知识是可以进行复用,区别在于不同公司使用的不同的编程语言,不同的前端控制框架,数据库框架。采用领域驱动设计的好处在于项目以领域模型为核心,而Spring MVC、Struts等前端控制框架或者Hibernate、Mybatis对象数据库框架属于外围技术基础,领域模型其实并不与这些基础技术产生耦合,所以在领域模型不变的情况下,我们是很容易对我们的基础设施进行更换的。

2019-11-06 0 评论 414 浏览
阅读全文

上下文映射图的英文是Context Map其实这个翻译挺难理解的,上下文映射图其实就是不同上下文是如何进行交流的关系。由于上下文映射图内容比较少。以下内容摘自《领域驱动设计精粹》。

三种集成方式

  1. RPC方式
  2. 消息队列或者发布-订阅机制。
  3. RESTful方式

上下文映射的种类

  • 合作关系:
    合作关系存在于两个团队之间。每个团队各自负责一个限界上下文。两个团队通过互相依赖的一整套目标联合起来形成合作关系。一损俱损,一荣俱荣。由于相互之间的联系非常紧密,他们经常会对同步日程安排和相互关联的工作。他们还必须使用持续集成对保持集成工作协调一致。

  • 共享内核:
    两个或者多个团队之间共享着一个小规模但却通用的模型。团队必须就要共享的模型元素达成一致。有可能他们当中只有一个团队会维护,构建及测试共享模型的代码。

2019-11-29 0 评论 265 浏览
阅读全文

战术设计是从微观视角对单个微服务的编码设计,而战略设计是从宏观视角对多个微服务的交互设计。从人体学来说,一个器官的内部构造属于战术设计,多个器官之间的协作属于战略设计。好比心肺器官之间的协作配合。

为什么需要战略设计?

假设我们在设计订单模型时,下单操作涉及到会员等相关规则,比如增加积分等操作。(此时我们还未将订单子域和会员子域进行剥离)。其实一个用户的下单操作不需要与会员的积分产生强烈耦合。一旦会员的积分制度发生变动,又得需要在订单子域中进行修改,而订单子域又是我们的核心域,频繁的对核心域进行更改的风险较大。那如果将会员体系从订单核心域剥离出去呢?我们便使得两个子域的职能更为清晰并且解耦,这也体现了单一职责的设计原则。

假设一个开发小组既负责订单逻辑又要负责会员积分逻辑,则使得这个开发小组本身的职责变得复杂。如果使将订单和会员拆分开来,通过上下文之间的交互(即微服务之间的交互)使得订单开发小组成员、会员开发小组各自关心自己的领域(即各自只关心各自的业务逻辑)。

好比流水线上的工人,有利于分工,大家专注于自己负责的工序。

战略设计的元素主要有子域、限界上下文、上下文映射图。

2019-11-28 0 评论 279 浏览
阅读全文

为什么使用资源库?

如果完全按照领域模型的角度,完全通过遍历对象的方法来获取所有关联的对象。这种模型会过于错综复杂。对象嵌套的层级或者关联的层级非常深。例如通过Customer.order.product.price层层遍历来获取当时客户订单的商品的价格。

那如果完全按照数据库模型的角度,模型中的对象不需要完全连接起来,对象关系网就能保持在一个可控范围。但是这又会回到之前传统开发模式中,零散的使用各个DAO从各个表抽取数据自行拼凑出我们想要的模型。

这里就会出现一个问题,Customer类需要保持客户所有已订的Order,还是通过CustomerID在数据库中查找Order列表呢?

对象关联还是纯数据库?

客户需要一种有效的方式来获取已存在的领域对象的引用。如果基础设施提供了这方面的遍历,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发人员可能使用查询从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过聚合的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而实体和值对象变成了单纯的数据容器。采用大多数处理数据库访问的技术复杂性很快就会使客户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。

我们需要找到一个恰当的方式。

我们不需要对那么很容易通过遍历来找到的持久对象进行查询访问,如不需要单独的去获取地址,而可以通过Person对象来获取地址,意味着当我们获取Person对象的时候,地址值对象已经是填充好的。我们不需要特地利用另外一个资源库的方法,获取地址再进行填充。但当获取Person的所有订单记录时,由于订单的数量有时候过于庞大,加载Person的时候其实并不一定需要查看订单记录,我们应该使用Repository的另外方法来获取Person的订单记录。

对值对象的全局搜索通常是没有意义的。如果确实需要在数据库中搜索一个已知的值对象,那么值得考虑一下,搜索结果可能实际上是一个实体,尚未识别出它的标识。

2019-11-21 0 评论 457 浏览
阅读全文
领域驱动设计DDD之聚合 置顶! 有更新!

为什么需要聚合?

当我们设计一个订单模块,用户下单时,我们需要确保用户的余额可供支付这笔订单,并且保存这个订单。通俗的理解就是当下单的时候,必须生成订单表记录,并且检查用户余额是否足够支付,并修改用户的余额表。再转换到我们领域驱动设计中,我们必须利用订单模型和账户模型联合来完成操作,并检查保证业务规则(余额可供支付)。那此时有两点缺陷,在保证事务的应用服务中,这些领域知识(业务规则)便从领域模型泄露应用服务层。代码中除了订单模型和账户模型在同一个应用服务中这点之外,客户程序员很难知道在下单请求中订单模型和账户模型之间必须在同一个事务中进行修改。

聚合描述

每个聚合都有一个根和一个边界,边界内定义了聚合的内部有什么。根则是聚合所包含的一个特定的实体。外部对象可以引用根,但不能引用聚合内部的其他对象,聚合内的对象之间可以相互引用,除了根实体外,其他实体拥有本地标识。

举个例子:

在汽修厂的软件中会使用到汽车的模型,这里的汽车就是根实体,因为它具有全局唯一的标识:汽车识别号。汽修厂想要跟踪每台汽车上四个轮胎的使用情况。轮胎在汽车里才是实体,它们拥有本地标识,如汽车的四轮分为左前轮,右前轮,左后轮,右后轮。当轮胎报废了之后我们便不再关心这些轮胎的生命周期了,我们也不会在系统中寻找某一个轮胎现在安在哪台车上。因此,汽车便是这个聚合的根实体,轮胎就在这个聚合的边界之内。

再举个例子:
在订单模块中,下单操作必须生成订单主表和购买商品附表,订单主表即是一个根,通过这个根我们可以找到当时这笔订单购买了哪些商品。但是我们并不会单独去查购买商品附表里的记录,脱离了订单主表,这些记录就没有了意义。

定义

我们应该将实体和值对象分门别类的聚集到聚合当中,并定义聚合的边界。在每个聚合当中,选择一个实体作为根。并通过根来控制边界内其他对象的所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保聚合中的对象满足所有固定规则,也可以确保在任何状态变化时聚合作为一个整体满足固定规则。

2019-11-18 0 评论 335 浏览
阅读全文
领域驱动设计DDD之领域服务 置顶! 有更新!

什么是领域服务?

在战术建模当中,并非所有模型都是事物。有些模型是对领域中的一些行为操作进行建模。此类模型我们称之为领域服务。当一些重要的领域操作无法放到实体、值对象或者聚合时,他们本质是行为而不是事物。如果我们不寻找一些对象来封装这些领域行为的话,又会演变成之前过程式的编程方式。我们希望在领域设计当中统一用模型对象进行交互。此时领域服务使用细粒度的领域对象如实体或者值对象进行交互,在服务内部描述领域知识得出结果并将其返回。

领域服务的参数和返回类型应该是领域对象。

三个特征:
  • 它是与领域相关的操作如执行一个显著的业务操作过程,但它又并不适合放入实体与值对象中。
  • 操作是无状态的。
  • 对领域对象进行转换,或以多个领域对象作为输入进行计算,结果产生一个值对象。

2019-11-16 0 评论 344 浏览
阅读全文
领域驱动设计DDD之工厂 置顶! 有更新!

为什么需要工厂

  1. 当创建一个复杂对象或聚合的过程很复杂并且暴露出了过多的内部结构时,我们则可以使用工厂进行封装。一个对象在它的生命周期中要承担大量的职责,如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。

  2. 我们设计好领域模型供客户方调用,但如果客户方也必须使用如何装配这个对象,则必须知道对象的内部结构。好比你去驾校学车,却得先学会发动机的原理。对客户方开发来说这是很不友好的。其次,复杂对象或者聚合当中的领域知识(业务规则)需要得到满足,如果让客户方自己装配复杂对象或聚合的话,就会将领域知识泄露到客户方代码中去。

  3. 对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或聚合的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。

最重要的一点就是隐藏创建对象的细节

2019-11-19 0 评论 326 浏览
阅读全文
领域驱动设计DDD之实体 置顶! 有更新!

什么是实体?

实体最主要有两点特征,一是唯一标识,二是连续性。

  • 唯一标志:
    当一些对象不是由属性定义,而是由一个唯一标志定义的话,我们就可以认为它是一个实体。好比我们不能通过一个人的外在特征去唯一定位一个人,因为人从小到大,从年轻到衰老其外在特征都是在改变的。而身份证号码可以贯穿一个人的一生而不发生变化。而且唯一标识不一定仅有一个属性表示,有可能通过多个属性标识某一个唯一对象,就好比数据库中的联合外键(可能有根据电话以及姓名唯一确定一个实体的情况)。

  • 连续性:
    对象的连续性体现在对象是有生命周期的。在这个生命周期内,对象内的属性可能是变化着的。好比银行账户表,它就属于一个实体,用户的银行卡号可以唯一的确定一个人的账户,而账户的内的余额随着时间变化(利息)或者随着交易变化。

2019-11-08 0 评论 311 浏览
阅读全文
领域驱动设计DDD之值对象 置顶! 有更新!

值对象是什么?

之前我们讲到实体,它最主要的特征在于概念上的标识。而领域中存在一些不需要进行标识的对象,它们主要是对事物的描述。如何理解这段话呢?可以通俗的理解,之前我们认为实体是数据库中的一条记录,这条记录会发生更改,例如学生的信息表。而值对象对应到数据库中的什么呢?它们不会单独地映射一张表而是可能对应表中的几个字段。比如学生信息表中拥有省、市、区、地址四个字段构成学生的住址,这四个字段构成的是一个整体用来描述学生的详细地址。

为什么我们将学生详细地址建模成值对象呢?

因为我们并不需要追踪学生详细地址的变化,我们并不关系它的连续性。


在很多DDD的博客文章中讲解到值对象都会用到了地址这一概念,可能有的人会误解地址天然就是值对象。并非如此,它有时可能作为值对象,有时可能作为实体。这需要看我们的业务场景。

值对象的情形:

  1. 当你和你的舍友在淘宝上购买商品,你们在订单上填的地址是一样的,并不需要严格区分这两个地址,快递直接送到这个地址即可。(地址映射到订单表中的几个字段上,如:省市区地址)

实体的情形:
2. 当你们宿舍的宽带坏了,需要报修,这时候你和你的舍友都打了电话报修。但是宽带人员并不会派两个人来抢修宽带。因为宽带人员知道你们是同一个地址。(宿舍地址映射到宽带公司系统中的一条地址用户表,以地址来区别用户)


2019-11-09 0 评论 253 浏览
阅读全文

业务场景

在进行业务开发时经常会有状态值的业务需要,例如一场考试有未开考、考试中、考试结束等状态或者一年四季有春天、夏天、秋天、冬天等状态。从前台传参到我们的业务模型再到数据库,其实这些状态是贯穿整个开发流程的。如果仅仅使用1、2、3、4来代表春夏秋冬的话,那代码的自解释能力就太差了。那如何利用好枚举类来更优雅的编码呢?

枚举类需要满足的功能

  1. 消除代码中的魔术数

  2. 根据数值获取状态的名称

  3. 根据数值获取状态

  4. 获取数值和状态名称为键值对的Map作为前端筛选项。

第一种枚举类实现

public enum SeasonEnum {

    /**/
    SPRING(1,"春天"),
    SUMMER(2,"夏天"),
    AUTUMN(3,"秋天"),
    WINTER(4,"冬天");

    public final int id;
    public final String desc;

    SeasonEnum(int id, String desc) {
        this.id = id;
        this.desc = desc;
    }


    /**
     * 根据数值获取对应的枚举
     * @param id
     * @return
     */
    public static SeasonEnum getEnumById(int id) {
        for (SeasonEnum seasonEnum : SeasonEnum.values()) {
            if (seasonEnum.id == id) {
                return seasonEnum;
            }
        }
        return null;
    }


    /**
     * 根据数值获取对应的枚举描述
     * @param id
     * @return
     */
    public static String getEnumDescById(int id) {
        final String unknown = "未知";
        for (SeasonEnum seasonEnum : SeasonEnum.values()) {
            if (seasonEnum.id == id) {
                return seasonEnum.desc;
            }
        }
        return unknown;
    }


    /**
     * 获取数值-描述键值对map
     * @return
     */
    public static Map getEnumMap() {
        Map<Object, Object> map = new HashMap<>();
        for (SeasonEnum seasonEnum : SeasonEnum.values()) {
            map.put(seasonEnum.id, seasonEnum.desc);
        }
        return map;
    }

}

2019-10-16 0 评论 267 浏览
阅读全文
Spring AOP 之Jdk动态代理剖析 置顶! 有更新!

代理模式

一说到代理,很多人都会立马想到设计模型中的代理模式,通过持有被代理对象并继承被代理对象的类便可以实现代理。假设我们要给ServiceA代理日志功能,就需要声明并实现日志代理类。如果要给ServiceA代理事务功能,就又需要声明并实现事务代理类。这时如何整合日志和事务代理功能就是一个问题了。其次,假设ServiceA当中有100个方法,都需要手工加上100次日志代码。再其次,假设ServiceA、ServiceB、ServiceC都需要代理日志的话,还得针对这三个Service生成不同的代理类。
我们可以看到静态代理的局限性:

  • 难以整合不同的代理类去代理同一个对象。
  • 难以在类内部的各个方法复用代理逻辑。
  • 难以在不同的类之间复用代理逻辑。
  • 需要大量的代理类才能满足我们的庞大的业务需求。

而动态代理是如何灵活的解决这些问题的呢?

JDK动态代理

首先我们先使用动态代理最基础的用法,不涉及Spring框架的最原始方法。

先声明一个HelloService接口:

public interface HelloService {
    void hello();
}

HelloService的实现类:

public class HelloServiceImpl implements HelloService {
    @Override
    public void hello() {
        System.out.println("hello!");
    }
}

2019-10-11 0 评论 326 浏览
阅读全文

Cglib动态代理

在之前的文章中我们介绍了JDK动态代理的解析,今天我们来剖析一下Cglib的动态代理解析。

Cglib代理例子

按照惯例我们先用一个简单的例子来说明

HelloService被代理类:

public class HelloService {
    public void hello() {
        System.out.println("hello!");
    }
}

事务增强类MethodInterceptor:

public class TransactionIntercepter implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("开启事务!");
        Object object = methodProxy.invokeSuper(o, objects);
        System.out.println("结束事务!");
        return object;
    }
    
}

2019-10-15 0 评论 94 浏览
阅读全文
Spring AOP详解 置顶! 有更新!

情景案例

小明辛苦忙了一整年终于完成了包含300个接口的业务系统项目。项目圆满上线并稳定运行了一段时间了。突然有一天总监说,对于会造成数据变化的所有接口,我们必须记录用户的操作日志。然后小明就吭哧吭哧给其中150个接口,挨个加上日志代码,累得真够呛。

过了一阵子总监又说,所有变化很少的数据全部都加上缓存,缓存涉及到刷新缓存、获取缓存、删除缓存的问题。于是乎,小明就又吭哧吭哧地给其中的100个接口加上缓存相关的代码。

又过了一阵子总监说,所有涉及充值退款费用相关的接口,需要生成发票单存入数据库。这时候小明又需要吭哧吭哧给涉及到的50个接口,挨个加上发票存储操作。

小明天天加班也没在工期内完成任务,并且原本的业务代码已经变得臃肿不堪了。

原本的代码:

/**
 * 业务方法
 */
public static void method() {

    // 业务操作
    doBusiness();

}

经过硬编码添加各种非业务性代码后的业务代码:

/**
 * 业务方法
 */
public static void method() {
    // 日志操作
    doLog();
    // 业务操作
    doBusiness();
    // 缓存操作
    doLog();
    // 发票操作
    doReceipt();

}

读者应该能明显感受到在没有AOP代理的情况下的缺点

  1. 业务代码和非业务代码混杂在一起,原本清晰的业务流程淹没在与业务不相关的代码中。
  2. 增加非业务性的功能时,都需要手工硬编码去实现,费时费力。
  3. 代码变得不好维护,一是代码耦合度高,二是需要通过硬编码的方式去拓展或者修改功能。

2019-10-07 0 评论 452 浏览
阅读全文

很多文章在谈论到BIO、NIO、AIO的时候仅仅是抛出一堆定义,以及一些生动的例子。看似很好理解。但是并没有将最基础的本质原理显现出来,如果没有没有从IO的原理出发的话是很难理解这三者之间的区别的。所以本篇文章从Java是如何进行IO操作为开头进行分析。

Java中的IO原理

首先Java中的IO都是依赖操作系统内核进行的,我们程序中的IO读写其实调用的是操作系统内核中的read&write两大系统调用。

那内核是如何进行IO交互的呢?

  1. 网卡收到经过网线传来的网络数据,并将网络数据写到内存中。
  2. 当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
  3. 将内存中的网络数据写入到对应socket的接收缓冲区中。
  4. 当接收缓冲区的数据写好之后,应用程序开始进行数据处理。

对应抽象到java的socket代码简单示例如下:

public class SocketServer {
  public static void main(String[] args) throws Exception {
    // 监听指定的端口
    int port = 8080;
    ServerSocket server = new ServerSocket(port);
    // server将一直等待连接的到来
    Socket socket = server.accept();
    // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
    InputStream inputStream = socket.getInputStream();
    byte[] bytes = new byte[1024];
    int len;
    while ((len = inputStream.read(bytes)) != -1) {
      //获取数据进行处理
      String message = new String(bytes, 0, len,"UTF-8");
    }
    // socket、server,流关闭操作,省略不表
  }
}

2019-10-04 0 评论 474 浏览
阅读全文

主从复制解决的问题

  • 数据分布:通过复制将数据分布到不同地理位置
  • 负载均衡:读写分离以及将读负载到多台从库
  • 备份:可作为实时备份
  • 高可用性:利用主主复制实现高可用

复制原理

复制的原理其实很简单,仅分为以下三步:

  1. 在主库上把数据更改记录到二进制日志binary log中,具体是在每次准备提交事务完成数据更新前,主库将数据更新的事件记录到二进制日志中去,Mysql会按照事务提交的顺序来记录二进制日志的。日志记录好之后,主库通知存储引擎提交事务。

  2. 从库会启动一个IO线程,该线程会连接到主库。而主库上的binlog dump线程会去读取主库本地的binlog日志文件中的更新事件。发往从库,从库接收到日志之后会将其记录到本地的中继日志relay-log当中。

  3. 从库中的SQL线程读取中继日志relay-log中的事件,将其重放到从库中。(在5.6版本之前SQL线程是单线程的,使得主从之间延迟更大)

两种复制方式

日志文件中记录的到底是什么呢?
mysql支持了两种日志格式,这两种日志格式也体现了各自的复制方式

2019-09-30 0 评论 1,637 浏览
阅读全文

Hutool简介

Hutool是一款小而全的开源开发工具类库,在github上拥有将近一万九的star,基本上你能想到的开发当中常需要用到的小轮子,基本上都有具备。学习一下hutool工具包可以避免我们在平常开发中重复造轮子。这款hutool开源库,更新频率快,jar包小仅1.5Mb。对比其他同款类型的基础工具类库来说,是一大优势。因为其他同款类型的基础工具类库多多少少都有基于apache commons做了一些封装。

对于很多小型公司来说,公司内部并没有完善的基础工具类库,使用hutool可以节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务同时可以最大限度的避免封装不完善带来的bug。

hutool github地址

Hutool主要组件

模块介绍
hutool-aopJDK动态代理封装,提供非IOC下的切面支持
hutool-bloomFilter布隆过滤,提供一些Hash算法的布隆过滤
hutool-cache简单缓存实现
hutool-core核心,包括Bean操作、日期、各种Util等
hutool-cron定时任务模块,提供类Crontab表达式的定时任务
hutool-crypto加密解密模块,提供对称、非对称和摘要算法封装
hutool-dbJDBC封装后的数据操作,基于ActiveRecord思想
hutool-dfa基于DFA模型的多关键字查找
hutool-extra扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等)
hutool-http基于HttpUrlConnection的Http客户端封装
hutool-log自动识别日志实现的日志门面
hutool-script脚本执行封装,例如Javascript
hutool-setting功能更强大的Setting配置文件和Properties封装
hutool-system系统参数调用封装(JVM信息等)
hutool-jsonJSON实现
hutool-captcha图片验证码实现
hutool-poi针对POI中Excel的封装
hutool-socket基于Java的NIO和AIO的Socket封装

hutool库的工具类非常全,一篇文章难以概括。以下就列举开发中常用到的工具类。

2020-04-02 0 评论 9 浏览
阅读全文

求二叉树中节点的最大距离

情况A: 路径经过左子树的最深节点,通过根节点,再到右子树的最深节点。
方案:计算两个节点到根节点的深度相加。
情况B: 路径不穿过根节点,而是左子树或右子树的最大距离路径,取其大者。
方案:计算两个节点到子树根节点的深度相加

fibonacci数列的动态规划算法?

利用空间换时间。使用临时变量保存之前计算好的值。

分配饼干问题?每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。Input:[1,2], [1,2,3]。Output: 2

贪心算法。对饼干、孩子满足度进行排序。然后先用小饼干给满足度低的孩子。不造成浪费。

如何计算二叉树的深度

public static int treeDepth(TreeNode root) {
    if (root == null) {
        return 0;
     }
    // 计算左子树的深度
    int left = treeDepth(root.left);
    // 计算右子树的深度
    int right = treeDepth(root.right);
    // 树root的深度=路径最长的子树深度 + 1
    return left >= right ? (left + 1) : (right + 1);
 }

如何打印二叉树每层的节点?

  • 借助一个队列,先把根节点入队,每打印一个节点的值时,也就是打印队列头的节点时,
  • 都会把它的的左右孩子入队,并且把该节点出队。直到队列为空。

二叉树的Z型遍历?

借助一个队列和一个栈,方法和打印层遍历类似,区别在于隔层利用栈来逆序遍历。

反转单链表?

  1. 可以利用栈逆序拼接。
  2. 利用三个指针引用,一次性遍历反转,在每次反转两个节点的时候,都需要保存下一个节点,以免丢失原链表。

随机链表的复制?

复制成 1->1'->2->2'->3->3' 的链表,然后再拆分出来 1'->2'->3'

2020-03-29 0 评论 14 浏览
阅读全文

★redis的主从复制怎么做的?

Redis主从复制可以根据是否是全量分为全量同步和增量同步。以下对其相应的同步过程及原理做下简要说明。
增量同步

Redis增量同步主要指Slave完成初始化后开始正常工作时,Master发生的写操作同步到Slave的过程。通常情况下,
Master每执行一个写命令就会向Slave发送相同的写命令,然后Slave接收并执行。
全量同步

Redis的全量同步过程主要分三个阶段:

同步快照阶段:Master创建并发送快照给Slave,Slave载入并解析快照。Master同时将此阶段所产生的新的写命令存储到缓冲区。
同步写缓冲阶段:Master向Slave同步存储在缓冲区的写操作命令。
同步增量阶段:Master向Slave同步写操作命令。

★redis为什么读写速率快性能好?

纯内存操作。
核心是基于非阻塞的 IO 多路复用机制。
C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。

redis为什么是单线程的?

  • 我们要去理解为什么多线程好,多线程好在IO慢的时候才能凸显出高性能,因为在一个线程等待IO的时候马上切换线程运行。
  • 而在Redis中数据都在内存当中,IO非常快,如果还是采用多线程的话,反而会浪费时间在线程的上下文切换。

缓存的优点?

  1. 减少了对数据库的读操作,数据库的压力降低
  2. 加快了响应速度

2020-03-12 5 评论 43 浏览
阅读全文