Dubbo笔记

1. 什么是 Dubbo
Apache Dubbo (incubating) |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC 框架,它提供了三大核心能力:**面向接口的远程方法调用(RPC),智能容错和负载均衡,以及服务自动注册和发现。**简单来说 Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
2. 什么是 RPC?RPC原理是什么?
2.1 RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP请求当然可以,但是可能会比较麻烦。 RPC 的出现就是为了让你调用远程方法像调用本地方法一样简单。
2.2 RPC原理是什么?
在这里插入图片描述
服务消费方(client)调用以本地调用方式调用服务;
(生成它需要动态代理技术)client stub(客户端根程序)接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;(序列化)
client stub找到服务地址,并将消息发送到服务端;
server stub收到消息后进行解码;(反序列化)
server stub根据解码结果调用本地的服务;
本地服务执行并将结果返回给server stub;
server stub将返回结果打包成消息并发送至消费方;
client stub接收到消息,并进行解码;
服务消费方得到最终结果。
时序图如下:
在这里插入图片描述
3 为什么要用 Dubbo?

<dubbo:application name="provider"/>
   <dubbo:registry address="zookeeper://103.160.52.24:2181"/>
   

3.1 负载均衡——同一个服务部署在不同的机器时该调用那一台机器上的服务。
3.2.服务调用链路生成——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。
3.3 服务访问压力以及时长统计、资源调度和治理——基于访问压力实时管理集群容量,提高集群利用率。
3.4 服务降级——某个服务挂掉之后调用备用服务。
4. Dubbo 的架构
在这里插入图片描述
调用关系说明:

服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
重要知识点总结
注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小

监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示

注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外
注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者
注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表

注册中心和监控中心都是可选的,服务消费者可以直连服务提供者
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
4.1 Dubbo 工作原理
在这里插入图片描述

图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
各层说明:
第一层:service层,接口层,给服务提供者和消费者来实现的
第二层:config层,配置层,主要是对dubbo进行各种配置的
第三层:proxy层,服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton
第四层:registry层,服务注册层,负责服务的注册与发现
第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
第七层:protocol层,远程调用层,封装rpc调用
第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
第九层:transport层,网络传输层,抽象mina和netty为统一接口
第十层:serialize层,数据序列化层,网络传输需要
Dubbo核心的配置
在这里插入图片描述
dubbo 支持不同的通信协议
**dubbo 协议:**默认就是走 dubbo 协议,单一长连接(建立连接过后可以持续发送请求,无须再建立连接),进行的是 NIO 异步通信,基于 hessian 作为序列化协议。使用的场景是:传输数据量小(每次请求在 100kb 以内),但是并发量很高。
rmi 协议
走 Java 二进制序列化,多个短连接,适合消费者和提供者数量差不多的情况,适用于文件的传输,一般较少用。

hessian 协议
走 hessian 序列化协议,多个短连接,适用于提供者数量比消费者数量还多的情况,适用于文件的传输,一般较少用。

http 协议
走表单序列化。

webservice
走 SOAP 文本序列化。

4.2 Dubbo 的负载均衡策略
在这里插入图片描述

4.2.1 Random LoadBalance(默认,基于权重的随机负载均衡机制)
随机,按权重设置随机概率。
在这里插入图片描述
xml 配置方式
服务端服务级别

<dubbo:service interface="..." loadbalance="roundrobin" />

客户端服务级别

<dubbo:reference interface="..." loadbalance="roundrobin" />

注解配置方式:
消费方基于基于注解的服务级别配置方式:

@Reference(loadbalance = "roundrobin")
HelloService helloService;

4.2.2:RoundRobinLoadBalance(轮询负载均衡)
均匀地将流量打到各个机器上去,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高
4.2.3 LeastActiveLoadBalance
自动感知一下,如果某个机器性能越差,那么接收的请求越少
4.2.4ConsistentHashLoadBalance(一致性 Hash 算法)
相同参数的请求一定分发到一个 provider 上去,provider 挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性 Hash 策略。

4.3 zookeeper宕机与dubbo直连的情况
在实际生产中,假如zookeeper注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用的本地缓存进行通讯,这只是dubbo健壮性的一种体现。
**注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。**所以,我们可以完全可以绕过注册中心——采用 dubbo 直连 ,即在服务消费方配置服务提供方的位置信息
xml配置方式:

<dubbo:reference id="userService" interface="com.zang.gmall.service.UserService" url="dubbo://localhost:20880" />

注解方式:

 @Reference(url = "127.0.0.1:20880")   
 HelloService helloService;

4.4 dubbo 集群容错策略

Failover Cluster 模式
失败自动切换,自动重试其他机器,默认就是这个,常见于读操作。(失败重试其它机器)

可以通过以下几种方式配置重试次数:

<dubbo:service retries=“2” />
或者

<dubbo:reference retries=“2” />
或者

dubbo:reference
<dubbo:method name=“findFoo” retries=“2” />
</dubbo:reference>
Failfast Cluster 模式
一次调用失败就立即失败,常见于非幂等性的写操作,比如新增一条记录(调用失败就立即失败)

Failsafe Cluster 模式
出现异常时忽略掉,常用于不重要的接口调用,比如记录日志。

配置示例如下:

<dubbo:service cluster=“failsafe” />
或者

<dubbo:reference cluster=“failsafe” />
Failback Cluster 模式
失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种。

Forking Cluster 模式
并行调用多个 provider,只要一个成功就立即返回。常用于实时性要求比较高的读操作,但是会浪费更多的服务资源,可通过 forks=“2” 来设置最大并行数。

Broadcast Cluster 模式
逐个调用所有的 provider。任何一个 provider 出错则报错(从 2.1.0 版本开始支持)。通常用于通知所有提供者更新缓存或日志等本地资源信息。
5.ZooKeeper
ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
ZooKeeper 的
目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

使用场景:
分布式协调
A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上对某个节点的值注册个监听器,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 系统立马就可以收到通知
在这里插入图片描述
分布式锁:
对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也尝试去创建那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。
在这里插入图片描述
元数据/配置信息管理(dubbo的注册中心)
实现分布式锁都有哪些方式(分布式锁,就是为了确保在分布式的环境下,相同任务只会执行成功的执行一次,后续的执行不会对这些已经产生了变化的业务再次产生影响。)
Redis 分布式锁(AP)
官方叫做 RedLock 算法,是 Redis 官方支持的分布式锁算法。
第一个最普通的实现方式,就是在 Redis 里使用 SET key value [EX seconds] [PX milliseconds] NX 创建一个 key,这样就算加锁。其中:

NX:表示只有 key 不存在的时候才会设置成功,如果此时 redis 中存在这个 key,那么设置失败,返回 nil。
EX seconds:设置 key 的过期时间,精确到秒级。意思是 seconds 秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。
PX milliseconds:同样是设置 key 的过期时间,精确到毫秒级。

SET resource_name my_random_value PX 30000 NX

setnx实现超卖示例
在这里插入图片描述
看门狗机制(定时延长超时时间),是一种锁续命机制。redisson可实现
在这里插入图片描述
在这里插入图片描述

最底层为保证原子性,使用lua脚本实现。
想用redis的高性能又希望保证锁不失效用RedLock

在这里插入图片描述

setnx获取锁的Java代码实现如下:

public boolean getLock(String lockKey) {
	RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
	JedisCommands commands = (JedisCommands) connection.getNativeConnection();
	boolean con = false;
	do {
		long now = System.currentTimeMillis();
		// Redis的GetSet返回的值必须是字符串,否则会抛异常,因而将其转换为字符串
		String nowTime = String.valueOf(now);
		con = false;
		// 返回1表示锁获取成功,返回0表示锁取失败
		String result = commands.set(lockKey, nowTime, "NX", "PX", expire);
		if ("1".equals(result)) {
			return true;
		} else {
			String oldTime = redisTemplate.opsForValue().get(lockKey);
			if (null != oldTime) {
				// 检查锁lockKey的值是不是超过了设定的时间,如2秒钟,如果超过了则继续尝试获取锁,
				// 直到获取到锁,或者数据未超期时退出,循环判断可以解决死锁的问题
				if (now - Long.parseLong(oldTime) >= expire) {// 数据已经过期了
					con = true;
				}
			}
		}
	} while (con);
	return false;
}

**zk实现分布式锁(CP)**节点具有互斥性和唯一性
某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。

package study01;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * zookeeper实现分布式锁
 * @author clay
 * @site www.java1234.com
 * @create 2021-03-13 15:07
 */
public class ZooKeeperSession {

        private static CountDownLatch connectedSemaphore = new CountDownLatch(1);

        private ZooKeeper zookeeper;
        private CountDownLatch latch;

        public ZooKeeperSession() {
            try {
                this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher());
                try {
                    connectedSemaphore.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("ZooKeeper session established......");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        /**
         * 获取分布式锁
         *
         * @param productId
         */
        public Boolean acquireDistributedLock(Long productId) {
            String path = "/product-lock-" + productId;

            try {
                zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
                return true;
            } catch (Exception e) {
                while (true) {
                    try {
                        // 相当于是给node注册一个监听器,去看看这个监听器是否存在
                        Stat stat = zk.exists(path, true);

                        if (stat != null) {
                            this.latch = new CountDownLatch(1);
                            this.latch.await(waitTime, TimeUnit.MILLISECONDS);
                            this.latch = null;
                        }
                        zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
                        return true;
                    } catch (Exception ee) {
                        continue;
                    }
                }

            }
            return true;
        }

        /**
         * 释放掉一个分布式锁
         *
         * @param productId
         */
        public void releaseDistributedLock(Long productId) {
            String path = "/product-lock-" + productId;
            try {
                zookeeper.delete(path, -1);
                System.out.println("release the lock for product[id=" + productId + "]......");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        /**
         * 建立 zk session 的 watcher
         */
        private class ZooKeeperWatcher implements Watcher {

            public void process(WatchedEvent event) {
                System.out.println("Receive watched event: " + event.getState());

                if (KeeperState.SyncConnected == event.getState()) {
                    connectedSemaphore.countDown();
                }

                if (this.latch != null) {
                    this.latch.countDown();
                }
            }

        }

        /**
         * 封装单例的静态内部类
         */
        private static class Singleton {

            private static ZooKeeperSession instance;

            static {
                instance = new ZooKeeperSession();
            }

            public static ZooKeeperSession getInstance() {
                return instance;
            }

        }

        /**
         * 获取单例
         *
         * @return
         */
        public static ZooKeeperSession getInstance() {
            return Singleton.getInstance();
        }

        /**
         * 初始化单例的便捷方法
         */
        public static void init() {
            getInstance();
        }

    }
}

redis 分布式锁和 zk 分布式锁的对比
redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小
5.1. Data model(数据模型)
ZooKeeper 数据模型采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且。每个节点还可以拥有 N 个子节点,最上层是根节点以“/”来代表。每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都一个唯一的路径标识。

强调一句:ZooKeeper **主要是用来协调服务的,而不是用来存储业务数据的,**所以不要放比较大的数据在 znode 上,ZooKeeper 给出的上限是每个结点的数据大小最大是 1M。
5. 2. znode(数据节点)
持久(PERSISTENT)节点 :一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
临时(EPHEMERAL)节点 :临时节点的生命周期是与 客户端会话(session) 绑定的,会话消失则节点消失 。并且,临时节点只能做叶子节点 ,不能创建子节点。
持久顺序(PERSISTENT_SEQUENTIAL)节点 :除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 /node1/app0000000001 、/node1/app0000000002 。
临时顺序(EPHEMERAL_SEQUENTIAL)节点 :除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
5.3 znode 数据结构
每个 znode 由 2 部分组成:
stat :状态信息
data : 节点存放的数据的具体内容
5.4 总结
ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。

为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。

ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持 znode 中存储的数据量较小的进一步原因)。

ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地明显,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)

ZooKeeper 有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个 znode 被创建了,除非主动进行 znode 的移除操作,否则这个 znode 将一直保存在 ZooKeeper 上。

ZooKeeper 底层其实只提供了两个功能:① 管理(存储、读取)用户程序提交的数据(文件系统);② 为用户程序提供数据节点监听服务(通知机制)。

Zookeeper 怎么保证主从节点的状态同步?
Zookeeper 的核心是原子广播机制,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模式和广播模式。
恢复模式

当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 server 具有相同的系统状态。
广播模式
一旦 leader 已经和多数的 follower 进行了状态同步后,它就可以开始广播消息了,即进入广播状态。这时候当一个 server 加入 ZooKeeper 服务中,它会在恢复模式下启动,发现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的 followers 支持。
说几个 zookeeper 常用的命令。
ls 查看命令
get 获取节点数据和更新信息
stat 获得节点的更新信息
create 创建节点/create -e 创建临时节点/ create -s 创建顺序节点 自动累加
delete path [version] 删除节点

分布式服务接口的幂等性如何设计(比如不能重复扣款)
例子:支付场景,不能重复扣款
利用 Redis,用 orderId 作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款
要求是支付一个订单,必须插入一条支付流水,order_id 建一个唯一键 unique key 。你在支付一个订单之前,先插入一条支付流水,order_id 就已经进去了。你就可以写一个标识到 Redis 里面去, set order_id payed ,下一次重复请求过来了,先查 Redis 的 order_id 对应的 value,如果是 payed 就说明已经支付过了。

分布式事务
分布式事务的实现主要有以下 6 种方案:
XA 方案
TCC 方案
SAGA 方案
本地消息表
可靠消息最终一致性方案
最大努力通知方案
两阶段提交方案/XA 方案
事务管理器负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。
这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景
在这里插入图片描述
Saga 方案
基本原理
业务流程中每个参与者都提交本地事务,若某一个参与者失败,则补偿前面已经成功的参与者。下图左侧是正常的事务流程,当执行到 T3 时发生了错误,则开始执行右边的事务补偿流程,反向执行 T3、T2、T1 的补偿服务 C3、C2、C1,将 T3、T2、T1 已经修改的数据补偿掉。
适用场景是:
业务流程长、业务流程多

优势
一阶段提交本地事务,无锁,高性能;
参与者可异步执行,高吞吐;
补偿服务易于实现,因为一个更新操作的反向操作是比较容易理解的。
缺点
不保证事务的隔离性。
集群部署时的分布式 Session 如何实现?
1.使用 JWT Token 储存用户身份,然后再从数据库或者 cache 中获取其他的信息。这样无论请求分配到哪个服务器都无所谓。
2.Spring Session + Redis
微服务之间是如何独立通讯的
1.同步
REST HTTP 协议

REST 请求在微服务中是最为常用的一种通讯方式, 它依赖于 HTTP\HTTPS 协议。RESTFUL 的特点是:

  1. 每一个 URI 代表 1 种资源 2. 客户端使用 GET、POST、PUT、DELETE 4 个表示操作方式的动词对服务端资源进行操作: GET 用来获取资源, POST 用来新建资源(也可以用于更新资源), PUT 用来更新资源, DELETE 用来删除资源 3. 通过操作资源的表现形式来操作资源 4. 资源的表现形式是 XML 或者 HTML 5. 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息
    举个例子,有一个服务方提供了如下接口:
@RestController
@RequestMapping("/communication")
public class RestControllerDemo {
    @GetMapping("/hello")
    public String s() {
        return "hello";
    }
}

另外一个服务需要去调用该接口,调用方只需要根据 API 文档发送请求即可获取返回结果。

@RestController
@RequestMapping("/demo")
public class RestDemo{
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/hello2")
    public String s2() {
        String forObject = restTemplate.getForObject("http://localhost:9013/communication/hello", String.class);
        return forObject;
    }
}

2.RPC TCP协议

  1. 执行客户端调用语句,传送参数 2. 调用本地系统发送网络消息 3. 消息传送到远程主机 4. 服务器得到消息并取得参数 5. 根据调用请求以及参数执行远程过程(服务) 6. 执行过程完毕,将结果返回服务器句柄 7. 服务器句柄返回结果,调用远程主机的系统网络服务发送结果 8. 消息传回本地主机 9. 客户端句柄由本地主机的网络服务接收消息 10. 客户端接收到调用语句返回的结果数据
    3.异步
    消息中间件
    常见的消息中间件有 Kafka、ActiveMQ、RabbitMQ、RocketMQ , 常见的协议有AMQP、MQTTP、STOMP、XMPP.

CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能能同时满足以下三点中的两个

一致性(Consistence) : 所有节点访问同一份最新的数据副本
可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
分区容错性(Partition tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
什么是网络分区:
分布式系统中,多个节点之间的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。
CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值