人人都能看懂系列:《分布式系统改造方案——之数据篇》

打工人打工魂,打工仔hellohello-tom又上线了🤣

好久没更新,前一段时间大家都知道郑州又是经历洪涝,又是经历疫情的(涝疫结合啊),让tom哥深深体会到生命的脆弱,人类的文明在灾难面前不堪一击,2021能活着真是万幸,既然活着那咱就要继续输出,充分体现我们打工人的素养,今天tom哥分享的主题就是

人人都能看懂系列:《分布式系统改造方案——之数据篇》。

分布式系统涉及的内容知识面非常的广泛,不是区区一篇文章就能讲明白,但凡复杂的升级方案与枯燥无味的理论知识(CAP、TCC)这些而言,如何在不断飙升的QPS面前从容不断的解决问题才是关键,接下来tom哥就说说在实操过程中我们可能常见的几种问题。

早期的架构基本都是这样,我们的应用程序在请求数据时,会先从缓存中获取数据,如果缓存中拿不到,再去数据库拿一次,拿成功之后在写入缓存,这样模式应该是很多公司前期开发模式。在这单体架构中会产生很多问题,tom哥从常见问题的几个方面去帮大家踩坑,并提供解决方案。

数据库

我们的系统一上线,靠着"老奸巨猾"的产品设计的套路,源源不断的用户开始在我们的平台注册,3个月内用户注册数量已经达到3000W,这时候单表3000W的user_info表在瑟瑟发抖,这时候该怎么优化呢,很多同学都会说分库分表,但是具体怎么分呢?tom哥会采用常见hash方案,首先我们可以把用户单独提出来做一个数据库(垂直拆分),这个数据库里我一次创建

user_info_0、user_info_1、....user_info_1023,

1024张用户表,我们预估每张表500W数据,算下来500W * 1024 = 51亿2千万,这样的用户预估对于大部分互联网公司绝对够用了。但是随之而来带来很多新的问题,历史数据迁移问题、用户信息查询问题等等。先说说历史数据迁移吧。

历史数据迁移

我们的线上系统是一直再跑的,同步又发生在线下,这期间很难做到一致性,看过tom哥前面的文章应该知道历史数据迁移的时候会采用数据双写方案,也就是2个阶段。

第一阶段上线我们会同时往老表和新表各写一份,而迁移程序会在线下去跑,当然迁移程序和线上程序也会存在一个先后顺序的问题,假如迁移程序在某个更新线程之后就很有可能会存在覆盖的情况(请广大同学自行脑补这个画面,一定要想明白哦~),所以我们必须加上版本号或者时间戳等方案,判断当前更新的数据是否等于数据库内存在的版本,以保证原子性,尤其是用户余额这一类的字段操作一定要谨慎。

//类似乐观锁的实现模式,如果能够实现幂等更优
update userInfo set balance = balance + #{money} where userId = #{userId} and version = #{version}

第二阶段在线下同步程序跑完之后,这时候老表和新表数据应该基本都一致了,这时候我们可以把老表的操作代码直接删除,查询相关的内容统一更换成新表的操作,然后再上线一次,就成功完成数据迁移的过程了。

用户信息查询

这个也是一个常见的问题场景,假设我们分了1024张表,用户数据均匀的落在了在1024表内,例如之前我们按照用户手机号去查询时,本来可以轻松的写成

select * from user_info where mobile like '%#{mobile}%'

换成现在的查询方式该怎么写呢

select * from user_info_0 where mobile like '%#{mobile}%'
select * from user_info_1 where mobile like '%#{mobile}%'
...
select * from user_info_1023 where mobile like '%#{mobile}%'

额...这不能把1024张表全部循环一遍吧,并且公司的运营人员可能会根据更多用户信息去检索用户,如果我们按照目前的分表方式确实需要过滤全部的表才能找到匹配电话号码的用户,这里要怎么做呢?

tom哥总结的分布式法则,要用合适的工具去做合适的事,这里我们引入elasticsearch nosql(PS:引入不同的中间件会大大提升系统的复杂度),我们可以根据用户需要搜索的条件将这些条件全部作为索引存入es内,这里tom哥是不建议数据库和es表结构保持一致的,可能查询时我们没有那么多需要过滤的内容,es作为nosql数据库,我们应该把需要的热数据存入es,,先从es内根据条件查到对应的用户信息,根据用户id再去对应的表查询详情数据,我们在es插入doc时,索引一个字段可以存入该条doc用户数据对应的数据库表名,这样就可以快速定位到从DB中那个表中查询到该用户的详细信息了。

引入es中间件我们也要同时考虑新库、老库数据迁移工程(解决思路可以继续使用数据双写方案),但是如何保证DB内数据与ES的一致性是新的问题,很多小伙伴应该都会遇到db与缓存不一致,db与nosql不一致这类问题,我们该如何呢,cap理论:

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

概念还是要熟悉一下,采用CP原则,使用最终一致性去解决问题,引入消息中间件去帮我们实现容错性,消息在消费不成功时,有一个重试机制,在重试达到阀值后可以人工处理或者转入死信队列,待服务恢复正常后重新消费保证最终结果的一致性。

说下实操,假设我们现在修改了用户信息,同时要修改用户的缓存,在DB更新成功之后我们可以往mq发送一个事件(要保证发送MQ和更新DB在一个事务内),MQ再接收到用户基础信息,异步去处理更新用户的缓存,如果这个过程消费失败了,mq会帮我们实现重试机制,这里不用担心消息丢失的问题,大部分厂商的队列在消息发送成功时都已经落盘,可靠性还是能保证的(除非宕机,停电,异步刷盘这类)。

es一个索引的文档性能在几亿还是没问题,但是太大的时候我们也要考虑继续分,很多互联网公司会按照时间range对索引进行二次分区,或者限定死了你只能查近3月、或近6月的数据,索引名字动态划分一下,别名对应到近期数据,历史数据就下沉,ES内也可以只对近期数据做预热等操作,道理就是这样。

大数据分析

系统越做越大,良好的借助大数据分析我们可以给用户提供更好的服务,大数据分析第一步肯定要有数据吧,需要把数据导入hlive这类合适的中间件去跑,但是我们的代码不可能说在写入es在同时同步到hlive吧,如果后续在增加类似mongodb、clickhouse这类存储中间件时代码会越来越臃肿,所以tom哥这里介绍基于MYSQL binlog扩展出来的数据同步方法canal。

MYSQL主从同步大家都知道吧,canal是模拟mysql 从库向主库发送dump命令,拉下来binlog数据发送到各个中间件进行,同步支持hlive,es,kafka等,这样代码侵入性就降低了很多,我们只是针对数据进行后续操作,大部分互联网公司也是这样玩的。所以架构可能会变成这样:

当然在同步的过程中可能存在的问题tom哥在图中也都标记出来了,canal是非常适合做无侵入的架构重构的中间件,具体使用方法,可以去canal官网查看教程。

还有系统做到后期,真正查询对应到mysql的请求很少,请求都是会落到mongodb、es、tidb等这些高效中间件上,至于mysql我们就老老实实发挥他的本质,做个稳定的数据容器把。

这一期就简单说到这,下次会继续说,缓存篇。

1、如何解决大key。

2、流量把redis也压垮了?

我是hellohello-tom,一个二线城市的程序员

posted @ 2021-08-09 11:47  hellohello-tom  阅读(4604)  评论(2编辑  收藏  举报