分布式系统改造方案——数据篇

分布式系统涉及的内容知识面非常的广泛,不是区区一篇文章就能讲明白,但凡复杂的升级方案与枯燥无味的理论知识(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。

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

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

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

1、如何解决大 key。2、流量把 redis 也压垮了?

作者:hellohello-tom

链接:https://www.cnblogs.com/doNetTom/p/15117612.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值