模拟验证 分布式CAP定理
一、回顾分布式CAP定理
CAP定理
又称CAP原则,指的是在一个分布式系统中,Consistency(一致性)
、 Availability(可用性)
、Partition tolerance(分区容错性)
,最多只能同时三个特性中的两个,三者不可兼得。
- 一致性:意味着所有节点同时看到相同的数据。简单来说,如果在一致的系统上执行读操作,它应该返回最近的写操作的值。即读取应该所有节点返回相同的数据,即最近写入的值。
- 可用性:分布式系统中的可用性确保系统100%的时间保持运行。无论节点的单独状态如何,每个请求都会获得(非错误)响应。但请注意,这并不能保证响应包含最新的写入。
- 分区容错性:此条件表明,无论消息在系统中的节点之间是否丢失或延迟,系统都不会失败。在分布式系统中,分区容错已经成为一种必需品,而不是一种选择。这是通过跨节点和网络的组合充分复制记录来实现的。
下面试着写一个分布式demo来验证CAP定理,项目结构如下
maven中两个模块distributed-A和distributed-B模拟分布式的两个节点,A节点和B节点;其中A节点8081端口启动,B节点8082端口;
向A节点写入数据,写入后向B节点同步该数据,同时访问B节点的数据,来验证CAP定理。
二、AP组合
分布式系统满足AP,即放弃一致性,也就是在数据同步的过程中对其中节点进行数据访问,返回的可能不是最新的数据。按照AP定理的说法,示例中只要A接收到写入的数据后,直接通过网络向B节点同步即可,因为网络有延迟和不可靠性,向B节点同步数据需要耗时,而这期间B节点返回一个非错误的响应。
// A节点的PushController
@RestController
@Slf4j
public class APushController {
// 模拟数据存储
private List<String> dataList = new ArrayList<>();
// 模拟A节点接收到数据写入
@GetMapping("/push")
public String push(@RequestParam("data") String data) {
// 本地数据存储
dataList.add(data);
// 同时同步数据至B节点
// 这里使用SpringBoot3官方推荐的RestClient进行http调用
RestClient restClient = RestClient.create();
String response = restClient.get()
.uri("http://localhost:8082/sendData?data=" + data)
.retrieve().body(String.class);
log.info("同步数据返回响应:{}", response);
return "success";
}
}
// B节点的PushController
@RestController
@Slf4j
public class PushController {
// 模拟数据存储
private List<String> dataList = new ArrayList<>();
// 用于接收来自A节点的同步数据
@GetMapping("/sendData")
@SneakyThrows
public String sendData(@RequestParam("data") String data) {
log.info("接收到数据:{}", data);
// 睡眠10s 模拟网络延迟
Thread.sleep(10000);
dataList.add(data);
return "success";
}
// 访问数据
@GetMapping("/getData")
public List<String> getData() {
return dataList;
}
}
访问A节点localhost:8081/push?data=1数据写入1,写入成功后A节点调用B节点的/sendData接口进行数据同步,B节点接收到数据后Thread.sleep(10000);模拟网络延迟。
同步期间调用B节点localhost:8082/getData获取数据。
10s后再次访问localhost:8082/getData获取数据
此时已验证了CAP定理中的AP,
虽然A节点在往B节点同步数据,但B节点依旧能正常向外提供服务,都会获得(非错误)响应,虽然不是最新的数据。
三、CP组合
分布式系统满足AP,即要求强一致性;即B节点应该返回的是最新写入的值1,而不是空列表。
对B节点的PushController进行稍加改造。
// B节点的PushController
@RestController
@Slf4j
public class PushController {
// 模拟数据存储
private List<String> dataList = new ArrayList<>();
private Object object = new Object();
// 用于接收来自A节点的同步数据
@GetMapping("/sendData")
@SneakyThrows
public String sendData(@RequestParam("data") String data) {
log.info("接收到数据:{}", data);
synchronized (object){
Thread.sleep(10000);
dataList.add(data);
}
return "success";
}
@GetMapping("/getData")
public List<String> getData() {
synchronized (object){
return dataList;
}
}
}
此时B节点接收到A节点的同步数据,使用synchronized进行上锁,同时getData方法也进行上锁,并且锁的是同一个对象,这样两个方法就达到了互斥,保证了在调用getData获取数据时是最新的数据;在数据同步时getData请求会阻塞,导致B节点无法向外提供服务(或返回错误响应),满足了CP,牺牲了可用性。
同步期间向B节点访问数据的请求被阻塞 无法及时响应
同步完成之后响应最新的数据
四、总结
CAP定理 分布式系统中,一致性、可用性和分区容忍性无法同时满足,根据实际业务场景需求权衡选择,确定最合适的方案。
以上为个人对CAP定理的理解,若有错误,还望批评指正
转载自:https://juejin.cn/post/7346888998486933558