开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

领域驱动设计:从理论到实践,一文带你掌握DDD! 前言 走进

发表于 2021-11-25

往期精选(欢迎转发~~)

  • Java全套学习资料(14W字),耗时半年整理
  • 2年经验总结,告诉你如何做好项目管理
  • 消息队列:从选型到原理,一文带你全部掌握
  • 我肝了三个月,为你写出了GO核心手册
  • RPC框架:从原理到选型,一文带你搞懂RPC
  • 如何看待程序员35岁职业危机?
  • 更多…

从0到1,从理论到实践,全面讲解DDD,需要学习DDD的同学,欢迎来戳~~

前言

学习DDD一个半月,最开始学习DDD的原因是因为我负责的业务线,涉及的系统非常多,想借鉴领域驱动设计的思想,看后续如何对系统进行重构。在没有学习DDD之前,感觉DDD可能属于那种“虚头巴脑”的东西,学完DDD之后,感觉。。。嗯。。。真香!

有了学习的动力,但是没有实际参与具体的项目,怎么办?那就去广泛涉猎相关的学习资料,刚好公司内部也进行DDD系列课程的培训,就赶紧报名,然后再通过网上的一些课程和博客资源,将DDD的基础知识系统化。

有了理论基础,没有实践,感觉还是很虚,套用马克思说过的一句话“实践是检验真理的唯一标准”,所以我想倒腾个DDD的Demo出来,刚好公司内部有DDD脚手架,更庆幸的是,我还找到一个应用到DDD的项目,再结合一个博主写的DDD Demo,就把这个Demo,结合实际的项目,通过DDD脚手架重构了一版,经过一个多星期的努力,我的DDD Demo就诞生了。经过这一番折腾,如果公司内部有项目需要按照DDD重构,我想我也可以!

为了证明该文章没有注水,下面列一下我的学习资料:

  • 小米内部DDD系列分享
  • 小米内部DDD脚手架
  • 小米内部授权认证项目(应用DDD)
  • 极客时间欧创新的《DDD 实战课》
  • 掘金“柏炎”的DDD系列文档和DDD Demo
  • 美团技术团队、阿里云开发社区、网上博客等

DDD Demo代码已经上传到GitHub中,大家可以自取:github.com/lml20070115…

1
bash复制代码git clone git@github.com:lml200701158/ddd-framework.git

走进DDD

为什么要用DDD

  • 面向对象设计,数据行为绑定,告别贫血模型
  • 降低复杂度,分而治之
  • 优先考虑领域模型,而不是切割数据和行为
  • 准确传达业务规则,业务优先
  • 代码即设计
  • 它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现业务和技术统一的架构演进
  • 领域知识共享,提升协助效率
  • 增加可维护性和可读性,延长软件生命周期
  • 中台化的基石

DDD作用

说到DDD,绕不开MVC,在MVC三层架构中,我们进行功能开发的之前,拿到需求,解读需求。往往最先做的一步就是先设计表结构,在逐层设计上层dao,service,controller。对于产品或者用户的需求都做了一层自我理解的转化。

用户需求在被提出之后经过这么多层的转化后,特别是研发需求在数据库结构这一层转化后,将业务以主观臆断行为进行了转化。一旦业务边界划分模糊,考虑不全,大量的逻辑补充堆积到了代码层实现,变得越来越难维护。

  • 消除信息不对称
  • 常规MVC三层架构中自底向上的设计方式做一个反转,以业务为主导,自顶向下的进行业务领域划分
  • 将大的业务需求进行拆分,分而治之

举个栗子:

说到这里大家可能还是有点模糊DDD与常见的mvc架构的区别。这里以电商订单场景为例。假如我们现在要做一个电商订单下单的需求。涉及到用户选定商品,下订单,支付订单,对用户下单时的订单发货。

MVC架构里面,我们常见的做法是在分析好业务需求之后,就开始设计表结构了,订单表,支付表,商品表等等。然后编写业务逻辑。这是第一个版本的需求,功能迭代饿了,订单支付后我可以取消,下单的商品我们退换货,是不是又需要进行加表,紧跟着对于的实现逻辑也进行修改。功能不断迭代,代码就不断的层层往上叠。

DDD架构里面,我们先进行划分业务边界。这里面核心是订单。那么订单就是这个业务领域里面的聚合逻辑体现。支付,商品信息,地址等等都是围绕着订单实体。订单本身的属性决定之后,类似于地址只是一个属性的体现。当你将订单的领域模型构建好之后,后续的逻辑边界与仓储设计也就随之而来了。

DDD基础概念

学习DDD前,有很多基础概念需要掌握:领域、子域、核心域、通用域、支撑域、实体、值对象、聚合、聚合根、通用语言、限界上下文、事件风暴、领域事件、领域服务、应用服务、工厂、资源库。

这幅图总结的很全,他把DDD划分不同的层级,最里层是值、属性、唯一标识等,这个是最基本的数据单位,但不能直接使用。然后是实体,这个把基础的数据进行封装,可以直接使用,在代码中就是封装好的一个个实体对象。之后就是领域层,它按照业务划分为不同的领域,比如订单领域、商品领域、支付领域等。最后是应用服务,它对业务逻辑进行编排,也可以理解为业务层。

领域和子域

在研究和解决业务问题时,DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。

领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。

领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小服务需要解决的问题域,构建合适的领域模型。

举个例子:

保险领域,我们可以把保险细分为承保、收付、再保以及理赔等子域,而承保子域还可以继续细分为投保、保全(寿险)、批改(财险)等子子域。

核心域、通用域和支撑域

子域可以根据重要程度和功能属性划分为如下:

  • 核心域:决定产品和公司核心竞争力的子域,它是业务成功的主要因素和公司的核心竞争力。
  • 通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能的子域。
  • 支撑域:但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域。

核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

很多公司的业务,表面看上去相似,但商业模式和战略方向是存在很大差异的,因此公司的关注点会不一样,在划分核心域、通用域和支撑域时,其结果也会出现非常大的差异。

比如同样都是电商平台的淘宝、天猫、京东和苏宁易购,他们的商业模式是不同的。淘宝是 C2C 网站,个人卖家对个人买家,而天猫、京东和苏宁易购则是 B2C 网站,是公司卖家对个人买家。即便是苏宁易购与京东都是 B2C 的模式,苏宁易购是典型的传统线下卖场转型成为电商,京东则是直营加部分平台模式。因此,在公司建立领域模型时,我们就要结合公司战略重点和商业模式,重点关注核心域。

通用语言和限界上下文

  • 通用语言:就是能够简单、清晰、准确描述业务涵义和规则的语言。
  • 限界上下文:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

通用语言

通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。那么,通用语言的价值也就很明了,它可以解决交流障碍这个问题,使领域专家和开发人员能够协同合作,从而确保业务需求的正确表达。

这个通用语言到场景落地,大家可能还很模糊,其实就是把领域对象、属性、代码模型对象等,通过代码和文字建立映射关系,可以通过Excel记录这个关系,这样研发可以通过代码知道这个含义,产品或者业务方可以通过文字知道这个含义,沟通起来就不会有歧义,说的简单一点,其实就是统一产品和研发的话术。

直接看下面这幅图(来源于极客时间欧创新的DDD实战课):

限界上下文

通用语言也有它的上下文环境,为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。

限界上下文是一个显式的语义和语境上的边界,领域模型便存在于边界之内。边界内,通用语言中的所有术语和词组都有特定的含义。把限界上下文拆解开看,限界就是领域的边界,而上下文则是语义环境。通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流。

实体和值对象

  • 实体 = 唯一身份标识 + 可变性【状态 + 行为】
  • 值对象 = 将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。

实体

DDD中要求实体是唯一的且可持续变化的。意思是说在实体的生命周期内,无论其如何变化,其仍旧是同一个实体。唯一性由唯一的身份标识来决定的。可变性也正反映了实体本身的状态和行为。

实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。

值对象

当你只关心某个对象的属性时,该对象便可作为一个值对象。 我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。

还是举个订单的例子,订单是一个实体,里面包含地址,这个地址可以只通过属性嵌入的方式形成的订单实体对象,也可以将地址通过json序列化一个string类型的数据,存到DB的一个字段中,那么这个Json串就是一个值对象,是不是很好理解?下面给个简单的图(同样是源于极客时间欧创新的DDD实战课):

聚合和聚合根

聚合

聚合:我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。

聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的服务很自然就是“高内聚、低耦合”的。

聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。

聚合根

如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

  • 首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
  • 其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
  • 最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

上面讲的还是有些抽象,下面看一个图就能很好理解(同样是源于极客时间欧创新的DDD实战课):

简单概括一下:

  • 通过事件风暴(我理解就是头脑风暴,不过我们一般都是先通过个人理解,然后再和相关核心同学进行沟通),得到实体和值对象;
  • 将这些实体和值对象聚合为“投保聚合”和“客户聚合”,其中“投保单”和“客户”是两者的聚合根;
  • 找出与聚合根“投保单”和“客户”关联的所有紧密依赖的实体和值对象;
  • 在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。

领域服务和应用服务

领域服务

当一些逻辑不属于某个实体时,可以把这些逻辑单独拿出来放到领域服务中,理想的情况是没有领域服务,如果领域服务使用不恰当,慢慢又演化回了以前逻辑都在service层的局面。

可以使用领域服务的情况:

  • 执行一个显著的业务操作
  • 对领域对象进行转换
  • 以多个领域对象作为输入参数进行计算,结果产生一个值对象

应用服务

应用层作为展现层与领域层的桥梁,是用来表达用例和用户故事的主要手段。

应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。

应用层相对来说是较“薄”的一层,除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。

领域事件

领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。

领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联,下面简单说明领域事件:

  • 事件发布:构建一个事件,需要唯一标识,然后发布;
  • 事件存储:发布事件前需要存储,因为接收后的事建也会存储,可用于重试或对账等;
  • 事件分发:服务内直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等;
  • 事件处理:先将事件存储,然后再处理。

比如下订单后,给用户增长积分与赠送优惠券的需求。如果使用瀑布流的方式写代码。一个个逻辑调用,那么不同用户,赠送的东西不同,逻辑就会变得又臭又长。这里的比较好的方式是,用户下订单成功后,发布领域事件,积分聚合与优惠券聚合监听订单发布的领域事件进行处理。

资源库【仓储】

仓储介于领域模型和数据模型之间,主要用于聚合的持久化和检索。它隔离了领域模型和数据模型,以便我们关注于领域模型而不需要考虑如何进行持久化。

我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。当日后需要再次使用这个领域对象时,根据 key 值到数据库查找到这条记录,然后将其恢复成领域对象,应用程序就可以继续使用它了,这就是领域对象持久化存储的设计思想。

DDD分层

DDD分层架构

严格分层架构:某层只能与直接位于的下层发生耦合。

松散分层架构:允许上层与任意下层发生耦合。

在领域驱动设计(DDD)中采用的是松散分层架构,层间关系不那么严格。每层都可能使用它下面所有层的服务,而不仅仅是下一层的服务。每层都可能是半透明的,这意味着有些服务只对上一层可见,而有些服务对上面的所有层都可见。

分层的作用,从上往下:

  • 用户交互层:web请求,rpc请求,mq消息等外部输入均被视为外部输入的请求,可能修改到内部的业务数据。
  • 业务应用层:与MVC中的service不同的不是,service中存储着大量业务逻辑。但在应用服务的实现中(以功能点为维度),它负责编排、转发、校验等。(在设计和开发时,不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦,时间一长你的服务就会演化为传统的三层架构,业务逻辑会变得混乱。)
  • 领域层:或称为模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手。
  • 基础设施层:主要有2方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现。

应用服务层直接调用基础设施层的一条线,这条线是什么意思呢?领域模型的建立是为了控制对于数据的增删改的业务边界,至于数据查询,不同的报表,不同的页面需要展示的数据聚合不具备强业务领域,因此常见的会使用CQRS方式进行查询逻辑的处理。其它的直接调用,原理类同。

各层数据转换

每一层都有自己特定的数据,可以做如下区分:

  • VO(View Object):视图对象,主要对应界面显示的数据对象。对于一个WEB页面,或者SWT、SWING的一个界面,用一个VO对象对应整个界面的值。
  • DTO(Data Transfer Object):数据传输对象,主要用于远程调用等需要大量传输对象的地方。比如我们一张表有100个字段,那么对应的PO就有100个属性。但是我们界面上只要显示10个字段,客户端用WEB service来获取数据,没有必要把整个PO对象传递到客户端,这时我们就可以用只有这10个属性的DTO来传递结果到客户端,这样也不会暴露服务端表结构.到达客户端以后,如果用这个对象来对应界面显示,那此时它的身份就转为VO。在这里,我泛指用于展示层与服务层之间的数据传输对象。
  • DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
  • PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。最形象的理解就是一个PO就是数据库中的一条记录,好处是可以把一条记录作为一个对象处理,可以方便的转为其它对象。

各个O的区别和具体使用场景,有些O是否一定需要,可以参考文章《【领域驱动系列2】浅析VO、DTO、DO、PO》

战略设计&战术设计

这篇文章有2个重要的概念一直没有提,分别为“战略设计”和“战术设计”。

战略设计

战略设计从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。

因为我给的Demo非常简单,所以就直接跳过了战略设计这个流程,但是实际的项目中,“战略设计”需要比较资深的工程师去掌控。

战略设计主要流程包括:建立统一语言、领域分解、领域建模

战略设计的工具包括:事件风暴、用例分析、四色建模、领域故事讲述,其中“事件风暴”是我们最常用的战略设计工具。

战术设计

战术设计从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。在我们的Demo中,就可以看到很多“战术设计”的影子。

因为文章篇幅原因,战略设计和战术设计就不继续展开,需要学习这块内容的同学,网上资料和相关书籍也很多,当然也可以私我哈。

是不是感觉这块内容比较抽象?直接对着Demo学习吧,很多东西你就会豁然开朗。

DDD实战

项目介绍

  • 主要是围绕用户、角色和两者的关系,构建权限分配领域模型。
  • 采用DDD 4层架构,包括用户接口层、应用层、领域层和基础服务层。
  • 数据通过VO、DTO、DO、PO转换,进行分层隔离。
  • 采用SpringBoot + MyBatis Plus框架,存储用MySQL。

工程目录

项目划分为用户接口层、应用层、领域层和基础服务层,每一层的代码结构都非常清晰,包括每一层VO、DTO、DO、PO的数据定义。对于每一层的公共代码,比如常量、接口等,都抽离到ddd-common中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
less复制代码./ddd-application  // 应用层
├── pom.xml
└── src
└── main
└── java
└── com
└── ddd
└── applicaiton
├── converter
│ └── UserApplicationConverter.java // 类型转换器
└── impl
└── AuthrizeApplicationServiceImpl.java // 业务逻辑
./ddd-common
├── ddd-common // 通用类库
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── ddd
│ └── common
│ ├── exception // 异常
│ │ ├── ServiceException.java
│ │ └── ValidationException.java
│ ├── result // 返回结果集
│ │ ├── BaseResult.javar
│ │ ├── Page.java
│ │ ├── PageResult.java
│ │ └── Result.java
│ └── util // 通用工具
│ ├── GsonUtil.java
│ └── ValidationUtil.java
├── ddd-common-application // 业务层通用模块
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── ddd
│ └── applicaiton
│ ├── dto // DTO
│ │ ├── RoleInfoDTO.java
│ │ └── UserRoleDTO.java
│ └── servic // 业务接口
│ └── AuthrizeApplicationService.java
├── ddd-common-domain
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── ddd
│ └── domain
│ ├── event // 领域事件
│ │ ├── BaseDomainEvent.java
│ │ └── DomainEventPublisher.java
│ └── service // 领域接口
│ └── AuthorizeDomainService.java
└── ddd-common-infra
├── pom.xml
└── src
└── main
└── java
└── com
└── ddd
└── infra
├── domain // DO
│ └── AuthorizeDO.java
├── dto
│ ├── AddressDTO.java
│ ├── RoleDTO.java
│ ├── UnitDTO.java
│ └── UserRoleDTO.java
└── repository
├── UserRepository.java // 领域仓库
└── mybatis
└── entity // PO
├── BaseUuidEntity.java
├── RolePO.java
├── UserPO.java
└── UserRolePO.java
./ddd-domian // 领域层
├── pom.xml
└── src
└── main
└── java
└── com
└── ddd
└── domain
├── event // 领域事件
│ ├── DomainEventPublisherImpl.java
│ ├── UserCreateEvent.java
│ ├── UserDeleteEvent.java
│ └── UserUpdateEvent.java
└── impl // 领域逻辑
└── AuthorizeDomainServiceImpl.java
./ddd-infra // 基础服务层
├── pom.xml
└── src
└── main
└── java
└── com
└── ddd
└── infra
├── config
│ └── InfraCoreConfig.java // 扫描Mapper文件
└── repository
├── converter
│ └── UserConverter.java // 类型转换器
├── impl
│ └── UserRepositoryImpl.java
└── mapper
├── RoleMapper.java
├── UserMapper.java
└── UserRoleMapper.java
./ddd-interface
├── ddd-api // 用户接口层
│ ├── pom.xml
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── ddd
│ │ └── api
│ │ ├── DDDFrameworkApiApplication.java // 启动入口
│ │ ├── converter
│ │ │ └── AuthorizeConverter.java // 类型转换器
│ │ ├── model
│ │ │ ├── req // 入参 req
│ │ │ │ ├── AuthorizeCreateReq.java
│ │ │ │ └── AuthorizeUpdateReq.java
│ │ │ └── vo // 输出 VO
│ │ │ └── UserAuthorizeVO.java
│ │ └── web // API
│ │ └── AuthorizeController.java
│ └── resources // 系统配置
│ ├── application.yml
│ └── resources // Sql文件
│ └── init.sql
└── ddd-task
└── pom.xml
./pom.xml

实战讲解

数据库

包括3张表,分别为用户、角色和用户角色表,一个用户可以拥有多个角色,一个角色可以分配给多个用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
SQL复制代码create table t_user
(
id bigint auto_increment comment '主键' primary key,
user_name varchar(64) null comment '用户名',
password varchar(255) null comment '密码',
real_name varchar(64) null comment '真实姓名',
phone bigint null comment '手机号',
province varchar(64) null comment '用户名',
city varchar(64) null comment '用户名',
county varchar(64) null comment '用户名',
unit_id bigint null comment '单位id',
unit_name varchar(64) null comment '单位名称',
gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间',
gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间',
deleted bigint default 0 not null comment '是否删除,非0为已删除'
)comment '用户表' collate = utf8_bin;

create table t_role
(
id bigint auto_increment comment '主键' primary key,
name varchar(256) not null comment '名称',
code varchar(64) null comment '角色code',
gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间',
gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间',
deleted bigint default 0 not null comment '是否已删除'
)comment '角色表' charset = utf8;

create table t_user_role (
id bigint auto_increment comment '主键id' primary key,
user_id bigint not null comment '用户id',
role_id bigint not null comment '角色id',
gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间',
gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改时间',
deleted bigint default 0 not null comment '是否已删除'
)comment '用户角色关联表' charset = utf8;

基础服务层

仓储(资源库)介于领域模型和数据模型之间,主要用于聚合的持久化和检索。它隔离了领域模型和数据模型,以便我们关注于领域模型而不需要考虑如何进行持久化。

比如保存用户,需要将用户和角色一起保存,也就是创建用户的同时,需要新建用户的角色权限,这个可以直接全部放到仓储中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public AuthorizeDO save(AuthorizeDO user) {
UserPO userPo = userConverter.toUserPo(user);
if(Objects.isNull(user.getUserId())){
userMapper.insert(userPo);
user.setUserId(userPo.getId());
} else {
userMapper.updateById(userPo);
userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery()
.eq(UserRolePO::getUserId, user.getUserId()));
}
List<UserRolePO> userRolePos = userConverter.toUserRolePo(user);
userRolePos.forEach(userRoleMapper::insert);
return this.query(user.getUserId());
}

仓储对外暴露的接口如下:

1
2
3
4
5
6
7
8
9
java复制代码// 用户领域仓储
public interface UserRepository {
// 删除
void delete(Long userId);
// 查询
AuthorizeDO query(Long userId);
// 保存
AuthorizeDO save(AuthorizeDO user);
}

基础服务层不仅仅包括资源库,与第三方的调用,都需要放到该层,Demo中没有该示例,我们可以看一个小米内部具体的实际项目,他把第三方的调用放到了remote目录中:

领域层

聚合&聚合根

我们有用户和角色两个实体,可以将用户、角色和两者关系进行聚合,然后用户就是聚合根,聚合之后的属性,我们称之为“权限”。

对于地址Address,目前是作为字段属性存储到DB中,如果对地址无需进行检索,可以把地址作为“值对象”进行存储,即把地址序列化为Json存,存储到DB的一个字段中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class AuthorizeDO {
// 用户ID
private Long userId;
// 用户名
private String userName;
// 真实姓名
private String realName;
// 手机号
private String phone;
// 密码
private String password;
// 用户单位
private UnitDTO unit;
// 用户地址
private AddressDTO address;
// 用户角色
private List<RoleDTO> roles;
}

领域服务

Demo中的领域服务比较薄,通过单位ID后去获取单位名称,构建单位信息:

1
2
3
4
5
6
7
8
9
java复制代码@Service
public class AuthorizeDomainServiceImpl implements AuthorizeDomainService {
@Override
// 设置单位信息
public void associatedUnit(AuthorizeDO authorizeDO) {
String unitName = "武汉小米";// TODO: 通过第三方获取
authorizeDO.getUnit().setUnitName(unitName);
}
}

我们其实可以把领域服务再进一步抽象,可以抽象出领域能力,通过这些领域能力去构建应用层逻辑,比如账号相关的领域能力可以包括授权领域能力、身份认证领域能力等,这样每个领域能力相对独立,就不会全部揉到一个文件中,下面是实际项目的领域层截图:

领域事件

领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。

这个Demo中,对领域事件的处理非常简单,还是一个应用内部的领域事件,就是每次执行一次具体的操作时,把行为记录下来。Demo中没有记录事件的库表,事件的分发还是同步的方式,所以Demo中的领域事件还不完善,后面我会再继续完善Demo中的领域事件,通过Java消息机制实现解耦,甚至可以借助消息队列,实现异步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码/**
* 领域事件基类
*
* @author louzai
* @since 2021/11/22
*/
@Getter
@Setter
@NoArgsConstructor
public abstract class BaseDomainEvent<T> implements Serializable {
private static final long serialVersionUID = 1465328245048581896L;
/**
* 发生时间
*/
private LocalDateTime occurredOn;
/**
* 领域事件数据
*/
private T data;
public BaseDomainEvent(T data) {
this.data = data;
this.occurredOn = LocalDateTime.now();
}
}

/**
* 用户新增领域事件
*
* @author louzai
* @since 2021/11/20
*/
public class UserCreateEvent extends BaseDomainEvent<AuthorizeDO> {
public UserCreateEvent(AuthorizeDO user) {
super(user);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码/**
* 领域事件发布实现类
*
* @author louzai
* @since 2021/11/20
*/
@Component
@Slf4j
public class DomainEventPublisherImpl implements DomainEventPublisher {

@Autowired
private ApplicationEventPublisher applicationEventPublisher;

@Override
public void publishEvent(BaseDomainEvent event) {
log.debug("发布事件,event:{}", GsonUtil.gsonToString(event));
applicationEventPublisher.publishEvent(event);
}
}

应用层

应用层就非常好理解了,只负责简单的逻辑编排,比如创建用户授权:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Transactional(rollbackFor = Exception.class)
public void createUserAuthorize(UserRoleDTO userRoleDTO){
// DTO转为DO
AuthorizeDO authorizeDO = userApplicationConverter.toAuthorizeDo(userRoleDTO);
// 关联单位单位信息
authorizeDomainService.associatedUnit(authorizeDO);
// 存储用户
AuthorizeDO saveAuthorizeDO = userRepository.save(authorizeDO);
// 发布用户新建的领域事件
domainEventPublisher.publishEvent(new UserCreateEvent(saveAuthorizeDO));
}

查询用户授权信息:

1
2
3
4
5
6
7
8
9
10
java复制代码@Override
public UserRoleDTO queryUserAuthorize(Long userId) {
// 查询用户授权领域数据
AuthorizeDO authorizeDO = userRepository.query(userId);
if (Objects.isNull(authorizeDO)) {
throw ValidationException.of("UserId is not exist.", null);
}
// DO转DTO
return userApplicationConverter.toAuthorizeDTO(authorizeDO);
}

细心的同学可以发现,我们应用层和领域层,通过DTO和DO进行数据转换。

用户接口层

最后就是提供API接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@GetMapping("/query")
public Result<UserAuthorizeVO> query(@RequestParam("userId") Long userId){
UserRoleDTO userRoleDTO = authrizeApplicationService.queryUserAuthorize(userId);
Result<UserAuthorizeVO> result = new Result<>();
result.setData(authorizeConverter.toVO(userRoleDTO));
result.setCode(BaseResult.CODE_SUCCESS);
return result;
}

@PostMapping("/save")
public Result<Object> create(@RequestBody AuthorizeCreateReq authorizeCreateReq){
authrizeApplicationService.createUserAuthorize(authorizeConverter.toDTO(authorizeCreateReq));
return Result.ok(BaseResult.INSERT_SUCCESS);
}

数据的交互,包括入参、DTO和VO,都需要对数据进行转换。

项目运行

  • 新建库表:通过文件”ddd-interface/ddd-api/src/main/resources/init.sql”新建库表。
  • 修改SQL配置:修改”ddd-interface/ddd-api/src/main/resources/application.yml”的数据库配置。
  • 启动服务:直接启动服务即可。
  • 测试用例:
    • 请求URL:http://127.0.0.1:8087/api/user/save
    • Post body:{“userName”:”louzai”,”realName”:”楼”,”phone”:13123676844,”password”:”***“,”unitId”:2,”province”:”湖北省”,”city”:”鄂州市”,”county”:”葛店开发区”,”roles”:[{“roleId”:2}]}

结语

谈谈我对DDD的理解,我觉得DDD不像一门技术,我理解的技术比如高并发、缓存、消息队列等,DDD更像是一项软技能,一种方法论,包含了很多设计理念。

因为文章篇幅原因,不可能涵盖DDD所有的内容,特别是“战略设计”的部分,基本是一笔带过,因为方法论基本都差不多,具体实操需要经验的积累,但是对于想入门DDD的同学,我觉得这篇文章还在值得大家去学习的。

毕竟接触DDD的时间还不长,所以有些知识点理解的不够深刻,或者有些偏颇,欢迎大家批评指正!

参考文章:

  • 极客时间:time.geekbang.org/column/intr…
  • 一文带你落地DDD:juejin.cn/post/700400…
    领域驱动设计在互联网业务开发中的实践:tech.meituan.com/2017/12/22/…
  • 浅析VO、DTO、DO、PO:developer.aliyun.com/article/269…

欢迎大家多多点赞,更多文章,请关注微信公众号“楼仔进阶之路”,点关注,不迷路~~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python的字符串详解

发表于 2021-11-25

这是我参与11月更文挑战的第21天,活动详情链接查看:2021最后一次更文挑战

之前我们学习过一个不可变的序列叫元组,今天我们探讨的字符串,也是一个不可变序列。

  1. 字符串的创建

一个概念: 字符串的驻留机制
那什么是字符串的驻留机制呢?
意思是: 仅保留一份相同且不可变字符串的方法,不同的值被存放在字符串的驻留池中,python的驻留机制对相同的字符串只保留一份拷贝,后续创建相同字符串时候,不会开辟新的空间,而是把该字符串的地址重新赋值给新建的变量。

1) 字符串的定义

1
2
3
4
5
6
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
a='itlaoxin'
b="itlaoxin"
c='''itlaoxiin'''
print(a,b,c,id(a),id(b),id(c))

输出结果
在这里插入图片描述

可以看到ID都是一样的。

在内存中只有一份

几点注意事项:
在交互模式下,能实现驻留机制的情况:

  • 字符串的长度为0 或者1时
  • 符合标识符的字符串
  • 字符串只在编译时候进行驻留,而非运行时
  • 【-5,256】之间的整数数字
  • 在这里插入图片描述
  1. 字符串的常用操作

关于字符串的操作,我们可以把字符串看成是关于字符的列表:

1) 查询操作

  • index() 查找字符串substr第一次出现的位置,如果查找的子串不存在,抛出异常
  • rindex() 查找字符串substr最后一次出现的问题,如果不存在,则报异常
  • find() 查找子串substr第一次出现的位置,不存在返回-1
  • rfind 查找子串substr最后一次出现的位置,若不存在返回 -1
1
2
3
4
5
6
7
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s='hello,world'
print(s.index('l')) #2
print(s.find('l')) #2
print(s.rfind('l')) #9
print(s.rindex('l')) #9

这里建议大家使用find,或者rfind,因为不会报异常

2) 字符串的常用操作

a) 大小写转换

  • upper() 把字符串中所有的字符都转化成大写字母(会产生新的字符串对象)
1
2
3
4
5
6
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello,ITlaoxin"
a=s.upper()
print(s)
print(a)
  • lower() 把字符串中所有的字符都转换成小写字母
1
2
3
4
5
6
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello,ITlaoxin"
a=s.lower()
print(s)
print(a)

输出结果:
hello,ITlaoxin
hello,itlaoxin

  • swapcase() 把字符串中所有的大写字母转换成小写字母,把所有的小写字母转换成大写字母
1
2
3
4
5
6
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello,ITlaoxin"
a=s.swapcase()
print(a,id(a))
print(s,id(s))
  • capitalize() 把第一个字符转换成大写,把其余的字符转换成小写
  • tilele( )把每个单词的第一个字符转换成大写,把每个读单词的剩余字符转换成小写
1
2
3
4
5
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello,ITlaoxin"
a=s.title()
print(a)

b) 字符串内容对齐操作

  • center() 居中对齐
1
2
3
4
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello,ITlaoxin"
print(s.center(20,'*'))

在这里插入图片描述
一共14个字符,定义20个字符,左右各三个

  • ljust() 左对齐
1
2
3
4
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello,ITlaoxin"
print(s.ljust(20,"*"))

在这里插入图片描述
如果不写* ,默认是空格

  • rjust 右对齐
1
2
3
4
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello,ITlaoxin"
print(s.rjust(20,"*"))

在这里插入图片描述

  • zfill 右对齐
    这种方式会用0填充
1
2
3
4
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello,ITlaoxin"
print(s.zfill(20))

在这里插入图片描述

c) 字符串的拆分

  • split() 分割,从左边开始,默认的分割符是空格,分割完后是列表
1
2
3
4
5
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello,ITlaoxin"
lst=s.split()
print(lst)

输出结果:

1
2
bash复制代码
['hello,ITlaoxin']

我们可以指定分割符,用sep=‘|’ 的形式

1
2
3
4
5
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello|ITlaoxin|gaosh"
lst=s.split(sep='|')
print(lst)

输出结果

1
bash复制代码['hello', 'ITlaoxin', 'gaosh']

如果这个地方我们用默认的空格会是什么结果:

1
2
3
4
5
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello|ITlaoxin|gaosh"
lst=s.split()
print(lst)

结果

1
bash复制代码['hello|ITlaoxin|gaosh']

可以看到,因为这个字符串中没有空格,所以他就是一个元素的列表。

1
2
3
4
5
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello|ITlaoxin|gaosh"
lst=s.split(sep='|',maxsplit=1)
print(lst)

结果:

1
bash复制代码['hello', 'ITlaoxin|gaosh']

这里只拆分了一次。

  • rsplit() 从字符右边开始拆分,默认拆分字符是空格,返回值是一个列表
    maxsplit可以指定最大拆分次数

这个和split的使用方法一样,只是rsplist是从右边开始拆分,splist从左边拆分

1
2
3
4
5
6
7
8
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s="hello|ITlaoxin|gaosh"
lst=s.split(sep='|',maxsplit=1)
print(lst)

lst1=s.rsplit(sep='|',maxsplit=1)
print(lst1)

结果如图所示:
在这里插入图片描述

d) 字符串的判断方法

  • isidentifier() 判断指定的字符串是否是合法的标识符
  • isspace() 判断指定的字符串是否全部由空白字符组成(回车,换行,水平指制表符)
  • issalpha() 判断字符串是否全部由字母组成
  • isdecimal( )判断指定字符串是否全部是十进制组成
  • isnumeric() 判断指定的字符串全部由数字组成
  • isalnum()判断指定字符串是否全部由字母和数字组成
1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s='hello,world,python'
print('1',s.isidentifier())
print('2','hello'.isidentifier())
print('3','\t'.isidentifier())
print('4','abc'.isspace())
print('5','abc'.isalpha())
print('6','1'.isspace())
print('7','123'.isnumeric())
print('8','abc123'.isalnum())
print('9','123abc!'.isalnum())

e) 字符串的其他操作

  • 字符串的替换replace()
1
2
3
4
5
6
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
s='hello,world,python'
print(s.replace('python','itlaoxin'))
s1='hello,python,python ,python'
print(s1.replace('python','itlaoxin',2))

结果:

1
2
bash复制代码hello,world,itlaoxin
hello,itlaoxin,itlaoxin ,python
  • 字符串的合并 join()
1
2
3
4
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
lst=['hello','java','python']
print('|'.join(lst))

结果:hello|java|python

f) 字符串的比较

使用运算符 >,>= ,<,<= ,= ,!=

1
2
3
4
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
print('1','itlaoxin'>'laoxin')
print('2','itlaoxin'>'itlaox')
1
2
3
bash复制代码结果:
1 False
2 True

如果第一个字母就不相同,就比较原始值 ord()

1
2
3
4
5
6
7
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
print('1','itlaoxin'>'laoxin')
print('2','itlaoxin'>'itlaox')
print('3','python'>'java')
## 相当于
print(ord('p'),ord('j'))

在这里插入图片描述

第三个相当于112与106比较

g) 字符串的切片

字符串是不可变类型,不具备增删改查的操作,切片是会产生新的对象的

1
2
3
4
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
a='hello,world,itlaoxin'
print(a[:5])

输出结果:
hello

不写起始位置,它会从index0开始切

1
2
3
4
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
a='hello,world,itlaoxin'
print(a[6:]) #world,itlaoxin

没有指定结束位置,会切到最后

step是指定步长

1
2
3
4
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
a='hello,world,itlaoxin'
print(a[1:8:2]) #el,o
1
2
3
4
5
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
a='hello,world,itlaoxin'
print(a[1:8:2])
print(a[::2]) #hlowrdiloi

h) 格式化字符串

为什么要格式化字符串呢?
字符串的拼接会产生新的Id,会造成空间浪费, 这个时候就需要使用字符串的格式化。

格式化字符串的两种方式:
% 做占位符

1
2
3
4
5
6
7
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
#第一种方式%

name='互联网老辛'
age=40
print('我叫%s,今年%d岁了'%(name,age))

{} 做占位符

要使用到format()方法

1
2
3
4
5
6
7
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
#第一种方式%

name='互联网老辛'
age='40'
print('我叫{0},今年{1}岁了'.format(name,age))

除此之外还可以表示精读和宽度:

1
2
3
4
5
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
print('%d'% 99)

print('%10d'% 99)

这里的10表示的就是宽度

精度:
保留3位小数

1
2
3
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
print('%.3f' % 3.11516)

混合使用:

1
2
3
bash复制代码# 作者:互联网老辛
# 开发时间:2021/4/4/0004 6s
print('%10.3f' % 3.11516)

%10.3f
总宽度为10,小数点保留3位

总结

到现在所有的数据类型的基本操作就介绍完了,这些应该都算是基础,接下来我们要进入到函数的章节。

我是互联网老辛,从事技术行业已经12年了,教育行业5年,也算是一个老人了。 今年开始把自己学到的东西分享出来,希望能够帮到大家,如果对你有用,可以点赞收藏。
您的每一次转发,收藏,都是对作者最大的鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

分库分表技术之ShardingJDBC(2)

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

Sharding-JDBC入门使用

搭建基础环境

  • 需求说明

创建数据库lg_order,模拟将订单表进行水平拆分,创建两张表pay_order_1与pay_order_2,这两张表是订单表拆分后的表,我们通过Sharding-Jdbc向订单表插入数据,按照一定的分片规则,主键为偶数的落入pay_order_1表 ,为奇数的落入pay_order_2表,再通过Sharding-Jdbc进行查询

  • 创建数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql复制代码CREATE DATABASE lg_order CHARACTER SET 'utf8';

DROP TABLE IF EXISTS pay_order_1;
CREATE TABLE pay_order_1 (
order_id BIGINT(20) PRIMARY KEY AUTO_INCREMENT ,
user_id INT(11) ,
product_name VARCHAR(128),
COUNT INT(11)
);

DROP TABLE IF EXISTS pay_order_2;
CREATE TABLE pay_order_2 (
order_id BIGINT(20) PRIMARY KEY AUTO_INCREMENT ,
user_id INT(11) ,
product_name VARCHAR(128),
COUNT INT(11)
);
  • 创建SpringBoot项目引入maven依赖

sharding-jdbc以jar包形式提供服务,所以要先引入maven依赖。

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>

分片规则配置(水平分表)

使用sharding-jdbc对数据库中水平拆分的表进行操作,通过sharding-jdbc对分库分表的规则进行配置,配置内容包括:数据源、主键生成策略、分片策略等。

application.properties

  • 基础配置
1
2
3
4
5
6
7
8
properties复制代码spring.application.name = sharding-jdbc-simple
server.servlet.context-path = /sharding-jdbc
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true

spring.main.allow-bean-definition-overriding = true
mybatis.configuration.map-underscore-to-camel-case = true
  • 数据源
1
2
3
4
5
6
7
8
properties复制代码# 定义数据源
spring.shardingsphere.datasource.names = db1

spring.shardingsphere.datasource.db1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://localhost:3306/lg_order?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456
  • 配置数据节点
1
2
properties复制代码#配置数据节点,指定节点的信息
spring.shardingsphere.sharding.tables.pay_order.actual-data-nodes = db1.pay_order_$->{1..2}

表达式db1.pay_order_$->{1..2}

$会被大括号中的{1..2}所替换

会有两种选择:db1.pay_order_1和db1.pay_order_2

  • 配置主键生成策略
1
2
3
properties复制代码#指定pay_order表 (逻辑表)的主键生成策略为 SNOWFLAKE
spring.shardingsphere.sharding.tables.pay_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.pay_order.key-generator.type=SNOWFLAKE

使用shardingJDBC提供的主键生成策略,全局主键

为避免主键重复,生成主键采用SNOWFLAKE分布式ID生成算法

  • 配置分片算法
1
2
3
properties复制代码#指定pay_order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.pay_order.table-strategy.inline.sharding-column = order_id
spring.shardingsphere.sharding.tables.pay_order.table-strategy.inline.algorithm-expression = pay_order_$->{order_id % 2 + 1}

分表策略表达式:pay_order_$-> {order_id % 2 + 1}

{order_id % 2 + 1} 结果是偶数操作pay_order_1表

{order_id % 2 + 1} 结果是奇数操作pay_order_2表

  • 打开SQL日志
1
2
properties复制代码# 打开sql输出日志
spring.shardingsphere.props.sql.show = true
  • 步骤总结
    1. 定义数据源
    2. 指定pay_order表的数据分布情况,分布在pay_order_1和pay_order_2
    3. 指定pay_order表的主键生成策略为SNOWFLAKE,是一种分布式自增算法,保证id全局唯一
    4. 定义pay_order分片策略,order_id为偶数的数据下沉到pay_order_1,为奇数下沉到在pay_order_2

编写程序

  • 新增订单
1
2
3
4
5
6
7
8
9
10
11
java复制代码@Mapper
@Component
public interface PayOrderDao {

/**
* 新增订单
* */
@Insert("insert into pay_order(user_id,product_name,COUNT) values(#{user_id},#{product_name},#{count})")
int insertPayOrder(@Param("user_id") int user_id, @Param("product_name") String product_name, @Param("count") int count);

}
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest(classes = RunBoot.class)
public class PayOrderDaoTest {

@Autowired
PayOrderDao payOrderDao;

@Test
public void testInsertPayOrder(){

for (int i = 1; i < 10; i++) {
//插入数据
payOrderDao.insertPayOrder(1,"小米电视",1);
}
}
}
  • 根据Id查询订单
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* 查询订单
* */
@Select({"<script>" +
"select " +
" * " +
" from pay_order p" +
" where p.order_id in " +
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>" +
" #{id} " +
"</foreach>"+
"</script>"})
List<Map> findOrderByIds(@Param("orderIds") List<Long> orderIds);
  • 测试
1
2
3
4
5
6
7
8
9
10
java复制代码@Test
public void testFindOrderByIds(){

List<Long> ids = new ArrayList<>();
ids.add(517020734275452928L); //order_1表
ids.add(517020734380310529L); //order_2表

List<Map> mapList = payOrderDao.findOrderByIds(ids);
System.out.println(mapList);
}

ShardingJDBC执行流程

当ShardingJDBC接收到发送的SQL之后,会执行下面的步骤,最终返回执行结果

image.png

  1. SQL解析:编写SQL查询的是逻辑表,执行时ShardingJDBC要解析SQL,解析的目的是为了找到需要改写的位置。
  2. SQL路由:SQL的路由是指将对逻辑表的操作,映射到对应的数据节点的过程。ShardingJDBC会获取分片键判断是否正确,正确就执行分片策略(算法)来找到真实的表。
  3. SQL改写:程序员面向的是逻辑表编写SQL,并不能直接在真实的数据库中执行,SQL改写用于将逻辑SQL改为在真实的数据库中可以正确执行的SQL。
  4. SQL执行:通过配置规则pay_order_$->{order_id % 2 + 1},可以知道当order_id为偶数时,应该向 pay_order_1表中插入数据,为奇数时向pay_order_2表插入数据。
  5. 将所有真正执行sql的结果进行汇总合并,然后返回。

Sharding-JDBC分库分表

水平分表

把一张表的数据按照一定规则,分配到同一个数据库的多张表中,每个表只有这个表的部分数据。在Sharding-JDBC入门使用中,我们已经完成了水平分表的操作。

水平分库

水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。接下来看一下如何使用Sharding-JDBC实现水平分库

将原来的lg_order数据库,拆分为lg_order_1和lg_order_2

  1. 分片规则配置

现在是两个数据库,所以要配置两份数据源信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
properties复制代码# 定义多个数据源
spring.shardingsphere.datasource.names = db1,db2

spring.shardingsphere.datasource.db1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://localhost:3306/lg_order_1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456

spring.shardingsphere.datasource.db2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.db2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db2.url = jdbc:mysql://localhost:3306/lg_order_2?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db2.username = root
spring.shardingsphere.datasource.db2.password = 123456

通过配置对数据库的分片策略,来指定数据库进行操作

1
2
3
4
properties复制代码# 分库策略,以user_id为分片键,分片策略为user_id % 2 + 1,user_id为偶数操作db1数据源,否则操作db2。
spring.shardingsphere.sharding.tables.pay_order.database-strategy.inline.sharding-column = user_id

spring.shardingsphere.sharding.tables.pay_order.database-strategy.inline.algorithm-expression = db$->{user_id % 2 + 1}
  1. 分库分表的策略
    • 分库策略,目的是将一个逻辑表,映射到多个数据源
1
2
properties复制代码# 分库找的是数据库 db$->{user_id % 2 + 1}
spring.shardingsphere.sharding.tables.逻辑表名称.database-strategy.分片策略.分片策略属性名 = 分片策略表达式
1
diff复制代码- 分表策略,如何将一个逻辑表,映射为多个实际表
1
2
properties复制代码#分表 找的是具体的表 pay_order_$->{order_id % 2 + 1}
spring.shardingsphere.sharding.tables.逻辑表名称.table-strategy.分片策略.algorithm-expression = 分片策略表达式
  1. Sharding-JDBC支持以下几种分片策略:
    • standard:标准分片策略
    • complex:符合分片策略
    • inline:行表达式分片策略,,使用Groovy的表达式.
    • hint:Hint分片策略,对应HintShardingStrategy。
    • none:不分片策略,对应NoneShardingStrategy。不分片的策略。

具体信息请查阅官方文档:shardingsphere.apache.org

  1. 插入测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Test
public void testInsertPayOrder(){

//user_1 为奇数,插入到 lg_order_1 数据库
for (int i = 0; i < 5; i++) {
//插入数据
payOrderDao.insertPayOrder(1,"海尔电视",1);
}

//user_2 为偶数,插入到 lg_order_2 数据库
for (int i = 0; i < 5; i++) {
//插入数据
payOrderDao.insertPayOrder(4,"王牌电视",1);
}
}

首先会根据分库策略找到对应的数据库db$->{user_id % 2 + 1}

然后再根据分表策略找到要插入数据的表pay_order_$->{order_id % 2 + 1}

  1. 查询测试
1
2
3
4
5
6
7
8
9
10
java复制代码@Test
public void testFindOrderByIds(){

List<Long> ids = new ArrayList<>();
ids.add(517399941648220160L); //lg_order_1数据库的 order_1表
ids.add(517399941518196736L); //lg_order_2数据库的 order_1表

List<Map> mapList = payOrderDao.findOrderByIds(ids);
System.out.println(mapList);
}

通过日志发现,sharding-jdbc将sql路由到了db1

原因在配置上有问题,数据库只指定了db1

  1. 修改数据节点配置
1
2
properties复制代码#数据节点: db1.pay_order_1 , db1.pay_order_2, db2.pay_order_1,db2.pay_order_2
spring.shardingsphere.sharding.tables.pay_order.actual-data-nodes = db$->{1..2}.pay_order_$->{1..2}

垂直分库

垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。

在使用微服务架构时,业务切割得足够独立,数据也会按照业务切分,保证业务数据隔离,大大提升了数据库的吞吐能力。

  1. 创建数据库
1
mysql复制代码CREATE DATABASE lg_user CHARACTER SET 'utf8';
  1. 在lg_user数据库中users创建表
1
2
3
4
5
6
7
mysql复制代码DROP TABLE IF EXISTS users;
CREATE TABLE users (
id BIGINT(20) PRIMARY KEY,
username VARCHAR(20) ,
phone VARCHAR(11),
STATUS VARCHAR(11)
);
  1. 规则配置
  • 配置数据源信息
1
2
3
4
5
6
properties复制代码spring.shardingsphere.datasource.names = db1,db2,db3
spring.shardingsphere.datasource.db3.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.db3.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db3.url = jdbc:mysql://localhost:3306/lg_user?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db3.username = root
spring.shardingsphere.datasource.db3.password = 123456
  • 配置数据节点
1
properties复制代码spring.shardingsphere.sharding.tables.users.actual-data-nodes = db$->{3}.users
1
2
properties复制代码spring.shardingsphere.sharding.tables.users.table-strategy.inline.sharding-column = id
spring.shardingsphere.sharding.tables.users.table-strategy.inline.algorithm-expression = users
  1. 测试插入与查询
  • UserDao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码@Mapper
@Component
public interface UsersDao {

/**
* 新增用户
* */
@Insert("INSERT INTO users(id, username,phone,status) VALUE(#{id},#{username},#{phone},#{status})")
int insertUser(@Param("id")Long id, @Param("username")String username, @Param("phone")String phone,@Param("status")String status);

/**
* 查询用户
* */
@Select({"<script>",
" select",
" * ",
" from users u ",
" where u.id in",
"<foreach collection='userIds' item='id' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
List<Map> selectUserbyIds(@Param("userIds")List<Long> userIds);

}
  • UserDaoTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest(classes = RunBoot.class)
public class UserDaoTest {

@Autowired
UsersDao usersDao;

@Test
public void testInsert(){

for (int i = 0; i < 10 ; i++) {
Long id = i + 100L;
usersDao.insertUser(id,"giao桑"+i,"13511112222", "1");
}
}

@Test
public void testSelect(){

List<Long> ids = new ArrayList<>();
ids.add(101L);
ids.add(105L);

List<Map> list = usersDao.selectUserbyIds(ids);
System.out.println(list);
}
}

Sharding-JDBC 操作公共表

什么是公共表

公共表属于系统中数据量较小,变动少,而且属于高频联合查询的依赖表。参数表、数据字典表等属于此类型。

可以将这类表在每个数据库都保存一份,所有更新操作都同时发送到所有分库执行。接下来看一下如何使用Sharding-JDBC实现公共表的数据维护。

image.png

公共表配置与测试

  1. 创建数据库

分别在lg_order_1、lg_order_2、lg_user都创建district表

1
2
3
4
5
6
mysql复制代码-- 区域表
CREATE TABLE district (
id BIGINT(20) PRIMARY KEY COMMENT '区域ID',
district_name VARCHAR(100) COMMENT '区域名称',
LEVEL INT COMMENT '等级'
);
  1. 在Sharding-JDBC的配置文件中指定公共表
1
2
3
4
5
properties复制代码# 指定district为公共表
spring.shardingsphere.sharding.broadcast-tables=district
# 主键生成策略
spring.shardingsphere.sharding.tables.district.key-generator.column=id
spring.shardingsphere.sharding.tables.district.key-generator.type=SNOWFLAKE
  1. 编写代码,操作公共表
  • DistrictDao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Mapper
@Component
public interface DistrictDao {

/**
* 插入数据
* */
@Insert("INSERT INTO district(district_name,level) VALUES(#{district_name},#{level})")
public void insertDist(@Param("district_name") String district_name,@Param("level") int level);

/**
* 删除数据
*/
@Delete("delete from district where id = #{id}")
int deleteDict(@Param("id") Long id);

}
  • DistrictDaoTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest(classes = RunBoot.class)
public class DistrictDaoTest {

@Autowired
DistrictDao districtDao;

@Test
public void testInsert(){
districtDao.insertDist("昌平区",2);
districtDao.insertDist("朝阳区",2);
}

@Test
public void testDelete(){
districtDao.deleteDict(523944169266216961L);
}
}

Sharding-JDBC读写分离

Sharding-JDBC读写分离则是根据SQL语义的分析,将读操作和写操作分别路由至主库与从库。它提供透明化读写分离,让使用方尽量像使用一个数据库一样使用主从数据库集群。

image.png

MySQL主从同步

为了实现Sharding-JDBC的读写分离,首先,要进行mysql的主从同步配置。

我们直接使用MyCat讲解中,在虚拟机上搭建的主从数据库

  • 在主服务器中的test数据库创建商品表
1
2
3
4
5
6
mysql复制代码CREATE TABLE products (
pid BIGINT(32) PRIMARY KEY ,
pname VARCHAR(50) DEFAULT NULL,
price INT(11) DEFAULT NULL,
flag VARCHAR(2) DEFAULT NULL
);
  • 主库新建表之后,从库会根据binlog日志,同步创建

sharding-jdbc实现读写分离

  1. 配置数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
properties复制代码
# 定义多个数据源
spring.shardingsphere.datasource.names = db1,db2,db3,m1,s1

spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://192.168.52.10:3306/test?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456

spring.shardingsphere.datasource.s1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s1.url = jdbc:mysql://192.168.52.11:3306/test?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.s1.username = root
spring.shardingsphere.datasource.s1.password = 123456
  1. 配置主库与从库的相关信息
  • ms1包含了m1和s1
1
2
properties复制代码spring.shardingsphere.sharding.master-slave-rules.ms1.master-data-source-name=m1
spring.shardingsphere.sharding.master-slave-rules.ms1.slave-data-source-names=s1
  1. 配置数据节点
1
2
properties复制代码#配置数据节点
spring.shardingsphere.sharding.tables.products.actual-data-nodes = ms1.products
  1. 编写测试代码
  • ProductsDao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Mapper
@Component
public interface ProductsDao {

/**
* 读写分离 插入
* */
@Insert("insert into products(pid,pname,price,flag) values(#{pid},#{pname},#{price},#{flag})")
int insertProduct(@Param("pid") Long pid, @Param("pname") String pname,@Param("price") int price,@Param("flag") String flag);

/**
* 读写分离 查询
* */
@Select({"select * from products"})
List<Map> findAll();
}
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest(classes = RunBoot.class)
public class ProductsDaoTest {

@Autowired
ProductsDao productsDao;

/**
* 测试插入
* */
@Test
public void testInsert(){

for (int i = 0; i < 5; i++) {
productsDao.insertProduct(100L+i,"小米手机",1888,"1");
}

}

/**
* 测试查询
* */
@Test
public void testSelect(){

List<Map> all = productsDao.findAll();
System.out.println(all);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

操作系统学习笔记(十三)~读者写者问题+哲学家就餐问题+管程

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

前言

Hello!小伙伴!

非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~

自我介绍 ଘ(੭ˊᵕˋ)੭

昵称:海轰

标签:程序猿|C++选手|学生

简介:因C语言结识编程,随后转入计算机专业,有幸拿过一些国奖、省奖…已保研。目前正在学习C++/Linux/Python

学习经验:扎实基础 + 多做笔记 + 多敲代码 + 多思考 + 学好英语!

6.4 读者写者问题

1、在读者写者问题中,能同时执行读写的是()。C
A.读者和写者
B.不同写者
C.不同读者
D.都不能

2、在读者优先的读者写者问题中,读者可以进入读的前提是()。A、B
A.没有读者和写者在读写
B.有读者在读
C.有写者在写
D.有写者在等

3、在读者代码
rc–;
If (rc==0)
V(W)
中V(W)可能唤醒其它读者。
×

解释:执行到这一步 说明已经是最后一个读者了

4、读者优先的读者写者问题中,一个写者先来,但有可能比后来的读者后运行。√

5、只有没有读者在读,写者就可以进入写。×

解释:如果有另个一个写者在写,这个写者也还是不能进入写。

6.5 哲学家就餐问题

1、哲学家就餐问题的解决方案如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码semephore *chopstick[5];   
semaphore *seat;



哲学家 i:
……
P(seat);
P(chopStick[i]);
P(chopStick[(i + 1) % 5]);
吃饭
V(chopStick[i]);
V(chopStick[(i + 1) % 5]);
V(seat);

解释:在这里插入图片描述
其中,信号量seat的初值为()。C
A.0
B.1
C.4
D.5

2、在哲学家就餐问题中,有以下代码:

1
2
3
cpp复制代码		 P(m);
test(i);
V(m);

在test[i]中的临界资源包括()。B
A.state[i]
B.state[(i+1)%5]
C.ph[i]
D.其它

3、当信号量的值等于2时,表示()。B、C
A.该信号量上有2个进程等待
B.有2个信号量可用
C.该信号量是同步信号量
D.该信号量是二值信号量

4、如果给5个哲学家6根筷子,则不会有死锁发生。√

5、互斥信号量的P和V操作一般在不同进程中。×

6.6 管程

1、引入条件变量后的管程内部,不存在()。C
A.条件队列
B.紧急队列
C.入口队列
D.条件变量

2、进程P调用wait操作唤醒进程Q后,P等待直到Q离开管程才允许的管程是()。A
A.Hoare管程
B.MESA管程
C.Hansen管程
D.系统管程

解释:
在这里插入图片描述
3、每个管程中只能有一个条件变量。×

4、在Hoare管程中,当一个管程内的进程P调用x.wait()时,如果紧急队列非空,则会唤醒第一个入口队列中的等待进程,P进入x的条件队列。×

解释:在这里插入图片描述
在这里插入图片描述

5、Linux和Windows系统中都有信号量同步机制。√

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用 Python 给代码安个进度条,太香了吧!

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」。

前言

今天和大家分享一个进度条可视化库,它的名字叫做 tqdm ,可以帮助我们监测程序运行的进度,用户只需要封装可迭代对象即可。

安装

通过命令行直接安装。

1
python复制代码pip install tqdm

也可以使用豆瓣镜像安装。

1
python复制代码pip install -i https://pypi.douban.com/simple tqdm

执行上述命令后,可以检查一下是否安装成功。

1
python复制代码pip show tqdm

使用方式

以下演示运行环境:jupyter notebook
不同运行环境使用方式稍有不同,可根据警告自行调整。

tqdm 主要参数可选参数众多,我们先看一下常用的一些参数。

主要参数

  • iterable: 可迭代的对象, 在手动更新时不需要进行设置
  • desc: str, 左边进度条的描述性文字
  • total: 总的项目数
  • leave: bool, 执行完成后是否保留进度条
  • file: 输出指向位置, 默认是终端, 一般不需要设置
  • ncols: 调整进度条宽度, 默认是根据环境自动调节长度, 如果设置为0, 就没有进度条, 只有输出的信息
  • unit: 描述处理项目的文字, 默认是’it’, 例如: 100 it/s, 处理照片的话设置为’img’ ,则为 100 img/s
  • unit_scale: 自动根据国际标准进行项目处理速度单位的换算, 例如 100000 it/s >> 100k it/s
  • colour: 进度条颜色,例如:’green’, ‘#00ff00’。

示例

直接将列表传入 tqdm()。

1
2
3
4
5
6
python复制代码from tqdm.notebook import tqdm
from time import sleep


for char in tqdm(['C', 'Python', 'Java', 'C++']):
sleep(0.25)

使用可迭代对象。

1
2
python复制代码for i in tqdm(range(100)):
sleep(0.05)

tqdm 提供了 trange() 方法可以代替 tqdm(range())。

1
2
3
4
python复制代码from tqdm.notebook import trange

for i in trange(100):
sleep(0.05)

我们在进度条前面添加描述性内容,这里把 tqdm 写在循环外,使用 set_description() 在进度条前面添加 “进度 %d”。

1
2
3
4
python复制代码pbar = tqdm(range(5))
for char in pbar:
pbar.set_description("进度 %d" %char)
sleep(1)

我们可以设置进度条的更新的间隔,下面我们设置总数为 total=100,然后分四次,使得进度条按 10%,20%,30%,40%的间隔来更新。

1
2
3
4
5
python复制代码with tqdm(total=100) as pbar:
for i in range(1, 5):
sleep(1)
# 更新进度
pbar.update(10*i)

更改进度条颜色。

1
2
3
4
5
python复制代码with tqdm(total=100, colour='pink') as pbar:
for i in range(1, 5):
sleep(1)
# 更新进度
pbar.update(10*i)

注:在使用 tqdm 显示进度条的时候,如果想要输出内容的话不能够使用 print ,print 会导致输出多行进度条,可以使用 tqdm.write()。

1
2
3
python复制代码for i in tqdm(range(5)):
tqdm.write("come on")
sleep(0.1)

对于多重循环可以指定多个进度条,设置 leave=False 第二个循环执行完后,进度条不保存。

1
2
3
python复制代码for i in trange(3, desc='1st loop'):
for i in trange(100, desc='2nd loop', leave=False):
sleep(0.01)


这就是今天要分享的内容,微信搜 Python新视野,每天带你了解更多有用的知识。更有整理的近千套简历模板,几百册电子书等你来领取哦!另外还有Python小白交流群,如果有兴趣可以通过上面的方式联系我哦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySql优化概述

发表于 2021-11-25

前言

关于MySql优化的文章已经不少,无非就是常见的加索引,以及避免使用索引失效的or、like%等,看得多了自己也能说上个三五条。

但是,这些优化的小点缺少系统的归纳和整理,导致我们在平时处理实际慢查询问题或者是面试的时候无法从大局、整体地视角来处理之。

这篇文章旨在对MySql调优形成一套系统的方法论,让大家在工作或者面试中能够有所帮助。这是我在掘金的第一篇文章,也是对自己掌握知识的一个总结,希望对读者朋友来说也是开卷有益的。

思路

既然限定了MySql数据库,就不去扯技术选型,各数据库性能比较之类的了,至少在现在时间点的国内,MySql还是最主流、使用率最高的数据库。

其次,基于InnoDB引擎对事务、行锁等方面的支持,MylSAM的使用场景也非常少,一般仅存在于面试题当中,只要知道MylSAM有一个全文索引的特点就行了。如下图片中给出了两大存储引擎的对比。

image.png

因此本文默认的前提是MySql数据库中的MylSAM引擎下的性能优化,会从以下几个方面展开:服务器参数配置、数据类型优化、查询优化、索引优化、性能监控。

服务器参数设置

这一块平时开发工程师们基本遇不到,数据库的搭建、参数配置一般都由公司里专业的运维或者DBA团队负责,自己只要负责建表开始的工作就行了。另外,现在公司一般也会使用云厂商提供的数据库服务来降低自己的运维成本。

因此,这个点不是重点,只要了解即可,平时和同事或者面试官能简单地说两句就成。以下列举了一些InnoDB相关的配置。

1
shell复制代码innodb_buffer_pool_size

该参数用于指定固定大小的内存来缓冲数据和索引,最大可设置为物理内存的80%

1
shell复制代码innodb_flush_log_at_trx_commit

该参数控制InnoDB将log buffer中的数据写入日志文件并flush磁盘的时间点,值分别为0、1、2:
0:每秒从缓冲区将log写入操作系统cache,并flush log到磁盘
1:每次提交事务,从缓冲区将log写入操作系统cache,并flush log到磁盘(默认)
2:每次提交事务,从缓冲区将log写入操作系统cache,每秒flush log到磁盘(建议)

1
shell复制代码innodb_thread_concurrency

该参数设置InnoDB线程的并发数,默认为0意为不受限制,如果要设置建议为cpu核心线程数的一倍或两倍

1
shell复制代码innodb_log_buffer_size

该参数用于确定日志文件所占用的内存大小,单位M

1
shell复制代码innodb_log_file_size

该参数用于确定日志文件的大小,单位M

1
shell复制代码innodb_file_per_table

该参数用于为每张表分配一个数据空间,即单独存储

数据类型优化

参数设置完成后的步骤是建表,根据开发设计文档来建表和建字段,这里面就有一些文章可作了,一个好的数据类型和合理的大小能够在这张表越来越大的时候体现它的作用。

  • 使用能够满足业务需求的最小数据类型。即应该尽量用可以正确存储数据的最小数据类型,更小的数据类型通常更快,占用更少的磁盘、内存和CPU缓存,并且处理室需要的CPU周期更少。但是,要确保没有低估需要存储值的范围,即不会造成业务不可用。虽然身处在大宽表横行和磁盘存储成本越来越低的情况下,这些占用空间几乎微乎足道,但这并不妨碍我们做一个优雅的、严谨的程序员。
  • 尽量避免null。如果查询中包含可为null的列,对mysql优化执行器来说很难优化,因为可为null的列使得索引、索引统计和值都更加复杂,因此建议在设计字段的时候就可以规定某些列为not null。
  • 在范式和反范式中寻求平衡。三范式原则诞生于磁盘比较昂贵的年代,在如今人们更希望能从一张表里查询一整个对象实体而避免关联,这就允许冗余甚至是反范式。在企业实战场景中,一般要根据业务场景的需要做到两者的混合使用。
  • 适当的冗余和适当的拆分。基于第三点提出了这一点建议,具体来说,适当的冗余是指当有两张或以上的表被频繁关联查询时,或者每次关联只为了取得部分小字段的值,但是join得到的记录很大,会造成不必要的IO时,就可以考虑冗余字段了。适当的拆分是指当表中存在类似于TEXT后者大VARCHAR类型的大字段时,如果大部分对该表的查询都不需要这个字段,就可以考虑拆分该字段到独立的表中,等要用时再关联查询。

查询优化

把查询优化和索引优化分开来讲是因为,查询优化不仅仅限于加索引和索引优化。查询优化还包括查询缓存、语法解析和预处理等。而索引优化也是一个大的点,因此分开来讲一下。

首先,优化查询要知道查询慢的原因是什么,才能针对性地做优化。一个慢查询的主要原因有网络、CPU、IO、上下文切换、系统调用、锁等待等等。有些我们能优化,有些我们也无能为力,因此我们要把力使在能使的地方。

不妨从头开始思考,查询性能低下的原因主要是访问的数据太多了,某些查询不可避免地要筛选大量的数据,我们可以通过减少访问的数据量的方式进行优化:1.确认应用程序是否在检索大量超过需要的数据2.确认MySql服务器是都在分析大量超过需要的数据行。就是说,是不是我们向数据库要的太多了,或者数据库处理的太多了。

针对第一点,我们要自查,是否向数据库请求了不需要的数据。例如,查询了不需要的记录,我们常常误以为MySql会只返回需要的数据,实际上MySql却是先返回全部结果再进行计算,再日常开发习惯中,经常是先用select语句查询大量的结果,然后获取前面的N行,这是不可取得,不妨直接在select后加limit限制。

此外,是否在多表关联时返回了全部的列或者总是取出全部的列(Mapper 中的BaseResult)。这和select * 如出一辙。虽然简化了写和筛选n多字段的工作,但是却是会影响查询的性能,不要这样做。

在应用层加缓存的方案不在此文章讨论中,因为通过redis等缓存技术方案或者布隆过滤器等拦截已经过滤掉了查询没有打到数据库。这里说的查询缓存是指MySql的查询缓存。在解析一个查询语句之前,如果查询缓存是打开的,那么MySql会优先检查这个查询是否命中缓存中的数据,如果恰好命中,那么会在返回结果之前检查用户权限,如果权限没有问题,MySql会跳过查询的阶段直接从缓存中拿到结果返回给客户端。

MySql查询完缓存后悔经过一下几个步骤:解析SQL、预处理、优化SQL执行计划。没错,MySql执行器悔帮我们优化SQL,这依托于内部的查询优化器,当然只是一些初级的优化,包括但不限于:重新定义关联表的顺序、使用等价变换规则来简化查询语句中的表达式、子查询优化等。更加深层次的优化需要开发者自己去完成。

索引优化

说了这么多,终于进入了索引篇章了,众所周知,优化查询最直给的方式就是加索引,或者分析已存在的索引做更改。

加索引可不是简单三个字就能糊弄过去的,索引怎么加,给哪些字段加,加了为什么没生效,这些都是随之未来的问题。就像文章开头提到的,or会使索引失效,那怎么办,有人会说用union all替代,那么这一定是最佳方案吗,要知道union all是要对一张表查两次,它的性能一定高于使用or的一次全表扫描吗,因为肯定还有别的where条件,能够确保都走索引吗,又或者,where条件里既有or又有like “%%”,这种情况呢?

索引是个大学问,几乎能单独出本书,可以从索引的定义、用处、分类、数据结构一节节讲下来,还有回表、索引覆盖、下推、聚簇索引等面试技术名词,都值得能够好好唠上一唠。本文篇幅不够,读者看到这里估计也没多少耐心。这篇文章只能说帮大家建立一个系统的方法论,从高处和大盘着眼,至于里面细枝末节的东西,无法面面俱到。

索引这么重要的东西,每篇MySQL优化的文章都会涉及到,大家出门左转右转都能看到,或者等工作中问题遇到了再针对性解决处理即可。这里就不谈细节了,总之,索引是个好东西,能够加快查询,我们在写sql的时候尽量要避免索引失效的问题。大家想看的话在评论区留言,之后我会再更新,包括explain看执行计划呀,每个列的释义呀,索引的匹配方式等等。

性能监控

对MySQL服务器的性能监控主要体现在两方面,事前和事后。一条sql可能在数据量小的时候没什么问题,返回还挺快,等系统运行时间久了,业务量上来了之后,就开始暴露问题了。可能我们的业务或者测试说点开哪个页面或者按钮的时候特别慢,特别卡,那就要注意了,可能有慢查询。但是sql那么多,具体怎么定位呢,这时候就有一条神命令:show profile直接在客户端命令行里敲就行,它是查询剖析工具,还可以指定具体的type,回车之后可以显示最近100条信息,包括客户端建立的连接耗时,和sql查询耗时,cpu占用情况等。在系统卡顿定位到慢查询的时候利用它就能迅速定位到具体的sql语句。show processlist用于查看连接的线程个数,来观察是否有大量线程处于不正常的状态或者其他不正常的特征。
另外一方面,性能监控的意思是在防患于未然,在系统卡死前通知运维和开发人员,可以设置服务器运行卡顿的阈值来未雨绸缪,早知道,早排查,早处理。

总结

本文从以下几个方面:服务器参数配置、数据类型优化、查询优化、索引优化、性能监控对MySql调优这一经典问题进行了较高维度的梳理和总结,希望大家看完后能够后面试官或者技术老大侃侃而谈,battle两三个回合,而不是加索引、不要select * 这种老掉牙的回复,最后祝大家都能够升职加薪!Bug越来越少!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

分享一下自己封装的Laravel常用工具类 工具类函数 硬核

发表于 2021-11-25

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」

封装常用的工具类,不写重复代码,能极大的提高开发效率。

工具类函数

Geom转成字符串

如果项目中有大量的计算经纬度需求,强烈建议使用PgSql的geometry类型

1
2
3
4
5
6
7
8
9
10
11
12
13
php复制代码public static function formatGeomToStr($geomJson)
{
if (empty($geomJson)) {
return null;
}
$geomStr = '';
$data = json_decode($geomJson, true);
if ($data['lng'] !== '' && $data['lat'] !== '') {
$geomStr = "POINT({$data['lng']} {$data['lat']})";
}

return $geomStr;
}

计算两个坐标之间的距离

基于经纬度进行计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public static function calcDistance($loc1, $loc2)
{
if (empty($loc1) || empty($loc2) || count($loc2) != 2 || count($loc1) != 2) {
return -1;
}

$radLat1 = deg2rad(floatval($loc1['lat']));
$radLat2 = deg2rad(floatval($loc2['lat']));
$radLng1 = deg2rad(floatval($loc1['lng']));
$radLng2 = deg2rad(floatval($loc2['lng']));
$a = $radLat1 - $radLat2;
$b = $radLng1 - $radLng2;
$s = 2 * asin(sqrt(pow(sin($a / 2), 2) + cos($radLat1) * cos($radLat2) * pow(sin($b / 2), 2))) * 6378.137;
return round($s, 3);
}

批量更新数据拼接sql

这是我司大佬整理的,我就拿来主义贡献给大家了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
php复制代码//批量写入
public static function batchUpdate($multipleData, $tableName)
{
$firstRow = current($multipleData);

$updateColumn = array_keys($firstRow);
$referenceColumn = isset($firstRow['id']) ? 'id' : current($updateColumn);
unset($updateColumn[0]);
// 拼接sql语句
$updateSql = "UPDATE " . $tableName . " SET ";
$sets = [];
$bindings = [];
foreach ($updateColumn as $uColumn) {
$setSql = '"' . $uColumn . '" = CASE ';
foreach ($multipleData as $data) {
$setSql .= 'WHEN "' . $referenceColumn . '" = ? THEN ? ';
$bindings[] = $data[$referenceColumn];
$bindings[] = $data[$uColumn];
}
$setSql .= 'ELSE "' . $uColumn . '" END ';
$sets[] = $setSql;
}
$updateSql .= implode(', ', $sets);
$whereIn = collect($multipleData)->pluck($referenceColumn)->values()->all();
$bindings = array_merge($bindings, $whereIn);
$whereIn = rtrim(str_repeat('?,', count($whereIn)), ',');
$updateSql = rtrim($updateSql, ", ") . ' WHERE "' . $referenceColumn . '" IN (' . $whereIn . ")";
return DB::connection('myProject')->update($updateSql, $bindings);
}

格式化时间

最常用的工具了吧,几乎每个项目都会用到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码public static function formatTimestampForClient($timestamp)
{
$formatString = '';
$now = time();
//一个小时内
$diffTime = $now - $timestamp;
if ($diffTime < 60) {
$formatString = '刚刚';
} else if ($diffTime < 3600) {
$formatString = intval($diffTime / 60) . "分钟前";
} else if ($diffTime < 12 * 3600) {
$formatString = intval($diffTime / 3600) . "小时前";
} else if ($diffTime < 24 * 3600) {
$formatString = "1天内";
} else if ($diffTime < 3 * 24 * 3600) {
$formatString = "3天内";
}

return $formatString;
}

获得随机字符串

第二个参数表示是否允许包括特殊字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
php复制代码public static function getRandomStr($len, $special = true)
{
$chars = array(
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
"l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
"w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
"S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2",
"3", "4", "5", "6", "7", "8", "9"
);

if ($special) {
$chars = array_merge($chars, array(
"!", "@", "#", "$", "?", "|", "{", "/", ":", ";",
"%", "^", "&", "*", "(", ")", "-", "_", "[", "]",
"}", "<", ">", "~", "+", "=", ",", "."
));
}

$charsLen = count($chars) - 1;
shuffle($chars); //打乱数组顺序
$str = '';
for ($i = 0; $i < $len; $i++) {
$str .= $chars[mt_rand(0, $charsLen)]; //随机取出一位
}
return $str;
}

根据生日计算星座

星座控看这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ini复制代码public static function getZodiacSign($birth)
{
$month = date('m', $birth);
$day = date('d', $birth);
$signs = [
["20" => "水瓶座"],
["19" => "双鱼座"],
["21" => "白羊座"],
["20" => "金牛座"],
["21" => "双子座"],
["22" => "巨蟹座"],
["23" => "狮子座"],
["23" => "处女座"],
["23" => "天秤座"],
["24" => "天蝎座"],
["22" => "射手座"],
["22" => "摩羯座"]
];
$signStart = array_key_first($signs[$month - 1]);
$signName = $signs[$month - 1][$signStart];
if ($day < $signStart) {
$sign = array_values($signs[($month - 2 < 0) ? $month = 11 : $month -= 2]);
$signName = array_shift($sign);
}
return $signName;
}

校验手机号的正确性

最常用的工具类之二

注意:各运营商投放的号段会有更新,可以不定期的查询一下,更新这个工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
php复制代码public static function checkPhoneNumber($phone_number)
{
//中国联通号码:130、131、132、145(无线上网卡)、155、156、185(iPhone5上市后开放)、186、176(4G号段)、175(2015年9月10日正式启用,暂只对北京、上海和广东投放办理),166,146
//中国移动号码:134、135、136、137、138、139、147(无线上网卡)、148、150、151、152、157、158、159、178、182、183、184、187、188、198
//中国电信号码:133、153、180、181、189、177、173、149、199
$g = "/^1[34578]\d{9}$/";
$g2 = "/^19[89]\d{8}$/";
$g3 = "/^166\d{8}$/";
if (preg_match($g, $phone_number)) {
return true;
} else if (preg_match($g2, $phone_number)) {
return true;
} else if (preg_match($g3, $phone_number)) {
return true;
}
return false;
}

生成唯一标识:32位自定义字符串

Uuid 是一个非常好用的工具

1
2
3
4
5
6
7
php复制代码public static function createUniqueId()
{
$uuid5 = Uuid::uuid4();
$uid = str_replace('-', '', $uuid5->toString());

return strtoupper($uid);
}

获得毫秒

1
2
3
4
csharp复制代码public static function getMicroSecond()
{
return intval(microtime(true) * 1000);
}

生成订单号

电商项目必备

1
2
3
4
5
php复制代码public static function createOrderId()
{
$microSecond = Utility::getMicroSecond();
return date("YmdHis", $microSecond / 1000) . sprintf("%03d", $microSecond % 1000) . rand(100000, 999999);
}

判断是否是json

1
2
3
4
5
6
7
8
9
10
11
12
php复制代码public static function isJson($value)
{
$data = json_decode($value, true);

if (json_last_error() !== JSON_ERROR_NONE) {
return false;
} else if (!is_array($data)) {
return false;
}

return true;
}

获得ip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
php复制代码public static function getIp()
{
if (isset($_SERVER)) {
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$realip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else if (isset($_SERVER['HTTP_CLIENT_IP'])) {
$realip = $_SERVER['HTTP_CLIENT_IP'];
} else {
$realip = $_SERVER['REMOTE_ADDR'];
}
} else {
if (getenv('HTTP_X_FORWARDED_FOR')) {
$realip = getenv('HTTP_X_FORWARDED_FOR');
} else if (getenv('HTTP_CLIENT_IP')) {
$realip = getenv('HTTP_CLIENT_IP');
} else {
$realip = getenv('REMOTE_ADDR');
}
}

return $realip;
}

获得N天前、N天后时间戳

传入N值是一个比较好的思路,我之前搞了几个3天前、7天前、30天前这类的工具。

都不如传入N值来的科学。

1
2
3
4
5
6
7
8
9
10
11
php复制代码//获取N天的0点时间戳
public static function getNDayTimestamp($n = 1)
{
return strtotime(date('Y-m-d', strtotime('+' . $n . ' day')));
}

//获取N天前0点时间戳
public static function getBeforeNDayTimestamp($n = 1)
{
return strtotime(date('Y-m-d', strtotime('-' . $n . ' day')));
}

手机号掩码

1
2
3
4
5
6
7
8
9
php复制代码public static function maskPhone($phone)
{
$strLen = strlen($phone);
if ($strLen < 4) {
return '';
} else {
return substr_replace($phone, "****", 3, 4);
}
}

判断时间戳是否是今天

1
2
3
4
5
6
7
8
php复制代码public static function isToday($timestamp = 0)
{
$res = false;
if (date('Ymd', $timestamp) == date('Ymd')) {
$res = true;
}
return $res;
}

大家还有哪些需要使用工具类的场景,欢迎在评论区留言,我来实现补充。

硬核文章推荐

PHP转Go 2021年年中总结

如何第一时间收到接口报错?不用测试妹子再质疑你是不是接口挂了。

Git使用实战:多人协同开发,紧急修复线上bug的Git操作指南。

性能优化反思:不要在for循环中操作DB

性能优化反思:不要在for循环中操作DB 进阶版

最后

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

力扣第106题-从中序与后序遍历序列构造二叉树 前言 一、思

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

前言

力扣第106题 从中序与后序遍历序列构造二叉树 如下所示:

根据一棵树的中序遍历与后序遍历构造二叉树。

注意:

你可以假设树中没有重复的元素。

例如,给出

1
2
ini复制代码中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]

返回如下的二叉树:

1
2
3
4
5
markdown复制代码    3
/ \
9 20
/ \
15 7

一、思路

题目意思很明确:利用中序和后序遍历还原二叉树。这一题昨天写的那一题非常的相似,思路也是差不多的,有兴趣的可以看一下# 力扣第105题-从前序与中序遍历序列构造二叉树

我们知道中序遍历和后序遍历有如下的特点:

  • 中序遍历:先 左子树节点,再 根节点,最后是 右子树节点
  • 后序遍历:先 左子树节点,再 右子树节点,最后是 根节点

如果问你二叉树的根节点是什么?你知道吗?

很显然二叉树的根节点就是 后序遍历 的最后一个元素了。那子树的根节点又是什么呢?这与当前子树的起始位置和节点数量有关了,不妨继续向下看以下图文分析。

图解算法

此处以实例中的 inorder = [9,3,15,20,7],postorder = [9,15,7,20,3] 作为例子,用表格画一下,就如下所示:

image.png

  1. 确定第一个根节点 root1,很显然是后序遍历的最后一个节点

image.png

  1. 再在中序遍历中找到这个根节点的位置,确定出左子树 left1 和右子树 right1。如下图所示:

image.png

  1. 我们再确认左子树 left1 的根节点

因为根节点 root1 在中序遍历中的位置是 2,说明右子树有 3 个节点。所以 left1 的根节点是 root1 向前移动 4 格的位置(要除去自身)

image.png

  1. 再确认右子树 right1 的根节点,只需要将 root1 前移动 1 格即可。(因为后续遍历是左右根,根节点前面就是右子树的最后一个节点,即右子树的根节点)

image.png

  1. 我们找到了所有的根节点,那么还原这个二叉树也就不是什么难事了。最终的结果如下图所示:

image.png

二、实现

实现代码

实现代码与思路中保持一致,为了避免多次在 中序遍历 中找目标值,所以先用 Map 存储了中序遍历所有的节点值和位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码    private Map<Integer, Integer> inMap;

public TreeNode buildTree(int[] inorder, int[] postorder) {
int n = inorder.length;
int m = postorder.length;
// 构造哈希映射,记住中序遍历中每个节点的位置
inMap = new HashMap<>();
for (int i=0; i<n; i++) {
inMap.put(inorder[i], i);
}
return dfs(postorder, 0, n-1, m-1);
}

/**
* @param postorder 后序遍历
* @param inLeft 中序遍历左子树起始位
* @param inRight 中序遍历右子树起始位
* @param postRoot 后续遍历中根节点的位置
* @return
*/
public TreeNode dfs(int[] postorder, int inLeft, int inRight, int postRoot) {
if (inLeft > inRight)
return null;
// 后序遍历中从后往前都是根节点
TreeNode root = new TreeNode(postorder[postRoot]);
int inorder_root = inMap.get(postorder[postRoot]);
root.left = dfs(postorder, inLeft, inorder_root-1, postRoot-(inRight-inorder_root)-1);
root.right = dfs(postorder, inorder_root+1, inRight, postRoot-1);
return root;
}

测试代码

1
2
3
4
5
6
java复制代码    public static void main(String[] args) {
int[] inorder = {9,3,15,20,7};
int[] postorder = {9,15,7,20,3};
TreeNode treeNode = new Number106().buildTree(inorder, postorder);
System.out.println("test");
}

结果

image.png

三、总结

感谢看到最后,非常荣幸能够帮助到你~♥

如果你觉得我写的还不错的话,不妨给我点个赞吧!如有疑问,也可评论区见~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

VSCode 中配置一键重启 SpringBoot 服务

发表于 2021-11-25

Spring Boot 安装开发者工具 spring-boot-devtools 后,可监听 classpath 下的文件修改并自动重启服务,这个功能对业务开发时效率提升明显。

spring-boot-devtools 使用了两个 classloader,一个用于加载第三方依赖的 class,另一个加载项目源码构建的 class,重启服务时只会销毁重建后者,所以重启速度较快。

但是很多时频繁候修改文件导致服务频繁重启,可能会导致 IDE 性能问题,出现卡顿现象,解决办法是将服务重启时机,由监听文件被修改后自动触发改为主动触发。Spring Boot 开发者工具提供了主动触发重启的机制,即监听对特定文件的修改(参考这里),这个“特定文件”通过下面配置指定。

1
properties复制代码spring.devtools.restart.trigger-file=.reloadtrigger

上面 .reloadtrigger 需放在 classpath 搜索路径下,例如可以放在 resources 目录下。

1
2
3
4
css复制代码src
+- main
+- resources
+- .reloadtrigger

配置后,当需要触发重启时,只要修改下 .reloadtrigger 文件即可,例如 linux 下可以这样修改文件(手动编辑或其他方式均可,只要让文件内容发生变更)

1
sh复制代码echo $(date) > ./src/main/resources/.realoadtrigger

上面代码放在一个脚本中,每次执行以下就可以触发 Spring Boot 服务重启了。

像 IDEA 这样的工具提供了重启操作,也是利用了上面的原理。在 VSCode 也可以实现类似的一键重启操作。方法是配置一个 Build Task(见下图),之后在 VSCode 中按 Shift+Command+B 组合键触发 Spring Boot 服务重启。

image.png

Build Task 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
json复制代码{
"version": "2.0.0",
"tasks": [
{
"label": "重启服务",
"type": "shell",
"command": "echo $(date) > ${workspaceFolder}/src/main/resources/.reloadtrigger",
"problemMatcher": [],
"presentation": {
// 静默执行,否则每次重启都会弹出输出面板
"reveal": "silent"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

结束!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Docker Swarm介绍

发表于 2021-11-25

前言

本篇是Docker第十三篇,Docker的使用至此就介绍完成,接下来继续Kubernetes。

Docker系列文章:
  1. 为什么要学习Docker
  2. Docker基本概念
  3. Docker镜像基本原理
  4. Docker容器数据卷
  5. Dockerfile
  6. Docker单机网络上
  7. Docker单机网络下
  8. Docker单机网络实战
  9. Docker隔离技术
  10. Docker限制
  11. Docker Compose
  12. Docker多机网络

为什么需要Docker Swarm

  1. 我们从Docker到Docker Compose都是在单机上完成,这样会带来一个很现实的问题就是高可用的问题,如果只部署到一台机器是无法做到高可用的,这样就不具备生产的条件;
  2. Docker Compose只是简单做了单机服务的编排、扩容,对于多机器的管理、发布、服务发现、负载均衡都没有很好的解决;
  3. 目前我们所有的容器都是在单个宿主机上进行网络通信,多机情况的网络通信也没有解决方案;

针对以上三点,Docker给出了Docker Swarm的解决方案,Docker swarm可以让用户轻松在多个机器上发布和管理应用,并且我们不需要关注每个容器实例具体落在哪一个节点,Docker swarm把我们的应用以服务的形式暴露出去,并内置服务发现和负载均衡,让运行在多个节点上的容器集群感觉就像只有一个应用在跑一样简单,可以轻松实现扩容和自动容错。Docker swarm集群通常有几个工作程序节点和至少一个管理程序节点,负责高效地处理工作程序节点的资源并确保集群有效地运行,提高了应用可用性。

Docker Swarm概念介绍

img

Manager Node

Manger 节点是负责管理工作的,从名字就可以看出,注意负责以下事情:

  1. 维护集群的状态;
  2. 对 Services 进行调度;
  3. 为 Swarm 集群提供外部可调用的 API 接口;
  4. 提供服务注册发现、负责均衡等功能;

Manager 节点需要时刻维护和保存当前 Swarm 集群中各个节点的一致性状态,在保证一致性上,Manager 节点采用 Raft 协议来保证分布式场景下的数据一致性;

Worker Node

Worker 节点是用来执行 Task 的;默认情况下 Manager 节点也同样是 Worker 节点,同样可以执行 Task;

image.png

Service

Services 是指一组任务的集合,服务定义了任务的属性,比如任务的个数、服务策略、镜像的版本号等等,服务有两种模式:

  1. replicated services 按照一定规则在各个工作节点上运行指定个数的任务;
  2. global services 每个工作节点上运行一个任务;

Task

Task是 Swarm 集群中的最小的调度单位,任务包含一个Docker容器和在容器内运行的命令,如果某一个任务奔溃,那么协调器将创建一个新的副本任务,该任务将生成一个新的容器;

Task调度

img

Task调度主要分为两部分: Manager节点的任务分配和Worker节点的任务执行;

Manager节点的任务分配主要有以下四步:

  1. 用户通过 Docker Engine Client 使用命令 docker service create 提交 Service 定义;
  2. Manager节点根据定义创建相应的 Task,并分配IP地址;
  3. 将Task分发到对应的节点上;
  4. 节点进行相应的初始化使得它可以执行Task;

Worker节点的任务执行主要有两步:

  1. 连接Manager节点的分配器检查该Task相关定义的信息;
  2. 验证通过以后,开始在 Worker 节点上执行Task;

注意,上述 Task 的执行过程是一种单向机制,比如它会按顺序的依次经历 assigned, prepared 和 running 等执行状态,不过在某些特殊情况下,在执行过程中,某个 Task 执行失败了,Manager 的编排器会直接将该 Task 以及它的 Container 给删除掉,然后在其它节点上另外创建并执行该 Task;

Docker Swarm网络

核心概念介绍

img

  1. Overlay Network:管理 Swarm 中 Docker 守护进程间的通信。你可以将服务附加到一个或多个已存在的 overlay 网络上,使得服务与服务之间能够通信;
  2. Ingress Network:一个特殊的 overlay 网络,用于服务节点间的负载均衡。当任何 Swarm 节点在发布的端口上接收到请求时,它将该请求交给一个名为 IPVS 的模块。IPVS 跟踪参与该服务的所有IP地址,选择其中的一个,并通过 ingress 网络将请求路由到它。初始化或加入 Swarm 集群时会自动创建 ingress 网络,大多数情况下,用户不需要自定义配置,但是 docker 17.05 和更高版本允许你自定义;
  3. Docker Gwbridge Network:一种桥接网络,将 overlay 网络连接到一个单独的 Docker 守护进程的物理网络。默认情况下,服务正在运行的每个容器都连接到本地 Docker 守护进程主机的 docker_gwbridge 网络,一种桥接网络,将 overlay 网络(包括 ingress 网络)连接到一个单独的 Docker 守护进程的物理网络。默认情况下,服务正在运行的每个容器都连接到本地 Docker 守护进程主机的 docker_gwbridge 网络;

image.png

流量分类

Docker Swarm 数据流量分为两个层面:

  1. 控制管理流量(control and management plane traffic): 包括 Swarm 管理消息,例如加入/退出 Swarm 的请求,这些流量总是被加密的;

image.png

  1. 应用数据流量(Application data plane traffic): 包括容器之间的数据交换,以及容器与外部网络的数据交换,关于这块的原理探讨放在实践的地方;

集群搭建

资源准备

节点全部使用CentOS8.2版, 这边准备了两个node节点和一个master节点:

  1. IP:172.16.0.191 主机名:demo-master-1 担任角色:Swarm Manager
  2. IP:172.16.0.45 主机名:demo-slave-1 担任角色:Swarm Node
  3. IP:192.168.0.231 主机名:demo-slave-2:Swarm Node

保证每个主机之间都能相互ping通并且2377端口可以telnet保持畅通, 每个节点都安装了Docker。

集群安装

  1. 初始化Master节点,命令执行后,该机器自动加入到swarm集群。这个会创建一个集群token,获取全球唯一的 token,作为集群唯一标识。后续将其他节点加入集群都会用到这个token值;
1
bash复制代码docker swarm init --advertise-addr 172.16.0.191

image.png

  1. 将Node节点加入集群;
1
bash复制代码docker swarm join --token SWMTKN-1-3cap7omkvmyuf0q1ybm868880eo5reoil8pcbovmejfzw6pil8-73hc367s4gitudqivrdirvu63 172.16.0.191:2377
  1. 查看Master节点信息;
1
bash复制代码docker node ls

image.png

  1. 相关命令;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bash复制代码# 创建服务
docker service create \
--image nginx \
--replicas 2 \
nginx

# 更新服务
docker service update \
--image nginx:alpine \
nginx

# 删除服务
docker service rm nginx

# 减少服务实例
docker service scale nginx=0

# 增加服务实例
docker service scale nginx=5

# 查看所有服务
docker service ls

# 查看服务的容器状态
docker service ps nginx

# 查看服务的详细信息。
docker service inspect nginx

实战

  1. 在Manager节点部署Nginx服务,服务数量为2个,对外暴露的端口是8080映射容器内部的80端口,使用Nginx镜像;
1
bash复制代码docker service create --replicas 2 --name nginx --publish 8080:80  nginx

image.png

  1. 查看容器分布状况;
1
bash复制代码docker service ps swarm-nginx

image.png

  1. 访问服务;
1
2
bash复制代码curl 172.16.0.45:8080
curl 192.168.0.231:8080

image.png

Internal

Internal容器与容器之间通过overlay网络进行访问,通过service name进行通信,但是service name所对应的ip不是真实ip而是VIP,我们可以下面这个案例进行验证:

img

  1. 开始实验前移除创建的服务,创建一个overlay的Network;
1
2
3
bash复制代码docker network create --driver overlay swarm-overlay
#查看网络状况
docker network ls

image.png

  1. 创建一个nginx的service ,使用swarm-overlay网络;
1
bash复制代码docker service create --name nginx -p 8080:80 --network swarm-overlay -d nginx
  1. 再创建一个busybox服务;
1
bash复制代码docker service create --name busybox -d --network swarm-overlay  busybox:1.28.3 sh -c 'while true; do sleep 7200; done'
  1. 查看服务列表;
1
bash复制代码docker service ls

image.png

  1. 进入busybox服务内部,使用ping命令访问nginx服务,我们会发现可以访问;
1
2
bash复制代码docker exec -it 2f55d73adfb4 sh
ping nginx

image.png

Ingress

当在任何一个Swarm节点去访问端口服务的时候会通过本节点的IPVS ( ip virtual service )到真正的Swarm节点上。提供以下三种功能:

  1. 外部访问的均衡负载;
  2. 服务端口暴露到各个Swarm节点;
  3. 内部通过IPVS进行均衡负载;

image.png

接着Internal案例继续进行探索,Swarm节点内部是如何进行转发的;

  1. 查看工作节点的转发规则,我们可以看到把请求转发到172.18.0.2:8000这个地址上去了;
1
bash复制代码iptables -nL -t nat

image.png

  1. 接下来我们查看下本机的网络情况,我们找到了docker_gwbridge,可以看到两个ip处于同一网段,那么172.18.0.2应该也连接上docker_gwbridge;

image.png

  1. 查看docker_gwbridge的interface 信息,我们会发现有多个interface;
1
bash复制代码brctl show

image.png

  1. 接下来我们查看下docker_gwbridge网络信息,我们可以发现ingress-sbox就是我们要找的命名空间,gateway_ingress-sbox就是所属的容器;
1
bash复制代码docker network inspect docker_gwbridge

image.png

  1. 进入ingress_sbox内部,查看iptables规则,可以看到发送到该ip地址下的8000端口的请求被负载掉了;
1
2
3
4
5
6
bash复制代码#查找ingress_sbox位置
ls /var/run/docker/netns
#进入ingress_sbox
nsenter --net=/var/run/docker/netns/ingress_sbox
#查看ingress_sbox iptables
iptables -nL -t mangle

image.png

  1. 查看负载的详细信息;
1
2
3
4
5
6
bash复制代码#在host安装ipvsadm
yum install -y ipvsadm
#再次进入ingress_sbox
nsenter --net=/var/run/docker/netns/ingress_sbox
#查看详细的规则
ipvsadm -l

image.png

  1. 接下来随便在一台主机找到nginx容器,查看器IP情况,我们会发现与ipvsadm的相对应;
1
2
3
4
5
6
7
bash复制代码#进入容器
docker exec -it 56c475bb5b2f /bin/bash
#安装一些命令
apt-get update
apt-get install net-tools
#查看网络情况
ifconfig

image.png

通过探究我们可以得出Docker Swarm网络情况如下:

img当我们访问任一节点的8080端口时,只要我们这个节点处于Swarm集群中,不管服务是否部署到这个节点都能访问,只要端口相同即可。我们本地的请求会被转发到Ingress_sbox这个Network Namespace中,在这个名称空间中再通过lvs转发到具体服务容器的ip和8080端口中去。

结束

欢迎大家点点关注,点点赞!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…191192193…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%