高并发常见场景:
-
读多写少
eg 用户中心
职能梳理
缓存同步
鉴权令牌
机房同步
Raft共识 -
强一致性
eg 电商系统
领域拆分
性能与锁
隔离与同步
分布式事务 -
写多读少
eg 链路跟踪系统
分布式链路跟踪
OLAP/OLTP
ELK与链路跟踪
ClickHouse -
读多写多
eg 游戏 直播类系统
业务做缓存
脚本引擎
流量分流架构
DNS与调度
经典架构则是:统一缓存 元数据 日志中心 WAF 压测验证
读多写少的高并发优化实践-用户中心
梳理数据表 ========================
特定场景精简字段 例如 用户账号表 只保留 账户密码和状态
数据主要分为四种:实体对象主表、辅助查询表、实体关系和历史数据
优化实体对象表
精简数据总长度;
减少表承担的业务职能;
减少统计计算查询;
实体数据更适合放在缓存当中;
尽量让实体能够通过 ID 或关系方式查找;
减少实时条件筛选方式的对外服务。
优化辅助查询表
定期核对整理数据,保证冗余数据的同步和完整
行业里也会用一些开源搜索引擎,辅助我们做类似的关系业务查询,比如用 ElasticSearch 做商品检索、用 OpenSearch 做文章检索等。这种可横向扩容的服务能大大降低数据库查询压力,但唯一缺点就是很难实现数据的强一致性,需要人工检测、核对两个系统的数据。
优化实体关系表
在关系类型数据中,建议额外用一个关系表来记录实体间 m:n 的关联关系
优化动作历史表
对于这种基于大量的数据统计后才能得到的结论数据,不建议对外提供实时统计计算服务,这种查询会严重拖慢我们的数据库,影响服务稳定。即使使用缓存临时保存统计结果,这也属于临时方案,建议用其他更具体的表去做类似的事情。
到底什么样的数据适合做缓存?
1)能够通过 ID 快速匹配的实体,以及通过关系快速查询的数据,适合放在长期缓存当中;
2)通过组合条件筛选统计的数据,也可以放到临时缓存,但是更新有延迟;
3)数据增长量大或者跟设计初衷不一样的表数据,这种不适合、也不建议去做做缓存。
缓存一致======================
做缓存是要考虑性价比(数据量、使用频率、缓存命中率三个角度去分析),一般来说,只有热点数据放到缓存才更有价值。
采用临时缓存
将数据临时放到缓存,等待 60 秒过期后数据就会被淘汰,如果有同样的数据查询需要,我们的代码会将数据重新填入缓存继续使用。这种临时缓存适合表中数据量大,但热数据少的情况,可以降低热点数据的压力。
解决缓存如果出现更新不及时的问题:
- 单条实体数据缓存刷新
- 关系型和统计型数据缓存刷新
1)首先考虑人工维护,更新较慢,存在延迟;
2)再者就是考虑通过订阅数据库来找到 ID 数据变化。如使用 Canal对 MySQL 的更新进行监控。这样变更信息会推送到 Kafka 内,我们可以根据对应的表和具体的 SQL 确认更新涉及的数据 ID,然后根据脚本内设定好的逻辑对相 关 key 进行更新。这种方式的好处是能及时更新简单的缓存,同时核心系统会给子系统广播同步数据更改,代码也不复杂;缺点是复杂的关联关系刷新,仍旧需要通过人工写逻辑来实现。
"Canal"指的是阿里巴巴开源的一个基于数据库增量日志解析,提供增量数据订阅&消费的中间件,它能够实现实时的数据同步和服务间的数据变更通知。
3)如果我们表内的数据更新很少,那么可以采用版本号缓存设计。
但一旦有任何更新,整个表内所有数据缓存一起过期。
当业务要读取某个信息的时候,业务会同时获取当前表的 version。如果发现缓存数据内的版本和当前表的版本不一致,那么就会更新这条数据。但如果 version 更新很频繁,就会严重降低缓存命中率,所以这种方案适合更新很少的表。
4)此外,关联型数据更新还可以通过识别主要实体 ID 来刷新缓存。这要保证其他缓存保存的 key 也是主要实体 ID,这样当某一条关联数据发生变化时,就可以根据主要实体 ID 对所有缓存进行刷新。这个方式的缺点是,我们的缓存要能够根据修改的数据反向找到它关联的主体 ID 才行。
5)最后,异步脚本遍历数据库刷新所有相关缓存。这个方式适用于两个系统之间同步数据,能够减少系统间的接口交互;缺点是删除数据后,还需要人工删除对应的缓存,所以更新会有延迟。但如果能配合订阅更新消息广播的话,可以做到准同步。
如果当 TTL 到期时,如果大量缓存请求没有命中,透传的流量会不会打沉我们的数据库?
这其实就是常提到的缓存穿透问题,如果缓存出现大规模并发穿透,那么很有可能导致我们服务宕机。
所以,数据库要是扛不住平时的流量,我们就不能使用临时缓存的方式去设计缓存系统,只能用长期缓存这种方式来实现热点缓存,以此避免缓存穿透打沉数据库的问题。
一般情况下,是长期缓存和临时缓存的混用。当我们要查询某个用户信息时,如果缓存中没有数据,长期缓存会直接返回没有找到,临时缓存则直接走更新流程。此外,我们的用户信息如果属于热点 key,并且在缓存中找不到的话,就直接返回数据不存在。
布隆过滤器(Bloom Filter)是一种空间效率非常高的概率型数据结构,用于判断一个元素是否在一个集合中。它能够以很低的内存消耗来快速测试一个元素是否“可能在”或“绝对不在”集合中。如果说key超过上千个,可以使用布隆过滤器判断。
在更新期间,为了防止高并发查询打沉数据库,我们将更新流程做了简单的 singleflight(请求合并)优化,只有先抢到缓存更新锁的线程,才能进入后端读取数据库并将结果填写到缓存中。而没有抢到更新锁的线程先 sleep 1 秒,然后直接读取缓存返回结果。这样可以保证后端不会有多个线程读取同一条数据,从而冲垮缓存和数据库服务(缓存的写并发没有读性能那么好)。
另外,hot_key 列表(也就是长期缓存的热点 key 列表)会在多个 Redis 中复制保存,如果要读取它,随机找一个分片就可以拿到全量配置。这些热缓存 key,来自于统计一段时间内数据访问流量,计算得出的热点数据。
那长期缓存的更新会异步脚本去定期扫描热缓存列表,通过这个方式来主动推送缓存,同时把 TTL 设置成更长的时间,来保证新的热数据缓存不会过期。当这个 key 的热度过去后,热缓存 key 就会从当前 set 中移除,腾出空间给其他地方使用。
当然,如果我们拥有一个很大的缓存集群,并且我们的数据都属于热数据,那么我们大可以脱离数据库,将数据都放到缓存当中直接对外服务,这样我们将获得更好的吞吐和并发。
最后,还有一种方式来缓解热点高并发查询,在每个业务服务器上部署一个小容量的 Redis 来保存热点缓存数据,通过脚本将热点数据同步到每个服务器的小 Redis 上,每次查询数据之前都会在本地小 Redis 查找一下,如果找不到再去大缓存内查询,通过这个方式缓解缓存的读取性能。
ps:缓存刷新方式
主键ID刷新
监控binlog刷新
条件划定刷新范围
版本号过期刷新
TTL过期刷新
异步脚本扫表刷新