diff --git a/SUMMARY.md b/SUMMARY.md index 5b16e96bef..49beac211a 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -85,6 +85,8 @@ - [架构](./软件工程/架构/架构.md) - [编程范式](./软件工程/架构/编程范式.md) - [设计原则](./软件工程/架构/设计原则.md) + - [系统设计](./软件工程/架构/系统设计/系统设计.md) + - [分布式](./软件工程/架构/系统设计/分布式.md) - [组件构建原则](./软件工程/架构/组件构建原则.md) - [软件架构](./软件工程/架构/软件架构.md) - [实现细节](./软件工程/架构/实现细节.md) diff --git a/assets/202031620440.png b/assets/202031620440.png new file mode 100644 index 0000000000..0e780ad178 Binary files /dev/null and b/assets/202031620440.png differ diff --git a/assets/202031620538.jpg b/assets/202031620538.jpg new file mode 100644 index 0000000000..67339134d5 Binary files /dev/null and b/assets/202031620538.jpg differ diff --git "a/\344\270\255\351\227\264\344\273\266/\345\210\206\345\270\203\345\274\217/Zookeeper.md" "b/\344\270\255\351\227\264\344\273\266/\345\210\206\345\270\203\345\274\217/Zookeeper.md" index 066e64365b..150991393b 100644 --- "a/\344\270\255\351\227\264\344\273\266/\345\210\206\345\270\203\345\274\217/Zookeeper.md" +++ "b/\344\270\255\351\227\264\344\273\266/\345\210\206\345\270\203\345\274\217/Zookeeper.md" @@ -51,7 +51,7 @@ watcher注册与异步通知机制,能够很好的实现分布式环境下不 - PERSISTENT - PERSISTENT_SEQUENTIAL(发生重复会自增) - EPHEMERAL - - EPHEMERAL_SEQUENTIAL + - EPHEMERAL_SEQUENTIAL(发生重复会自增) - 节点Znode可以包含数据和子节点(但是EPHEMERAL类型的节点不能有子节点) ## JAVA操作 @@ -161,87 +161,6 @@ public static String getServer() { } ``` -## 实现分布式锁 - -### 解决方案 - -- 数据库 -- redis -- zookeeper - - 实现简单,失效时间容易控制 -- SpringCloud内置全局锁 - -### 原理 - -多个jvm同时在zookeeper.上创建同一个相同的节点(/lock) , 因为zookeeper节点是唯一的,如果是唯一的话,那么同时如果有多个客户端创建相同的节点/lock的话,最终只有看谁能够快速的抢资源,谁就能创建/lock节点,这个时候节点类型应该使用临时类型。 - -当一个JVM释放锁后(关闭zk连接),临时节点会被删除,等待锁的其他JVM会收到节点被删除的通知,这些等待的JVM会重新参与到竞争 - -需要注意的是,要根据业务设置锁等待时间,避免死锁 - -### 实现 - -- 上锁 - -```java -public void lock() { - // 尝试获取锁,如果成功,就真的成功了 - if (tryLock()) { - System.out.println(Thread.currentThread().getName() + "获取锁成功"); - // 否则等待锁 - } else { - waitLock(); - // 当等待被唤醒后重新去竞争锁 - lock(); - } -} -private boolean tryLock() { - try { - // 通过zk创建临时节点的成功与否来表示是否获得锁 - zkClient.createEphemeral("/lock"); - return true; - } catch (Exception e) { - return false; - } -} -private void waitLock() { - // 监听节点被删除的事件 - zkClient.subscribeDataChanges("/lock", new IZkDataListener() { - @Override - public void handleDataDeleted(String s) throws Exception { - // 如果节点被删除,唤醒latch - if (latch != null) { - latch.countDown(); - } - } - }); - // 如果zk有lock这个锁 - if (zkClient.exists("/lock")) { - // 在这里进行等待,直至被上面的事件监听唤醒 - latch = new CountDownLatch(1); - try { - latch.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - // 等待完成删除所有监听事件,避免监听器堆积影响性能 - zkClient.unsubscribeAll(); -} -``` - -- 释放锁 - -```java -public void release() { - if (zkClient != null) { - // 关闭zk客户端,临时节点也随之被删除,相当于释放锁,让其他人去竞争 - zkClient.close(); - System.out.println(Thread.currentThread().getName()+"释放锁完成"); - } -} -``` - ## master选举 ### 原理 diff --git "a/\344\270\255\351\227\264\344\273\266/\346\266\210\346\201\257\351\230\237\345\210\227/rabbitMQ.md" "b/\344\270\255\351\227\264\344\273\266/\346\266\210\346\201\257\351\230\237\345\210\227/rabbitMQ.md" index 4fcdf7e954..922aa0e0a9 100644 --- "a/\344\270\255\351\227\264\344\273\266/\346\266\210\346\201\257\351\230\237\345\210\227/rabbitMQ.md" +++ "b/\344\270\255\351\227\264\344\273\266/\346\266\210\346\201\257\351\230\237\345\210\227/rabbitMQ.md" @@ -309,16 +309,6 @@ args.put("deadRoutingKey", deadRoutingKey); Queue queue = new Queue("user_queue", true, false, false, args); ``` -## 解决分布式事务 - -MQ解决分布式事务原理: 采用最终一致性原理 - -- 生产者一定要将数据投递到MQ服务器中(消息确认机制) -- MQ消费者消息能够正确消费消息,采用手动ACK模式(当消费者消费消息失败,则不确认消息,消息进行重试) -- 当生产者出错回滚,发送到补偿队列的消息会检测生产者的数据是否提交成功,如果没有,则补偿队列的消费者会重新执行一遍生产者没有提交的事务 - -![批注 2020-03-16 164628](/assets/批注%202020-03-16%20164628.png) - ## 消息可靠投递方案 ![批注 2019-07-21 113405](/assets/批注%202019-07-21%20113405.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237/Redis.md" "b/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237/Redis.md" index 3928bb0eb8..b27e730244 100644 --- "a/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237/Redis.md" +++ "b/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237/Redis.md" @@ -291,67 +291,4 @@ slaveof 127.0.0.1 6379 ![2020225163638](/assets/2020225163638.png) -原理同HashMap - -## 实现分布式锁 - -### 原理 - -1.获取锁的时候,对某个key执行setnx,加锁(如果设置成功(获得锁)返回1,否则返回0),并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。 - -2.获取锁的时候还设置一个获取的超时时间(防止死锁),若超过这个时间则放弃获取锁。 - -3.释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放 - -### 实现 - -```java -public class RedisLock { - - private StringRedisTemplate template; - - private static final String LOCK_KEY = "LOCK"; - - private String identifyValue; - - public RedisLock(StringRedisTemplate template) {this.template = template;} - - /** - * @param acquireTimeout 获取锁之前的超时时间 - * @param expireTime 锁的过期时间 - * @return - */ - public boolean lock(long acquireTimeout, long expireTime) { - // 获取锁的时间 - long inTime = System.currentTimeMillis(); - identifyValue = UUID.randomUUID().toString(); - for (; ; ) { - // 判断获取锁是否超时 - if (System.currentTimeMillis() - inTime >= acquireTimeout) { - return false; - } - // 通过setnx的方式来获取锁 - if (template.opsForValue().setIfAbsent(LOCK_KEY, identifyValue, expireTime, TimeUnit.MILLISECONDS)) { - // 获取锁成功 - return true; - } - // 获取锁失败,继续自旋 - } - } - - public void release() { - if (identifyValue == null){ - throw new IllegalStateException("没有获取锁"); - } - // 删除的时候验证value,必须确保释放的锁是自己创建的 - if (!identifyValue.equals(template.opsForValue().get(LOCK_KEY))){ - throw new IllegalStateException("锁的value不一致"); - } - template.delete(LOCK_KEY); - } -} -``` - -### 与zookeeper比较 - -相对比来说Redis比Zookeeper性能要好,从可靠性角度分析,Zookeeper可靠性比Redis更好。因为Redis有效期不是很好控制,可能会产生有效期延迟 \ No newline at end of file +原理同HashMap \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" "b/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" index d474475da1..166c75a946 100644 --- "a/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" +++ "b/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" @@ -11,15 +11,26 @@ ![批注 2019-10-31 194221](/assets/批注%202019-10-31%20194221.png) - 一致性(Consistency):服务A、B、C三个结点都存储了用户数据, 三个结点的数据需要保持同一时刻数据一致性。 + - 对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性 - 可用性(Availability):服务A、B、C三个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结 点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。 + - 对于用户的每一个操作请求总是能够在有限的时间内返回结果 - 分区容忍性(Partition Tolerance):分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区导致数据的不完整及无法访问等问题。 +最多只能同时满足其中两项 + ![202031017918](/assets/202031017918.png) +分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的,所以CAP理论实际上是要在可用性和一致性之间做权衡 + +- 保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性 +- 保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致 + ## BASE理论 - BA:(Basically Available ),基本可用 + - 分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性 - S:( Soft State),软状态,状态可以在一段时间内不同步 + - 允许系统不同节点的数据副本之间进行同步的过程存在时延 - E:(Eventually Consistent ),最终一致,在一定的时间窗口内, 最终数据达成一致即可 ## 柔性事务与刚性事务 @@ -27,6 +38,35 @@ - 柔性事务满足BASE理论(基本可用,最终一致) - 刚性事务满足ACID理论 +## Paxos + +对多个节点产生的值,该算法能保证只选出唯一一个值 + +### 节点类型 + +- 提议者(Proposer):提议一个值 +- 接受者(Acceptor):对每个提议进行投票 +- 告知者(Learner):被告知投票的结果,不参与投票过程 + +![202031620538](/assets/202031620538.jpg) + +## Raft + +分布式一致性协议,主要是用来竞选主节点 + +有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段 + +当 Candidate获得超过半数票时,就成为Leader节点 +如果有多个Candidate获得相同的票数,则重新开始投票 +每个节点设置的随机竞选超时时间不同,因此下一次再次出现多个 Candidate 并获得同样票数的概率很低 + +### 数据同步 + +- 自客户端的修改都会被传入 Leader。此时该修改还未被提交,只是写入日志中 +- Leader 会把修改复制到所有 Follower +- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交 +- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致 + ## 解决方案 ### 两阶段提交(2PC) @@ -39,6 +79,10 @@ - 优点:实现强一致性 - 缺点:整个事务的执行需要由协调者在多个节点之间去协调 + - 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞等待状态,无法进行其它操作 + - 协调者如果发生故障会造成很大影响 + - 当在提交阶段网络发生异常,只有部分参与者commit了消息,造成数据不一致 + - 任意一个节点失败就会导致整个事务失败 ### 三阶段提交(3PC) @@ -59,6 +103,22 @@ ![批注 2019-10-31 201018](/assets/批注%202019-10-31%20201018.png) +#### 本地消息表 + +- 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中 +- 之后将本地消息表中的消息转发到消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发 +- 分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作 + +![202031620440](/assets/202031620440.png) + +#### 补偿的方式 + +- 生产者一定要将数据投递到MQ服务器中(消息确认机制) +- MQ消费者消息能够正确消费消息,采用手动ACK模式(当消费者消费消息失败,则不确认消息,消息进行重试) +- 当生产者出错回滚,发送到补偿队列的消息会检测生产者的数据是否提交成功,如果没有,则补偿队列的消费者会重新执行一遍生产者没有提交的事务 + +![批注 2020-03-16 164628](/assets/批注%202020-03-16%20164628.png) + ## LCN ### 原理 diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/\346\236\266\346\236\204/\347\263\273\347\273\237\350\256\276\350\256\241/\345\210\206\345\270\203\345\274\217.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/\346\236\266\346\236\204/\347\263\273\347\273\237\350\256\276\350\256\241/\345\210\206\345\270\203\345\274\217.md" new file mode 100644 index 0000000000..64bf0cb213 --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/\346\236\266\346\236\204/\347\263\273\347\273\237\350\256\276\350\256\241/\345\210\206\345\270\203\345\274\217.md" @@ -0,0 +1,160 @@ +# 分布式 + +## 分布式锁 + +在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁 + +阻塞锁使用一个互斥量来实现: + +- 0代表其他进程在使用锁 +- 1代表未锁定 + +可以用一个整数表示,或者也可以用某个数据是否存在来表示 + +### 数据库唯一索引 + +获得锁时向表中插入一条记录,释放锁时删除这条记录 + +- 锁没有失效时间,容易死锁 +- 是非阻塞的,获取锁失败就报错 +- 不可重入 + +### redis setnx + +1.获取锁的时候,对某个key执行setnx,加锁(如果设置成功(获得锁)返回1,否则返回0),并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。 + +2.获取锁的时候还设置一个获取的超时时间(防止死锁),若超过这个时间则放弃获取锁。 + +3.释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放 + +#### 实现 + +```java +public class RedisLock { + + private StringRedisTemplate template; + + private static final String LOCK_KEY = "LOCK"; + + private String identifyValue; + + public RedisLock(StringRedisTemplate template) {this.template = template;} + + /** + * @param acquireTimeout 获取锁之前的超时时间 + * @param expireTime 锁的过期时间 + * @return + */ + public boolean lock(long acquireTimeout, long expireTime) { + // 获取锁的时间 + long inTime = System.currentTimeMillis(); + identifyValue = UUID.randomUUID().toString(); + for (; ; ) { + // 判断获取锁是否超时 + if (System.currentTimeMillis() - inTime >= acquireTimeout) { + return false; + } + // 通过setnx的方式来获取锁 + if (template.opsForValue().setIfAbsent(LOCK_KEY, identifyValue, expireTime, TimeUnit.MILLISECONDS)) { + // 获取锁成功 + return true; + } + // 获取锁失败,继续自旋 + } + } + + public void release() { + if (identifyValue == null){ + throw new IllegalStateException("没有获取锁"); + } + // 删除的时候验证value,必须确保释放的锁是自己创建的 + if (!identifyValue.equals(template.opsForValue().get(LOCK_KEY))){ + throw new IllegalStateException("锁的value不一致"); + } + template.delete(LOCK_KEY); + } +} +``` + +#### 与zookeeper比较 + +相对比来说Redis比Zookeeper性能要好,从可靠性角度分析,Zookeeper可靠性比Redis更好。因为Redis有效期不是很好控制,可能会产生有效期延迟 + + +### redis redlock + +使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用 + +计算获取锁消耗的时间,只有消耗的时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,才认为获取锁成功 +如果获取锁失败,就到每个实例上释放锁 + +### zookeeper临时节点 + +多个进程同时在zookeeper.上创建同一个相同的节点(/lock) , 因为zookeeper节点是唯一的,如果是唯一的话,那么同时如果有多个客户端创建相同的节点/lock的话,最终只有看谁能够快速的抢资源,谁就能创建/lock节点,这个时候节点类型应该使用临时类型。 + +当一个进程释放锁后(关闭zk连接或者会话超时),临时节点会被删除,等待锁的其他进程会收到节点被删除的通知,这些等待的进程会重新参与到竞争 + +需要注意的是,要根据业务设置锁等待时间,避免死锁 + +#### 实现 + +- 上锁 + +```java +public void lock() { + // 尝试获取锁,如果成功,就真的成功了 + if (tryLock()) { + System.out.println(Thread.currentThread().getName() + "获取锁成功"); + // 否则等待锁 + } else { + waitLock(); + // 当等待被唤醒后重新去竞争锁 + lock(); + } +} +private boolean tryLock() { + try { + // 通过zk创建临时节点的成功与否来表示是否获得锁 + zkClient.createEphemeral("/lock"); + return true; + } catch (Exception e) { + return false; + } +} +private void waitLock() { + // 监听节点被删除的事件 + zkClient.subscribeDataChanges("/lock", new IZkDataListener() { + @Override + public void handleDataDeleted(String s) throws Exception { + // 如果节点被删除,唤醒latch + if (latch != null) { + latch.countDown(); + } + } + }); + // 如果zk有lock这个锁 + if (zkClient.exists("/lock")) { + // 在这里进行等待,直至被上面的事件监听唤醒 + latch = new CountDownLatch(1); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + // 等待完成删除所有监听事件,避免监听器堆积影响性能 + zkClient.unsubscribeAll(); +} +``` + +- 释放锁 + +```java +public void release() { + if (zkClient != null) { + // 关闭zk客户端,临时节点也随之被删除,相当于释放锁,让其他人去竞争 + zkClient.close(); + System.out.println(Thread.currentThread().getName()+"释放锁完成"); + } +} +``` \ No newline at end of file diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/\346\236\266\346\236\204/\347\263\273\347\273\237\350\256\276\350\256\241/\347\263\273\347\273\237\350\256\276\350\256\241.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/\346\236\266\346\236\204/\347\263\273\347\273\237\350\256\276\350\256\241/\347\263\273\347\273\237\350\256\276\350\256\241.md" new file mode 100644 index 0000000000..d6595ba5aa --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/\346\236\266\346\236\204/\347\263\273\347\273\237\350\256\276\350\256\241/\347\263\273\347\273\237\350\256\276\350\256\241.md" @@ -0,0 +1,53 @@ +# 系统设计 + +## 性能 + +### 性能指标 + +- 响应时间 + - 某个请求从发出到接收到响应消耗的时间 +- 吞吐量 + - 系统在单位时间内可以处理的请求数量,通常使用每秒的请求数来衡量 +- 并发用户数 + - 系统能同时处理的并发用户请求数量 + +### 性能优化 + +- 集群 + - 将多台服务器组成集群,使用负载均衡将请求转发到集群中 +- 缓存 + - 缓存对于性能的提升体现在响应时间上 +- 异步 + - 将消息发送到消息队列之后立即返回,之后这个操作会被异步处理 + +## 伸缩性 + +不断向集群中添加服务器来缓解不断上升的用户并发访问压力和不断增长的数据存储需求 + +如果系统存在性能问题,那么单个用户的请求总是很慢的。 +如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢 + +只要集群中的服务器是无状态的,那么往集群中添加服务器后进行负载均衡是很容易的 + +## 扩展性 + +添加新功能时对现有系统的其它应用无影响 + +- 使用消息队列对上下游应用解耦 +- 使用分布式服务将业务与服务分离,服务都是一些可复用的服务,添加新功能时,只要调用已有的服务即可 + +## 可用性 + +### 冗余 + +保证高可用的主要手段是使用冗余 + +对于应用服务器来说,保证是无状态的,就可以实现冗余 +而对于存储服务器,需要通过主从复制来实现冗余 + +### 监控 + +### 服务降级 + +## 安全性 +