Spring Cloud Alibaba学习笔记
Spring Cloud Alibaba入门
Spring Cloud简介
Spring Cloud Alibaba 简介

版本兼容关系
- 使用 spring cloud alibaba 时特别需要注意版本间的兼容关系,这些关系包括 spring cloud alibaba、spring cloud 与 spring boot 间的版本兼容关系,包括 spring cloud alibaba 与使用的 alibaba 中间件版本间的兼容关系。
Spring Cloud 与 Spring Boot 版本

Spring Cloud Alibaba版本说明

演示环境搭建
- 本例实现消费者对提供者的调用,但并未使用到 Spring Cloud,而是使用的 Spring 提供的 RestTemplate 实现。不过后续 Spring Cloud 的运行环境就是在此基础上修改出来的。使用 MySQL 数据库,使用 Spring Data JPA 作为持久层技术。
- 源代码github地址:https://github.com/shouwangyw/springcloudalibaba-workspace
- 父工程pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.yw.sca.example</groupId>
<artifactId>sca-example-parent</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<modules>
<module>01-provider-8081</module>
<module>01-consumer-8080</module>
</modules>
<dependencyManagement>
<dependencies>
<!-- druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
创建提供者工程 01-provider-8081
- 创建工程:创建一个 Spring Initializr 工程,并命名为 01-provider-8081。
- 定义实体类:
@Data
@Accessors(chain = true)
@Entity
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "fieldHandler"})
public class Depart {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
}
- 定义Repository接口:
public interface DepartRepository extends JpaRepository<Depart, Integer> {
}
- 定义Service接口:
public interface DepartService {
boolean save(Depart depart);
boolean deleteById(int id);
boolean update(Depart depart);
Depart findById(int id);
List<Depart> list();
}
- 定义Service实现类:
@Service
public class DepartServiceImpl implements DepartService {
@Autowired
private DepartRepository departRepository;
@Override
public boolean save(Depart depart) {
return departRepository.save(depart) != null;
}
@Override
public boolean deleteById(int id) {
if (departRepository.existsById(id)) {
departRepository.deleteById(id);
return true;
}
return false;
}
@Override
public boolean update(Depart depart) {
return departRepository.save(depart) != null;
}
@Override
public Depart findById(int id) {
if (departRepository.existsById(id)) {
return departRepository.getOne(id);
}
return new Depart().setName("no this depart");
}
@Override
public List<Depart> list() {
return departRepository.findAll();
}
}
- 定义Controller接口:
@RestController
@RequestMapping("/provider/depart")
public class DepartController {
@Autowired
private DepartService departService;
@PostMapping("/save")
public boolean save(@RequestBody Depart depart) {
return departService.save(depart);
}
@DeleteMapping("/del/{id}")
public boolean delete(@PathVariable("id") int id) {
return departService.deleteById(id);
}
@PutMapping("/update")
public boolean update(@RequestBody Depart depart) {
return departService.update(depart);
}
@GetMapping("/get/{id}")
public Depart findById(@PathVariable("id") int id) {
return departService.findById(id);
}
@GetMapping("/list")
public List<Depart> list() {
return departService.list();
}
}
- application.yml 配置文件:
server:
port: 8081
spring:
jpa:
generate-ddl: true
show-sql: true
hibernate:
ddl-auto: none
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.254.128:3306/test?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
logging:
pattern:
console: level-%level %msg%n
level:
root: info
org.hibernate: info
org.hibernate.type.descriptor.sql.BasicBinder: trace
org.hibernate.type.descriptor.sql.BasicExtractor: trace
com.yw: debug
- 启动类:
@SpringBootApplication
public class Provider018081 {
public static void main(String[] args) {
SpringApplication.run(Provider018081.class, args);
}
}
创建消费者工程 01-consumer-8080
- 创建工程:创建一个 Spring Initializr 工程,并命名为 01-consumer-8080。
- 定义实体类
@Data
@Accessors(chain = true)
public class Depart {
private Integer id;
private String name;
}
- 定义Controller接口:
@RestController
@RequestMapping("/consumer/depart")
public class DepartController {
@Autowired
private RestTemplate restTemplate;
// 直连
private static final String SERVICE_PROVIDER = "http://localhost:8081/provider/depart";
@PostMapping("/save")
public Boolean save(@RequestBody Depart depart) {
return restTemplate.postForObject(SERVICE_PROVIDER + "/save", depart, Boolean.class);
}
@DeleteMapping("/del/{id}")
public void delete(@PathVariable("id") int id) {
restTemplate.delete(SERVICE_PROVIDER + "/del/" + id);
}
@PutMapping("/update")
public void update(@RequestBody Depart depart) {
restTemplate.put(SERVICE_PROVIDER + "/update", depart, Boolean.class);
}
@GetMapping("/get/{id}")
public Depart findById(@PathVariable("id") int id) {
return restTemplate.getForObject(SERVICE_PROVIDER + "/get/" + id, Depart.class);
}
@GetMapping("/list")
public List<Depart> list() {
return restTemplate.getForObject(SERVICE_PROVIDER + "/list", List.class);
}
}
- 启动类:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Consumer018080 {
public static void main(String[] args) {
SpringApplication.run(Consumer018080.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 接下来就可以分别启动提供者和消费者工程,测试一下接口。
Nacos Discovery服务注册与发现
Nacos概述
注册中心简介
- 前面的例子存在一个问题:消费者直接连接的提供者。这样做的问题是,若提供者出现宕机,或消费者存在高并发情况,那么提供者就会出现问题。所以,我们就需要一个服务注册中心,就像之前的 Zookeeper 一样。提供者对于消费者来说是透明的,不固定的。
- 所有提供者将自己提供服务的名称及自己主机详情(IP、端口、版本等)写入到另一台主机中的一个列表中,这台主机称为服务注册中心,而这个表称为服务注册表。
- 所有消费者需要调用微服务时,其会从注册中心首先将服务注册表下载到本地,然后根据消费者本地设置好的负载均衡策略选择一个服务提供者进行调用。
- 可以充当 Spring Cloud 服务注册中心的服务器很多,如 Zookeeper、Eureka、Consul 等。Spring Cloud Alibaba 中使用的注册中心为 Alibaba 的中间件 Nacos。
Nacos简介
- Nacos 官网:https://nacos.io/


- Naming andConfigrationService
- Nacos = Eureka + Spring Cloud Config + Spring Cloud Bus + MQ
- Nacos = 服务注册中心 + 配置中心
Nacos环境搭建
搭建单机Nacos
- 下载安装:
# 下载
wget https://github.com/alibaba/nacos/releases/download/1.3.1/nacos-server-1.3.1.zip
# 解压安装
unzip nacos-server-1.3.1.zip
- 查看、修改配置:
[root@centos128 conf]# pwd
/usr/apps/nacos/conf
[root@centos128 conf]# vim application.properties
- 启动:
# 切换目录
cd nacos/bin
# 启动命令
sh startup.sh -m standalone
- 访问:http://192.168.254.128:8848/nacos/index.html,默认账号和密码是nacos/nacos

搭建Nacos集群
- 无论是 Nacos Discovery 还是 Nacos Config,单机版 Nacos 都存在单点问题。所以需要搭建高可用的 Nacos 集群。
- 首先分别在三台机器上安装Nacos,例如:192.168.254.128、192.168.254.130、192.168.254.132
- 修改配置:重命名每台机器上的的 cluster.conf.example 为 cluster.conf。然后打开该文件,在其中写入三个 nacos 的 ip:port。
192.168.254.128:8848
192.168.254.130:8848
192.168.254.132:8848
- 然后,分别启动这三个Nacos服务即可。
将数据持久化到外置MySQL
- 默认情况下,Nacos 中的配置数据是被持久到到内置的 MySQL 数据库中的,但使用内置数据库,存在很多问题。生产环境下,Nacos 一般会连接到外部的 MySQL。当然,目前 Nacos 仅支持 MySQL,并且要求是 5.6.5 及其以上版本。
- 查找 SQL 脚本文件:若要连接外置 MySQL,则外置 MySQL 中就要有相应的数据库及表。这些表的创建语句 Nacos 官方已经给出了 SQL 的脚本文件,在 Nacos 解压目录的 config 子目录中 nacos-mysql.sql。
- 运行脚本文件:运行脚本文件 nacos-mysql.sql,在 DBMS 中可以查看到创建的 DB 及表。
- 修改 Nacos 配置:打开 Nacos 安装目录下的 conf/application.properties 文件进行修改

示例演示
- 父工程添加依赖:
<properties>
// ...
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- spring-cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
</dependency>
<!-- spring-cloud-alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
</dependency>
// ...
</dependencyManagement>
<dependencies>
<!-- nacos discovery依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- actuator监控检测 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
// ...
</dependencies>
创建提供者工程 02-provider-nacos-8081
- 定义工程:复制 01-provider-8081 工程,并重命名为 02-provider-nacos-8081。
- 修改 application.yml 配置文件:

- 启动提供者工程,然后在Nacos中查看服务列表:

创建消费者工程 02-consumer-nacos-8080
- 定义工程:复制 01-provider-8081 工程,并重命名为 02-consumer-nacos-8080。
- application.yml 配置文件:

- 修改成负载均衡方式,通过提供者服务名调用:

- 启动消费者工程,然后访问测试地址:http://localhost:8080/consumer/depart/get/1
- 通过 DiscoveryClient 读取Nacos中配置的数据:
@RestController
@RequestMapping("/consumer/depart")
public class DepartController {
@Autowired
private DiscoveryClient client;
@GetMapping("/discovery")
public List<String> discoveryHandle() {
List<String> services = client.getServices();
for (String serviceName : services) {
List<ServiceInstance> instances = client.getInstances(serviceName);
for (ServiceInstance instance : instances) {
String serviceId = instance.getServiceId();
String host = instance.getHost();
int port = instance.getPort();
URI uri = instance.getUri();
System.out.println("serviceId = " + serviceId);
System.out.println("host = " + host);
System.out.println("port = " + port);
System.out.println("uri = " + uri);
}
}
return services;
}
}
- 访问下:http://localhost:8080/consumer/depart/discovery

Raft 算法
- Raft算法动画演示:http://thesecretlivesofdata.com/raft/
基础
- Nacos Discovery 集群为了保证集群中数据的一致性,其采用了 Raft 算法。这是一种通过对日志进行管理来达到一致性的算法。Raft 通过选举 Leader 并由 Leader 节点负责管理日志复制来实现各个节点间数据的一致性。
Nacos Discovery是AP的
- Eureka 是 AP 的,Zookeeper 是 CP 的。
- 默认情况下,Nacos Discovery 集群是 AP 的。但其也支持 CP 模式,需要进行转换。若要转换为 CP 的,可以提交如下 PUT 请求,完成 AP 到 CP 的转换。
http://192.168.254.128:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP

角色、任期及角色转变

- 在 Raft 中,节点有三种角色:
- Leader:唯一负责处理客户端写请求的节点,也可以处理客户端读请求,同时负责日志复制工作。
- Candidate:Leader 选举的候选人,其可能会成为 Leader。
- Follower:可以处理客户端读请求,负责同步来自于 Leader 的日志,当接收到其它Cadidate 的投票请求后可以进行投票,当发现 Leader 挂了,其会转变为 Candidate 发起Leader 选举。
Leader选举
我要选举
若 follower 在心跳超时范围内没有接收到来自于 leader 的心跳,则认为 leader 挂了。此时其首先会使其本地 term 增一。然后 follower 会完成以下步骤:
- 此时若接收到了其它 candidate 的投票请求,则会将选票投给这个 candidate。
- 由 follower 转变为 candidate,本地 term 加一。
- 若之前尚未投票,则向自己投一票。
- 向其它节点发出投票请求,然后等待响应。
我要投票
follower 在接收到投票请求后,其会根据以下情况来判断是否投票:
- 发来投票请求的 candidate 的 term 不能小于我的 term。
- 在我当前 term 内,我的选票还没有投出去。
- 若接收到多个 candidate 的请求,我将采取先来先服务方式投票。
等待响应
当一个 Candidate 发出投票请求后会等待其它节点的响应结果。这个响应结果可能有三种情况:
- 收到过半选票,成为新的 leader。然后会将消息广播给所有其它节点,以告诉大家我是新的 Leader 了,其中就包含新的 term。
- 接收到别的 candidate 发来的新 leader 通知,比较了新 leader 的 term 并不比我的 term 小,则自己转变为 follower。
- 经过一段时间后,没有收到过半选票,也没有收到新 leader 通知,则重新发出选举
票数相同
- 若在选举过程中出现了各个 candidate 票数相同的情况,是无法选举出 Leader 的。当出现了这种情况时,其采用了 randomized election timeouts 策略来解决这个问题。其会让这些 candidate 重新发起选举,只不过发起时间不同:各个 candidate 的选举发起时间是在一个给定范围内等待随机时长 timeout 之后开始的。timeout 较小的会先开始选举,一般情况下其会优先获取到过半选票成为新的 leader。
数据同步
状态机

- Raft 算法一致性的实现,是基于日志复制状态机的。状态机的最大特征是,不同 Server 中的状态机若当前状态相同,然后接受了相同的输入,则一定会得到相同的输出。
处理流程
当 leader 接收到 client 的写操作请求后,大体会经历以下流程:
- leader 将数据封装为日志。
- leader 将日志并行发送给所有 follower,然后等待接收 follower 响应。
- 当 leader 接收到过半响应后,将日志 commit 到自己的状态机,状态机会输出一个结果,同时日志状态变为了 committed。
- 同时 leader 还会通知所有 follower 将日志 apply 到它们本地的状态机,日志状态变为了 applied。
- 在 apply 通知发出的同时,leader 也会向 client 发出成功处理的响应。
AP支持

- Log 由 term index、log index 及 command 构成。为了保证可用性,各个节点中的日志可以不完全相同,但 leader 会不断给 follower 发送 log,以使各个节点的 log 最终达到相同。即 raft 算法不是强一致性的,而是最终一致的。
脑裂
- Raft 集群存在脑裂问题。在多机房部署中,由于网络连接问题,很容易形成多个分区。而多分区的形成,很容易产生脑裂,从而导致数据不一致。
- 由于三机房部署的容灾能力最强,所以生产环境下,三机房部署是最为常见的。下面以三机房部署为例进行分析,根据机房断网情况,可以分为五种情况:
情况一:不确定

- 这种情况下,B 机房中的主机是感知不到 Leader 的存在的,所以 B 机房中的主机会发起新一轮的 Leader 选举。由于 B 机房与 C 机房是相连的,虽然 C 机房中的 Follower 能够感知到 A 机房中的 Leader,但由于其接收到了更大 term 的投票请求,所以 C 机房的 Follower 也就放弃了 A 机房中的 Leader,参与了新 Leader 的选举。
- 若新 Leader 出现在 B 机房,A 机房是感知不到新 Leader 的诞生的,其不会自动下课,所以会形成脑裂。但由于 A 机房 Leader 处理的写操作请求无法获取到过半响应,所以无法完成写操作。但 B 机房 Leader 的写操作处理是可以获取到过半响应的,所以可以完成写操作。故,A 机房与 B、C 机房中出现脑裂,且形成了数据的不一致。
- 若新 Leader 出现在 C 机房,A 机房中的 Leader 则会自动下课,所以不会形成脑裂。
情况二:会脑裂

- 这种情况与情况一基本是一样的。
情况三:不会脑裂

- A、C 可以正常对外提供服务,但 B 无法选举出新的 Leader,无法提供服务,没有形成脑裂。
情况四:不会脑裂

- A、B、C 均可以对外提供服务,不受影响。
情况五:不会脑裂

- A 机房无法处理写操作请求,但可以对外提供读服务。
- B、C 机房由于失去了 Leader,均会发起选举,但由于均无法获取过半支持,所以均无法选举出新的 Leader。
Leader宕机处理
请求到达前Leader挂了
- client 发送写操作请求到达 Leader 之前 Leader 就挂了,因为请求还没有到达集群,所以这个请求对于集群来说就没有存在过,对集群数据的一致性没有任何影响。Leader 挂了之后,会选举产生新的 Leader。
- 由于 Stale Leader 并未向 client 发送成功处理响应,所以 client 会重新发送该写操作请求。
未开始同步数据前Leader挂了
- client 发送写操作请求给 Leader,请求到达 Leader 后,Leader 还没有开始向 Followers 复制数据 Leader 就挂了。这时集群会选举产生新的 Leader,Stale Leader 重启后会作为Follower 重新加入集群,并同步新 Leader 中的数据以保证数据一致性。之前接收到 client 的数据被丢弃。
- 由于 Stale Leader 并未向 client 发送成功处理响应,所以 client 会重新发送该写操作请求。
同步完部分后Leader挂了
- client 发送写操作请求给 Leader,Leader 接收完数据后开始向 Follower 复制数据。在部分 Follower 复制完后 Leader 挂了**(可能过半也可能不过半)**。由于 Leader 挂了,就会发起新的 Leader 选举。
- 若 Leader 产生于已经复制完日志的 Follower,其会继续将前面接收到的写操作请求完成,并向 client 进行响应。
- 若 Leader 产生于尚未复制日志的 Follower,那么原来已经复制过日志的 Follower 则会将这个没有完成的日志放弃。由于 client 没有接收到响应,所以 client 会重新发送该写操作请求。
apply通知发出后Leader挂了
- client 发送写操作请求给 Leader,Leader 接收完数据后开始向 Follower 复制数据。Leader 成功接收到过半 Follower 复制完毕的响应后,Leader 将日志写入到状态机。此时 Leader 向Follower 发送 apply 通知。在发送通知的同时,也会向 client 发出响应。此时 leader 挂了。
- 由于 Stale Leader 已经向 client 发送成功接收响应,且 apply 通知已经发出,说明这个写操作请求已经被 server 成功处理。
Nacos Config服务配置中心
- 集群中每一台主机的配置文件都是相同的,对配置文件的更新维护就成为了一个棘手的问题,Nacos 是可以对 Spring Cloud 中各个微服务配置文件进行统一维护管理的配置中心。
原理
Spring Cloud Config 工作原理

Nacos Config 工作原理

对比
- Nacos Config 无需消息总线系统,系统搭建成本与复杂度比 Spring Cloud Config 低很多。
- Nacos Config 不存在羊群效应,其是定点更新。
- Nacos Config 有远程配置更新后,会自动更新到 client。其采用了长轮询 Pull 模型。而Spring Cloud Config 需要 client 提交请求。
获取远程配置
- 这里实现的需求是,应用从 Nacos Config 中读取指定的配置文件内容。
- 在父工程添加依赖
<!--nacos config依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
创建提供者工程 03-provider-nacos-config-8081
- 定义工程:复制 02-provider-nacos-8081 工程,并重命名为 03-provider-nacos-config-8081。
- 定义 bootstrap.yml 配置文件:删除原有的 application.yml 文件,新建 bootstrap.yml。
spring:
application:
# 根据微服务名称来找动态配置文件
name: msc-provider-depart
cloud:
nacos:
config:
server-addr: 192.168.254.128:8848
file-extension: yml
Nacos配置
- 启动 Nacos,然后打开 nacos 页面,打开配置列表,新建配置:

动态更新配置
- 这里要实现的需求是:从数据库中根据 id 查询到的 depart 的 name 值,在浏览器上显示的并不是 DB 中的 name,而是来自于 nacos 的动态配置 depart.name。
- 修改 Nacos 配置数据:直接在 nacos config 配置页面修改配置信息,例如添加一个 depart.name 属性。修改完毕后,再次发布即可。

- 修改 DepartServiceImpl 类:直接修改 03-provider-nacos-config-8081 工程中的接口实现类。

- 启动工程,测试一下:http://localhost:8081/provider/depart/get/1
多环境选择的实现
新增多环境配置文件
- 克隆文件:在 Nacos config 服务器中克隆两个配置文件。


- 修改配置文件内容:分别修改两个配置文件中的 depart.name 属性。

修改应用的配置文件
- 修改 03-provider-nacos-config-refresh-8081 工程的 bootstrap.yml 文件。在其中添加多环境选择配置。

测试
- 重启工程,访问下:http://localhost:8081/provider/depart/get/1

长轮询 Pull 模型
- Nacos Config Server 中配置数据的变更,Nacos Config Client 是如何知道的呢?Nacos 采用的是长轮询机制的 Pull 模型。
- 长轮询 Pull 模型整合了 Push 与 Pull 模型的优势。Client 仍定时发起 Pull 请求,查看 Server端数据是否更新。若发生了更新,则 Server 立即将更新数据以响应的形式发送给 Client 端;若没有发生更新,Server 端并不立即向 Client 返回响应,而是临时性的保持住这个连接一段时间。若在此时间段内,Server 端数据发生了变更,则立即将变更数据返回给 Client。若仍未发生变更,则放弃这个连接。等待着下一次 Client 的 Pull 请求。
- 长轮询 Pull 模型,是 Push 与 Pull 模型的整合,既降低了 Push 模型中长连接的维护问题,又降低了 Push 模型实时性较低的问题。
- 若使用 Push 模型,需要在 Server 与 Client 间通过心跳机制维护一个长连接。这个长连接的维护成本是比较高的。其适合于 Client 数量不多,且 Server 端数据变化较频繁的场景。
- 若使用 Pull 模型,其无需维护长连接,但其实时性不好。
版权声明:本文为yangwei234原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。
