Dunwu Blog

大道至简,知易行难

《玩转 Spring 全家桶》笔记

第一章:初识 Spring (4 讲)

01 | Spring 课程介绍

02 | 一起认识 Spring 家族的主要成员

Spring Framework - 用于构建企业级应用的轻量级一站式解决方案

Spring Boot - 快速构建基于 Spring 的应用程序

Spring Cloud - 简化分布式系统的开发

03 | 跟着 Spring 了解技术趋势

04 | 编写你的第一个 Spring 程序

第二章:JDBC 必知必会 (10 讲)

05 | 如何配置单数据源

直接配置所需的 Bean

数据源相关

  • DataSource(根据选择的连接池实现决定)

事务相关(可选)

  • PlatformTransactionManager(DataSourceTransactionManager)
  • TransactionTemplate

操作相关(可选)

  • JdbcTemplate

Spring Boot 做了哪些配置

DataSourceAutoConfiguration

  • 配置 DataSource

DataSourceTransactionManagerAutoConfiguration

  • 配置 DataSourceTransactionManager

JdbcTemplateAutoConfiguration

  • 配置 JdbcTemplate

符合条件时才进行配置

数据源相关配置属性

通用

  • spring.datasource.url=jdbc:mysql://localhost/test
  • spring.datasource.username=dbuser
  • spring.datasource.password=dbpass
  • spring.datasource.driver-class-name=com.mysql.jdbc.Driver(可选)

初始化内嵌数据库

  • spring.datasource.initialization-mode=embedded|always|never
  • spring.datasource.schema 与 spring.datasource.data 确定初始化 SQL ⽂文件
  • spring.datasource.platform=hsqldb | h2 | oracle | mysql | postgresql(与前者对应)

06 | 如何配置多数据源

配置多数据源的注意事项

不同数据源的配置要分开

关注每次使用的数据源

  • 有多个 DataSource 时系统如何判断
  • 对应的设施(事务、ORM 等)如何选择 DataSource

Spring Boot 中的多数据源配置

手工配置两组 DataSource 及相关内容

与 Spring Boot 协同工作(二选一)

  • 配置@Primary 类型的 Bean
  • 排除 Spring Boot 的自动配置
  • DataSourceAutoConfiguration
  • DataSourceTransactionManagerAutoConfiguration
  • JdbcTemplateAutoConfiguration

07 | 那些好用的连接池们:HikariCP

在 Spring Boot 中的配置

Spring Boot 2.x

  • 默认使用 HikariCP
  • 配置 spring.datasource.hikari.* 配置

Spring Boot 1.x

  • 默认使用 Tomcat 连接池,需要移除 tomcat-jdbc 依赖
  • spring.datasource.type=com.zaxxer.hikari.HikariDataSource

常用 HikariCP 配置参数

常用配置

  • spring.datasource.hikari.maximumPoolSize=10
  • spring.datasource.hikari.minimumIdle=10
  • spring.datasource.hikari.idleTimeout=600000
  • spring.datasource.hikari.connectionTimeout=30000
  • spring.datasource.hikari.maxLifetime=1800000

其他配置详见 HikariCP 官网

08 | 那些好用的连接池们:Alibaba Druid

数据源配置

直接配置 DruidDataSource

通过 druid-spring-boot-starter

  • spring.datasource.druid.*

Filter 配置

  • spring.datasource.druid.filters=stat,config,wall,log4j (全部使用默认值)

密码加密

  • spring.datasource.password=<加密密码>
  • spring.datasource.druid.filter.config.enabled=true
  • spring.datasource.druid.connection-properties=config.decrypt=true;config.decrypt.key=<public-key>

SQL 防注入

  • spring.datasource.druid.filter.wall.enabled=true
  • spring.datasource.druid.filter.wall.db-type=h2
  • spring.datasource.druid.filter.wall.config.delete-allow=false
  • spring.datasource.druid.filter.wall.config.drop-table-allow=false

Druid Filter

  • 用于定制连接池操作的各种环节
  • 可以继承 FilterEventAdapter 以便方便地实现 Filter
  • 修改 META-INF/druid-filter.properties 增加 Filter 配置

09 | 如何通过 Spring JDBC 访问数据库

Spring 的 JDBC 操作类

spring-jdbc

  • core,JdbcTemplate 等相关核心接口和类
  • datasource,数据源相关的辅助类
  • object,将基本的 JDBC 操作封装成对象
  • support,错误码等其他辅助工具

常用的 Bean 注解

通过注解定义 Bean

  • @Component
  • @Repository
  • @Service
  • @Controller
  • @RestController

简单的 JDBC 操作

JdbcTemplate

  • query
  • queryForObject
  • queryForList
  • update
  • execute

SQL 批处理

JdbcTemplate

  • batchUpdate
  • BatchPreparedStatementSetter

NamedParameterJdbcTemplate

  • batchUpdate
  • SqlParameterSourceUtils.createBatch

10 | 什么是 Spring 的事务抽象(上)

11 | 什么是 Spring 的事务抽象(下)

Spring 的事务抽象

一致的事务模型

  • JDBC/Hibernate/myBatis
  • DataSource/JTA

事务抽象的核心接口

PlatformTransactionManager

  • DataSourceTransactionManager
  • HibernateTransactionManager
  • JtaTransactionManager

TransactionDefinition

  • Propagation
  • Isolation
  • Timeout
  • Read-only status

事务传播特性

传播性 描述
PROPAGATION_REQUIRED 0 当前有事务就用当前的,没有就用新的
PROPAGATION_SUPPORTS 1 事务可有可无,不是必须的
PROPAGATION_MANDATORY 2 当前一定要有事务,不然就抛异常
PROPAGATION_REQUIRES_NEW 3 无论是否有事务,都起个新的事务
PROPAGATION_NOT_SUPPORTED 4 不支持事务,按非事务方式运行
PROPAGATION_NEVER 5 不支持事务,如果有事务则抛异常
PROPAGATION_NESTED 6 当前有事务就在当前事务里再起一个事务

事务隔离特性

隔离性 脏读 不可重复读 幻读
ISOLATION_READ_UNCOMMITTED 1 ✔️️️ ✔️️️ ✔️️️
ISOLATION_READ_COMMITTED 2 ✔️️️ ✔️️️
ISOLATION_REPEATABLE_READ 3 ✔️️️
ISOLATION_SERIALIZABLE 4

编程式事务

TransactionTemplate

  • TransactionCallback
  • TransactionCallbackWithoutResult

PlatformTransactionManager

  • 可以传入 TransactionDefinition 进行定义

声明式事务

开启事务注解的方式

  • @EnableTransactionManagement
  • <tx:annotation-driven/>

一些配置

  • proxyTargetClass
  • mode
  • order

@Transactional

  • transactionManager
  • propagation
  • isolation
  • timeout
  • readOnly
  • 怎么判断回滚

12 | 了解 Spring 的 JDBC 异常抽象

Spring 的 JDBC 异常抽象

Spring 会将数据操作的异常转换为 DataAccessException

无论使用何种数据访问方式,都能使用一样的异常

Spring 是怎么认识那些错误码的

通过 SQLErrorCodeSQLExceptionTranslator 解析错误码

ErrorCode 定义

  • org/springframework/jdbc/support/sql-error-codes.xml
  • Classpath 下的 sql-error-codes.xml

13 | 课程答疑(上)

14 | 课程答疑(下)

第三章:O/R Mapping 实践 (9 讲)

15 | 认识 Spring Data JPA

Java Persistence API

JPA 为对象关系映射提供了一种基于 POJO 的持久化模型

  • 简化数据持久化代码的开发工作
  • 为 Java 社区屏蔽不同持久化 API 的差异

Spring Data

在保留底层存储特性的同时,提供相对一致的、基于 Spring 的编程模型

主要模块

  • Spring Data Commons
  • Spring Data JDBC
  • Spring Data JPA
  • Spring Data Redis
  • ……

16 | 定义 JPA 的实体对象

常用 JPA 注解

实体

  • @Entity、@MappedSuperclass
  • @Table(name)

主键

  • @Id
  • @GeneratedValue(strategy, generator)
  • @SequenceGenerator(name, sequenceName)

映射

  • @Column(name, nullable, length, insertable, updatable)
  • @JoinTable(name)、@JoinColumn(name)

关系

  • @OneToOne、@OneToMany、@ManyToOne、@ManyToMany
  • @OrderBy

Lombok

Project Lombok 能够自动嵌入 IDE 和构建工具,提升开发效率

常用功能

  • @Getter / @Setter
  • @ToString
  • @NoArgsConstructor / @RequiredArgsConstructor / @AllArgsConstructor
  • @Data
  • @Builder
  • @Slf4j / @CommonsLog / @Log4j2

17 | 开始我们的线上咖啡馆实战项目:SpringBucks

18 | 通过 Spring Data JPA 操作数据库

Repository

@EnableJpaRepositories

Repository<T, ID> 接口

  • CrudRepository<T, ID>
  • PagingAndSortingRepository<T, ID>
  • JpaRepository<T, ID>

定义查询

根据方法名定义查询

  • find…By… / read…By… / query…By… / get…By…
  • count…By…
  • …OrderBy…[Asc / Desc]
  • And / Or / IgnoreCase
  • Top / First / Distinct

分页查询

  • PagingAndSortingRepository<T, ID>
  • Pageable / Sort
  • Slice<T> / Page<T>

19 | Spring Data JPA 的 Repository 是怎么从接口变成 Bean 的

Repository Bean 是如何创建的

JpaRepositoriesRegistrar

  • 激活了 @EnableJpaRepositories
  • 返回了 JpaRepositoryConfigExtension

RepositoryBeanDefinitionRegistrarSupport.registerBeanDefinitions

  • 注册 Repository Bean(类型是 JpaRepositoryFactoryBean)

RepositoryConfigurationExtensionSupport.getRepositoryConfigurations

  • 取得 Repository 配置

JpaRepositoryFactory.getTargetRepository

  • 创建了 Repository

接口中的方法是如何被解释的

RepositoryFactorySupport.getRepository 添加了 Advice

  • DefaultMethodInvokingMethodInterceptor
  • QueryExecutorMethodInterceptor

AbstractJpaQuery.execute 执行具体的查询

语法解析在 Part 中

20 | 通过 MyBatis 操作数据库

在 Spring 中使用 MyBatis

简单配置

  • mybatis.mapper-locations = classpath*:mapper/**/*.xml
  • mybatis.type-aliases-package = 类型别名的包名
  • mybatis.type-handlers-package = TypeHandler 扫描包名
  • mybatis.configuration.map-underscore-to-camel-case = true

Mapper 的定义与扫描

  • @MapperScan 配置扫描位置
  • @Mapper 定义接口
  • 映射的定义—— XML 与注解

21 | 让 MyBatis 更好用的那些工具:MyBatis Generator

MyBatis Generator(http://www.mybatis.org/generator/)

22 | 让 MyBatis 更好用的那些工具:MyBatis PageHelper

MyBatis PageHepler(https://pagehelper.github.io)

23 | SpringBucks 实战项目进度小结

第四章:NoSQL 实践 (7 讲)

24 | 通过 Docker 辅助开发

Docker 常用命令

镜像相关

  • docker pull <image>
  • docker search <image>

容器相关

  • docker run
  • docker start/stop <容器名>
  • docker ps <容器名>
  • docker logs <容器名>

docker run 的常用选项

docker run [OPTIONS] IMAGE [COMMAND] [ARG…]

选项说明

  • -d,后台运行容器
  • -e,设置环境变量
  • –expose / -p 宿主端口:容器端口
  • –name,指定容器名称
  • –link,链接不同容器
  • -v 宿主目录:容器目录,挂载磁盘卷

国内 Docker 镜像配置

官方 Docker Hub

官方镜像

阿里云镜像

25 | 在 Spring 中访问 MongoDB

Spring 对 MongoDB 的支持

  • Spring Data MongoDB
    • MongoTemplate
    • Repository 支持

Spring Data MongoDB 的基本用法

注解

  • @Document
  • @Id

MongoTemplate

  • save / remove
  • Criteria / Query / Update

Spring Data MongoDB 的 Repository

@EnableMongoRepositories

对应接口

  • MongoRepository<T, ID>
  • PagingAndSortingRepository<T, ID>
  • CrudRepository<T, ID>

26 | 在 Spring 中访问 Redis

Spring 对 Redis 的支持

  • Spring Data Redis
    • 支持的客户端 Jedis / Lettuce
    • RedisTemplate
    • Repository 支持

Jedis 客户端的简单使用

  • Jedis 不是线程安全的
  • 通过 JedisPool 获得 Jedis 实例
  • 直接使用 Jedis 中的方法

27 | Redis 的哨兵与集群模式

  • JedisSentinelPool
  • JedisCluster

28 | 了解 Spring 的缓存抽象

Spring 的缓存抽象

为不同的缓存提供一层抽象

  • 为 Java 方法增加缓存,缓存执行结果
  • 支持 ConcurrentMap、EhCache、Caffeine、JCache(JSR-107)
  • 接口
    • org.springframework.cache.Cache
    • org.springframework.cache.CacheManager

基于注解的缓存

@EnableCaching

  • @Cacheable
  • @CacheEvict
  • @CachePut
  • @Caching
  • @CacheConfig

29 | Redis 在 Spring 中的其他用法

与 Redis 建立连接

配置连接工厂

  • LettuceConnectionFactory 与 JedisConnectionFactory
    • RedisStandaloneConfiguration
    • RedisSentinelConfiguration
    • RedisClusterConfiguration

读写分离

Lettuce 内置支持读写分离

  • 只读主、只读从
  • 优先读主、优先读从

LettuceClientConfiguration

LettucePoolingClientConfiguration

LettuceClientConfigurationBuilderCustomizer

RedisTemplate

RedisTemplate<K, V>

  • opsForXxx()

StringRedisTemplate

Redis Repository

实体注解

  • @RedisHash
  • @Id
  • @Indexed

处理不同类型数据源的 Repository

如何区分这些 Repository

  • 根据实体的注解
  • 根据继承的接口类型
  • 扫描不同的包

30 | SpringBucks 实战项目进度小结

第五章:数据访问进阶 (8 讲)

31 | Project Reactor 介绍(上)

32 | Project Reactor 介绍(下)

一些核心的概念

Operators - Publisher / Subscriber

  • Nothing Happens Until You subscribe()
  • Flux [ 0..N ] - onNext()、onComplete()、onError()
  • Mono [ 0..1 ] - onNext()、onComplete()、onError()

Backpressure

  • Subscription
  • onRequest()、onCancel()、onDispose()

线程调度 Schedulers

  • immediate() / single() / newSingle()
  • elastic() / parallel() / newParallel()

错误处理

  • onError / onErrorReturn / onErrorResume
  • doOnError / doFinally

33 | 通过 Reactive 的方式访问 Redis

Spring Data Redis

Lettuce 能够支持 Reactive 方式

Spring Data Redis 中主要的支持

  • ReactiveRedisConnection
  • ReactiveRedisConnectionFactory
  • ReactiveRedisTemplate
  • opsForXxx()

34 | 通过 Reactive 的方式访问 MongoDB

Spring Data MongoDB

MongoDB 官方提供了支持 Reactive 的驱动

  • mongodb-driver-reactivestreams

Spring Data MongoDB 中主要的支持

  • ReactiveMongoClientFactoryBean
  • ReactiveMongoDatabaseFactory
  • ReactiveMongoTemplate

35 | 通过 Reactive 的方式访问 RDBMS

Spring Data R2DBC

R2DBC (https://spring.io/projects/spring-data-r2dbc)

  • Reactive Relational Database Connectivity

支持的数据库

  • Postgres(io.r2dbc:r2dbc-postgresql)
  • H2(io.r2dbc:r2dbc-h2)
  • Microsoft SQL Server(io.r2dbc:r2dbc-mssql)

一些主要的类

  • ConnectionFactory
  • DatabaseClient
    • execute().sql(SQL)
    • inTransaction(db -> {})
  • R2dbcExceptionTranslator
    • SqlErrorCodeR2dbcExceptionTranslator

R2DBC Repository 支持

一些主要的类

  • @EnableR2dbcRepositories
  • ReactiveCrudRepository<T, ID>
  • @Table / @Id
  • 其中的方法返回都是 Mono 或者 Flux
  • 自定义查询需要自己写 @Query

36 | 通过 AOP 打印数据访问层的摘要(上)

37 | 通过 AOP 打印数据访问层的摘要(下)

Spring AOP 的一些核心概念

概念 含义
Aspect 切面
Join Point 连接点,Spring AOP 里总是代表一次方法执行
Advice 通知,在连接点执行的动作
Pointcut 切入点,说明如何匹配连接点
Introduction 引入,为现有类型声明额外的方法和属性
Target object 目标对象
AOP proxy AOP 代理对象,可以是 JDK 动态代理,也可以是 CGLIB 代理
Weaving 织入,连接切面与目标对象或类型创建代理的过程

常用注解

  • @EnableAspectJAutoProxy
  • @Aspect
  • @Pointcut
  • @Before
  • @After / @AfterReturning / @AfterThrowing
  • @Around
  • @Order

如何打印 SQL

HikariCP

Alibaba Druid

38 | SpringBucks 实战项目进度小结

第六章:Spring MVC 实践 (14 讲)

39 | 编写第一个 Spring MVC Controller

认识 Spring MVC

DispatcherServlet

  • Controller
  • xxxResolver
  • ViewResolver
  • HandlerExceptionResolver
  • MultipartResolver
  • HandlerMapping

Spring MVC 中的常⽤用注解

  • @Controller
  • @RestController
  • @RequestMapping
  • @GetMapping / @PostMapping
  • @PutMapping / @DeleteMapping
  • @RequestBody / @ResponseBody / @ResponseStatus

40 | 理解 Spring 的应用上下文

Spring 的应用程序上下文

关于上下文常用的接口

  • BeanFactory
  • DefaultListableBeanFactory
  • ApplicationContext
  • ClassPathXmlApplicationContext
  • FileSystemXmlApplicationContext
  • AnnotationConfigApplicationContext
  • WebApplicationContext

41 | 理解请求的处理机制

一个请求的大致处理流程

绑定一些 Attribute

  • WebApplicationContext / LocaleResolver / ThemeResolver

处理 Multipart

  • 如果是,则将请求转为 MultipartHttpServletRequest

Handler 处理

  • 如果找到对应 Handler,执行 Controller 及前后置处理器逻辑处理返回的 Model ,呈现视图

42 | 如何定义处理方法(上)

定义映射关系

@Controller

@RequestMapping

  • path / method 指定映射路路径与⽅方法
  • params / headers 限定映射范围
  • consumes / produces 限定请求与响应格式

一些快捷方式

  • @RestController
  • @GetMapping / @PostMapping / @PutMapping / @DeleteMapping / @PatchMapping

定义处理方法

  • @RequestBody / @ResponseBody / @ResponseStatus
  • @PathVariable / @RequestParam / @RequestHeader
  • HttpEntity / ResponseEntity

定义类型转换

自己实现 WebMvcConfigurer

  • Spring Boot 在 WebMvcAutoConfiguration 中实现了一个
  • 添加自定义的 Converter
  • 添加自定义的 Formatter

定义校验

  • 通过 Validator 对绑定结果进行校验
    • Hibernate Validator
  • @Valid 注解
  • BindingResult

Multipart 上传

  • 配置 MultipartResolver
  • Spring Boot 自动配置 MultipartAutoConfiguration
  • 支持类型 multipart/form-data
  • MultipartFile 类型

43 | 如何定义处理方法(下)

44 | Spring MVC 中的视图解析机制(上)

45 | Spring MVC 中的视图解析机制(下)

视图解析的实现基础

ViewResolver 与 View 接口

  • AbstractCachingViewResolver
  • UrlBasedViewResolver
  • FreeMarkerViewResolver
  • ContentNegotiatingViewResolver
  • InternalResourceViewResolver

DispatcherServlet 中的视图解析逻辑

  • initStrategies()
    • initViewResolvers() 初始化了了对应 ViewResolver
  • doDispatch()
    • processDispatchResult()
      • 没有返回视图的话,尝试 RequestToViewNameTranslator
      • resolveViewName() 解析 View 对象

使用 @ResponseBody 的情况

  • 在 HandlerAdapter.handle() 的中完成了 Response 输出
    • RequestMappingHandlerAdapter.invokeHandlerMethod()
      • HandlerMethodReturnValueHandlerComposite.handleReturnValue()
        • RequestResponseBodyMethodProcessor.handleReturnValue()

重定向

两种不同的重定向前缀

  • redirect:
  • forward:

46 | Spring MVC 中的常用视图(上)

Spring MVC 支持的视图

支持的视图列表

配置 MessageConverter

  • 通过 WebMvcConfigurer 的 configureMessageConverters()
  • Spring Boot 自动查找 HttpMessageConverters 进行注册

Spring Boot 对 Jackson 的支持

  • JacksonAutoConfiguration
    • Spring Boot 通过 @JsonComponent 注册 JSON 序列化组件
    • Jackson2ObjectMapperBuilderCustomizer
  • JacksonHttpMessageConvertersConfiguration
    • 增加 jackson-dataformat-xml 以支持 XML 序列化

47 | Spring MVC 中的常用视图(下)

使用 Thymeleaf

添加 Thymeleaf 依赖

  • org.springframework.boot:spring-boot-starter-thymeleaf

Spring Boot 的自动配置

  • ThymeleafAutoConfiguration
    • ThymeleafViewResolver
Thymeleaf 的一些默认配置
  • spring.thymeleaf.cache=true
  • spring.thymeleaf.check-template=true
  • spring.thymeleaf.check-template-location=true
  • spring.thymeleaf.enabled=true
  • spring.thymeleaf.encoding=UTF-8
  • spring.thymeleaf.mode=HTML
  • spring.thymeleaf.servlet.content-type=text/html
  • spring.thymeleaf.prefix=classpath:/templates/
  • spring.thymeleaf.suffix=.html

48 | 静态资源与缓存

Spring Boot 中的静态资源配置

核心逻辑

  • WebMvcConfigurer.addResourceHandlers()

常用配置

  • spring.mvc.static-path-pattern=/**
  • spring.resources.static-locations=classpath:/META-INF/
    resources/,classpath:/resources/,classpath:/static/,classpath:/public/

Spring Boot 中的缓存配置

常用配置(默认时间单位都是秒)

  • ResourceProperties.Cache
  • spring.resources.cache.cachecontrol.max-age=时间
  • spring.resources.cache.cachecontrol.no-cache=true/false
  • spring.resources.cache.cachecontrol.s-max-age=时间

49 | Spring MVC 中的异常处理机制

Spring MVC 的异常解析

核心接口

  • HandlerExceptionResolver

实现类

  • SimpleMappingExceptionResolver
  • DefaultHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • ExceptionHandlerExceptionResolver

异常处理方法

处理方法

  • @ExceptionHandler

添加位置

  • @Controller / @RestController
  • @ControllerAdvice / @RestControllerAdvice

50 | 了解 Spring MVC 的切入点

Spring MVC 的拦截器

核心接口

  • HandlerInteceptor
    • boolean preHandle()
    • void postHandle()
    • void afterCompletion()

针对 @ResponseBody 和 ResponseEntity 的情况

  • ResponseBodyAdvice

针对异步请求的接口

  • AsyncHandlerInterceptor

拦截器的配置方式

常规方法

  • WebMvcConfigurer.addInterceptors()

Spring Boot 中的配置

  • 创建一个带 @Configuration 的 WebMvcConfigurer 配置类
  • 不能带 @EnableWebMvc(想彻底自己控制 MVC 配置除外)

51 | SpringBucks 实战项目进度小结

52 | 课程答疑

第七章:访问 Web 资源 (5 讲)

53 | 通过 RestTemplate 访问 Web 资源

Spring Boot 中的 RestTemplate

  • Spring Boot 中没有自动配置 RestTemplate
  • Spring Boot 提供了 RestTemplateBuilder
  • RestTemplateBuilder.build()

常用方法

GET 请求

  • getForObject() / getForEntity()

POST 请求

  • postForObject() / postForEntity()

PUT 请求

  • put()

DELETE 请求

  • delete()

构造 URI

构造 URI

  • UriComponentsBuilder

构造相对于当前请求的 URI

  • ServletUriComponentsBuilder

构造指向 Controller 的 URI

  • MvcUriComponentsBuilder

54 | RestTemplate 的高阶用法

传递 HTTP Header

  • RestTemplate.exchange()
  • RequestEntity<T> / ResponseEntity<T>

类型转换

  • JsonSerializer / JsonDeserializer
  • @JsonComponent

解析泛型对象

  • RestTemplate.exchange()
  • ParameterizedTypeReference<T>

55 | 简单定制 RestTemplate

RestTemplate ⽀支持的 HTTP 库

通用接口

  • ClientHttpRequestFactory

默认实现

  • SimpleClientHttpRequestFactory

Apache HttpComponents

  • HttpComponentsClientHttpRequestFactory

Netty

  • Netty4ClientHttpRequestFactory

OkHttp

  • OkHttp3ClientHttpRequestFactory

优化底层请求策略

连接管理

  • PoolingHttpClientConnectionManager
  • KeepAlive 策略

超时设置

  • connectTimeout / readTimeout

SSL 校验

  • 证书检查策略

56 | 通过 WebClient 访问 Web 资源

了解 WebClient

WebClient

  • 一个以 Reactive 方式处理 HTTP 请求的非阻塞式的客户端

支持的底层 HTTP 库

  • Reactor Netty - ReactorClientHttpConnector
  • Jetty ReactiveStream HttpClient - JettyClientHttpConnector

WebClient 的基本用法

创建 WebClient

  • WebClient.create()
  • WebClient.builder()

发起请求

  • get() / post() / put() / delete() / patch()

获得结果

  • retrieve() / exchange()

处理 HTTP Status

  • onStatus()

应答正文

  • bodyToMono() / bodyToFlux()

57 | SpringBucks 实战项目进度小结

第八章: Web 开发进阶 (9 讲)

58 | 设计好的 RESTful Web Service(上)

59 | 设计好的 RESTful Web Service(下)

如何实现 Restful Web Service

  • 识别资源
  • 选择合适的资源粒度
  • 设计 URI
  • 选择合适的 HTTP 方法和返回码
  • 设计资源的表述

识别资源

  • 找到领域名词
  • 能用 CRUD 操作的名词
  • 将资源组织为集合(即集合资源)
  • 将资源合并为复合资源
  • 计算或处理函数

资源的粒度

站在客户端的角度,要考虑

  • 可缓存性
  • 修改频率
  • 可变性

站在服务端的角度,要考虑

  • 网络效率
  • 表述的多少
  • 客户端的易用程度

构建更好的 URI

  • 使用域及子域对资源进行合理的分组或划分
  • 在 URI 的路径部分使用斜杠分隔符 ( / ) 来表示资源之间的层次关系
  • 在 URI 的路径部分使用逗号 ( , ) 和分号 ( ; ) 来表示非层次元素
  • 使用连字符 ( - ) 和下划线 ( _ ) 来改善长路径中名称的可读性
  • 在 URI 的查询部分使用“与”符号 ( & ) 来分隔参数
  • 在 URI 中避免出现文件扩展名 ( 例例如 .php,.aspx 和 .jsp )

60 | 什么是 HATEOAS

61 | 使用 Spring Data REST 实现简单的超媒体服务(上)

62 | 使用 Spring Data REST 实现简单的超媒体服务(下)

认识 HAL

HAL

  • Hypertext Application Language
  • HAL 是一种简单的格式,为 API 中的资源提供简单一致的链接

HAL 模型

  • 链接
  • 内嵌资源
  • 状态

Spring Data REST

Spring Boot 依赖

  • spring-boot-starter-data-rest

常用注解与类

  • @RepositoryRestResource
  • Resource<T>
  • PagedResource<T>

如何访问 HATEOAS 服务

配置 Jackson JSON

  • 注册 HAL 支持

操作超链接

  • 找到需要的 Link
  • 访问超链接

63 | 分布式环境中如何解决 Session 的问题

常见的会话解决方案

  • 粘性会话 Sticky Session
  • 会话复制 Session Replication
  • 集中会话 Centralized Session

认识 Spring Session

Spring Session

  • 简化集群中的用户会话管理
  • 无需绑定容器特定解决方案

支持的存储

  • Redis
  • MongoDB
  • JDBC
  • Hazelcast

实现原理

定制 HttpSession

  • 通过定制的 HttpServletRequest 返回定制的 HttpSession
  • SessionRepositoryRequestWrapper
  • SessionRepositoryFilter
  • DelegatingFilterProxy

基于 Redis 的 HttpSession

引入依赖

  • spring-session-data-redis

基本配置

  • @EnableRedisHttpSession
  • 提供 RedisConnectionFactory
  • 实现 AbstractHttpSessionApplicationInitializer
  • 配置 DelegatingFilterProxy

64 | 使用 WebFlux 代替 Spring MVC(上)

65 | 使用 WebFlux 代替 Spring MVC(下)

认识 WebFlux

什么是 WebFlux

  • 用于构建基于 Reactive 技术栈之上的 Web 应用程序
  • 基于 Reactive Streams API ,运行在非阻塞服务器上

为什么会有 WebFlux

  • 对于非阻塞 Web 应用的需要
  • 函数式编程

关于 WebFlux 的性能

  • 请求的耗时并不会有很大的改善
  • 仅需少量固定数量的线程和较少的内存即可实现扩展

WebMVC v.s. WebFlux

  • 已有 Spring MVC 应⽤用,运行正常,就别改了
  • 依赖了大量阻塞式持久化 API 和网络 API,建议使用 Spring MVC
  • 已经使用了非阻塞技术栈,可以考虑使用 WebFlux
  • 想要使用 Java 8 Lambda 结合轻量级函数式框架,可以考虑 WebFlux

WebFlux 中的编程模型

两种编程模型

  • 基于注解的控制器
  • 函数式 Endpoints

基于注解的控制器

常用注解

  • @Controller
  • @RequestMapping 及其等价注解
  • @RequestBody / @ResponseBody

返回值

  • Mono<T> / Flux<T>

66 | SpringBucks 实战项目进度小结

第九章:重新认识 Spring Boot (8 讲)

67 | 认识 Spring Boot 的组成部分

Spring Boot 的特性

  • 方便地创建可独立运行的 Spring 应用程序
  • 直接内嵌 Tomcat、Jetty 或 Undertow
  • 简化了项目的构建配置
  • 为 Spring 及第三方库提供自动配置
  • 提供生产级特性
  • 无需生成代码或进行 XML 配置

Spring Boot 的四大核心

  • 自动配置 - Auto Configuration
  • 起步依赖 - Starter Dependency
  • 命令行界面 - Spring Boot CLI
  • Actuator

68 | 了解自动配置的实现原理

了解自动配置

自动配置

  • 基于添加的 JAR 依赖自动对 Spring Boot 应⽤用程序进行配置
  • spring-boot-autoconfiguration

开启自动配置

  • @EnableAutoConfiguration
    • exclude = Class<?>[]
  • @SpringBootApplication

自动配置的实现原理

@EnableAutoConfiguration

  • AutoConfigurationImportSelector
  • META-INF/spring.factories
    • org.springframework.boot.autoconfigure.EnableAutoConfiguration

条件注解

  • @Conditional
  • @ConditionalOnClass
  • @ConditionalOnBean
  • @ConditionalOnMissingBean
  • @ConditionalOnProperty
  • ……

了解自动配置的情况

观察自动配置的判断结果

  • –debug

ConditionEvaluationReportLoggingListener

  • Positive matches
  • Negative matches
  • Exclusions
  • Unconditional classes

69 | 动手实现自己的自动配置

主要工作内容

  • 编写 Java Config
    • @Configuration
  • 添加条件
    • @Conditional
  • 定位自动配置
    • META-INF/spring.factories

条件注解

条件注解

  • @Conditional

类条件

  • @ConditionalOnClass
  • @ConditionalOnMissingClass

属性条件

  • @ConditionalOnProperty

Bean 条件

  • @ConditionalOnBean
  • @ConditionalOnMissingBean
  • **@ConditionalOnSingleCandidate**

资源条件

  • @ConditionalOnResource

Web 应用条件

  • @ConditionalOnWebApplication
  • @ConditionalOnNotWebApplication

其他条件

  • @ConditionalOnExpression
  • @ConditionalOnJava
  • @ConditionalOnJndi

自动配置的执行顺序

执行顺序

  • @AutoConfigureBefore
  • @AutoConfigureAfter
  • @AutoConfigureOrder

70 | 如何在低版本 Spring 中快速实现类似自动配置的功能

需求与问题

核心的诉求

  • 现存系统,不打算重构
  • Spring 版本 3.x,不打算升级版本和引入 Spring Boot
  • 期望能够在少改代码的前提下实现一些功能增强

面临的问题

  • 3.x 的 Spring 没有条件注解
  • 无法自动定位需要加载的自动配置

核心解决思路

条件判断

  • 通过 BeanFactoryPostProcessor 进行判断

配置加载

  • 编写 Java Config 类
  • 引入配置类
  • 通过 component-scan
  • 通过 xml 文件 import

Spring 的扩展点

BeanPostProcessor

  • 针对 Bean 实例

  • 在 Bean 创建后提供定制逻辑回调

BeanFactoryPostProcessor

  • 针对 Bean 定义

  • 在容器创建 Bean 前获取配置元数据

  • Java Config 中需要定义为 static 方法

关于 Bean 的一些定制

生命周期回调

  • InitializingBean / @PostConstruct / init-method
  • DisposableBean / @PostDestory / destroy-method

XXXAware

  • ApplicationContextAware
  • BeanFactoryAware
  • BeanNameAware

一些常用操作

判断类是否存在

  • ClassUtils.isPresent()

判断 Bean 是否已定义

  • ListableBeanFactory.containsBeanDefinition()
  • ListableBeanFactory.getBeanNamesForType()

注册 Bean 定义

  • BeanDefinitionRegistry.registerBeanDefinition()

    • GenericBeanDefinition
  • BeanFactory.registerSingleton()

71 | 了解起步依赖及其实现原理

Maven 依赖管理技巧

了解你的依赖

  • mvn dependency:tree
  • IDEA Maven Helper 插件

排除特定依赖

  • exclusion

统一管理依赖

  • dependencyManagement
  • Bill of Materials - bom

Spring Boot 的 starter 依赖

Starter Dependencies

  • 直接面向功能
  • 一站获得所有相关依赖,不再复制粘贴

官方的 Starters

  • spring-boot-starter-*

72 | 定制自己的起步依赖

主要内容

  • autoconfigure 模块,包含自动配置代码
  • starter 模块,包含指向自动配置模块的依赖及其他相关依赖

命名方式

  • xxx-spring-boot-autoconfigure
  • xxx-spring-boot-starter

注意事项

  • 不要使用 spring-boot 作为依赖的前缀
  • 不要使用 spring-boot 的配置命名空间
  • starter 中仅添加必要的依赖
  • 声明对 spring-boot-starter 的依赖

73 | 深挖 Spring Boot 的配置加载机制

外化配置加载顺序

  • 开启 DevTools 时,~/.spring-boot-devtools.properties
  • 测试类上的 @TestPropertySource 注解
  • @SpringBootTest#properties 属性
  • 命令行参数( --server.port=9000
  • SPRING_APPLICATION_JSON 中的属性
  • ServletConfig 初始化参数
  • ServletContext 初始化参数
  • java:comp/env 中的 JNDI 属性
  • System.getProperties()
  • 操作系统环境变量
  • random.* 涉及到的 RandomValuePropertySource
  • jar 包外部的 application-{profile}.properties 或 .yml
  • jar 包内部的 application-{profile}.properties 或 .yml
  • jar 包外部的 application.properties 或 .yml
  • jar 包内部的 application.properties 或 .yml
  • @Configuration 类上的 @PropertySource
  • SpringApplication.setDefaultProperties() 设置的默认属性

application.properties

默认位置

  • ./config
  • ./
  • CLASSPATH 中的 /config
  • CLASSPATH 中的 /

修改名字或路路径

  • spring.config.name
  • spring.config.location
  • spring.config.additional-location

Relaxed Binding

命名风格 使用范围 示例
短划线分隔 Properties 文件
YAML 文件
系统属性
geektime.spring-boot.first-demo
驼峰式 Properties 文件
YAML 文件
系统属性
geektime.springBoot.firstDemo
下划线分割 Properties 文件
YAML 文件
系统属性
geektime.spring_boot.first_demo
全⼤大写,下划线分隔 环境变量 GEEKTIME_SPRINGBOOT_FIRSTDEMO

74 | 理解配置背后的 PropertySource 抽象

添加 PropertySource

  • <context:property-placeholder>
  • PropertySourcesPlaceholderConfigurer
  • PropertyPlaceholderConfigurer
  • @PropertySource
  • @PropertySources

Spring Boot 中的 @ConfigurationProperties

  • 可以将属性绑定到结构化对象上
  • 支持 Relaxed Binding
  • 支持安全的类型转换
  • @EnableConfigurationProperties

定制 PropertySource

主要步骤

  • 实现 PropertySource<T>
  • Environment 取得 PropertySources
  • 将自己的 PropertySource 添加到合适的位置

切入位置

  • EnvironmentPostProcessor
  • BeanFactoryPostProcessor

第十章:运行中的 Spring Boot (11 讲)

75 | 认识 Spring Boot 的各类 Actuator Endpoint

Actuator

目的

  • 监控并管理应用程序

访问方式

  • HTTP
  • JMX

依赖

  • spring-boot-starter-actuator

一些常用 Endpoint

ID 说明 默认开启 默认 HTTP 默认 JMX
beans 显示容器中的 Bean 列表 Y N Y
caches 显示应用中的缓存 Y N Y
conditions 显示配置条件的计算情况 Y N Y
configprops 显示 @ConfigurationProperties 的信息 Y N Y
env 显示 ConfigurableEnvironment 中的属性 Y N Y
health 显示健康检查信息 Y Y Y
httptrace 显示 HTTP Trace 信息 Y N Y
info 显示设置好的应用信息 Y Y Y
loggers 显示并更新日志配置 Y N Y
metrics 显示应用的度量信息 Y N Y
mappings 显示所有的 @RequestMapping 信息 Y N Y
scheduledtasks 显示应用的调度任务信息 Y N Y
shutdown 优雅地关闭应用程序 N N Y
threaddump 执行 Thread Dump Y N Y
heapdump 返回 Heap Dump 文件,格式为 HPROF Y N N/A
prometheus 返回可供 Prometheus 抓取的信息 Y N N/A

如何访问 Actuator Endpoint

HTTP 访问

  • /actuator/<id>

端口与路径

  • management.server.address=
  • management.server.port=
  • management.endpoints.web.base-path=/actuator
  • management.endpoints.web.path-mapping.<id>=路径

开启 Endpoint

  • management.endpoint.<id>.enabled=true
  • management.endpoints.enabled-by-default=false

暴露 Endpoint

  • management.endpoints.jmx.exposure.exclude=
  • management.endpoints.jmx.exposure.include=*
  • management.endpoints.web.exposure.exclude=
  • management.endpoints.web.exposure.include=info, health

76 | 动手定制自己的 Health Indicator

Spring Boot 自带的 Health Indicator

目的

  • 检查应用程序的运行状态

状态

  • DOWN - 503
  • OUT_OF_SERVICE - 503
  • UP - 200
  • UNKNOWN - 200

机制

  • 通过 HealthIndicatorRegistry 收集信息
  • HealthIndicator 实现具体检查逻辑

配置项

  • management.health.defaults.enabled=true|false
  • management.health.<id>.enabled=true
  • management.endpoint.health.show-details=never|whenauthorized|always

内置 HealthIndicator 清单

  • CassandraHealthIndicator

  • ElasticsearchHealthIndicator

  • MongoHealthIndicator

  • SolrHealthIndicator

  • CouchbaseHealthIndicator

  • InfluxDbHealthIndicator

  • Neo4jHealthIndicator

  • DiskSpaceHealthIndicator

  • JmsHealthIndicator

  • RabbitHealthIndicator

  • DataSourceHealthIndicator

  • MailHealthIndicator

  • RedisHealthIndicator

自定义 Health Indicator

方法

  • 实现 HealthIndicator 接口
  • 根据自定义检查逻辑返回对应 Health 状态
  • Health 中包含状态和详细描述信息

77 | 通过 Micrometer 获取运行数据

认识 Micrometer

特性

  • 多维度度量量
  • 支持 Tag
  • 预置大量探针
  • 缓存、类加载器器、GC、CPU 利利⽤用率、线程池……
  • 与 Spring 深度整合

支持多种监控系统

  • Dimensional

    • AppOptics, Atlas, Azure Monitor, Cloudwatch, Datadog, Datadog StatsD, Dynatrace, Elastic, Humio, Influx, KairosDB, New Relic, Prometheus, SignalFx, Sysdig StatsD, Telegraf
      StatsD, Wavefront
  • Hierarchical

    • Graphite, Ganglia, JMX, Etsy StatsD

一些核心度量指标

核心接口

  • Meter

内置实现

  • Gauge, TimeGauge
  • Timer, LongTaskTimer, FunctionTimer
  • Counter, FunctionCounter
  • DistributionSummary

Micrometer in Spring Boot 2.x

一些 URL

  • /actuator/metrics
  • /actuator/prometheus

一些配置项

  • management.metrics.export.*
  • management.metrics.tags.*
  • management.metrics.enable.*
  • management.metrics.distribution.*
  • management.metrics.web.server.auto-time-requests

核心度量项

  • JVM、CPU、文件句柄数、日志、启动时间

其他度量项

  • Spring MVC、Spring WebFlux
  • Tomcat、Jersey JAX-RS
  • RestTemplate、WebClient
  • 缓存、数据源、Hibernate
  • Kafka、RabbitMQ

自定义度量指标

  • 通过 MeterRegistry 注册 Meter
  • 提供 MeterBinder Bean 让 Spring Boot ⾃自动绑定
  • 通过 MeterFilter 进⾏行行定制

78 | 通过 Spring Boot Admin 了解程序的运行状态

Spring Boot Admin

目的

  • 为 Spring Boot 应用程序提供一套管理界面

主要功能

  • 集中展示应用程序 Actuator 相关的内容
  • 变更通知

快速上手

服务端

  • de.codecentric:spring-boot-admin-starter-server:2.1.3
  • @EnableAdminServer

客户端

  • de.codecentric:spring-boot-admin-starter-client:2.1.3
  • 配置服务端及 Endpoint
  • spring.boot.admin.client.url=http://localhost:8080
  • management.endpoints.web.exposure.include=*

安全控制

安全相关依赖

  • spring-boot-starter-security

服务端配置

  • spring.security.user.name
  • spring.security.user.password

79 | 如何定制 Web 容器的运行参数

内嵌 Web 容器

可选容器列表

  • spring-boot-starter-tomcat
  • spring-boot-starter-jetty
  • spring-boot-starter-undertow
  • spring-boot-starter-reactor-netty

修改容器器配置

端口

  • server.port
  • server.address

压缩

  • server.compression.enabled
  • server.compression.min-response-size
  • server.compression.mime-types

Tomcat 特定配置

  • server.tomcat.max-connections=10000
  • server.tomcat.max-http-post-size=2MB
  • server.tomcat.max-swallow-size=2MB
  • server.tomcat.max-threads=200
  • server.tomcat.min-spare-threads=10

错误处理

  • server.error.path=/error
  • server.error.include-exception=false
  • server.error.include-stacktrace=never
  • server.error.whitelabel.enabled=true

其他

  • server.use-forward-headers
  • server.servlet.session.timeout

编程方式

  • WebServerFactoryCustomizer<T>
  • TomcatServletWebServerFactory
  • JettyServletWebServerFactory
  • UndertowServletWebServerFactory

80 | 如何配置容器支持 HTTP/2(上)

配置 HTTPS 支持

通过参数进行配置

  • server.port=8443
  • server.ssl.*
    • server.ssl.key-store
    • server.ssl.key-store-type,JKS 或者 PKCS12
    • server.ssl.key-store-password=secret

生成证书文件

命令

  • keytool -genkey -alias 别名
    • -storetype 仓库类型 -keyalg 算法 -keysize 长度
    • -keystore 文件名 -validity 有效期

说明

  • 仓库类型,JKS、JCEKS、PKCS12 等
  • 算法,RSA、DSA 等
  • 长度,例如 2048

客户端 HTTPS 支持

配置 HttpClient ( >= 4.4 )

  • SSLContextBuilder 构造 SSLContext
  • setSSLHostnameVerifier(new NoopHostnameVerifier())

配置 RequestFactory

  • HttpComponentsClientHttpRequestFactory
  • setHttpClient()

81 | 如何配置容器支持 HTTP/2(下)

配置 HTTP/2 支持

前提条件

  • Java >= JDK 9
  • Tomcat >= 9.0.0
  • Spring Boot 不支持 h2c,需要先配置 SSL

配置项

  • server.http2.enabled

客户端 HTTP/2 支持

HTTP 库选择

  • OkHttp( com.squareup.okhttp3:okhttp:3.14.0 )
  • OkHttpClient

RestTemplate 配置

  • OkHttp3ClientHttpRequestFactory

82 | 如何编写命令行运行的程序

关闭 Web 容器

控制依赖

  • 不添加 Web 相关依赖

配置方式

  • spring.main.web-application-type=none

编程方式

  • SpringApplication
  • setWebApplicationType()
  • SpringApplicationBuilder
  • web()
  • 在调用 SpringApplicationrun() 方法前设置 WebApplicationType

常用工具类

不同的 Runner

  • ApplicationRunner
  • 参数是 ApplicationArguments
  • CommandLineRunner
  • 参数是 String[]

返回码

  • ExitCodeGenerator

83 | 了解可执行 Jar 背后的秘密

认识可执行 Jar

其中包含

  • Jar 描述,META-INF/MANIFEST.MF
  • Spring Boot Loader,org/springframework/boot/loader
  • 项目内容,BOOT-INF/classes
  • 项目依赖,BOOT-INF/lib

其中不包含

  • JDK / JRE

如何找到程序的入口

Jar 的启动类

  • MANIFEST.MF
  • Main-Class: org.springframework.boot.loader.JarLauncher

项目的主类

  • @SpringApplication
  • MANIFEST.MF
  • Start-Class: xxx.yyy.zzz

84 | 如何将 Spring Boot 应用打包成 Docker 镜像文件

什么是 Docker 镜像

  • 镜像是静态的只读模板
  • 镜像中包含构建 Docker 容器器的指令
  • 镜像是分层的
  • 通过 Dockerfile 来创建镜像

Dockerfile

通过 Maven 构建 Docker 镜像

准备工作

  • 提供一个 Dockerfile
  • 配置 dockerfile-maven-plugin 插件

执行构建

  • mvn package
  • mvn dockerfile:build

检查结果

  • docker images

85 | SpringBucks 实战项目进度小结

第十一章:Spring Cloud 及 Cloud Native 概述 (5 讲)

86 | 简单理解微服务

微服务就是一些协同工作的小而自治的服务。

微服务的优点

  • 易于部署
  • 与组织结构对齐
  • 可组合性
  • 可替代性

微服务的代价

  • 架构复杂
  • 运维复杂

87 | 如何理解云原生(Cloud Native)

云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。

云原生应用要求

  • DevOps
  • 持续交付
  • 微服务
  • 容器

Cloud Native Computing Foundation,缩写 CNCF

88 | 12-Factor App(上)

89 | 12-Factor App(下)

12-Factor 为构建 SaaS 应用提供了方法论。

参考资料:https://12factor.net/zh_cn/

  • 基准代码 - 一份基准代码,多份部署。解决方案:git

  • 依赖 - 显式声明依赖关系。解决方案:maven、gradle

  • 配置 - 在环境中存储配置。解决方案:apollo

  • 后端服务 - 把后端服务当作附加资源

  • 构建,发布,运行 - 严格分离构建和运行。解决方案:CI/CD(如:jenkins、sonar 等)

  • 进程 - 以一个或多个无状态进程运行应用

  • 端口绑定 - 通过端口绑定提供服务

  • 并发 - 通过进程模型进行扩展

  • 易处理 - 快速启动和优雅终止可最大化健壮性

  • 开发环境与线上环境等价 - 尽可能的保持开发,预发布,线上环境相同

  • 日志 - 把日志当作事件流

  • 管理进程 - 后台管理任务当作一次性进程运行

90 | 认识 Spring Cloud 的组成部分

Spring Cloud 的主要功能

  • 服务发现
  • 服务熔断
  • 配置服务
  • 服务安全
  • 服务网关
  • 分布式消息
  • 分布式跟踪
  • 各种云平台支持

第十二章:服务注册与发现 (9 讲)

91 | 使用 Eureka 作为服务注册中心

  • SpringCloud 启动包
    • 服务端 - spring-cloud-starter-netflix-eureka-server
    • 客户端 - spring-cloud-starter-netflix-eureka-client
  • 注解
    • 服务端启动注解 - @EnableEurekaServer
    • 客户端启动注解
      • 通用注解 - @EnableDiscoveryClient
      • Eureka 特定注解 - @EnableEurekaClient
  • 要点
    • Eureka 默认端口 8761
  • 配置
    • eureka.client.serviceUrl.defaultZone - 注册地址,如 http://localhost:10001/eureka/
    • eureka.client.register-with-eureka - 是否将自己注册到 Eureka Server,默认为 true
    • eureka.client.fetch-registry - 是否从 Eureka Server 获取注册信息,默认为 true

92 | 使用 Spring Cloud Loadbalancer 访问服务

  • 如何获得服务地址
    • org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient
    • org.springframework.cloud.client.discovery.DiscoveryClient - 通用接口,推荐方式
  • 负载均衡客户端
    • @LoadBalanced
    • 实际是通过 ClientHttpRequestInterceptor 实现的
    • LoadBalancerInterceptor
    • LoadBalancerClient
    • RibbonLoadBalancerClient

93 | 使用 Feign 访问服务

声明式 REST Web 服务客户端

  • SpringCloud 启动包
    • spring-cloud-starter-openfeign
  • 注解
    • 启动注解 - @EnableFeignClients
    • 定义接口注解 - @FeignClient
  • 配置 - org.springframework.cloud.openfeign.FeignClientsConfiguration

94 | 深入理解服务发现背后的 DiscoveryClient

  • 服务端抽象接口 - org.springframework.cloud.client.serviceregistry.ServiceRegistry
    • EurekaServiceRegistry
    • EurekaRegistration
    • EurekaAutoServiceRegistration
    • EurekaClientAutoConfiguration
  • 客户端抽象接口 - org.springframework.cloud.client.discovery.DiscoveryClient
    • @EnableDiscoveryClient
  • 负载均衡抽象接口 - org.springframework.cloud.client.loadbalancer.LoadBalancerClient

95 | 使用 Zookeeper 作为服务注册中心

  • SpringCloud 启动包
    • spring-cloud-starter-zookeeper-discovery
  • 配置
    • ZookeeperAutoConfiguration
    • ZookeeperDiscoveryAutoConfiguration

96 | 使用 Consul 作为服务注册中心

  • SpringCloud 启动包
    • spring-cloud-starter-consul-discovery
  • 配置
    • ConsulAutoConfiguration

97 | 使用 Nacos 作为服务注册中心

  • SpringCloud 启动包
    • spring-cloud-starter-alibaba-nacos-discovery
  • 配置
    • NacosDiscoveryAutoConfiguration

98 | 如何定制自己的 DiscoveryClient

DiscoveryClient

  • EurekaDiscoveryClient
  • ZooKeeperDiscoveryClient
  • ConsulDiscoveryClient
  • NacosDiscoveryClient

LoadBalancerClient

  • RibbonLoadBalancerClient

自定义 DiscoveryClient 步骤

  • 返回该 DiscoveryClient 能提供的服务名列表
  • 返回指定服务对应的 ServiceInstance 列表
  • 返回 DiscoveryClient 的顺序
  • 返回 HealthIndicator 里显示的描述

自定义 RibbonClient 支持

  • 实现 ServerList<T extends Server>
  • Ribbon 提供了 AbstractServerList
  • 提供一个配置类,声明 ServerList Bean 实例

99 | SpringBucks 实战项目进度小结

第十三章:服务熔断 (7 讲)

100 | 使用 Hystrix 实现服务熔断(上)

断路器模式

在断路器对象中封装受保护的方法调用

该对象监控调用和断路情况

调用失败触发阈值后,后序调用直接由断路器返回错误,不再执行实际调用

101 | 使用 Hystrix 实现服务熔断(下)

  • Hystrix 应用

    • 注解
    • @HystrixCommand
      • fallbackMethod
      • commandProperties
        • @HystrixProperty
  • SpringCloud 启动包

    • spring-cloud-starter-netflix-hystrix
  • 注解

    • @EnableCircuitBreaker - 断路器开启注解
  • Feign 支持

    • feign.hystrix.enabled=true
    • @FeignClientfallback / fallbackFactory
  • 配置

    • HystrixCircuitBreakerAutoConfiguration

102 | 如何观察服务熔断

Spring Cloud 对于熔断的监控支持

  • Hystrix Metrics Stream
    • spring-boot-starter-actuator
    • /actuator/hystrix.stream
  • Hystrix Dashboard
    • spring-cloud-starter-netflix-hystrix-dashboard
    • @EnableHystrixDashboard
    • /hystirx

聚合集群熔断信息

  • SpringCloud 启动包 - spring-cloud-starter-netflix-turbines
  • 注解 - @EnableTurbine
  • /turbine/stream?cluster=集群名

103 | 使用 Resilience4j 实现服务熔断

Hystrix 官方已经停止维护,因此建议选择其他产品来替代。例如:Resilience4J

  • Resilience4J 实现

    • 基于 ConcurrentHashMap 的内存断路器
    • CircuitBreakerRegistry
    • CircuitBreakerConfig
  • Resilience4J 依赖

    • resilience4j-spring-boot2
  • 注解

    • @CircuitBreaker
  • 配置

    • CircuitBreakerProperties

104 | 使用 Resilience4j 实现服务限流(上)

Bulkhead

  • 目的
    • 防止下游依赖被并发请求冲击
    • 防止发生雪崩
  • 用法
    • BulkheadRegistry / BulkheadConfig
    • @Bulkhead(name = “xxx”)

105 | 使用 Resilience4j 实现服务限流(下)

RateLimit

  • 目的
    • 限制特定时间内的执行次数
  • 用法
    • RateLimiterRegistry / RateLimiterConfig
    • @RateLimiter
  • 配置
  • RateLimiterPropertis

106 | SpringBucks 实战项目进度小结

第十四章:服务配置 (7 讲)

107 | 基于 Git 的配置中心(上)

目的

提供针对外置配置的 HTTP API

  • SpringCloud 启动包 - spring-cloud-config-server
  • 注解 - @EnableConfigServer

108 | 基于 Git 的配置中心(下)

109 | 基于 Zookeeper 的配置中心

110 | 深入理解 Spring Cloud 的配置抽象

实现

  • 类似于 Spring 的 Environment 和 PropertySource
  • 在上下文中增加 Spring Cloud Config 的 PropertySource

PropertySource 子类

  • ZooKeeperPropertySource
  • ConsulPropertySource
  • ConsulFilePropertySource

PropertySourceLocator

EnvironmentRepositry

配置刷新

  • /actuator/refresh
  • Spring Cloud Bus - RegfreshRemoteApplicationEvent

ZooKeeperConfigBootstrapConfiguration

ZooKeeperConfigAutoConfiguration

111 | 基于 Consul 的配置中心

SpringCloud 启动包 - spring-cloud-starter-consual-config

配置文件 - bootstrap.propertis | yml

112 | 基于 Nacos 的配置中心

SpringCloud 启动包 - spring-cloud-starter-alibaba-nacos-config

配置文件 - bootstrap.propertis | yml

113 | SpringBucks 实战项目进度小结

第十五章:Spring Cloud Stream (4 讲)

114 | 认识 Spring Cloud Stream

Spring Cloud Stream 是一款用于构建消息驱动的微服务应用程序的轻量级框架。

特性

  • 声明式编程模型
  • 引入多种概念抽象:发布订阅、消费组、分区
  • 支持多种消息中间件:RabbitMQ、Kafka

概念

  • Binding
    • 生产者、消费者与 MQ 之间的桥梁
    • @EnableBinding
    • @Input /SubscribableChannel
    • @Output / MessageChannel
  • 消费组
  • 分区

生产消息

  • 使用 MessageChannel 的 send()
  • @SendTo

消费消息

  • @StreamListener
  • @Payload / @Headers / @Header

115 | 通过 Spring Cloud Stream 访问 RabbitMQ

SpringCloud 启动包 - spring-cloud-starter-stream-rabbit

SpringBoot 启动包 - spring-boot-starter-amqp

配置

org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration

116 | 通过 Spring Cloud Stream 访问 Kafka

SpringCloud 启动包 - spring-cloud-starter-stream-kafka

配置 - org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration

Spring 定时任务

  • TaskScheduler / Trigger / TriggerContext
  • 配置定时任务
    • @EnableScheduling
    • <task:scheduler />
    • @Scheduled

Spring 事件机制

  • ApplicationEvent
  • 发送事件
    • ApplicationEventPublisher
    • ApplicationEventPublisherAware
  • 监听事件
    • ApplicationListener<T>
    • @EventListener

117 | SpringBucks 实战项目进度小结

第十六章:服务链路追踪 (6 讲)

118 | 通过 Dapper 理解链路治理

119 | 使用 Spring Cloud Sleuth 实现链路追踪

SpringCloud 启动包 - spring-cloud-starter-sleuth、spring-cloud-starter-zipkin

120 | 如何追踪消息链路

121 | 除了链路还要治理什么

122 | SpringBucks 实战项目进度小结

参考资料

《24 讲吃透分布式数据库》笔记

开篇词 吃透分布式数据库,提升职场竞争力

导论:什么是分布式数据库?聊聊它的前世今生

分布式数据库的核心

数据分片

  • 水平分片:按行进行数据分割,数据被切割为一个个数据组,分散到不同节点上。

  • 垂直分片:按列进行数据切割,一个数据表的模式(Schema)被切割为多个小的模式。

数据同步

数据库同步用于帮助数据库恢复一致性。

分布式数据库发展就是一个由合到分,再到合的过程

  1. 早期的关系型商业数据库的分布式能力可以满足大部分用户的场景,因此产生了如 Oracle 等几种巨无霸数据库产品;
  2. OLAP 领域首先寻求突破,演化出了大数据技术与 MPP 类型数据库,提供功能更强的数据分析能力;
  3. 去 IOE 引入数据库中间件,并结合应用平台与开源单机数据库形成新一代解决方案,让商业关系型数据库走下神坛,NoSQL 数据库更进一步打破了关系型数据库唯我独尊的江湖地位;
  4. 新一代分布式 OLTP 数据库正式完成了分布式领域对数据库核心特性的完整支持,它代表了分布式数据库从此走向了成熟,也表明了 OLAP 与 OLTP 分布式场景下,分别在各自领域内取得了胜利;
  5. HTAP 和多模式数据处理的引入,再一次将 OLAP 与 OLTP 融合,从而将分布式数据库推向如传统商业关系型数据库数十年前那般的盛况,而其产生的影响要比后者更为深远。

SQL vs NoSQL:一次搞清楚五花八门的“SQL”

数据分片:如何存储超大规模的数据?

数据分片概论

想提升系统对于数据的处理,有两种思路:

垂直扩展:提升硬件设备,获得更好的 CPU、更大的内存。但这种方式容易达到瓶颈。

水平扩展:采用分而治之的思想,将数据拆分成多个分区,分散到一组便宜的机器上。这种方式性价比更高,不过也会引入数据同步等复杂的问题。

分片算法

哈希分片

范围分片

分布式 ID

UUID:性能较差,且离散度不高

雪花算法

数据复制:如何保证数据在分布式场景下的高可用?

主从复制

  • 复制同步模式
    • 同步复制:如果由于从库已崩溃,存在网络故障或其他原因而没有响应,则主库也无法写入该数据。
    • 半同步复制:其中部分从库进行同步复制,而其他从库进行异步复制。也就是,如果其中一个从库同步确认,主库可以写入该数据。
    • 异步复制:不管从库的复制情况如何,主库可以写入该数据。而此时,如果主库失效,那么还未同步到从库的数据就会丢失。
  • 复制延迟 - 提高系统的查询性能,可以通过添加从节点来实现。但是如果使用同步复制,每次写入都需要同步所有从节点,会造成一部分从节点已经有数据,但是主节点还没写入数据。而异步复制的问题是从节点的数据可能不是最新的。
  • 复制与高可用性
    • 从节点故障。由于每个节点都复制了从主库那里收到的数据更改日志,因此它知道在发生故障之前已处理的最后一个事务,由此可以凭借此信息从主节点或其他从节点那里恢复自己的数据。
    • 主节点故障。在这种情况下,需要在从节点中选择一个成为新的主节点,此过程称为故障转移,可以手动或自动触发。其典型过程为:第一步根据超时时间确定主节点离线;第二步选择新的主节点,这里注意新的主节点通常应该与旧的主节点数据最为接近;第三步是重置系统,让它成为新的主节点。
  • 复制方式
    • 基于语句的复制:主库记录它所执行的每个写请求(一般以 SQL 语句形式保存),每个从库解析并执行该语句,就像从客户端收到该语句一样。但这种复制会有一些潜在问题,如语句使用了获取当前时间的函数,复制后会在不同数据节点上产生不同的值。另外如自增列、触发器、存储过程和函数都可能在复制后产生意想不到的问题。但可以通过预处理规避这些问题。使用该复制方式的分布式数据库有 VoltDB、Calvin。
    • 日志(WAL)同步:WAL 是一组字节序列,其中包含对数据库的所有写操作。它的内容是一组低级操作,如向磁盘的某个页面的某个数据块写入一段二进制数据,主库通过网络将这样的数据发送给从库。这种方法避免了上面提到的语句中部分操作复制后产生的一些副作用,但要求主从的数据库引擎完全一致,最好版本也要一致。如果要升级从库版本,那么就需要计划外停机。PostgreSQL 和 Oracle 中使用了此方法。
    • 行复制:它由一系列记录组成,这些记录描述了以行的粒度对数据库表进行的写操作。它与特定存储引擎解耦,并且第三方应用可以很容易解析其数据格式。
    • ETL 工具:该功能一般是最灵活的方式。用户可以根据自己的业务来设计复制的范围和机制,同时在复制过程中还可以进行如过滤、转换和压缩等操作。但性能一般较低,故适合处理子数据集的场景。

一致性与 CAP 模型:为什么需要分布式一致性?

实践:设计一个最简单的分布式数据库

概要:什么是存储引擎,为什么需要了解它?

存储引擎

数据库的一般架构

  1. 传输层:它是接受客户端请求的一层。用来处理网络协议。同时,在分布式数据库中,它还承担着节点间互相通信的职责。
  2. 查询层:请求从传输层被发送到查询层。在查询层,协议被进行解析,如 SQL 解析;后进行验证与分析;最后结合访问控制来决定该请求是否要被执行。解析完成后,请求被发送到查询优化器,在这里根据预制的规则,数据分布并结合数据库内部的统计,会生成该请求的执行计划。执行计划一般是树状的,包含一系列相关的操作,用于从数据库中查询到请求希望获取的数据。
  3. 执行层:执行计划被发送到执行层去运行。执行层一般包含本地运行单元与远程运行单元。根据执行计划,调用不同的单元,而后将结果合并返回到传输层。执行层本地运行单元其实就是存储引擎。它一般包含如下一些功能
    1. 事务管理器:用来调度事务并保证数据库的内部一致性(这与模块一中讨论的分布式一致性是不同的);
    2. 锁管理:保证操作共享对象时候的一致性,包括事务、修改数据库参数都会使用到它;
    3. 存储结构:包含各种物理存储层,描述了数据与索引是如何组织在磁盘上的;
    4. 内存结构:主要包含缓存与缓冲管理,数据一般是批量输入磁盘的,写入之前会使用内存去缓存数据;
    5. 提交日志:当数据库崩溃后,可以使用提交日志恢复系统的一致性状态。

内存与磁盘

内存特点:查询快、更昂贵、持久化比较复杂

行式存储与列式存储

列式存储非常适合处理分析聚合类型的任务,如计算数据趋势、平均值,等等。因为这些数据一般需要加载一列的所有行,而不关心的列数据不会被读取,从而获得了更高的性能。

数据文件与索引文件

数据文件最传统的形式为堆组织表(Heap-Organized Table),数据的放置没有一个特别的顺序,一般是按照写入的先后顺序排布。这种数据文件需要一定额外的索引帮助来查找数据。

索引文件的分类模式一般为主键索引与二级索引两类。前者是建立在主键上的,它可能是一个字段或多个字段组成。而其他类型的索引都被称为二级索引。主键索引与数据是一对一关系,而二级索引很有可能是一对多的关系,即多个索引条目指向一条数据。

面向分布式的存储引擎特点

内存型数据库会倾向于选择分布式模式来进行构建。原因也是显而易见的,由于单机内存容量相比磁盘来说是很小的,故需要构建分布式数据库来满足业务所需要的容量。

列式存储也与分布式数据库存在天然的联系。原因是针对 OLAP 的分析数据库,一个非常大的应用场景就是要分析所有数据。

分布式索引:如何在集群中快速定位数据?

读取路径

存储引擎处理查询请求一般流程:

  1. 寻找分片和目标节点;
  2. 检查数据是否在缓存与缓冲中;
  3. 检查数据是否在磁盘文件中;
  4. 合并结果。

索引数据表

SSTable 文件是一个排序的、不可变的、持久化的键值对结构,其中键值对可以是任意字节的字符串,支持使用指定键来查找值,或通过给定键范围遍历所有的键值对。每个 SSTable 文件包含一系列的块。SSTable 文件中的块索引(这些块索引通常保存在文件尾部区域)用于定位块,这些块索引在 SSTable 文件被打开时加载到内存。在查找时首先从内存中的索引二分查找找到块,然后一次磁盘寻道即可读取到相应的块。另一种方式是将 SSTable 文件完全加载到内存,从而在查找和扫描中就不需要读取磁盘。

内存缓冲

内存中常用的快速搜索数据结构是跳表。典型代表:Redis 使用跳表实现 zset。

布隆过滤

日志型存储:为什么选择它作为底层存储?

事务处理与恢复(上):数据库崩溃后如何保证数据不丢失?

事务处理与恢复(下):如何控制并发事务?

引擎拓展:解读当前流行的分布式存储引擎

概要:分布式系统都要解决哪些问题?

错误侦测:如何保证分布式系统稳定?

领导选举:如何在分布式系统内安全地协调操作?

再谈一致性:除了 CAP 之外的一致性模型还有哪些?

数据可靠传播:反熵理论如何帮助数据库可靠工作?

分布式事务(上):除了 XA,还有哪些原子提交算法吗?

分布式事务(下):Spanner 与 Calvin

共识算法:一次性说清楚 Paxos、Raft 等算法的区别

知识串讲:如何取得性能和可扩展性的平衡?

发展与局限:传统数据库在分布式领域的探索

数据库中间件:传统数据库向分布式数据库的过渡

现状解读:分布式数据库的最新发展情况

加餐 1 概念解析:云原生、HTAP、图与内存数据库

加餐 2 数据库选型:我们该用什么分布式数据库?

参考资料

《Dubbo 源码解读与实战》笔记

开篇词 深入掌握 Dubbo 原理与实现,提升你的职场竞争力

Apache Dubbo是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:

  • 面向接口的远程方法调用;
  • 可靠、智能的容错和负载均衡;
  • 服务自动注册和发现能力。

Dubbo 是一个分布式服务框架,致力于提供高性能、透明化的 RPC 远程服务调用方案以及服务治理方案,以帮助我们解决微服务架构落地时的问题。

Dubbo 源码环境搭建:千里之行,始于足下

Dubbo 核心组件

Registry - 注册中心。负责服务地址的注册与查找,服务的 Provider 和 Consumer 只在启动时与注册中心交互。注册中心通过长连接感知 Provider 的存在,在 Provider 出现宕机的时候,注册中心会立即推送相关事件通知 Consumer。

Provider - 服务提供者。在它启动的时候,会向 Registry 进行注册操作,将自己服务的地址和相关配置信息封装成 URL 添加到 ZooKeeper 中。

Consumer - 服务消费者。在它启动的时候,会向 Registry 进行订阅操作。订阅操作会从 ZooKeeper 中获取 Provider 注册的 URL,并在 ZooKeeper 中添加相应的监听器。获取到 Provider URL 之后,Consumer 会根据负载均衡算法从多个 Provider 中选择一个 Provider 并与其建立连接,最后发起对 Provider 的 RPC 调用。 如果 Provider URL 发生变更,Consumer 将会通过之前订阅过程中在注册中心添加的监听器,获取到最新的 Provider URL 信息,进行相应的调整,比如断开与宕机 Provider 的连接,并与新的 Provider 建立连接。Consumer 与 Provider 建立的是长连接,且 Consumer 会缓存 Provider 信息,所以一旦连接建立,即使注册中心宕机,也不会影响已运行的 Provider 和 Consumer。

Monitor - 监控中心。用于统计服务的调用次数和调用时间。Provider 和 Consumer 在运行过程中,会在内存中统计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。监控中心在上面的架构图中并不是必要角色,监控中心宕机不会影响 Provider、Consumer 以及 Registry 的功能,只会丢失监控数据而已。

Container - 服务运行容器。

Dubbo 核心模块

  • dubbo-common 模块: Dubbo 的一个公共模块,其中有很多工具类以及公共逻辑,如 Dubbo SPI 实现、时间轮实现、动态编译器等。
  • dubbo-remoting 模块: Dubbo 的远程通信模块,其中的子模块依赖各种开源组件实现远程通信。在 dubbo-remoting-api 子模块中定义该模块的抽象概念,在其他子模块中依赖其他开源组件进行实现,例如,dubbo-remoting-netty4 子模块依赖 Netty 4 实现远程通信,dubbo-remoting-zookeeper 通过 Apache Curator 实现与 ZooKeeper 集群的交互。
  • dubbo-rpc 模块: Dubbo 中对远程调用协议进行抽象的模块,其中抽象了各种协议,依赖于 dubbo-remoting 模块的远程调用功能。dubbo-rpc-api 子模块是核心抽象,其他子模块是针对具体协议的实现,例如,dubbo-rpc-dubbo 子模块是对 Dubbo 协议的实现,依赖了 dubbo-remoting-netty4 等 dubbo-remoting 子模块。 dubbo-rpc 模块的实现中只包含一对一的调用,不关心集群的相关内容。
  • dubbo-cluster 模块: Dubbo 中负责管理集群的模块,提供了负载均衡、容错、路由等一系列集群相关的功能,最终的目的是将多个 Provider 伪装为一个 Provider,这样 Consumer 就可以像调用一个 Provider 那样调用 Provider 集群了。
  • dubbo-registry 模块: Dubbo 中负责与多种开源注册中心进行交互的模块,提供注册中心的能力。其中, dubbo-registry-api 子模块是顶层抽象,其他子模块是针对具体开源注册中心组件的具体实现,例如,dubbo-registry-zookeeper 子模块是 Dubbo 接入 ZooKeeper 的具体实现。
  • dubbo-monitor 模块: Dubbo 的监控模块,主要用于统计服务调用次数、调用时间以及实现调用链跟踪的服务。
  • dubbo-config 模块: Dubbo 对外暴露的配置都是由该模块进行解析的。例如,dubbo-config-api 子模块负责处理 API 方式使用时的相关配置,dubbo-config-spring 子模块负责处理与 Spring 集成使用时的相关配置方式。有了 dubbo-config 模块,用户只需要了解 Dubbo 配置的规则即可,无须了解 Dubbo 内部的细节。
  • dubbo-metadata 模块: Dubbo 的元数据模块。dubbo-metadata 模块的实现套路也是有一个 api 子模块进行抽象,然后其他子模块进行具体实现。
  • dubbo-configcenter 模块: Dubbo 的动态配置模块,主要负责外部化配置以及服务治理规则的存储与通知,提供了多个子模块用来接入多种开源的服务发现组件。

Dubbo 的配置总线:抓住 URL,就理解了半个 Dubbo

Dubbo 中任意的一个实现都可以抽象为一个 URL,Dubbo 使用 URL 来统一描述了所有对象和配置信息,并贯穿在整个 Dubbo 框架之中。Dubbo URL 格式如下:

1
protocol://username:password@host:port/path?key=value&key=value
  • protocol:URL 的协议。我们常见的就是 HTTP 协议和 HTTPS 协议,当然,还有其他协议,如 FTP 协议、SMTP 协议等。
  • username/password:用户名/密码。 HTTP Basic Authentication 中多会使用在 URL 的协议之后直接携带用户名和密码的方式。
  • host/port:主机/端口。在实践中一般会使用域名,而不是使用具体的 host 和 port。
  • path:请求的路径。
  • parameters:参数键值对。一般在 GET 请求中会将参数放到 URL 中,POST 请求会将参数放到请求体中。

Dubbo 中和 URL 相关的核心类:

  • URL - 定义了 URL 的结构;
  • URLBuilder, 辅助构造 URL;
  • URLStrParser, 将字符串解析成 URL 对象。

Dubbo 中的 URL 示例

URL 在 SPI 中的应用:RegistryFactory.getRegistry() 方法。

URL 在服务暴露中的应用:ZookeeperRegistry.doRegister() 方法。

URL 在服务订阅中的应用:Registry.doSubscribe() 方法

Dubbo SPI 精析,接口实现两极反转(上)

Dubbo 通过 SPI 机制来实现微内核架构,以达到 OCP 原则(即“对扩展开放,对修改封闭”的原则)。

JDK SPI 要点:

  • 在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件
  • 此文件记录了该 jar 包提供的服务接口的具体实现类

JDK SPI 源码分析

ServiceLoader.load() 方法,首先会尝试获取当前使用的 ClassLoader;查找失败后使用 SystemClassLoader;然后调用 reload() 方法。

在 reload() 方法中,首先会清理 providers 缓存(LinkedHashMap 类型的集合),该缓存用来记录 ServiceLoader 创建的实现对象,其中 Key 为实现类的完整类名,Value 为实现类的对象。之后创建 LazyIterator 迭代器,用于读取 SPI 配置文件并实例化实现类对象。

Dubbo SPI 精析,接口实现两极反转(下)

Dubbo 按照 SPI 配置文件的用途,将其分成了三类目录。

  • META-INF/services/ 目录:该目录下的 SPI 配置文件用来兼容 JDK SPI 。
  • META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件。
  • META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件。

Dubbo 将 SPI 配置文件改成了 KV 格式,例如:

1
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

SPI 核心实现

@SPI 注解

Dubbo SPI 的核心逻辑几乎都封装在 ExtensionLoader 之中。

ExtensionLoader 中三个核心的静态字段。

  • strategies(LoadingStrategy[]类型): LoadingStrategy 接口有三个实现(通过 JDK SPI 方式加载的),分别对应前面介绍的三个 Dubbo SPI 配置文件所在的目录
  • EXTENSION_LOADERS(ConcurrentMap<Class, ExtensionLoader>类型) :Dubbo 中一个扩展接口对应一个 ExtensionLoader 实例,该集合缓存了全部 ExtensionLoader 实例,其中的 Key 为扩展接口,Value 为加载其扩展实现的 ExtensionLoader 实例。
  • EXTENSION_INSTANCES(ConcurrentMap<Class<?>, Object>类型):该集合缓存了扩展实现类与其实例对象的映射关系。在前文示例中,Key 为 Class,Value 为 DubboProtocol 对象。

海量定时任务,一个时间轮搞定

时间轮是一种高效的、批量管理定时任务的调度模型。时间轮一般会实现成一个环形结构,类似一个时钟,分为很多槽,一个槽代表一个时间间隔,每个槽使用双向链表存储定时任务;指针周期性地跳动,跳动到一个槽位,就执行该槽位的定时任务。

需要注意的是,单层时间轮的容量和精度都是有限的,对于精度要求特别高、时间跨度特别大或是海量定时任务需要调度的场景,通常会使用多级时间轮以及持久化存储与时间轮结合的方案。

核心接口和类:

  • TimerTask 接口
  • Timer 接口
  • Timeout 接口
  • HashedWheelTimeout 类
  • HashedWheelBucket 类
  • HashedWheelTimer 类

ZooKeeper 与 Curator,求你别用 ZkClient 了(上)

Dubbo 目前支持 Consul、etcd、Nacos、ZooKeeper、Redis 等多种开源组件作为注册中心,并且在 Dubbo 源码也有相应的接入模块。

ZooKeeper 是一个针对分布式系统的、可靠的、可扩展的协调服务,它通常作为统一命名服务、统一配置管理、注册中心(分布式集群管理)、分布式锁服务、Leader 选举服务等角色出现。

ZooKeeper 集群中的角色

  • Client 节点:从业务角度来看,这是分布式应用中的一个节点,通过 ZkClient 或是其他 ZooKeeper 客户端与 ZooKeeper 集群中的一个 Server 实例维持长连接,并定时发送心跳。从 ZooKeeper 集群的角度来看,它是 ZooKeeper 集群的一个客户端,可以主动查询或操作 ZooKeeper 集群中的数据,也可以在某些 ZooKeeper 节点(ZNode)上添加监听。当被监听的 ZNode 节点发生变化时,例如,该 ZNode 节点被删除、新增子节点或是其中数据被修改等,ZooKeeper 集群都会立即通过长连接通知 Client。
  • Leader 节点:ZooKeeper 集群的主节点,负责整个 ZooKeeper 集群的写操作,保证集群内事务处理的顺序性。同时,还要负责整个集群中所有 Follower 节点与 Observer 节点的数据同步。
  • Follower 节点:ZooKeeper 集群中的从节点,可以接收 Client 读请求并向 Client 返回结果,并不处理写请求,而是转发到 Leader 节点完成写入操作。另外,Follower 节点还会参与 Leader 节点的选举。
  • Observer 节点:ZooKeeper 集群中特殊的从节点,不会参与 Leader 节点的选举,其他功能与 Follower 节点相同。引入 Observer 角色的目的是增加 ZooKeeper 集群读操作的吞吐量,如果单纯依靠增加 Follower 节点来提高 ZooKeeper 的读吞吐量,那么有一个很严重的副作用,就是 ZooKeeper 集群的写能力会大大降低,因为 ZooKeeper 写数据时需要 Leader 将写操作同步给半数以上的 Follower 节点。引入 Observer 节点使得 ZooKeeper 集群在写能力不降低的情况下,大大提升了读操作的吞吐量。

ZNode 节点类型有如下四种:

  • 持久节点。 持久节点创建后,会一直存在,不会因创建该节点的 Client 会话失效而删除。
  • 持久顺序节点。 持久顺序节点的基本特性与持久节点一致,创建节点的过程中,ZooKeeper 会在其名字后自动追加一个单调增长的数字后缀,作为新的节点名。
  • 临时节点。 创建临时节点的 ZooKeeper Client 会话失效之后,其创建的临时节点会被 ZooKeeper 集群自动删除。与持久节点的另一点区别是,临时节点下面不能再创建子节点。
  • 临时顺序节点。 基本特性与临时节点一致,创建节点的过程中,ZooKeeper 会在其名字后自动追加一个单调增长的数字后缀,作为新的节点名。

ZooKeeper 与 Curator,求你别用 ZkClient 了(下)

代理模式与常见实现

代理模式

JDK 动态代理

JDK 动态代理的核心是 InvocationHandler 接口。

CGLIB

CGLib(Code Generation Library)是一个基于 ASM 的字节码生成库。它允许我们在运行时对字节码进行修改和动态生成。CGLib 采用字节码技术实现动态代理功能,其底层原理是通过字节码技术为目标类生成一个子类,并在该子类中采用方法拦截的方式拦截所有父类方法的调用,从而实现代理的功能。

因为 CGLib 使用生成子类的方式实现动态代理,所以无法代理 final 关键字修饰的方法(因为 final 方法是不能够被重写的)。这样的话,CGLib 与 JDK 动态代理之间可以相互补充:在目标类实现接口时,使用 JDK 动态代理创建代理对象;当目标类没有实现接口时,使用 CGLib 实现动态代理的功能。在 Spring、MyBatis 等多种开源框架中,都可以看到 JDK 动态代理与 CGLib 结合使用的场景。

CGLib 的实现有两个重要的成员组成。

  • Enhancer:指定要代理的目标对象以及实际处理代理逻辑的对象,最终通过调用 create() 方法得到代理对象,对这个对象所有的非 final 方法的调用都会转发给 MethodInterceptor 进行处理。
  • MethodInterceptor:动态代理对象的方法调用都会转发到 intercept 方法进行增强。

Javassist

Javassist 是一个开源的生成 Java 字节码的类库,其主要优点在于简单、快速,直接使用 Javassist 提供的 Java API 就能动态修改类的结构,或是动态生成类。

Netty 入门,用它做网络编程都说好(上)

Netty I/O 模型设计

传统阻塞 I/O 模型

I/O 多路复用模型

针对传统的阻塞 I/O 模型的缺点,I/O 复用的模型在性能方面有不小的提升。I/O 复用模型中的多个连接会共用一个 Selector 对象,由 Selector 感知连接的读写事件,而此时的线程数并不需要和连接数一致,只需要很少的线程定期从 Selector 上查询连接的读写状态即可,无须大量线程阻塞等待连接。当某个连接有新的数据可以处理时,操作系统会通知线程,线程从阻塞状态返回,开始进行读写操作以及后续的业务逻辑处理。

Netty 就是采用了上述 I/O 复用的模型。由于多路复用器 Selector 的存在,可以同时并发处理成百上千个网络连接,大大增加了服务器的处理能力。另外,Selector 并不会阻塞线程,也就是说当一个连接不可读或不可写的时候,线程可以去处理其他可读或可写的连接,这就充分提升了 I/O 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程切换。

Netty 线程模型设计

Netty 采用了 Reactor 线程模型的设计。 Reactor 模式,也被称为 Dispatcher 模式,核心原理是 Selector 负责监听 I/O 事件,在监听到 I/O 事件之后,分发(Dispatch)给相关线程进行处理

单 Reactor 单线程

Reactor 对象监听客户端请求事件,收到事件后通过 Dispatch 进行分发。如果是连接建立的事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接建立之后的业务请求。如果不是连接建立的事件,而是数据的读写事件,则 Reactor 会将事件分发对应的 Handler 来处理,由这里唯一的线程调用 Handler 对象来完成读取数据、业务处理、发送响应的完整流程。当然,该过程中也可能会出现连接不可读或不可写等情况,该单线程会去执行其他 Handler 的逻辑,而不是阻塞等待。

单 Reactor 单线程的优点就是:线程模型简单,没有引入多线程,自然也就没有多线程并发和竞争的问题。

但其缺点也非常明显,那就是性能瓶颈问题,一个线程只能跑在一个 CPU 上,能处理的连接数是有限的,无法完全发挥多核 CPU 的优势。一旦某个业务逻辑耗时较长,这唯一的线程就会卡在上面,无法处理其他连接的请求,程序进入假死的状态,可用性也就降低了。正是由于这种限制,一般只会在客户端使用这种线程模型。

单 Reactor 多线程

在单 Reactor 多线程的架构中,Reactor 监控到客户端请求之后,如果连接建立的请求,则由 Acceptor 通过 accept 处理,然后创建一个 Handler 对象处理连接建立之后的业务请求。如果不是连接建立请求,则 Reactor 会将事件分发给调用连接对应的 Handler 来处理。到此为止,该流程与单 Reactor 单线程的模型基本一致,唯一的区别就是执行 Handler 逻辑的线程隶属于一个线程池

很明显,单 Reactor 多线程的模型可以充分利用多核 CPU 的处理能力,提高整个系统的吞吐量,但引入多线程模型就要考虑线程并发、数据共享、线程调度等问题。在这个模型中,只有一个线程来处理 Reactor 监听到的所有 I/O 事件,其中就包括连接建立事件以及读写事件,当连接数不断增大的时候,这个唯一的 Reactor 线程也会遇到瓶颈。

主从 Reactor 多线程

为了解决单 Reactor 多线程模型中的问题,我们可以引入多个 Reactor。其中,Reactor 主线程负责通过 Acceptor 对象处理 MainReactor 监听到的连接建立事件,当 Acceptor 完成网络连接的建立之后,MainReactor 会将建立好的连接分配给 SubReactor 进行后续监听。

当一个连接被分配到一个 SubReactor 之上时,会由 SubReactor 负责监听该连接上的读写事件。当有新的读事件(OP_READ)发生时,Reactor 子线程就会调用对应的 Handler 读取数据,然后分发给 Worker 线程池中的线程进行处理并返回结果。待处理结束之后,Handler 会根据处理结果调用 send 将响应返回给客户端,当然此时连接要有可写事件(OP_WRITE)才能发送数据。

主从 Reactor 多线程的设计模式解决了单一 Reactor 的瓶颈。主从 Reactor 职责明确,主 Reactor 只负责监听连接建立事件,SubReactor 只负责监听读写事件。整个主从 Reactor 多线程架构充分利用了多核 CPU 的优势,可以支持扩展,而且与具体的业务逻辑充分解耦,复用性高。但不足的地方是,在交互上略显复杂,需要一定的编程门槛。

Netty 线程模型

Netty 同时支持上述几种线程模式

Netty 抽象出两组线程池:BossGroup 专门用于接收客户端的连接,WorkerGroup 专门用于网络的读写。BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,相当于一个事件循环组,其中包含多个事件循环 ,每一个事件循环是 NioEventLoop。

NioEventLoop 表示一个不断循环的、执行处理任务的线程,每个 NioEventLoop 都有一个 Selector 对象与之对应,用于监听绑定在其上的连接,这些连接上的事件由 Selector 对应的这条线程处理。每个 NioEventLoopGroup 可以含有多个 NioEventLoop,也就是多个线程。

每个 Boss NioEventLoop 会监听 Selector 上连接建立的 accept 事件,然后处理 accept 事件与客户端建立网络连接,生成相应的 NioSocketChannel 对象,一个 NioSocketChannel 就表示一条网络连接。之后会将 NioSocketChannel 注册到某个 Worker NioEventLoop 上的 Selector 中。

每个 Worker NioEventLoop 会监听对应 Selector 上的 read/write 事件,当监听到 read/write 事件的时候,会通过 Pipeline 进行处理。一个 Pipeline 与一个 Channel 绑定,在 Pipeline 上可以添加多个 ChannelHandler,每个 ChannelHandler 中都可以包含一定的逻辑,例如编解码等。Pipeline 在处理请求的时候,会按照我们指定的顺序调用 ChannelHandler。

Netty 入门,用它做网络编程都说好(下)

Channel

Channel 是 Netty 对网络连接的抽象,核心功能是执行网络 I/O 操作。不同协议、不同阻塞类型的连接对应不同的 Channel 类型。

常用的 NIO Channel 类型。

  • NioSocketChannel:对应异步的 TCP Socket 连接。
  • NioServerSocketChannel:对应异步的服务器端 TCP Socket 连接。
  • NioDatagramChannel:对应异步的 UDP 连接。

ChannelFuture

Selector

Selector 是对多路复用器的抽象,也是 Java NIO 的核心基础组件之一。Netty 就是基于 Selector 对象实现 I/O 多路复用的,在 Selector 内部,会通过系统调用不断地查询这些注册在其上的 Channel 是否有已就绪的 I/O 事件,例如,可读事件(OP_READ)、可写事件(OP_WRITE)或是网络连接事件(OP_ACCEPT)等,而无须使用用户线程进行轮询。这样,我们就可以用一个线程监听多个 Channel 上发生的事件。

EventLoop

EventLoopGroup

简易版 RPC 框架实现(上)

简易版 RPC 框架实现(下)

本地缓存:降低 ZooKeeper 压力的一个常用手段

重试机制是网络操作的基本保证

ZooKeeper 注册中心实现,官方推荐注册中心实践

Dubbo Serialize 层:多种序列化算法,总有一款适合你

Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?

Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工

Transporter 层核心实现:编解码与线程模型一文打尽(上)

Transporter 层核心实现:编解码与线程模型一文打尽(下)

Exchange 层剖析:彻底搞懂 Request-Response 模型(上)

Exchange 层剖析:彻底搞懂 Request-Response 模型(下)

核心接口介绍,RPC 层骨架梳理

从 Protocol 起手,看服务暴露和服务引用的全流程(上)

从 Protocol 起手,看服务暴露和服务引用的全流程(下)

加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker(上)

加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker(下)

复杂问题简单化,代理帮你隐藏了多少底层细节?

加餐:HTTP 协议 + JSON-RPC,Dubbo 跨语言就是如此简单

Filter 接口,扩展 Dubbo 框架的常用手段指北

加餐:深潜 Directory 实现,探秘服务目录玄机

路由机制:请求到底怎么走,它说了算(上)

路由机制:请求到底怎么走,它说了算(下)

加餐:初探 Dubbo 动态配置的那些事儿

负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上)

负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下)

集群容错:一个好汉三个帮(上)

集群容错:一个好汉三个帮(下)

加餐:多个返回值不用怕,Merger 合并器来帮忙

加餐:模拟远程调用,Mock 机制帮你搞定

加餐:一键通关服务发布全流程

加餐:服务引用流程全解析

服务自省设计方案:新版本新方案

元数据方案深度剖析,如何避免注册中心数据量膨胀?

加餐:深入服务自省方案中的服务发布订阅(上)

加餐:深入服务自省方案中的服务发布订阅(下)

配置中心设计与实现:集中化配置 and 本地化配置,我都要(上)

配置中心设计与实现:集中化配置 and 本地化配置,我都要(下)

结束语 认真学习,缩小差距

参考资料

《分布式技术原理与算法解析》笔记

开篇词丨四纵四横,带你透彻理解分布式技术

分布式缘何而起:从单兵,到游击队,到集团军

分布式系统的指标:啥是分布式的三围

分布式互斥:有你没我,有我没你

分布式选举:国不可一日无君

分布式共识:存异求同

分布式事务:Allornothing

分布式锁:关键重地,非请勿入

答疑篇:分布式技术是如何引爆人工智能的?

分布式体系结构之集中式结构:一人在上,万人在下

分布式体系结构之非集中式结构:众生平等

分布式调度架构之单体调度:物质文明、精神文明一手抓

定义:单体调度是指,一个集群中只有一个节点运行调度进程,该节点对集群中的其他节点具有访问权限,可以搜集其他节点的资源信息、节点状态等进行统一管理,同时根据用户下发的任务对资源的需求,在调度器中进行任务与资源匹配,然后根据匹配结果将任务指派给其他节点。

架构:单体调度器也叫作集中式调度器,指的是使用中心化的方式去管理资源和调度任务。

特点:单体调度器拥有全局资源视图和全局任务,可以很容易地实现对任务的约束并实施全局性的调度策略

单体调度代表:K8S、Borg 等。

分布式调度架构之两层调度:物质文明、精神文明两手抓

定义:在两层调度器中,资源的使用状态同时由中央调度器和第二层调度器管理,但中央调度器一般只负责宏观的、大规模的资源分配,业务压力比较小;第二层调度器负责任务与资源的匹配,因此第二层调度可以有多个,以支持不同的任务类型。

特点:解决了单体调度架构中,中央服务器的单点瓶颈问题;相较于单体调度而言,提升了调度效率;支持多种类型的任务。

两层调度代表:YARN、Mesos 等。

分布式调度架构之共享状态调度:物质文明、精神文明多手协商抓

定义:共享状态调度架构沿袭了单体架构的模式,通过将单体调度器分解为多个调度器,每个调度器都有全局的资源状态信息,从而实现最优的任务调度。

分布式通信之远程调用:我是你的千里眼

本地过程调用(Local Procedure Call, LPC),是指运行在同一台机器上的进程之间的互相通信。

远程过程调用(Remote Procedure Call, RPC),是指不同机器中运行的进程之间的相互通信,某一机器上运行的进程在不知道底层通信细节的情况下,就像访问本地服务一样,去调用远程机器上的服务。

分布式通信之发布订阅:送货上门

分布式通信之消息队列:货物自取

CAP 理论:这顶帽子我不想要

CAP 是指:在一个分布式系统中, 一致性、可用性和分区容错性,最多只能同时满足其中两项。

  • 一致性(C:Consistency) - 多个数据副本是否能保持一致
  • 可用性(A:Availability)- 分布式系统在面对各种异常时可以提供正常服务的能力
  • 分区容错性(P:Partition Tolerance) - 分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障

在分布式系统中,分区容错性必不可少,因为需要总是假设网络是不可靠的;CAP 理论实际在是要在可用性和一致性之间做权衡。

  • CP - 需要让所有节点下线成为不可用的状态,等待同步完成。
  • AP - 在同步过程中允许读取所有节点的数据,但是数据可能不一致。

分布式数据存储系统之三要素:顾客、导购与货架

数据的生产和消费

数据特征:结构化数据、半结构化数据、非结构化数据

分区和复制

数据分布方式之哈希与一致性哈希:“掐指一算”与“掐指两算”的事

分布式数据存储选型的考量维度:

数据均匀:数据存储、访问尽量均衡

数据稳定:当数据存储集群扩容或缩容时,数据分布规则应尽量稳定,不要出现大范围的数据迁移。

节点异构性:应考虑集群中不同节点硬件配置的差异,将数据承载根据配置尽量均衡

分布式数据复制技术:分身有术

数据复制是指,如何让主备数据库保持数据一致的技术。

复制技术分类

  • 同步 - 注重一致性(CP 模型)。数据更新时,主节点必须要同步所有从节点,才提交更新。
  • 异步 - 注重可用性(AP 模型)。数据更新时,主节点处理完后,直接提交更新;从节点异步进行数据的同步。
  • 半同步 - 采用折中处理。数据更新时,主节点同步部分从节点(通常为一个节点或一半节点)成功后,才提交更新。

很多分布式存储支持通过配置,切换复制策略,以满足不同场景的需要。

分布式数据之缓存技术:“身手钥钱”随身带

分布式高可靠之负载均衡:不患寡,而患不均

负载均衡(Load Balancing)是指将请求或流量均衡地分配到多个服务器或节点上,以实现资源的最优化利用和高效的响应速度。

负载均衡常见策略

  • 随机负载均衡
    • 策略 - 将请求随机分发到候选服务器
    • 特点 - 调用量越大,负载越均衡
    • 适合场景 - 适合服务器硬件相同的场景
  • 轮询负载均衡
    • 策略 - 将请求依次分发到候选服务器
    • 特点 - 请求完全均匀分发
    • 场景 - 适合服务器硬件相同的场景
  • 最小活跃数负载均衡
    • 策略 - 将请求分发到连接数/请求数最少的候选服务器
    • 特点 - 根据候选服务器当前的请求连接数,动态分配
    • 适合场景 - 适用于对系统负载较为敏感或请求连接时长相差较大的场景
  • 哈希负载均衡
    • 策略 - 根据一个 key (可以是唯一 ID、IP 等),通过哈希计算得到一个数值,用该数值在候选服务器列表的进行取模运算,得到的结果便是选中的服务器
    • 特点 - 保证特定用户总是请求到相同的服务器,若服务器宕机,会话会丢失
    • 适合场景 - 可以保证同一 IP 的客户端的请求会转发到同一台服务器上,用来实现会话粘滞(Sticky Session)
  • 一致性哈希负载均衡
    • 策略 - 相同的请求尽可能落到同一个服务器上。尽可能是指:服务器可能发生上下线,少数服务器的变化不应该影响大多数的请求。当某台候选服务器宕机时,原本发往该服务器的请求,会基于虚拟节点,平摊到其它候选服务器,不会引起剧烈变动。
    • 优点 - 加入和删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。
    • 缺点 - 加减节点会造成哈希环中部分数据无法命中。当使用少量节点时,节点变化将大范围影响哈希环中数据映射,不适合少量数据节点的分布式方案。普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
    • 适合场景 - 一致性哈希可以很好的解决稳定性问题,可以将所有的存储节点排列在首尾相接的 Hash 环上,每个 key 在计算 Hash 后会顺时针找到临接的存储节点存放。而当有节点加入或退出时,仅影响该节点在 Hash 环上顺时针相邻的后续节点。

分布式高可靠之流量控制:大禹治水,在疏不在堵

分布式高可用之故障隔离:当断不断,反受其乱

分布式高可用之故障恢复:知错能改,善莫大焉

答疑篇:如何判断并解决网络分区问题?

知识串联:以购买火车票的流程串联分布式核心技术

搭建一个分布式实验环境:纸上得来终觉浅,绝知此事要躬行

特别放送丨那些你不能错过的分布式系统论文

分布式理论基础

Time, Clocks, and the Ordering of Events in a Distributed System

The Byzantine Generals Problem

Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services

CAP Twelve Years Later: How the “Rules” Have Changed

BASE: An Acid Alternative

A Simple Totally Ordered Broadcast Protocol

Virtual Time and Global States of Distributed Systems

分布式一致性算法

Paxos Made Simple

Paxos Made Practical

Paxos Made Live: An Engineering Perspective

Raft: In Search of an Understandable Consensus Algorithm

ZooKeeper: Wait-Free Coordination for Internet-Scale Systems

Using Paxos to Build a Scalable, Consistent, and Highly Available Datastore
Impossibility of Distributed Consensus With One Faulty Process

A Brief History of Consensus, 2PC and Transaction Commit

Consensus in the Presence of Partial Synchrony

分布式数据结构

Chord: A Scalable Peer-to-Peer Lookup Service for Internet Applications

Pastry: Scalable, Distributed Object Location, and Routing for Large-Scale Peerto-Peer Systems

Kademlia: A Peer-to-Peer Information System Based on the XOR Metric

A Scalable Content-Addressable Network

Ceph: A Scalable, High-Performance Distributed File System

The Log-Structured-Merge-Tree

HBase: A NoSQL Database

Tango: Distributed Data Structure over a Shared Log

分布式系统实战

The Google File System

BigTable: A Distributed Storage System for Structured Data

The Chubby Lock Service for Loosely-Coupled Distributed Systems

Finding a Needle in Haystack: Facebook’s Photo Storage

Windows Azure Storage: A Highly Available Cloud Storage Service with Strong Consistency

Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing

Scaling Distributed Machine Learning with the Parameter Server

Dremel: Interactive Analysis of Web-Scale Datasets

Pregel: A System for Large-Scale Graph Processing

Spanner: Google’s Globally-Distributed Database

Dynamo: Amazon’s Highly Available Key-value Store

S4: Distributed Stream Computing Platform

Storm @Twitter

Large-scale Cluster Management at Google with Borg

F1 - The Fault-Tolerant Distributed RDBMS Supporting Google’s Ad Business

Cassandra: A Decentralized Structured Storage System

MegaStore: Providing Scalable, Highly Available Storage for Interactive Services

Dapper, a Large-Scale Distributed Systems Tracing Infrastructure

Kafka: A distributed Messaging System for Log Processing

Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases

参考资料

微服务之注册和发现

服务注册和发现的基本原理

服务定义是服务提供者和服务消费者之间的约定,但是在微服务架构中,如何达成这个约定呢?这就依赖于服务注册和发现机制。

注册和发现的角色

在微服务架构下,服务注册和发现机制中主要有三种角色:

  • 服务提供者(RPC Server / Provider)
  • 服务消费者(RPC Client / Consumer)
  • 服务注册中心(Registry)

服务发现通常依赖于注册中心来协调服务发现的过程,其步骤如下:

  1. 服务提供者将接口信息以注册到注册中心。
  2. 服务消费者从注册中心读取和订阅服务提供者的地址信息。
  3. 如果有可用的服务,注册中心会主动通知服务消费者。
  4. 服务消费者根据可用服务的地址列表,调用服务提供者的接口。

这个过程很像是生活中的房屋租赁,房东将租房信息挂到中介公司,房客从中介公司查找租房信息。房客如果想要租房东的房子,通过中介公司牵线搭桥,联系上房东,双方谈妥签订协议,就可以正式建立起租赁关系。

主流的服务注册与发现的解决方案,主要有两种:

  • 应用内注册与发现:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现。
  • 应用外注册与发现:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现。

应用内注册与发现

应用内注册与发现方案是:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现。最典型的案例要属 Netflix 开源的 Eureka,官方架构图如下:

Eureka 的架构主要由三个重要的组件组成:

  • Eureka Server:注册中心的服务端,实现了服务信息注册、存储以及查询等功能。
  • 服务端的 Eureka Client:集成在服务端的注册中心 SDK,服务提供者通过调用 SDK,实现服务注册、反注册等功能。
  • 客户端的 Eureka Client:集成在客户端的注册中心 SDK,服务消费者通过调用 SDK,实现服务订阅、服务更新等功能。

应用外注册与发现

应用外注册与发现方案是:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现。最典型的案例是开源注册中心 Consul。

Consul 实现应用外服务注册和发现主要依靠三个重要的组件:

  • Consul:注册中心的服务端,实现服务注册信息的存储,并提供注册和发现服务。
  • Registrator:一个开源的第三方服务管理器项目,它通过监听服务部署的 Docker 实例是否存活,来负责服务提供者的注册和销毁。
  • Consul Template:定时从注册中心服务端获取最新的服务提供者节点列表并刷新 LB 配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息。

注册中心的基本功能

从服务注册和发现的流程,可以看出,注册中心是服务发现的核心组件。常见的注册中心组件有:Nacos、Consul、Zookeeper 等。

注册中心的实现主要涉及几个问题:注册中心需要提供哪些接口,该如何部署;如何存储服务信息;如何监控服务提供者节点的存活;如果服务提供者节点有变化如何通知服务消费者,以及如何控制注册中心的访问权限。

元数据定义

构建微服务的首要问题是:服务提供者和服务消费者通信时,如何达成共识。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。

常见的定义服务元数据的方式有:

  • XML 文件 - 如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。
  • IDL 文件 - 如果企业内部存在多个跨语言服务,建议使用 IDL 文件方式进行描述服务。
  • REST API - 如果存在对外开放服务调用的情形的话,使用 REST API 方式则更加通用。

XML 文件

XML 配置方式通过在服务提供者和服务消费者之间维持一份对等的 XML 配置文件,来保证服务消费者按照服务提供者的约定来进行服务调用。在这种方式下,如果服务提供者变更了接口定义,不仅需要更新服务提供者加载的接口描述文件 server.xml,还需要同时更新服务消费者加载的接口描述文件 client.xml。但这种方式对业务代码侵入性比较高,XML 配置有变更的时候,服务消费者和服务提供者都要更新,所以适合公司内部联系比较紧密的业务之间采用。支持 XML 文件的主流 RPC 有:阿里的 Dubbo(XML 配置示例:基于 Spring XML 开发微服务应用)、微博的 Motan。

XML 文件这种方式的服务发布和引用主要分三个步骤:

(1)服务提供者定义接口,并实现接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// The demo service definition.
service DemoService {
rpc sayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

(2)服务提供者进程启动时,通过加载 xml 配置文件将接口暴露出去。

1
2
3
4
5
6
7
8
9
10
11
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-provider"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:protocol name="dubbo" port="20890"/>
<bean id="demoService" class="org.apache.dubbo.samples.basic.impl.DemoServiceImpl"/>
<dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoService"/>
</beans>

(3)服务消费者进程启动时,通过加载 xml 配置文件来引入要调用的接口。

1
2
3
4
5
6
7
8
9
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-consumer"/>
<dubbo:registry group="aaa" address="zookeeper://127.0.0.1:2181"/>
<dubbo:reference id="demoService" check="false" interface="org.apache.dubbo.samples.basic.api.DemoService"/>
</beans>

IDL 文件

IDL 就是接口描述语言(interface description language)的缩写,通过一种中立、通用的方式来描述接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。也就是说,IDL 主要用于跨语言的服务之间的调用。支持 IDL 文件的主流 RPC 有:阿里的 Dubbo(XML 配置示例:IDL 定义跨语言服务),Facebook 的 Thrift,Google 的 gRPC

以 gRPC 协议为例,gRPC 协议使用 Protobuf 简称 proto 文件来定义接口名、调用参数以及返回值类型。比如文件 helloword.proto 定义了一个接口 SayHello 方法,它的请求参数是 HelloRequest,它的返回值是 HelloReply。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// The greeter service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}

}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

假如服务提供者使用的是 Java 语言,那么利用 protoc 插件即可自动生成 Server 端的 Java 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private class GreeterImpl extends GreeterGrpc.GreeterImplBase {

@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}

@Override
public void sayHelloAgain(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello again " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}

假如服务消费者使用的也是 Java 语言,那么利用 protoc 插件即可自动生成 Client 端的 Java 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void greet(String name) {
logger.info("Will try to greet " + name + " ...");
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloReply response;
try {
response = blockingStub.sayHello(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
logger.info("Greeting: " + response.getMessage());
try {
response = blockingStub.sayHelloAgain(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
logger.info("Greeting: " + response.getMessage());
}

假如服务消费者使用的是其他语言,也可以利用相应的插件生成代码。

由此可见,gRPC 协议的服务描述是通过 proto 文件来定义接口的,然后再使用 protoc 来生成不同语言平台的客户端和服务端代码,从而具备跨语言服务调用能力。

有一点特别需要注意的是,在描述接口定义时,IDL 文件需要对接口返回值进行详细定义。如果接口返回值的字段比较多,并且经常变化时,采用 IDL 文件方式的接口定义就不太合适了。一方面可能会造成 IDL 文件过大难以维护,另一方面只要 IDL 文件中定义的接口返回值有变更,都需要同步所有的服务消费者都更新,管理成本就太高了。

REST API

REST API 方式主要被用作 HTTP 或者 HTTPS 协议的接口定义,即使在非微服务架构体系下,也被广泛采用。由于 HTTP 本身就是公开标准网络协议,所以几乎没有什么额外学习成本。支持 REST API 的主流 RPC 有:Eureka,下面以 Eureka 为例。

服务提供者定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class ProviderController {

private final DiscoveryClient discoveryClient;

public ProviderController(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}

@GetMapping("/send")
public String send() {
String services = "Services: " + discoveryClient.getServices();
System.out.println(services);
return services;
}

}

服务消费者消费接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class ConsumerController {

private final LoadBalancerClient loadBalancerClient;
private final RestTemplate restTemplate;

public ConsumerController(LoadBalancerClient loadBalancerClient,
RestTemplate restTemplate) {
this.loadBalancerClient = loadBalancerClient;
this.restTemplate = restTemplate;
}

@GetMapping("/recv")
public String recv() {
ServiceInstance serviceInstance = loadBalancerClient.choose("eureka-provider");
String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/send";
System.out.println(url);
return restTemplate.getForObject(url, String.class);
}

}

元数据存储

注册中心本质上是一个用于保存元数据的分布式存储。你如果明白了这一点,就会了解实现一个注册中心的所有要点都是围绕这个目标去构建的。

想要构建微服务,首先要解决的问题是,服务提供者如何发布一个服务,服务消费者如何引用这个服务。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。

服务的元数据信息通常有以下信息:

  • 服务节点信息,如 IP、端口等。
  • 接口定义,如接口名、请求参数、响应参数等。
  • 请求失败的重试次数
  • 序列化方式
  • 压缩方式
  • 通信协议
  • 等等

在具体存储时,注册中心一般会按照“服务 - 分组 - 节点信息”的层次化的结构来存储。以 ZooKeeper 为例:

  • 在 ZooKeeper 中,数据按目录层级存储,每个目录叫作 znode,并且其有一个唯一的路径标识。
  • znode 可以包含数据和子 znode。
  • znode 中的数据可以有多个版本,比如某一个 znode 下存有多个数据版本,那么查询这个路径下的数据需带上版本信息。

img

注册中心 API

既然是分布式存储,势必要提供支持读写数据的接口,也就是 API,一般来说,需要支持以下功能:

  • 服务注册接口:服务提供者通过调用服务注册接口来完成服务注册。
  • 服务反注册接口:服务提供者通过调用服务反注册接口来完成服务注销。
  • 心跳汇报接口:服务提供者通过调用心跳汇报接口完成节点存活状态上报。
  • 服务订阅接口:服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表。
  • 服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表。

除此之外,为了便于管理,注册中心还必须提供一些后台管理的 API,例如:

  • 服务查询接口:查询注册中心当前注册了哪些服务信息。
  • 服务修改接口:修改注册中心中某一服务的信息。

服务健康检测

注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。注册中心通常使用长连接或心跳探测方式检查服务健康状态

还是以 ZooKeeper 为例,它是基于 ZooKeeper 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态检测的。在 ZooKeeper 中,客户端和服务端建立连接后,会话也随之建立,并生成一个全局唯一的 Session ID。服务端和客户端维持的是一个长连接,在 SESSION_TIMEOUT 周期内,服务端会检测与客户端的链路是否正常,具体方式是通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过 SESSION_TIMEOUT 后服务端都没有收到客户端的心跳消息,则服务端认为这个 Session 就已经结束了,ZooKeeper 就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。

服务状态变更通知

一旦注册中心探测到有服务提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。注册中心通常基于服务状态订阅来实现服务状态变更通知。

继续以 ZooKeeper 为例,基于 ZooKeeper 的 Watcher 机制,来实现服务状态变更通知给服务消费者的。服务消费者在调用 ZooKeeper 的 getData 方法订阅服务时,还可以通过监听器 Watcher 的 process 方法获取服务的变更,然后调用 getData 方法来获取变更后的数据,刷新本地缓存的服务节点信息。

集群部署

注册中心作为服务提供者和服务消费者之间沟通的桥梁,它的重要性不言而喻。所以注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。根据 CAP 理论,三种特性无法同时达成,必须在可用性和一致性之间做取舍。于是,根据不同侧重点,注册中心可以分为 CP 和 AP 两个阵营:

  • CP 型注册中心 - 牺牲可用性来换取数据强一致性,最典型的例子就是 ZooKeeper,etcd,Consul 了。ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过 Paxos 算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。而 etcd 和 Consul 集群内都是通过 Raft 协议来保证强一致性,如果出现脑裂的话, 注册中心也不可用。
  • AP 型注册中心 - 牺牲一致性(只保证最终一致性)来换取可用性,最典型的例子就是 Eureka 了。对比下 Zookeeper,Eureka 不用选举一个 Leader,每个 Eureka 服务器单独保存服务注册地址,因此有可能出现数据信息不一致的情况。但是当网络出现问题的时候,每台服务器都可以完成独立的服务。

以开源注册中心 ZooKeeper 为例,ZooKeeper 集群中包含多个节点,服务提供者和服务消费者可以同任意一个节点通信,因为它们的数据一定是相同的,这是为什么呢?这就要从 ZooKeeper 的工作原理说起:

  • 每个 Server 在内存中存储了一份数据,Client 的读请求可以请求任意一个 Server。
  • ZooKeeper 启动时,将从实例中选举一个 leader(Paxos 协议)。
  • Leader 负责处理数据更新等操作(ZAB 协议)。
  • 一个更新操作成功,当且仅当大多数 Server 在内存中成功修改 。

通过上面这种方式,ZooKeeper 保证了高可用性以及数据一致性。

img

注册中心的扩展功能

多注册中心

对于服务消费者来说,要能够同时从多个注册中心订阅服务;

对于服务提供者来说,要能够同时向多个注册中心注册服务。

并行订阅服务

如果只支持串行订阅,如果服务消费者订阅的服务较多,并且某些服务节点的初始化连接过程中出现连接超时的情况,则后续所有的服务节点的初始化连接都需要等待它完成,这就会导致消费者启动非常慢。

可以每订阅一个服务就单独用一个线程来处理,这样的话即使遇到个别服务节点连接超时,其他服务节点的初始化连接也不受影响,最慢也就是这个服务节点的初始化连接耗费的时间,最终所有服务节点的初始化连接耗时控制在了 30 秒以内。

批量注销服务

在与注册中心的多次交互中,可能由于网络抖动、注册中心集群异常等原因,导致个别调用失败。对于注册中心来说,偶发的注册调用失败对服务调用基本没有影响,其结果顶多就是某一个服务少了一个可用的节点。但偶发的反注册调用失败会导致不可用的节点残留在注册中心中,变成“僵尸节点”。

需要定时去清理注册中心中的“僵尸节点”,如果支持批量注销服务,就可以一次调用就把该节点上提供的所有服务同时注销掉。

服务变更信息增量更新

为了减少服务消费者从注册中心中拉取的服务可用节点信息的数据量,这个时候可以通过增量更新的方式,注册中心只返回变化的那部分节点信息。尤其在只有少数节点信息变更时,此举可以大大减少服务消费者从注册中心拉取的数据量,从而最大程度避免产生网络风暴。

心跳开关保护机制

在网络频繁抖动的情况下,注册中心中可用的节点会不断变化,这时候服务消费者会频繁收到服务提供者节点变更的信息,于是就不断地请求注册中心来拉取最新的可用服务节点信息。当有成百上千个服务消费者,同时请求注册中心获取最新的服务提供者的节点信息时,可能会把注册中心的带宽给占满,尤其是注册中心是百兆网卡的情况下。

所以针对这种情况,需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息

我曾经就遇到过这种情况,一个可行的解决方案就是给注册中心设置一个开关,当开关打开时,即使网络频繁抖动,注册中心也不会通知所有的服务消费者有服务节点信息变更,比如只给 10% 的服务消费者返回变更,这样的话就能将注册中心的请求量减少到原来的 1/10。

当然打开这个开关也是有一定代价的,它会导致服务消费者感知最新的服务节点信息延迟,原先可能在 10s 内就能感知到服务提供者节点信息的变更,现在可能会延迟到几分钟,所以在网络正常的情况下,开关并不适合打开;可以作为一个紧急措施,在网络频繁抖动的时候,才打开这个开关。

服务节点摘除保护机制

服务提供者在进程启动时,会注册服务到注册中心,并每隔一段时间,汇报心跳给注册中心,以标识自己的存活状态。如果隔了一段固定时间后,服务提供者仍然没有汇报心跳给注册中心,注册中心就会认为该节点已经处于“dead”状态,于是从服务的可用节点信息中移除出去。

如果遇到网络问题,大批服务提供者节点汇报给注册中心的心跳信息都可能会传达失败,注册中心就会把它们都从可用节点列表中移除出去,造成剩下的可用节点难以承受所有的调用,引起“雪崩”。但是这种情况下,可能大部分服务提供者节点是可用的,仅仅因为网络原因无法汇报心跳给注册中心就被“无情”的摘除了。

这个时候就需要根据实际业务的情况,设定一个阈值比例,即使遇到刚才说的这种情况,注册中心也不能摘除超过这个阈值比例的节点

这个阈值比例可以根据实际业务的冗余度来确定,我通常会把这个比例设定在 20%,就是说注册中心不能摘除超过 20% 的节点。因为大部分情况下,节点的变化不会这么频繁,只有在网络抖动或者业务明确要下线大批量节点的情况下才有可能发生。而业务明确要下线大批量节点的情况是可以预知的,这种情况下可以关闭阈值保护;而正常情况下,应该打开阈值保护,以防止网络抖动时,大批量可用的服务节点被摘除。

白名单机制

在实际的微服务测试和部署时,通常包含多套环境,比如生产环境一套、测试环境一套。开发在进行业务自测、测试在进行回归测试时,一般都是用测试环境,部署的 RPC Server 节点注册到测试的注册中心集群。但经常会出现开发或者测试在部署时,错误的把测试环境下的服务节点注册到了线上注册中心集群,这样的话线上流量就会调用到测试环境下的 RPC Server 节点,可能会造成意想不到的后果。

为了防止这种情况发生,注册中心需要提供一个保护机制,你可以把注册中心想象成一个带有门禁的房间,只有拥有门禁卡的 RPC Server 才能进入。在实际应用中,注册中心可以提供一个白名单机制,只有添加到注册中心白名单内的 RPC Server,才能够调用注册中心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。

静态注册中心

因为服务提供者是向服务消费者提供服务的,服务是否可用,服务消费者应该比注册中心更清楚。因此,可以直接在服务消费者端,根据调用服务提供者是否成功来判定服务提供者是否可用。如果服务消费者调用某一个服务提供者节点连续失败超过一定次数,可以在本地内存中将这个节点标记为不可用。并且每隔一段固定时间,服务消费者都要向标记为不可用的节点发起保活探测,如果探测成功了,就将标记为不可用的节点再恢复为可用状态,重新发起调用。

参考资料

微服务之服务调用

RPC 简介

通过注册中心,服务消费者和服务提供者就可以感知彼此。但是,要实现交互还必须解决通信问题。

在单体应用中,一次服务调用发生在同一台机器上的同一个进程内部,因此也被称为本地方法调用。在微服务应用中,由于服务提供者和服务消费者运行在不同物理机器上的不同进程内,因此也被称为远程方法调用,简称 RPC(Remote Procedure Call)

RPC 是微服务架构的基石,它提供了一种应用间通信的方式。RPC 的主要作用是:

  • 屏蔽远程调用跟本地调用的差异,让用户像调用本地一样去调用远程方法。
  • 隐藏底层网络通信的复杂性,让用户更聚焦于业务逻辑。

RPC 核心原理

RPC 是如何像本地方法调用一样,完成一次请求处理的呢?我们不妨推导一二。首先,服务消费者和服务提供者通常位于网络上两个不同地址,要想交换信息,必须先建立网络连接;建立网络连接后,如果要想识别彼此的信息,必须遵循相同的通信协议;服务提供者和服务消费者,需要采用某种方式数据传输;为了减少传输数据量,还要对数据进行压缩,即序列化。

它的通信流程中需要注意以下环节:

  • 传输方式:RPC 是一个远程调用,因此必然需要通过网络传输数据,且 RPC 常用于业务系统之间的数据交互,需要保证其可靠性,所以 RPC 一般默认采用 TCP 来传输。
  • 序列化:在网络中传输的数据只能是二进制数据,而 RPC 请求时,发送的都是对象。因此,请求方需要将请求参数转为二进制数据,即序列化。RPC 响应方接受到请求,要将二进制数据转换为请求参数,需要反序列化
  • 通信协议:请求方和响应方要互相识别彼此的信息,需要约定好彼此数据的格式,即协议。大多数的协议至少分成两部分,分别是数据头和消息体。数据头一般用于身份识别,包括协议标识、数据大小、请求类型、序列化类型等信息;消息体主要是请求的业务参数信息和扩展属性等。
  • 动态代理:为了屏蔽底层通信细节,使用户聚焦自身业务,因此 RPC 框架一般引入了动态代理,通过依赖注入等技术,拦截方法调用,完成远程调用的通信逻辑。

下图诠释了以上环节是如何串联起来的:

通信协议

通信协议的作用

只有二进制才能在网络中传输,所以 RPC 请求在发送到网络中之前,需要把方法调用的请求参数转成二进制;转成二进制后,写入本地 Socket 中,然后被网卡发送到网络设备中。

在传输过程中,RPC 并不会把请求参数的所有二进制数据整体一下子发送到对端机器上,中间可能会拆分成好几个数据包,也可能会合并其他请求的数据包(合并的前提是同一个 TCP 连接上的数据),至于怎么拆分合并,这其中的细节会涉及到系统参数配置和 TCP 窗口大小。对于服务提供方应用来说,他会从 TCP 通道里面收到很多的二进制数据,那这时候怎么识别出哪些二进制是第一个请求的呢?

这就好比让你读一篇没有标点符号的文章,你要怎么识别出每一句话到哪里结束呢?很简单啊,我们加上标点,完成断句就好了。为了避免语义不一致的事情发生,我们就需要在发送请求的时候设定一个边界,然后在收到请求的时候按照这个设定的边界进行数据分割。这个边界语义的表达,就是我们所说的协议。

通信协议要解决的是:客户端和服务端如何建立连接、管理连接以及服务端如何处理请求的问题。

常见网络协议

HTTP 通信是基于应用层 HTTP 协议的,而 HTTP 协议又是基于传输层 TCP 协议的。一次 HTTP 通信过程就是发起一次 HTTP 调用,而一次 HTTP 调用就会建立一个 TCP 连接,经历三次握手的过程来建立连接。完成请求后,再经历一次四次挥手的过程来断开连接。

TCP 通信的过程分为四个步骤:服务器监听客户端请求连接确认数据传输。当客户端和服务端建立网络连接后,就可以发起请求了。但网络不一定总是可靠的,经常会遇到网络闪断、连接超时、服务端宕机等各种异常,通常的处理手段有两种:链路存活检测断连重试

通过两种通信方式的对比,不难看出:HTTP 通信由于每次都要建立 TCP 连接,而建立连接又较为耗时,所以 HTTP 通信性能是不如 TCP 通信的

为何需要设计 RPC 协议

既然有了现成的 HTTP 协议,还有必要设计 RPC 协议吗?

有必要。因为 HTTP 这些通信标准协议,数据包中的实际请求数据相对于数据包本身要小很多,有很多无用的内容;并且 HTTP 属于无状态协议,无法将请求和响应关联,每次请求要重新建立连接。这对于高性能的 RPC 来说,HTTP 协议难以满足需求,所以有必要设计一个紧凑的私有协议

如何设计 RPC 协议

首先,必须先明确消息的边界,即确定消息的长度。因此,至少要分为:消息长度+消息内容两部分。

接下来,我们会发现,在使用过程中,仅消息长度,不足以明确通信中的很多细节:如序列化方式是怎样的?是否消息压缩?压缩格式是怎样的?如果协议发生变化,需要明确协议版本等等。

综上,一个 RPC 协议大概会由下图中的这些参数组成:

可扩展的协议

前面所述的协议属于定长协议头,那也就是说往后就不能再往协议头里加新参数了,如果加参
数就会导致线上兼容问题。

为了保证能平滑地升级改造前后的协议,我们有必要设计一种支持可扩展的协议。其关键在于让协议头支持可扩展,扩展后协议头的长度就不能定长了。那要实现读取不定长的协议头里面的内容,在这之前肯定需要一个固定的地方读取长度,所以我们需要一个固定的写入协议头的长度。整体协议就变成了三部分内容:固定部分、协议头内容、协议体内容。

序列化

有兴趣深入了解 JDK 序列化方式,可以参考:Java 序列化

由于,网络传输的数据必须是二进制数据,而调用方请求的出参、入参都是对象。因此,必须将对象转换可传输的二进制,并且要求转换算法是可逆的。

  • 序列化(serialize):序列化是将对象转换为二进制数据。
  • 反序列化(deserialize):反序列化是将二进制数据转换为对象。

序列化就是将对象转换成二进制数据的过程,而反序列就是反过来将二进制转换为对象的过程。

序列化技术

Java 领域,常见的序列化技术如下

序列化技术选型

市面上有如此多的序列化技术,那么我们在应用时如何选择呢?

序列化技术选型,需要考量的维度,根据重要性从高到低,依次有:

  • 安全性:是否存在漏洞。如果存在漏洞,就有被攻击的可能性。
  • 兼容性:版本升级后的兼容性是否很好,是否支持更多的对象类型,是否是跨平台、跨语言的。服务调用的稳定性与可靠性,要比服务的性能更加重要。
  • 性能
    • 时间开销:序列化、反序列化的耗时性能自然越小越好。
    • 空间开销:序列化后的数据越小越好,这样网络传输效率就高。
  • 易用性:类库是否轻量化,API 是否简单易懂。

鉴于以上的考量,序列化技术的选型建议如下:

  • JDK 序列化:性能较差,且有很多使用限制,不建议使用。
  • ThriftProtobuf:适用于对性能敏感,对开发体验要求不高
  • Hessian:适用于对开发体验敏感,性能有要求
  • JacksonGsonFastjson:适用于对序列化后的数据要求有良好的可读性(转为 json 、xml 形式)。

序列化问题

由于 RPC 每次通信,都要经过序列化、反序列化的过程,所以序列化方式,会直接影响 RPC 通信的性能。除了选择合适的序列化技术,如何合理使用序列化也非常重要。

RPC 序列化常见的使用不当的情况如下:

  • 对象过于复杂、庞大 - 对象过于复杂、庞大,会降低序列化、反序列化的效率,并增加传输开销,从而导致响应时延增大。

    • 过于复杂:存在多层的嵌套,比如 A 对象关联 B 对象,B 对象又聚合 C 对象,C 对象又关联聚合很多其他对象
    • 过于庞大:比如一个大 List 或者大 Map
  • 对象有复杂的继承关系 - 对象关系越复杂,就越浪费性能,同时又很容易出现序列化上的问题。大多数序列化框架在进行序列化时,如果发现类有继承关系,会不停地寻找父类,遍历属性。

  • 使用序列化框架不支持的类作为入参类 - 比如 Hessian 框架,他天然是不支持 LinkHashMap、LinkedHashSet 等,而且大多数情况下最好不要使用第三方集合类,如 Guava 中的集合类,很多开源的序列化框架都是优先支持编程语言原生的对象。因此如果入参是集合类,应尽量选用原生的、最为常用的集合类,如 HashMap、ArrayList。

序列化要点

前面已经列举了常见的序列化问题,既然明确了问题,就要针对性预防。RPC 序列化时要注意以下几点:

  1. 对象要尽量简单,没有太多的依赖关系,属性不要太多,尽量高内聚;
  2. 入参对象与返回值对象体积不要太大,更不要传太大的集合;
  3. 尽量使用简单的、常用的、开发语言原生的对象,尤其是集合类;
  4. 对象不要有复杂的继承关系,最好不要有父子类的情况。

网络传输

一次 RPC 调用,本质就是服务消费者与服务提供者间的一次网络信息交换的过程。可见,通信时 RPC 实现的核心。

常见的网络 IO 模型有:同步阻塞(BIO)、同步非阻塞(NIO)、异步非阻塞(AIO)。

同步阻塞方式(BIO)

同步阻塞方式的工作流程大致为:客户端每发一次请求,服务端就生成一个线程去处理。当客户端同时发起的请求很多时,服务端需要创建很多的线程去处理每一个请求,如果达到了系统最大的线程数瓶颈,新来的请求就没法处理了。

BIO 适用于连接数比较小的业务场景,这样的话不至于系统中没有可用线程去处理请求。这种方式写的程序也比较简单直观,易于理解。

img

同步非阻塞方式 (NIO)

同步非阻塞方式 (NIO) 的工作流程大致为:客户端每发一次请求,服务端并不是每次都创建一个新线程来处理,而是通过 I/O 多路复用技术进行处理。就是把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使系统在单线程的情况下可以同时处理多个客户端请求。这种方式的优势是开销小,不用为每个请求创建一个线程,可以节省系统开销。

IO 多路复用

IO 多路复用(Reactor 模式)在高并发场景下使用最为广泛,很多知名软件都应用了这一技术,如:Netty、Redis、Nginx 等。什么是 IO 多路复用?字面上的理解,多路就是指多个通道,也就是多个网络连接的 IO,而复用就是指多个通道复用在一个复用器上。IO 多路复用分为 select,poll 和 epoll。

零拷贝

系统内核处理 IO 操作分为两个阶段——等待数据和拷贝数据。等待数据,就是系统内核在等待网卡接收到数据后,把数据写到内核中;而拷贝数据,就是系统内核在获取到数据后,将数据拷贝到用户进程的空间中。

img

应用进程的每一次写操作,都会把数据写到用户空间的缓冲区中,再由 CPU 将数据拷贝到系统内核的缓冲区中,之后再由 DMA 将这份数据拷贝到网卡中,最后由网卡发送出去。这里我们可以看到,一次写操作数据要拷贝两次才能通过网卡发送出去,而用户进程的读操作则是将整个流程反过来,数据同样会拷贝两次才能让应用程序读取到数据。

应用进程的一次完整的读写操作,都需要在用户空间与内核空间中来回拷贝,并且每一次拷贝,都需要 CPU 进行一次上下文切换(由用户进程切换到系统内核,或由系统内核切换到用户进程),这样很浪费 CPU 和性能。

所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,可以通过一种方式,直接将数据写入内核或从内核中读取数据,再通过 DMA 将内核中的数据拷贝到网卡,或将网卡中的数据 copy 到内核。

img

Netty 的零拷贝偏向于用户空间中对数据操作的优化,这对处理 TCP 传输中的拆包粘包问题有着重要的意义,对应用程序处理请求数据与返回数据也有重要的意义。

Netty 框架中很多内部的 ChannelHandler 实现类,都是通过 CompositeByteBuf、slice、wrap 操作来处理 TCP 传输中的拆包与粘包问题的。

Netty 的 ByteBuffer 可以采用 Direct Buffers,使用堆外直接内存进行 Socketd 的读写
操作,最终的效果与我刚才讲解的虚拟内存所实现的效果是一样的。

Netty 还提供 FileRegion 中包装 NIO 的 FileChannel.transferTo() 方法实现了零拷
贝,这与 Linux 中的 sendfile 方式在原理上也是一样的。

NIO vs BIO

NIO 适用于连接数比较多并且请求消耗比较轻的业务场景,比如聊天服务器。这种方式相比 BIO,相对来说编程比较复杂。

BIO 与 NIO 最重要的区别是数据打包和传输的方式:BIO 以流的方式处理数据,而 NIO 以块的方式处理数据

  • 面向流的 BIO 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
  • 面向块的 NIO 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 NIO 缺少一些面向流的 BIO 所具有的优雅性和简单性。

img

异步非阻塞方式 (AIO)

异步非阻塞方式 (AIO) 的大致工作流程为:客户端只需要发起一个 I/O 操作然后立即返回,等 I/O 操作真正完成以后,客户端会得到 I/O 操作完成的通知,此时客户端只需要对数据进行处理就好了,不需要进行实际的 I/O 读写操作,因为真正的 I/O 读取或者写入操作已经由内核完成了。这种方式的优势是客户端无需等待,不存在阻塞等待问题。

AIO 适用于连接数比较多而且请求消耗比较重的业务场景,比如涉及 I/O 操作的相册服务器。这种方式相比另外两种,编程难度最大,程序也不易于理解。

参考资料

HBase Java API 管理功能

初始化 Admin 实例

1
2
3
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Admin admin = connection.getAdmin();

管理命名空间

查看命名空间

1
2
3
4
TableName[] tableNames = admin.listTableNamesByNamespace("test");
for (TableName tableName : tableNames) {
System.out.println(tableName.getName());
}

创建命名空间

1
2
NamespaceDescriptor namespace = NamespaceDescriptor.create("test").build();
admin.createNamespace(namespace);

修改命名空间

1
2
3
4
NamespaceDescriptor namespace = NamespaceDescriptor.create("test")
.addConfiguration("Description", "Test Namespace")
.build();
admin.modifyNamespace(namespace);

删除命名空间

1
admin.deleteNamespace("test");

管理表

创建表

1
2
3
4
5
TableName tableName = TableName.valueOf("test:test");
HTableDescriptor tableDescriptor = new HTableDescriptor(tableName);
HColumnDescriptor columnDescriptor = new HColumnDescriptor(Bytes.toBytes("cf"));
tableDescriptor.addFamily(columnDescriptor);
admin.createTable(tableDescriptor);

删除表

1
admin.deleteTable(TableName.valueOf("test:test"));

修改表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 原始表
TableName tableName = TableName.valueOf("test:test");
HColumnDescriptor columnDescriptor = new HColumnDescriptor("cf1");
HTableDescriptor tableDescriptor = new HTableDescriptor(tableName)
.addFamily(columnDescriptor)
.setValue("Description", "Original Table");
admin.createTable(tableDescriptor, Bytes.toBytes(1L), Bytes.toBytes(10000L), 50);

// 修改表
HTableDescriptor newTableDescriptor = admin.getTableDescriptor(tableName);
HColumnDescriptor newColumnDescriptor = new HColumnDescriptor("cf2");
newTableDescriptor.addFamily(newColumnDescriptor)
.setMaxFileSize(1024 * 1024 * 1024L)
.setValue("Description", "Modified Table");

// 修改表必须先禁用再想修改
admin.disableTable(tableName);
admin.modifyTable(tableName, newTableDescriptor);

禁用表

需要注意:HBase 表在删除前,必须先禁用。

1
admin.disableTable(TableName.valueOf("test:test"));

启用表

1
admin.enableTable(TableName.valueOf("test:test"));

查看表是否有效

1
2
boolean isOk = admin.isTableAvailable(tableName);
System.out.println("Table available: " + isOk);

参考资料

HBase Java API 其他高级特性

计数器

HBase 提供了一种高级功能:计数器(counter)。HBase 计数器可以用于实时统计,无需延时较高的批量处理操作。HBase 有一种机制可以将列当作计数器:即读取并修改(其实就是一种 CAS 模式),其保证了在一次操作中的原子性。否则,用户需要对一行数据加锁,然后读取数据,再对当前数据做加法,最后写回 HBase 并释放行锁,这一系列操作会引起大量的资源竞争问题。

早期的 HBase 版本会在每次计数器更新操作调用一次 RPC 请求,新版本中可以在一次 RPC 请求中完成多个计数器的更新操作,但是多个计数器必须在同一行。

计数器使用 Shell 命令行

计数器不需要初始化,创建一个新列时初始值为 0,第一次 incr 操作返回 1。

计数器使用 incr 命令,增量可以是正数也可以是负数,但是必须是长整数 Long:

1
incr '<table>','<row>','<column>',['<increment-value>']

计数器使用的例子:

1
2
3
4
5
6
7
8
9
10
11
hbase(main):001:0> create 'counters','daily','weekly','monthly'
0 row(s) in 1.2260 seconds

hbase(main):002:0> incr 'counters','20190301','daily:hites',1
COUNTER VALUE = 1

hbase(main):003:0> incr'counters','20190301','daily:hites',1
COUNTER VALUE = 2

hbase(main):004:0> get_counter 'counters','20190301','daily:hites'
COUNTER VALUE = 2

需要注意的是,增加的参数必须是长整型 Long,如果按照错误的格式更新了计数器(如字符串格式),下次调用 incr 会得到错误的结果:

1
2
3
4
5
hbase(main):005:0> put 'counters','20190301','daily:clicks','1'
0 row(s) in 1.3250 seconds

hbase(main):006:0> incr'counters','20190301','daily:clicks',1
COUNTER VALUE = 3530822107858468865

单计数器

操作一个计数器,类似 shell 命令 incr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HTable table  = new HTable(conf, "counters");

long cnt1 = table.incrementColumnValue(Bytes.toBytes("20190301"),
Bytes.toBytes("daily"),
Bytes.toBytes("hits"),
1L);

long cnt2 = table.incrementColumnValue(Bytes.toBytes("20190301"),
Bytes.toBytes("daily"),
Bytes.toBytes("hits"),
1L);

long current = table.incrementColumnValue(Bytes.toBytes("20190301"),
Bytes.toBytes("daily"),
Bytes.toBytes("hits"),
0);

多计数器

使用 Tableincrement() 方法可以操作一行的多个计数器,需要构建 Increment 实例,并且指定行键:

1
2
3
4
5
6
7
8
9
10
11
12
HTable table  = new HTable(conf, "counters");

Increment incr1 = new Increment(Bytes.toBytes("20190301"));
incr1.addColumn(Bytes.toBytes("daily"), Bytes.toBytes("clicks"),1);
incr1.addColumn(Bytes.toBytes("daily"), Bytes.toBytes("hits"), 1);
incr1.addColumn(Bytes.toBytes("weekly"), Bytes.toBytes("clicks"), 2);
incr1.addColumn(Bytes.toBytes("weekly"), Bytes.toBytes("hits"), 2);

Result result = table.increment(incr1);
for(Cell cell : result.rawCells()) {
// ...
}

Increment 类还有一种构造器:

1
Increment(byte[] row, RowLock rowLock)

rowLock 参数可选,可以设置用户自定义锁,可以限制其他写程序操作此行,但是不保证读的操作性。

连接管理

连接管理简介

在 HBase Java API 中,Connection 类代表了一个集群连接,封装了与多台服务器(Matser/Region Server)的底层连接以及与 zookeeper 的连接。Connection 通过 ConnectionFactory 类实例化,而连接的生命周期则由调用者管理,调用者必须显示调用 close() 来释放连接。Connection 是线程安全的。创建 Connection 实例的开销很高,因此一个进程只需要实例化一个 Connection 即可。

Table 接口用于对指定的 HBase 表进行 CRUD 操作。一般,通过 Connection 获取 Table 实例,用完后,调用 close() 释放连接。

Admin 接口主要用于创建、删除、查看、启用/禁用 HBase 表,以及一些其他管理操作。一般,通过 Connection 获取 Admin 实例,用完后,调用 close() 释放连接。

TableAdmin 实例都是轻量级且并非线程安全的。建议每个线程只实例化一个 TableAdmin 实例。

连接池

问题:HBase 为什么没有提供 Connection 的连接池来获取更好的性能?是否需要自定义 Connection 连接池?

答:不需要。官方对于 Connection 的使用说明中,明确指出:对于高并发多线程访问的应用程序,一个进程中只需要预先创建一个 Connection

问题:HBase 老版本中 HTablePool 为什么废弃?是否需要自定义 Table 的连接池?

答:不需要。Table 和 Admin 的连接本质上是复用 Connection,实例化是一个较为轻量级的操作,因此,并不需要缓存或池化。实际上,HBase Java API 官方就是这么建议的。

下面是管理 HBase 连接的一个正确编程模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 所有进程共用一个 connection 对象
connection = ConnectionFactory.createConnection(config);

// 每个线程使用单独的 table 对象
Table table = connection.getTable(TableName.valueOf("tableName"));
try {
...
} finally {
table.close();
}

Admin admin = connection.getAdmin();
try {
...
} finally {
admin.close();
}

参考资料

HBase 数据模型

HBase 是一个面向 的数据库管理系统,这里更为确切的而说,HBase 是一个面向 列族 的数据库管理系统。表 schema 仅定义列族,表具有多个列族,每个列族可以包含任意数量的列,列由多个单元格(cell)组成,单元格可以存储多个版本的数据,多个版本数据以时间戳进行区分。

HBase 逻辑存储结构

  • **Table**:Table 由 Row 和 Column 组成。
  • **Row**:Row 是列族(Column Family)的集合。
  • Row KeyRow Key 是用来检索记录的主键
    • Row Key 是未解释的字节数组,所以理论上,任何数据都可以通过序列化表示成字符串或二进制,从而存为 HBase 的键值。
    • 表中的行,是按照 Row Key 的字典序进行排序。这里需要注意以下两点:
      • 因为字典序对 Int 排序的结果是 1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。如果你使用整型的字符串作为行键,那么为了保持整型的自然序,行键必须用 0 作左填充。
      • 行的一次读写操作是原子性的 (不论一次读写多少列)。
    • 所有对表的访问都要通过 Row Key,有以下三种方式:
      • 通过指定的 Row Key 进行访问;
      • 通过 Row Key 的 range 进行访问,即访问指定范围内的行;
      • 进行全表扫描。
  • **Column Family**:即列族。HBase 表中的每个列,都归属于某个列族。列族是表的 Schema 的一部分,所以列族需要在创建表时进行定义。
    • 一个表的列族必须作为表模式定义的一部分预先给出,但是新的列族成员可以随后按需加入。
    • 同一个列族的所有成员具有相同的前缀,例如 info:formatinfo:geo 都属于 info 这个列族。
  • **Column Qualifier**:列限定符。可以理解为是具体的列名,例如 info:formatinfo:geo 都属于 info 这个列族,它们的列限定符分别是 formatgeo。列族和列限定符之间始终以冒号分隔。需要注意的是列限定符不是表 Schema 的一部分,你可以在插入数据的过程中动态创建列。
  • **Column**:HBase 中的列由列族和列限定符组成,由 :(冒号) 进行分隔,即一个完整的列名应该表述为 列族名 :列限定符
  • **Cell**:Cell 是行,列族和列限定符的组合,并包含值和时间戳。HBase 中通过 row keycolumn 确定的为一个存储单元称为 Cell,你可以等价理解为关系型数据库中由指定行和指定列确定的一个单元格,但不同的是 HBase 中的一个单元格是由多个版本的数据组成的,每个版本的数据用时间戳进行区分。
    - Cell 由行和列的坐标交叉决定,是有版本的。默认情况下,版本号是自动分配的,为 HBase 插入 Cell 时的时间戳。Cell 的内容是未解释的字节数组。

  • **Timestamp**:Cell 的版本通过时间戳来索引,时间戳的类型是 64 位整型,时间戳可以由 HBase 在数据写入时自动赋值,也可以由客户显式指定。每个 Cell 中,不同版本的数据按照时间戳倒序排列,即最新的数据排在最前面。

img

HBase 物理存储结构

HBase 自动将表水平划分成区域(Region)。每个 Region 由表中 Row 的子集构成。每个 Region 由它所属的表的起始范围来表示(包含的第一行和最后一行)。初始时,一个表只有一个 Region,随着 Region 膨胀,当超过一定阈值时,会在某行的边界上分裂成两个大小基本相同的新 Region。在第一次划分之前,所有加载的数据都放在原始 Region 所在的那台服务器上。随着表变大,Region 个数也会逐渐增加。Region 是在 HBase 集群上分布数据的最小单位。

HBase 数据模型示例

下图为 HBase 中一张表的:

  • RowKey 为行的唯一标识,所有行按照 RowKey 的字典序进行排序;
  • 该表具有两个列族,分别是 personal 和 office;
  • 其中列族 personal 拥有 name、city、phone 三个列,列族 office 拥有 tel、addres 两个列。

img

图片引用自 : HBase 是列式存储数据库吗 https://www.iteblog.com/archives/2498.html

HBase 表特性

Hbase 的表具有以下特点:

  • 容量大:一个表可以有数十亿行,上百万列;
  • 面向列:数据是按照列存储,每一列都单独存放,数据即索引,在查询时可以只访问指定列的数据,有效地降低了系统的 I/O 负担;
  • 稀疏性:空 (null) 列并不占用存储空间,表可以设计的非常稀疏 ;
  • 数据多版本:每个单元中的数据可以有多个版本,按照时间戳排序,新的数据在最上面;
  • 存储类型:所有数据的底层存储格式都是字节数组 (byte[])。

参考资料

HBase Java API 高级特性之协处理器

简述

在使用 HBase 时,如果你的数据量达到了数十亿行或数百万列,此时能否在查询中返回大量数据将受制于网络的带宽,即便网络状况允许,但是客户端的计算处理也未必能够满足要求。在这种情况下,协处理器(Coprocessors)应运而生。它允许你将业务计算代码放入在 RegionServer 的协处理器中,将处理好的数据再返回给客户端,这可以极大地降低需要传输的数据量,从而获得性能上的提升。同时协处理器也允许用户扩展实现 HBase 目前所不具备的功能,如权限校验、二级索引、完整性约束等。

参考资料