Skip to content

Commit 525f67c

Browse files
committed
📝 Writing docs.
1 parent b26aa98 commit 525f67c

2 files changed

Lines changed: 147 additions & 524 deletions

File tree

docs/distributed/分布式原理.md

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ tags:
3030
- [5. 共识性问题](#5-共识性问题)
3131
- [5.1. Paxos](#51-paxos)
3232
- [5.2. Raft](#52-raft)
33-
- [6. 参考资料](#6-参考资料)
33+
- [6. 分布式缓存问题](#6-分布式缓存问题)
34+
- [6.1. 缓存雪崩](#61-缓存雪崩)
35+
- [6.2. 缓存穿透](#62-缓存穿透)
36+
- [6.3. 缓存预热](#63-缓存预热)
37+
- [6.4. 缓存更新](#64-缓存更新)
38+
- [6.5. 缓存降级](#65-缓存降级)
39+
- [7. 参考资料](#7-参考资料)
3440

3541
<!-- /TOC -->
3642

@@ -264,15 +270,19 @@ ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE
264270

265271
### 4.3. 本地消息表(异步确保)
266272

267-
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理
273+
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性
268274

269-
基本思路就是:
275+
1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
276+
2. 之后将本地消息表中的消息转发到 Kafka 等消息队列(MQ)中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
277+
3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
270278

271-
- 消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过 MQ 发送到消息的消费方。如果消息发送失败,会进行重试发送。
272-
- 消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
273-
- 生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
279+
<div align="center">
280+
<img src="https://raw.githubusercontent.com/dunwu/JavaWeb/master/images/distributed/architecture/分布式事务本地消息.jpg" />
281+
</div>
274282

275-
这种方案遵循 BASE 理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像 2PC 那样复杂的实现(当调用链很长的时候,2PC 的可用性是非常低的),也不会像 TCC 那样可能出现确认或者回滚不了的情况。
283+
这种方案遵循 BASE 理论,采用的是最终一致性。
284+
285+
本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。
276286

277287
#### 本地消息表优缺点
278288

@@ -427,7 +437,57 @@ _注:此处 log 并非是指日志消息,而是各种事件的发生记录
427437
<img src="https://raw.githubusercontent.com/dunwu/JavaWeb/master/images/distributed/architecture/raft-sync-log-04.gif" />
428438
</div>
429439

430-
## 6. 参考资料
440+
## 6. 分布式缓存问题
441+
442+
### 6.1. 缓存雪崩
443+
444+
缓存雪崩是指:在高并发场景下,由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
445+
446+
解决方案:
447+
448+
- 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
449+
- 还有一个简单的方案,就是将缓存失效时间分散开,不要所有缓存时间长度都设置成 5 分钟或者 10 分钟;比如我们可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
450+
451+
缓存失效时产生的雪崩效应,将所有请求全部放在数据库上,这样很容易就达到数据库的瓶颈,导致服务无法正常提供。尽量避免这种场景的发生。
452+
453+
### 6.2. 缓存穿透
454+
455+
缓存穿透是指:用户查询的数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
456+
457+
当在流量较大时,出现这样的情况,一直请求 DB,很容易导致服务挂掉。
458+
459+
解决方案:
460+
461+
1. 在封装的缓存 SET 和 GET 部分增加个步骤,如果查询一个 KEY 不存在,就以这个 KEY 为前缀设定一个标识 KEY;以后再查询该 KEY 的时候,先查询标识 KEY,如果标识 KEY 存在,就返回一个协定好的非 false 或者 NULL 值,然后 APP 做相应的处理,这样缓存层就不会被穿透。当然这个验证 KEY 的失效时间不能太长。
462+
2. 如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,一般只有几分钟。
463+
3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。
464+
465+
### 6.3. 缓存预热
466+
467+
缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
468+
469+
解决方案:
470+
471+
1. 直接写个缓存刷新页面,上线时手工操作下;
472+
2. 数据量不大,可以在项目启动的时候自动进行加载;
473+
3. 定时刷新缓存;
474+
475+
### 6.4. 缓存更新
476+
477+
除了缓存服务器自带的缓存失效策略之外(Redis 默认的有 6 中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
478+
479+
1. 定时去清理过期的缓存;
480+
2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
481+
482+
两者各有优劣,第一种的缺点是维护大量缓存的 key 是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
483+
484+
### 6.5. 缓存降级
485+
486+
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
487+
488+
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
489+
490+
## 7. 参考资料
431491

432492
- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
433493
- [区块链技术指南](https://www.gitbook.com/book/yeasy/blockchain_guide/details)

0 commit comments

Comments
 (0)