服务治理——熔断
熔断是兜底杀手锏之一,在一个远程调用框架中,一个服务接口被调用时,如果出错应该优先采取重试的方案,如果多次超时——表明服务确实不可用之后——才会考虑熔断,以避免“灾害”进一步的扩大。
熔断本身没有特别难的算法,但是需要考虑比较多的细节。
熔断是兜底杀手锏之一,在一个远程调用框架中,一个服务接口被调用时,如果出错应该优先采取重试的方案,如果多次超时——表明服务确实不可用之后——才会考虑熔断,以避免“灾害”进一步的扩大。
熔断本身没有特别难的算法,但是需要考虑比较多的细节。
幂等性描述一项操作被执行多次后,不会改变第一次执行产生的副作用。
练手项目,解决项目中的幂等性检查要求,代码地址:point_right: Github - TIdempotent。
1.Tony. 企业融资渠道有哪些?[EB/OL], https://www.zhihu.com/question/23939018
/answer/29132581, 2015.5.
2.周梅. ERP 实施中存在的问题及解决方案[J]. 中国集体经济,2013,28: 35-36.
3.于洪涛. SAP 坚定云转型之路[EB/OL], http://www.cnbp.net/news/detail/13618, 2016.9.
4.James Lewis , Martin Fowler . Microservices a definition of this new architectural term
[EB/OL], https://martinfowler.com/articles/microservices.html, 2014.3.
5.IT168 企业级. 年终盘点篇:2017 年度微服务调查报告出炉[EB/OL],
http://www.sohu.com/a/216277536_374240, 2018.1.
6.Henry Robinson. What Is CAP Theorem?[EB/OL], https://www.quora.com/What-
Is-CAP-Theorem-1, 2010.9.
7. Dan Pritchett. BASE: An Acid Alternative[J]. ACM Queue, 2008, 6(3):48-55.
15.Hunt P, Konar M, Junqueira F P. ZooKeeper: Wait-free Coordination for Internet-scale
Systems[C]. Usenix Annual Technical Conference. 2010:653–710.
16.Zachary Tong, Clinton Gormley. Elasticsearch 权威指南[M]. O’Reilly Media, Inc,
2015, 27-35.
17.Baron.Scbwartz. 高性能 MySQL[M]. 北京:电子工业出版社, 2013.5, 1-33.
18.Redis Documentation[EB/OL], https://redis.io/documentation, 2018.3.
这个项目的业务部分的原型是司库云,但是重点不在业务,因此这里仅仅简单介绍一下。平时我们接触的最多的应该是共享单车、在线聊天、支付宝这种 To C 业务,To B 业务主要是由企业提出的,和个人不同,一个企业总是会有部门、上下级等关系,因此 To B 业务与 To C 业务的主要区别有以下几条:
近年,似乎所有传统企业都在试图往互联网靠拢,上次听一位朋友讲到某机关部门布了一个集群供“大数据处理”(上千条级别),再如某企业开发云服务软件,测试时专门有一项“大数据用例”(上万条级别)。
将业务上云后,正如之前很多人困惑的那样:不就是把代码放到云主机上跑吗?但问题的关键不是技术新老,而是为什么要迁移到云端,主要是当下业务变更迅速、弹性大,使用传统的机房部署服务,应用实例能使用的硬件资源是固定的,如果数据量超过了机器可处理的范围,要么换一台更高档的机器,要么将整个实例复制到另一台机器上,再在网关处做负载均衡,当然这两种方式都有些缺陷。因此,技术的变革都是被逼出来的,当下云原生应用能够提供弹性扩展、资源预警等功能,且有更人性化的操作界面,取代传统的运维方式也是必然的。微服务是当下实现应用上云的首选架构风格,有丰富的资源(当然最重要的是比较火),这个小项目也是我对微服务的初步探究。
架构风格关注的是如何使用一些连接件来组合软件组件,在 Web 应用中,我们会使用覆盖网络来描述软件的架构,连接件可以是 HTTP 协议、数据库连接器等,在桌面应用中,连接器可以是读取用户输入的管道,等等。
系统架构关注的是软件组件是如何实例化的,比如要几台服务器、哪些组件要复制等。
平时说的架构一般指的是架构风格,但对实现细节的深究是成为架构师的必经之路。
图中,小圆形、方形、三角这些小图形是服务实例,包围小图形的双层方形是 Docker 容器实例,立方体是物理服务器(一般是云提供商提供的虚拟机实例)
传统的单体式应用(monolithic)在实践中往往存在着诸多问题,比如,在达到一定的复杂度后,不仅部署效率会降低,由开发不小心引入的任何 Bug 都会弄垮整个服务,且模块之间高度耦合,添加新功能的代价变得更高,由此也对测试带来了更大的压力。
微服务架构模式有点像SOA,他们都由多个服务构成,因此对 SOA 缺陷的讨论可以参照下面对微服务的讨论。但是,从另一个角度看,微服务架构模式是一个不包含 Web 服务(WS-)和 ESB 服务的 SOA,微服务应用乐于采用简单轻量级协议,比如 REST,而不是 WS-,在微服务内部避免使用 ESB 以及 ESB 类似功能,微服务架构模式也拒绝使用 canonical schema 等 SOA 概念,因此可以认为微服务是轻量版的 SOA。简而言之,它们之间的主要区别是对服务的治理方式不同。
微服务的概念由来已久,2011 年 5 月在威尼斯附近举办的软件架构师研讨会提出了“微服务”这个概念,Adrian Cockcroft 将其描述为一种“细粒度的 SOA”。
在国内,加快互联网+步伐成为许多传统企业的必然选择。业务场景、用户习惯和行为在迅速变化,许多传统行业线上业务出现急速增长。每个月都要进行业务系统更新的企业比例超过了半数,比如金融行业的移动支付、互联网理财等,汽车制造行业的营销、电商、售后服务等线上业务比例迅速提高。IT 团队业务开发、迭代都以每月、甚至每周来计,需要 7*24 小时响应,这些给系统开发和运维带来极大挑战。在服务的复杂化、线上访问压力大、交付速度无法满足业务需求等现状的前提下,寻求架构上的转型正是大势所趋。
微服务相对以传统方式部署的应用来说,具有易扩展、访问便捷、安全、性价比高等特点,将传统的单体式应用进行拆分后成为一个个微服务后,每一个微服务都可以单独进行部署,可以根据企业用户的具体需求提供服务,另外服务之间的弱耦合性也使得扩展变得更加容易。
总而言之,微服务应用相对单体式应用的优势体现在下面这些方面:
开发和运维在传统情况下是完全分割开来的,开发主要负责完成功能需求,并且知道如何去优化功能,运维要明白如何部署应用服务集群、中间件集群外,同时对所使用的工具的原理要有一定程度的了解,比如某个服务器的 CPU 打满了,原因可能是代码写得太差了,也可能是数据库某张热表没有给字段加上索引。现在 DevOps 的概念盛行,开发有时也需要承担环境的维护。
实施,说简单的,就是帮用户装软件的,该人员必须对软件整体具有较好的理解,因为实施会直面用户,如果被用户问倒可不只是丢自己一个人的脸,当然,一定程度的口才也是必要的。实施在 ERP 时代是很重要的一个职位,在当下云服务盛行的情况,实施往往会被派去为用户部署私有云环境(前提是有这个必要)。
提到无状态我们都会想到 HTTP,HTTP 协议是一种无状态的协议,这意味着 HTTP 服务器不会保存任何用户信息,实现登录、购物车等带状态的功能时都需要额外借助 Session、分布式缓存、数据库等存储方案。以购物车为例,其中 Session 相当于将购物车的状态保存到了服务实例的内存中,而分布式缓存和数据库方案则将状态转移到了另一个服务器内(假设应用和缓存、数据库服务器是部署在不同的服务器上的)。
如果一个服务是有状态的,随着实例的运行,服务实例在内存中可能会多出一些与业务相关的数据,实例间产生差异后,我们后续就不能随意地对服务进行复制了,两次 HTTP 的可能会产生不同的结果,如下图所示。
因此有必要在设计的时候就保证服务是无状态的。
CAP是描述分布式系统特性常用的一种理论,它使用数据一致性(Consistency)、服务可用性(Availability)和分区容错性(Partition-tolerance)三个指标来定义一个分布式系统,这三个特性不能被同时完全满足,其中,P 是必须要满足的,因为一般的业务系统并不允许网络中的消息被随意丢弃,因此多数的讨论都集中于 C 和 A 之间的权衡。
如果需要满足强一致性,则在对数据进行读写操作时势必都需要进行加锁操作并使用事务来保证分布式一致性,但同时也会对系统的效率产生非常大的影响,起到反作用、影响用户的体验,所以在设计时往往会放宽这个要求,采取最终一致性作为实现目标。服务的高可用性要求请求必须能够完成,这可以通过复制服务实例来实现,服务实例的复制需要投入更多地成本与维护人力,需要根据具体场景进行具体分析。
eBay 的架构师 Dan Pritchett 源于对大规模分布式系统的实践总结,提出了 BASE 理论。BASE理论是对 CAP 理论的延伸,核心思想是即使无法做到 CAP 理论要求的强一致性,但应用可以采用适当的方式达到最终一致性(Eventual Consitency)。BASE 是指基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventual Consistency)。基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
在 BASE 的提出者 Dan Pritchett 的论文《BASE: An Acid Alternative》中提出了一种实现 BASE 的经典模式,概括来讲,就是服务之间是通过消息队列连接的,消息队列会保证将消息传递给目标服务,但不保证送到的时间。
接下来,我希望对项目中使用到的一些技术进行简短总结,尽量引入一些代码和图来辅助说明。
REST 是现今一套比较成熟的 API 设计理论,主要思想是将网络上的资源抽象为 URI,并通过 HTTP 协议中的字段和动词进行描述和操作,被广泛地应用于 WEB 应用 API 的设计当中。
RPC 是常见的实现服务之间远程调用的协议(在 Java 这种面向对象的语言上实现的理应称作 RMI 协议,但是叫习惯了就无所谓了),服务实例作为不同的进程运行于多台服务器上,在发起请求时,请求的发起方称为客户端、接收者称为服务端,它们通过指定的网络层甚至传输层协议进行通信,可以自动对消息进行序列化并包装为底层消息格式进行传输,并在服务端进行反序列化得到请求。因此 RPC 的可靠性和效率与底层协议本身的效率和对对象进行的序列化和反序列化的效率息息相关。
调用 RPC 接口与调用本地方法形式上是相同的,这大大减少了代码的冗余、提升了开发效率,但是 RPC 本质上与普通方法调用却有着截然不同的执行流程,因此必须与普通方法区分开来、不能滥用。在设计微服务时,也必须考虑服务划分的粒度,如果分得过细就有可能导致一次请求需要过多的网络开销。
单机环境下,资源竞争者都是来自机器内部的进程或线程,那么实现锁的方案只需要借助单机资源就可以了,比如借助磁盘、内存、寄存器来实现。而在分布式环境下,资源竞争者生存环境更复杂了,原有依赖单机的方案不再发挥作用,这时候就需要一个大家都认可的协调者出来,帮助解决竞争问题,那这个协调者称之为分布式锁。
通过在执行修改操作前加分布式锁,可以很好地保证临界区资源的互斥访问,一定程度上维护了数据的一致性,但是在客户端进行读写的复合操作时加锁又是不充分的,因为读和写操作之间存在一定的时间差,如果在这期间数据被其他线程所修改,那么接下来的写操作就会覆盖这个修改,导致业务层面上的不一致,解决办法是引入乐观锁,本文的乐观锁是通过为实体类添加版本号来实现的,每次进行修改操作时需要比较对象与数据库中记录的版本,只有大于的情况才能执行。
微服务系统中的多个服务往往拥有自己独立部署的数据库,在跨服务对数据进行写操作时若发生错误就有可能产生数据不一致的情况,利用单纯的加锁机制是不能保证安全性的。分布式事务是指会涉及到操作多个数据库的事务,目的是为了保证系统中各服务能保持数据一致性。分布式事务处理的关键是必须有一种方法可以知道事务的所有参与者的动作,事务最终必须统一提交或统一回滚。文中采取的解决办法是引入 TCC 分布式事务,将业务操作划分为 try、confirm、cancel 三个部分,try 负责对业务资源的锁定,confirm 负责提交事务、正式执行业务操作,cancel 负责回滚事务并释放锁定的资源,在业务流程中执行所有的 try 完毕后,由协调者根据事务的执行情况来统一调用所有事务参与者的 confirm 提交或 cancel 回滚。TCC 在本地事务的基础上进行多个实例间的协调,可以在很大程度上保证跨服务业务操作的一致性。
Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具,例如配置管理,服务发现,断路器,智能路由,微代理,控制总线等。
Sping Cloud 的实现基础是Spring Boot,在构建项目前需要先引入所需基础设施的依赖。
比如若需要使用 ZooKeeper 的服务发现功能,只需要在 Maven 的 pom.xml 文件中添加名为”spring-cloud-starter-zookeeper-discovery”的依赖。
1 | <dependency> |
并在属性文件中指定 ZooKeeper 服务器的地址,就可以在应用中通过注入 DiscoveryClient 来使用 ZooKeeper 的服务发现功能了。
1 | @Autowired |
其他的功能也可以依法炮制。从某种意义上来说,Spring Cloud 更像是通过 Spring Boot 自动配置机制实现的由众多独立子项目组成的大型综合项目。
Spring Boot 的主要目标是解决传统 Spring 项目中配置文件过于繁杂的问题,随着业务的复杂度增加,配置变得越来越难维护且无法定制,因此 Spring Boot 的提出者利用注解和属性文件来取代配置文件,因为注解是与代码紧紧相依的,代码中可以直接通过注解来获取到属性文件中的属性,并按用户的需求来执行 Bean 的初始化过程,从而实现配置的高度可定制化。Spring Boot 的关键特性是自动配置,实现自动配置的方式是在应用启动后从 Spring Boot 提供的包内获取配置好的 Bean,并对常用的配置设定默认值,比如其内置的 Tomcat 服务器的默认占用端口即为 8080,并且支持在属性文件中修改,这在很大程度上减少了配置项的数量,同时也降低了运维人员的压力。
Spring Cloud 和 Dubbo 区别?为什么不使用 Dubbo???
Docker 执行流程?为什么用它来部署环境?
Docker 是一个基于容器的应用开发、部署和运行平台,它为开发者和系统管理员们提供了一种新式的应用部署方式,具有灵活、轻量、可移植等特点。传统的部署云服务的方式是通过虚拟机完成的,虚拟机会在宿主机上运行一个完整的操作系统、通过 hypervisor 来间接使用宿主机的硬件资源,实际上这远远超出了应用运行所必须的资源要求。而容器正相反,它在操作系统中作为进程运行,与所有其他容器共享同一内核、占用相同容量的内存空间,相对来说会更加轻量。
MySQL 是一种常用的开源数据库,MySQL 软件采用了双授权政策,分社区版和商业版,具有体积小、速度快、总体拥有成本低、开放源码等特点。
Redis 有哪些数据结构?
Redis 是一个开源的使用 ANSI C 语言编写、支持网络、支持持久化的日志型、Key-Value 数据库,并提供多种语言的 API。
Nginx 执行流程?反向代理解释一下?为什么使用 Nginx 作为静态页面服务器?负载均衡原理是什么?
Nginx 是一款高性能的 HTTP 和反向代理服务器软件,具有高性能、高并发、低 CPU 内存消耗的特点,功能包括反向代理、负载均衡、访问控制等,且可以根据需要自定义扩展组件。
ZooKeeper 执行流程?服务发现原理是什么?分布式锁原理是什么?
ZooKeeper 是一个分布式的开放源码的分布式应用程序协调服务,可以为分布式应用提供一致性服务,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
ZooKeeper 将资源抽象为文件系统,使用节点来表示数据在该文件系统中保存的路径。为了实现服务发现机制,每个实例在加入应用服务时会在 ZooKeeper 服务的同一命名空间下创建临时节点,并在退出时由 ZooKeeper 自动删除,这样任一个实例都可以通过查询该命名空间下来获得微服务集群内服务的注册情况。ZooKeeper 本身不提供锁服务,但是可以使用节点来表示锁,如果一个客户端需要为一个资源上锁,就可以为该资源所代表的路径下创建一个顺序节点,按照节点创建的顺序进行标号,客户端监听该路径下的节点,如果自己创建的节点标号是最小的就获取到锁,当释放锁时需要删除自己创建的节点,这样基本实现了客户端之间的互斥访问。
RabbitMQ 执行流程?消息队列的数据结构是怎样的?
RabbitMQ 是一个在 AMQP 基础上完成的、可复用的企业消息系统。AMQP 协议定义了消息队列需要具有的面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全性等特点。RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持包括 Java 在内的多种客户端,主要应用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
Elasticsearch 执行流程?文档数据结构是怎样的?
Elasticsearch 是一个分布式、可扩展、实时的搜索与数据分析引擎。擅长全文搜索、结构化数据的实时统计、复杂的语言处理等。并且原生支持分布式部署,可以通过管理多节点来提高扩容性和可用性,并在硬件故障时确保数据安全。
微服务的首要任务是对模块进行拆分,整个系统的主要模块如下图所示。
需要实现的主要功能包括:
每个模块成为一个独立的服务,它们具有明确清晰的功能边界,且相互之间只暴露必要的远程接口,形成类似下图的结构。
部分功能的实现涉及到一些中间件,更详细的架构图如下图所示。
其中:
npm run build
编译成为静态文件,将产生的整个文件夹移动到 Nginx 的静态文件目录下;我将大部分业务功能忽略了,这些功能中大部分都属于简单的 CRUD 操作,下面介绍一些技术性稍微强一些的功能的实现细节。
服务发现功能的主要运行流程大体分成三个阶段,通过定时器来定时执行:
当实例退出集群时,代表其服务注册信息的临时节点会被自动清除,此时 Nginx 服务器经过同样的刷新过程将该实例移除出代理目标,由此完成服务的自动伸缩。
在实际环境中启动服务之前,需要先将项目打包成为 Docker 镜像,利用 Docker 技术只要服务器上安装有 Docker 环境即可启动应用,很大程度上减少了重复修改服务本身配置文件的麻烦,因为业务模块的框架是基于 SpringCloud 的,在启动后利用 SpringBoot 自动配置的便利,可以在初始化时调用 ZooKeeper 服务器提供的接口将服务注册到 ZooKeeper 的服务注册中心。服务发现模块的执行流程图如下图所示。
其中,更新 Nginx 配置文件的主要代码如下所示。
1 | // 构建nginx.conf配置文件 |
在将服务实例信息转换为 Nginx 配置文件后,需要循环遍历所有现存的 Nginx 服务器,对服务器地址的请求若出错需要一定次数的重试,尽可能地更新。
客户端或其他模块通过 REST 协议来访问服务的接口,访问请求由网关服务器进行拦截,再通过负载均衡技术来发布到某个服务的实例上。
考虑多用户登录系统并同时对同一实体执行更新操作时,就有可能发生竞态条件。如果使用普通的加锁方式只能保证同一进程内的线程同步、不能保证多个服务实例进程间的互斥访问,解决办法是引入 ZooKeeper 的分布式锁和时间戳机制,在每次修改实体后更新实体的版本,并在每次对实体 ID 加锁后再与数据库中保留的最后一次修改时的版本进行校验,加锁将使得临界区代码在集群内只能被一个服务实例的线程调用,在修改完毕退出后更新实体版本,另一个线程进入后再根据版本进行校验,从而保证了服务间的并发写的一致性。
时间戳的实现比较简单,而分布式锁的实现则涉及到很多细节,已知的包括:
发债模块在对单据进行提交操作时实际上需要调用审批服务提供的 RPC 接口,在提交后用户即可在审批模块中查找到这些单据,并可以对其进行审批或取消审批、驳回等操作。首先需要解释的是服务为何如此划分,需要结合企业的具体业务来考虑,因为审批是十分宽泛的场景,不只是发债单据需要审批,为了提高模块的可扩展性,将其划分出来作为单独的服务是合理的。放款提交操作的时序图如下图所示。
和其他增删改查操作相同的是,在具体的业务处理之前需要对放款进行加锁和版本校验,成功后发债服务会将该放款信息通过调用服务接口的方式提交到审批服务,审批服务会先根据放款的单据类型从数据库查找其审批流的注册信息及审批流,并成功便新增一条审批流实例到数据库,返回成功信息给发债服务,发债服务再根据返回值来修改放款的审批状态;如果查询或新增失败,审批会返回错误信息,使得发债服务也不会进行任何处理,或者处理后回滚到最初的状态。
发债模块并不了解审批模块内部的实现方式,二者使用相互独立的两个数据库,因此在执行审批操作时是可能发生不一致的情况的,为此需要引入 TCC 补偿机制,对提交操作需要提供一个 confirm 接口和一个 cancel 接口,代表统一的提交和回滚。
放款提交的具体执行过程如下图所示。
在登记放款页面点击提交按钮后会将放款单的 ID、版本号等信息发送到后台,程序的初步执行流程仍然是上分布式锁、校验版本号及设置新版本号和更新时间,接下来由于涉及到跨服务的写库操作所以会显得更加复杂。
首先服务调用方的发债服务需要生成本次调用的调用 ID,其作用之后再作论述,因为发债服务的本次调用是事务内的第一次调用,所以还需要生成一个链路 ID,用于标识一次事务,这些 ID 都需要全局唯一,所以和实体 ID 一样需要使用分布式唯一 ID 算法来生成。发债服务在调用审批服务的接口时,除了传递本来需要传递的单据信息之外,还需要将调用 ID 和链路 ID 打包一同传递,以便之后审批服务将 TCC 事务的参与信息上传到缓存中间件。事务的参与者分为根参与者和枝叶参与者,根参与者为事务的发起者,在这里即为发债服务,枝叶参与者包括事务通过传播到达的所有其余服务器,参与者包含的属性包括服务器本身的地址及 TCC 补偿事务所需的 confirm 接口和 cancel,只要某个服务接口需要添加事务属性,它就需要将自己的参与者信息上传到缓存中间件中,因此这里审批服务还有一个上传参与者信息的过程。实现 TCC 补偿事务中根参与者执行流程的关键代码如下所示。
1 | private Object interceptRoot(Participator participator, MethodWrapper methodWrapper) |
在前面的准备工作完成后,审批服务正式进入业务处理流程,首先需要根据登记放款的单据类型去数据库中查找审批流注册信息,因为审批存在多级审批的场景,因此每条审批流关联了自己下一阶段的审批流,这里为了创建审批流实例需要找到源头第一条审批流,并将发债传递来的单据信息填入即可得到审批流实例。
保存审批流实例时不能直接保存到实体本身代表的数据库表中,而是要暂时保存到一个 dr 表中暂存,表名为“实体名_dr”,因为此时事务并没有完成,如果直接保存到实体表中,直接去审批流页面查询是能查出的,也就是说会发生“脏读”的现象,暂时保存到这个暂存表中,还可以作为日志,供系统发生故障时手动恢复之用。审批服务执行完毕之后,发债服务需要使用其返还值来更新发债数据库中的放款状态。但是事务还未完成,因为审批服务还没有将审批实例实际保存到实体表中,发债服务需要先使用链路 ID 从缓存中间件中获取所有事物参与者信息,然后根据提交操作是否成功来决定是调用 confirm 还是 cancel,这里 confirm 的逻辑是将 dr 表中的审批流实例转移到实体表中,而 cancel 是将该审批流实例从 dr 表中移除、发债服务回滚。
为了应对偶尔的网络不稳定等情况,根参与者需要在异常发生时再重试一定的次数,另外,试想如果请求的超时也被当成故障处理了,那么多次的写操作很有可能会为系统引入脏数据了,因此 confirm 和 cancel 接口需要额外地保证接口的幂等性,为了校验接口是否被多次调用,需要客户端在发出请求时带上此次请求的调用 ID,服务端需要缓存调用 ID 并对请求的重复性进行校验。
最后发债服务释放放款 ID 上的锁,完成一次提交操作。其他审批接口、结算管理的相应接口实现思路是类似的,在此不另加论述。
总而言之,提交操作是需要跨服务写库的,除了在修改操作的加锁和版本校验之外,还需要注意TCC 的执行流程:
调用 ID 和链路 ID 的作用,前者可以用于保证接口调用的幂等性,后者主要用来定位事务内的参与者。
用一个常见的用户购买商品场景为例,需要先确保用户能够支付,然后锁住用户和商户的账户,这是 try 阶段;如果 try 成功,即预留资源成功了,接下来再对用户账户扣款、商户账户入账,如果因为超时等原因失败了,需要事务补偿,即重试,这是 confirm 阶段;如果 try 失败,则释放所有的锁,这是 cancel 阶段。
当前的系统仍存在很多可改进的地方。
ZooKeeper 集群通过 Zab 协议保证高一致性,重新选举 Leader 比较耗时,且当节点数量不足以构成容错集群时,ZooKeeper 倾向于返回空;故提出改用 Eureka 等其他倾向于提供高可用性的具有服务发现功能的产品。
Nginx 服务器作为网关,存在单点故障风险,但是这里的服务发现只能保证被 Nginx 代理的那些服务能弹性伸缩,对 Nginx 本身却没有什么办法,就算复制了 Nginx 服务器,客户端也只能发到一个 IP 上。这里提出使用基于操作系统的方案对 Nginx 集群实现验活及负载均衡,主要是 Keepalived 等软件或独立的 DNS 服务器,但是论文题目限于 Saas 层展开,本人水平有限没法解释。
HTTP 是应用层的,Spring Boot 封装了一个 RestTemplate 可以很方便地发 HTTP 请求。而 TCP 是传输层的,如何包装消息需要另外再讨论,常见的框架如 Netty,提供了长连接、心跳检测等功能,可以提高开发效率。
Kryo 是一个优秀的 Java 序列化方案,大概用了很多优化方案,这就是很零碎的编程问题了。
TCC 的 confir 和 cance 都有可能出错,一般都需要手动恢复,异常日志指的是 redo 和 undo 日志,redo 是未执行前的数据值,undo 是执行修改后的数据值。
考虑 TCC 事务发生异常的情况,做好 redo/undo 日志,供出错时手动执行恢复。
部门开始推一个大项目 NCC,后台几乎沿用原 NC(有近十年历史),将原来 JavaSwing 画的重量前端换成了一种“轻量前端”,其实这种轻量前端一点都不轻量;SQL 完全是自己用字符串拼接起来的;开发模式是前后端分离联调的方式,但是因为后台代码都是部署在一个服务里头的,没有司库云(微服务架构)复杂。
对 Controller、Service 分层的讨论放在后面。
保存操作有以下特点。
1 | { |
执行流程和 save 几乎一样,只是系统中不会真的删除一条数据,而是采用“逻辑删除”的方案。
查询操作的过滤条件是由前台给出的,我没有经历过重客户端时代 NC 的开发,但是在 Web 应用中也这么做有点奇怪。
分页查询的思路比较奇怪,是先按前端传递的过滤条件查出所有合适记录的 pk,然后在 Controller 层对这些 pk 进行分页:
1 | // ids指的是查出的所有合适记录的pk |
最后再调用一个批量查询的 Service 接口进行查询。
轻量前端指的是用 React 开发的浏览器客户端,既然是轻量前端,那么做的事情肯定不会太多了,那么很多工作就必须要转移到后端了,因此在 NCC 中又引入了一个 Action 层,作为 Controller。
后台 Service 层将读操作和写操作划分到了两个接口内,实际上应用层可以根据读写进行复制,一般来说读操作更频繁,可以多创建几个实例提供服务,并在前头使用负载均衡器进行反向代理。
SOA 是一种架构风格,与微服务的主要区别是 SOA 多了一个 ESB(企业服务总线),在 SOA 中,当一个服务需要调用另一个服务时,它需要先从 ESB 中获取那个服务的调用方法、消息格式等,而微服务可以看作一种轻量化的 SOA,它没有集中的一个 ESB,服务之间通过公开的 RPC 接口来相互调用。
实际上 NC 没有 ESB 这玩意。首先需要将 Service 接口及其对应的实现类注册到一个 xml 文件内,服务间调用通过 NCLocator.find(class)实现:如果配置文件中没有设置服务器 URL,则直接本地反射实例化 Service 的实现类;如果配置文件中设置了服务器 URL,则使用 JDK Proxy 做动态代理,将本地方法调用转换为远程调用。
元数据是描述数据的数据,实际开发业务前需要先使用相关工具”画”出元数据,NCC 的开发极大地依赖于元数据的正确性,老的功能节点的元数据若轻易修改,很有可能导致原功能出错。
模板是对页面的抽象,平台的有关部门做了一个画模板的工具,由后端人员登录到工作桌面画模板,可以减轻前端开发人员的工作压力(可能是领导觉得前端比较稀缺?)。可以从元数据导出模板,并根据原型图进行适配。
随着业务的复杂化,将许多公共组件抽象出来集中管理是必须的,尤其是 NC 这么复杂的庞然大物,其业务拿出两个小时来也没办法讲完 1%,有点夸张,但是事实是这里的需求很多都是从会计等行业转来的,一次给我们讲需求时从 NC 上满屏的标签中挑了部分讲了两小时,大家都是崩溃的…因此 NC 中引入了三大模板的概念,它们分别是:查询模板、打印模板、计算模板,分别对查询 sql、打印、???进行了抽象。
Spring 的核心原理是 IoC,而 IoC 的主要实现方式是 DI 或依赖查找。DI 我们比较眼熟,主要有”属性注入”、”构造器注入”及”方法注入”。NCC 后台为 NC,已经有 10 来年历史,其中实现了一套依赖查找的模式,如下代码所示。
1 | IXxxService xxxService = ServiceLocator.find(IXxxService.class); |
当然接口和实现类之间不是自动关联在一起的,还需要定义一个 upm 文件来配置他们之间的关联关系。
这实际上和比较原始的 Spring 用法有点像,如下代码所示。
1 | XxxBean bean = application.getBean(XxxBean.class); |
就是普通的锁。
1 | boolean isOK=false; |
共享锁适用于读者写者的情景,即某类操作可以并存、而和其他类操作不能的情况。
使用方法和排他锁差不多:
1 | PKLock.getInstance().acquireBatchLock( |
对同一 PK 来说,共享锁和排他锁是不能共存的。
动态锁为一种特殊的锁,进行加锁的人员不用自己释放,其调用序列一般为:
pklock.acquireLock… pklock.addDynamicLock…
如果是远程调用,远程调用结束后系统会自动释放所有的动态锁
动态锁其实是对前面两种锁的封装,不过动态锁不需要主动解锁,中间件会在后台事务结束前进行统一解锁;并且在同一次中间件事务中,可以重复申请相同 PK 的锁。
1 | boolean isOK = PKLock.getInstance().addDynamicLock( PK ); |
动态锁的实现原理是事件机制,动态锁最后会发出一个解锁事件,业务操作执行结束后会触发这个事件。
有了锁机制可以保证并发操作不会破坏数据一致性,但是不能保证用户的操作是对最新版本的数据进行的,因此需要引入时间戳机制。
主要通过比较当前对象的ts 值(时间戳)与数据库中该对象的 TS 值来判断用户修改的是否为最新版本的数据。在每次对数据进行修改操作时,都需要更新相应数据的时间戳标志 ts,这个一般由系统维护,不需要在业务中特殊处理。
上面提到了动态锁机制但是没有具体说解锁的时机,假设所有含写操作的业务操作都需要加锁,难道要把加锁解锁逻辑都写一遍?NC 中为了解决这个问题也实现了一套 AOP 机制,原理是 JDK Proxy,NC 会为所有在配置文件中注册的 Service 创建动态代理,在动态代理中可以对锁等公共功能进行操作,从而大大简化业务代码。在这方面来讲,NC 也可以看作 Spring 的弱化版。
NCC 后台是传统的单体式应用,所有模块几乎都放在一个服务里(用户买什么就把什么包含进来),跨模块的组件之间可能存在相互引用的关系,比如 a 需要将某些信息存入 b 中,就需要调用 b 提供的接口,而 b 在某些情况下也需要从 a 中获取某些信息,但 b 所在的 B 模块是一个更通用的组件,它不会为每个下游业务都新增代码。在 NCC 中,面对这种情况一般是由 a 来提供一个回调接口,将这个接口名保存到数据库中,然后告诉 b:”这就是 a 的接口啦”,类似于 RPC,不过将耦合的部分转移到了数据库中。
数据库里的数据不会被直接删除,而是采取逻辑删除的策略,每条数据都有一个额外的dr(delete remark)字段用于标识一条数据是否被删除,查询时需要根据这个字段进行过滤。
采用 Oracle 作为数据库,一是因为客户主要是大企业,安全、稳定有更高要求;二,数据量也确实不大,可能也用不着进行分库分表;三,作为延续了十几年的系统,当时“去 IOE 化”还没有提出,Oracle 仍是数据库领域最好的选择;最后,业务十分复杂,且底层 ORM 代码中存在大量直接使用字符串拼接出来的 SQL,想要完全移植到 MySQL 等数据库是不大现实的。
以上我对最近工作中涉及到的东西进行了简单总结,嗯…产品底层仍有很多我未知的部分,一些与业务关联较大的代码也不适合拿出来分享,于是暂时告一段落吧。
公司比较看重这批新员工(基本来自 985 院校),做了比较多的培训,从公司文化到衣着讲究都专门开了课,很切合应届毕业生的需求(强制参加的…)。
规范类似设计模式,是对最佳实践的总结,有点收获,这里把还记得的部分总结一下。
基本没听(其实是因为不知道下午要考试),内容和阿里 Java 规范很接近,有时间考个证书。
这个大佬比较惨了,大家都是程序员、还都是偏后台的那种,讲课时间还是刚吃完饭那会,大家不是特别能接受,反正我是没听进去。
公司一个架构师大佬讲的,大部分内容仍然来自阿里 Java 规范,掺杂了一些干货。
元组/行:一个持久化对象。
属性/字段/列:某类持久化对象的属性,属性集一般使用 U 表示。
超键:能唯一标识一条元组的属性集。
候选键:不包含多余属性的超键。
主键:被用户选作元组标识的候选键。
主属性:构成候选键的属性。
函数依赖(FD):由业务确定的属性之间的依赖关系,即由一组属性可以唯一确定另一组属性。
关系模式(R(U)):指在属性集中存在的所有函数依赖。
平凡函数依赖:在 R(U)中存在 X→Y,若 Y 包含于 X,则称 X→Y 为平凡函数依赖。
完全依赖:在 R(U)中,如果 X→Y,并且对于 X 的任何真子集 X’都有 X’-/->Y’,则称 Y 完全依赖于 X,记作 X→Y。比如确定一批书的印刷时间需要给出版次和印次,不管是单独给出版次或印次都无法确认这批书。
部分依赖:与完全依赖相反,如果 X→Y,且 X 中存在一个真子集 X’,使得 X’→Y 成立,则称 Y 部分依赖于 X。比如学号和姓名可以确定一个学生,但其实学号就可以确定一个学生了。
传递依赖:指的是形如 X→Y→Z 的函数依赖。
多值依赖(MVD):原始定义比较难理解,用一个特殊情况来说明,就是在同一个表里存在 X:Y、X:Z 分别是 1 对多关系,即有 Y→X、Z→X,比如[课程, 学生, 先修课]是一张表,其中课程对学生、课程对先修课都是 1 对多关系。
1NF:不能有复合字段(不可分的原子值)。
实现方法:如果有,则拆成多个字段。
例子:一个人可能有多个联系电话,比如手机号、固话等,应该被拆分到多列内,或者多行保存同一人的多个电话。
身份证号 | 姓名 | 手机号 | 固定电话 |
---|---|---|---|
3303012313 | 赵 | 18012341234 | 123-12341234 |
身份证号 | 姓名 | 手机号 |
---|---|---|
3303012313 | 赵 | 18012341234 |
3303012313 | 赵 | 123-12341234 |
缺陷:
2NF:实体属性完全依赖于主键、不能仅依赖主键的一部分属性(严格地说,非主属性完全依赖于候选键)。
实现方法:如果存在对主键的部分依赖,则拆出来作为一个新实体。
例子:
缺陷:
3NF:非主键属性不能间接依赖于主键(严格地说,不能有非主属性对候选键的传递依赖)。
实现方法:如果存在上述传递依赖,则将依赖的两个非主属性拆出成为新表,原表和新表之间是 1 对多的关系。
例子:
缺陷:
BCNF:消除任何属性对候选键的传递依赖(在 3NF 的基础上,不允许候选键属性传递依赖于候选键的属性)。
实现方法:同 3NF。
例子:
缺陷:
4NF:消除非平凡且非 FD 的多值依赖。
实现方法:
例子:
缺陷
1 | select * from t; |
缓存命中率 = 查询命中数 / 总查询数
缓存命中率是度量缓存有效性的常用参数,为了提高缓存命中率,最好对不变数据进行缓存。
缓存淘汰是在服务器内存不够用时触发的,将一些价值较小的数据从缓存中淘汰,减少不必要的 swap,提升内存的有效利用率。
缓存淘汰还有一个问题:是先写入还是先淘汰呢?
首先看先写入再淘汰的情况,如下图所示。
我们交换操作 3 和 4 的顺序,第一步数据库操作成功,第二步如果淘汰缓存失败,则会出现 DB 中是新数据,Cache 中是旧数据的情况,即产生了数据不一致的问题。
然后是先淘汰再写入的情况,同样如上图所示。
写和读操作是并发执行的,应用实例 1 首先发起对缓存的淘汰,应用实例 2 读缓存 miss,于是从数据库读,而应用实例 1 这才将新值写入数据库,此时实例 2 得到的数就是脏数据了,注意在分布式环境下这种执行顺序完全是有可能的。
一、单选题(每道题 2 分,共 12 分)
1、现有一张系统用户操作日志表,合理命名应该是( )
A、XTYHRZB
B、SysUserOpLog
C、Sys_User_Oplog
D、SYS_T_USER_OPLOG
2、下面数据库设计不符合规范的是( )
A、大数据量表进行分库分表
B、为了方便查看,表中使用明文存储用户密码
C、使用数字格式存储时间
D、将 json 格式数据存储到 Nosql 数据库
3、下面 SQL 编写不符合规范的是( )
A、select column1, column2 from table1
B、select column1, b.columna from table1, table2 b where column1 = b.columna
C、insert into table1(column1,column2) values(?,?)
D、update table1 set column1 = ‘aaa’ where id = ‘001’
4、下面关于性能优化不正确做法的是( )
A、尽量避免一条 SQL 从大于 4 张表中取数
B、如果一条 SQL 有三层以上的嵌套查询,在设计上考虑解决
C、为了提升代码执行效率,大量的公式运算交给数据库后端运行
D、为常用的分组排序字段建立索引
5、下面不正确的索引建立方法的是( )
A、业务表唯一标识字段必须制定唯一索引
B、只要是查询可能用到的字段,都要建立索引
C、尽量使用数据量少的索引
D、索引列上不使用函数计算
6、下面数据库操作符合规范的是( )
A、为了方便,应用程序使用超级管理员账号
B、为了快速解决问题,直接在后端数据库中修改业务数据
C、修改表结构告知影响人员,并预留一定时间评估、审核
D、程序测试完成后直接上线新功能
二、多选题(每道题 6 分,共 60 分)
1、关于领域模型命名,下列哪些说法符合规范:( )
A、数据对象命名:xxxDO,xxx 即为数据表名,例如:ResellerAccountDO。
B、数据传输对象:xxxDTO,xxx 为业务领域相关的名称,例如 ProductDTO。
C、展示层对象:xxxVO,xxx 一般为网页名称,例如 RecommendProductVO。
D、POJO 是 DO/DTO/BO/VO 的统称,命名成 xxxPOJO。
2、关于代码注释,下列哪些说法符合规范:( )
A、所有的抽象方法(包括接口中的方法)必须要用 javadoc 注释。
B、所有的方法,包括私有方法,最好都增加注释,有总比没有强。
C、过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。
D、我的命名和代码结构非常好,可以减少注释的内容。
3、关于类命名,下列哪些说法符合规范:( )
A、抽象类命名使用 Abstract 或 Base 开头。
B、异常类命名使用 Exception 结尾。
C、测试类命名以它要测试的类的名称开始,以 Test 结尾。
D、如果使用到了设计模式,建议在类名中体现出具体模式。例如代理模式的类命名:LoginProxy;观察者模式命名:ResourceObserver。
4、关于加锁,下列哪些说法符合规范:( )
A、可以只锁代码区块的情况下,就不要锁整个方法体。
B、高并发的业务场景下,要考虑加锁及同步处理带来的性能损耗,能用无锁数据结构,就不要用锁。
C、能用对象锁的情况下,就不要用类锁。
D、加锁时需要保持一致的加锁顺序,否则可能会造成死锁。
5、关于方法的返回值是否可以为 null,下列哪些说法符合规范:( )
A、方法的返回值可以为 null,如果是集合,必须返回空集合。
B、方法的返回值可以为 null,不强制返回空集合,或者空对象等。
C、方法实现者必须添加注释,充分说明什么情况下会返回 null 值。
D、防止 NPE 是调用者的责任。
6、关于控制语句,下列哪些说法符合规范:( )
A、推荐 if-else 的方式可以改写成卫语句的形式。
B、尽量减少 try-catch 块内的逻辑,定义对象、变量、获取数据库连接等操作可以移到 try-catch 块外处理。
C、if ( condition) statements; 单行语句不需要使用大括号。
D、在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
7、关于捕获异常和抛异常,下列哪些说法符合规范:( )
A、如果需要捕获不同类型异常,为了方便处理,可以使用 catch(Exception e){…}。
B、不要捕获异常后不处理,丢弃异常信息。
C、捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
D、异常定义时区分 unchecked / checked 异常,避免直接使用 RuntimeException 抛出。
8、关于变量和常量定义,下列哪些符合规范:( )
A、Long a=2L;//大写的 L。
B、Long a=2l; //小写的 l。
C、常量只定义一次,不再赋值,所以不需要命名规范。
D、不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护。
9、数组使用 Arrays.asList 转化为集合,下列说法哪些正确的:( )
A、数组元素的修改,会影响到转化过来的集合。
B、数组元素的修改,不会影响到转化过来的集合。
C、对于转换过来的集合,它的 add/remove/clear 方法会抛出: UnsupportedOperationException。
D、Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
10、关于并发处理,下列哪些说法符合规范:( )
A、线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
B、同步处理时,能锁部分代码区块的情况下不要锁整个方法;高并发时,同步调用应该考虑到性能损耗。
C、创建线程或线程池时,推荐给线程指定一个有意义的名称,方便出错时回溯。
D、推荐使用 Executors.newFixedThreadPool(int x)生成指定大小的线程池。
三、问答题(4 道题,共 28 分)
1、人力云(HR)业务需要建立人员表(含姓名、年龄、性别)和人员工作情况表(含工作开始时间、工作结束时间、工作单位、工作简介),请写出具体的建表语句。(8 分)
2、请列举你所了解的分库分表方式。(6 分)
3、现有业务需求要修改表结构,开发库已经修改测试完成,需要在生产库同步修改并发布新程序,请问你具体的步骤。(5 分)
4、现在有一张表 tablea,主键 key1,只有主键含有唯一索引,字段 columna,columnb,columnc,数量级 100 万,现在按照主键升序,每页 100 条分页,请写去取出第 900000 到 900100 条数据的 SQL 语句。(9 分)
一、单项选择
1.D 数据库大小写不敏感,应该全部用大写
2.B 选项 C 时间用数字格式存储,可以方便最后的国际化时区转化
3.B 连接语句要对每个表给出别名,对每个字段的使用都要带上表别名,否则有子查询的情况下,语句可能出现执行计划错误,甚至结果集错误。
4.C 选项 B 建议使用 NoSQL 缓存或其他可以在架构上优化的措施来减少复杂的数据库操作;选项 C 公式运算容易使得索引失效因此不建议在数据库中使用;选项 D 中的建议是因为 order by 和 group by 会使用到索引。
5.B 选项 B 索引是有代价的,对于一些值重复性较大的列最好不建索引;选项 C 也是考虑在写操作时更新索引会拉低性能。
6.C 操作一般是越谨慎越好,数据库操作最好交给专业人员来做(DBA),选项 B 是为了避免数据库出问题时互相甩锅。
二、多项选择
1.ABC 选项 C 我似乎没选,因为把 VO 当成 DO 来用了,而且现在开发都是前后台分离,很少用到页面模板;选项 D 中 POJO 确实是 DO/DTO/BO/VO 的统称,但不能把所有都命名成 XxxPOJO,会分不清该类的职责。
2.ACD 一般接口和抽象类里的抽象方法需要加上注释,而类里面的私有方法没有这个要求,也要避免注释过多的情况。
3.ABCD
4.ABCD 使用锁时要考虑锁的粒度、性能等特性,同时应该避免死锁等问题。
5.BCD 我和大部分人一样非常厌恶方法返回 null,尽量返回空集合或空对象能减少很多烦恼,但是手册中认为方法可以返回 null,防止 NPE 是调用者的责任,我觉得原因很大程度上是因为难以避免远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
6.ABD 选项 A 卫语句指的是处理完一个分支后直接返回;选项 B 强调我们分清稳定代码和非稳定代码,不要将不会出错的稳定代码也加到 try 中,这是不负责任的。
7.BCD 异常最好有业务含义,能提高后期排错效率。
8.AD 选项 B 不规范是因为 l 和 1 容易混淆。
9.ACD 最好看一下 Arrays.asList()源码。
10.ABC FixedThreadPool 不推荐使用,因为其允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。
三、简答题
第一题:表名和字段名按规范,表名前缀 HR,使用有意义的单词,单词间用_分割,有中文注释。
第二题:读写分离,冷热分离,按照特定业务纬度分表,使用 ID Hash 取余平均分表等。
第三题:
1、明确影响范围
2、预留应变空间时间
3、交由专家评估合理性和性能
4、脚本交由专人在生产库执行,记录;
5、错峰操作。
第四题:参考规范 3 性能优化 22 ,或者先查询 id,在根据 id 查询数据等都可。
补充:后一个方法为什么可行呢?因为规范要求所有删除都是逻辑删除,能保证库里的数据 id 是从 1 到 1000000 排列的。
有的题目比较抠细节,但是上课好好听应该不难。
大部分规范都取自阿里 Java 编码规范,但很多数据库优化策略很有见地,在日后多作总结。
之前一直在用的老 SSD 只有 128GB,上边装了 Ubuntu 作为宿主 OS,另外常备一个 Windows7 的虚拟机,时常面临空间不够的问题,一般是用 Vmware 中 Hard Disk 的Compact Disk功能来挤出一些空间来,但是这样频繁的硬盘读写对硬盘寿命多少有些影响。
后来换新机时自带的 SSD 是 256GB 的,当时出于方便不想重装环境就用老的 SSD 替换了下来,现在抽出一块时间把数据都转移到新硬盘上,尽量不影响平时的工作。