分布式系统
2021-06-29 11:59:59 64 举报
AI智能生成
分布式系统
作者其他创作
大纲/内容
群发红包如何设计
发红包
判断账户额度是否大于金额,如果大于就保存红包到redis和数据库,然后更新账户
抢红包
判断红包个数,然后在redis更新缓存,并插入红包记录。然后通过MQ进行异步解偶写入数据库。
金额算法
线性切割法
把总金额看成一根绳子,切N-1刀
二倍均值法
每次抢到的金额=随机区间(0,M/N✖️2)
如果为100的红包5个人去抢(除去最后一次,任何一个人抢到的红包都不会大于人均的两倍)
超买超卖
分布式锁
单体使用synchronized
分布式使用redis或者zookeeper
redis速度快
并发量不是特别大,支持可靠性选择zookeeper
数据一致性
分布式事务
seata
数据可靠性投递
rabbitMQ保证数据不丢失
生产者发送消息confirm机制,发送完消息等待异步回调确保消息发送成功
数据持久化
消费端手动ack
为什么要进行业务拆分
如何进行系统拆分
系统拆分布式系统,拆成微服务架构,是要拆很多轮的。
如果多人维护一个服务,<=3个人维护一个服务;最理想情况下,1个人负责1个或2~3个服务。
一个服务的代码不要太多,1万行左右,两三万撑死
比如第一次拆分,就是将以前多个模块拆分开来,比如订单系统,商品系统,仓储系统,用户系统等等。
但是后面每个系统又变的越来越复杂,比如采购系统里面又分成了供应商管理系统,采购单管理系统,订单系统又拆分成购物车系统,价格系统,订单管理系统。
但是后面每个系统又变的越来越复杂,比如采购系统里面又分成了供应商管理系统,采购单管理系统,订单系统又拆分成购物车系统,价格系统,订单管理系统。
spi机制
jdk spi
比如有个工程A,有个接口A,接口A在工程A里是没有实现类的——>系统在运行时,怎么给接口A选择一个实现类呢?
可以搞一个jar包,META-INF/services/,放上一个文件,文件名就是接口名,接口A,接口A的实现类=com.xxx.service.实现类A2.
让工程A来依赖你的这个jar包,然后在系统运行的时候,工程A跑起来,对接口A,就会扫描自己依赖的所有的jar包,在每个jar里找找,有没有META-INF/services文件夹,如果有,在里面找找,有没有接口A这个名字的文件,如果有,在里面找一下你指定的接口A的实现是你jar包里的哪个类?
让工程A来依赖你的这个jar包,然后在系统运行的时候,工程A跑起来,对接口A,就会扫描自己依赖的所有的jar包,在每个jar里找找,有没有META-INF/services文件夹,如果有,在里面找找,有没有接口A这个名字的文件,如果有,在里面找一下你指定的接口A的实现是你jar包里的哪个类?
场景主要用在插件扩展的场景
比如java定义了一套jdbc的接口,但是java没有提供实现类
实际项目跑的时候,要使用jdbc接口的哪些实现类呢?
一般来说,我们要根据自己使用的数据库,比如mysql,就将mysql-jdbc-connector.jar引进来;oracle,就把oracle-jdbc-connector.jar引进来
一般来说,我们要根据自己使用的数据库,比如mysql,就将mysql-jdbc-connector.jar引进来;oracle,就把oracle-jdbc-connector.jar引进来
分布式接口的幂等性如何设计?
(比如不能重复扣款)
(比如不能重复扣款)
解决方式,创建一个set根据id存储,下次发现已经存在就说明重复了,分布式系统,可以用redis set去存储
(1)每个请求必须有唯一的标识,比如订单支付请求,肯定得包含订单id,一个订单id只能支付一次。
(2)每次处理完请求,必须有一个记录标识这个请求处理过了,比如常见的方案是在mysql中记录个状态。
比如支付之前记录一条这个订单的支付流水,而且支付流水采用orderId作为唯一键(unique key)。只有成功插入这个支付流水,才可以执行实际的支付扣款。否则整个事务回滚。
比如支付之前记录一条这个订单的支付流水,而且支付流水采用orderId作为唯一键(unique key)。只有成功插入这个支付流水,才可以执行实际的支付扣款。否则整个事务回滚。
(3)不通过数据库做,可以使用redis,set order_id payed,下一次重复请求过来了,去redis发现已经支付过了,就不再扣款了。
分布式系统中的接口调用如何保证顺序性
调用接口可以带一个order id,然后创建一个接入服务,如果需要进行顺序执行,就使用同一个orderId,然后中间接入服务根据orderId给到同一个系统中执行。
如果一个系统中有多个线程处理接口,可以创建多个内存队列对应每个线程,根据orderId放到同一个内存队列中,让同一个线程按顺序执行队列中的请求
高并发原因,在用户发送请求网络原因导致发到接入服务就顺序不对,非要严格要求顺序,使用分布式锁,保证顺序性。然后根据排序序号或者时间,去判断已经执行到第几个,保证顺序性。
如果要用分布式锁,系统可能会很慢,最好使用MQ进行处理。
如果要用分布式锁,系统可能会很慢,最好使用MQ进行处理。
zookeeper使用场景
分布式协调
A系统通过MQ发送数据给B系统,系统A在zookeeper里写入【orderId=1】的node节点,然后注册一个监听器。系统B处理完MQ的信息后,去zookeeper修改【orderId=1】的node节点更新它的值为finish。然后系统A通过zookeeper监听到系统B已经执行成功了。
分布式锁
zookeeper如果拿到锁就加锁,拿不到锁就对这个锁注册一个监听器。等锁被释放了以后,zookeeper就会通知监听的系统。
配置信息管理
kafka,dubbo基于zookeeper保存元数据
HA高可用性
比如hdfs,yarn等很多大数据系统,都基于zk来开发HA高可用性机制,就是一个重要进程一般都会做主备两个,主进程挂了,立马通知zk感知到进行主备切换。
分布式锁
Redis分布式锁
Redis普通分布式锁
set my:lock 随机值 NX PX 30000
set orderId:1:lock uuid NX PX 30000
set orderId:1:lock uuid NX PX 30000
随机值一定要随机,比如UUID。根据lua脚本进行解锁(过期时间问题,需要对比value值进行锁删除)
NX:设置必须没有这个key值才能成功
如果redis没有orderId:1:lock,那么设置成功。如果有就返回失败。
PX 30000:过了30s之后,key自动过期,被删除
几个系统同时去redis使用上面的命令添加锁,但是通过NX只有一个系统能获取并加锁。
拿到锁的系统去执行一些处理,然后释放这把锁
如果其他系统没有拿到这把锁,那么需要自己每隔1s自动尝试一下,看是否能拿到这把锁
拿到锁的系统去执行一些处理,然后释放这把锁
如果其他系统没有拿到这把锁,那么需要自己每隔1s自动尝试一下,看是否能拿到这把锁
删除锁执行lua脚本
删除这把锁的时候,执行lua脚本,找到那个key的value,跟自己传过去的value比较下,如果是一样的,那么才会删除这个key
为什么要比较value?
如果设置了锁之后,过了30s,当初设置的锁已经过期不存在了,然后被其他系统写了锁。
30s以后执行完了,然后如果只根据key去删除,那么可能删除了别的系统的锁。
如果设置了锁之后,过了30s,当初设置的锁已经过期不存在了,然后被其他系统写了锁。
30s以后执行完了,然后如果只根据key去删除,那么可能删除了别的系统的锁。
缺点
redis如果只做了单机,那么分布式锁就失效了
redis主从架构,master保存了锁数据,但是还没同步到slave就挂掉了。导致分布式锁机制失效
RedLock算法
1.获取当前时间戳,单位是毫秒
2.跟上面类似,轮流尝试在每隔master节点创建锁,过期时间较短,一般就几十毫秒。
3.尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n/2+1)。
4.客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
5.要是锁建立失败了,那么就依次删除这个锁
6.只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
2.跟上面类似,轮流尝试在每隔master节点创建锁,过期时间较短,一般就几十毫秒。
3.尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n/2+1)。
4.客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
5.要是锁建立失败了,那么就依次删除这个锁
6.只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
Redission
看门狗:如果锁到了过期时间失效了,Redission能够续期。防止由于到了过期时间锁释放的问题。
zk分布式锁
获取锁:尝试去创建一个临时节点,如果这个临时节点之前并不存在,就创建成功了,那么这个锁就属于你了。
其他节点尝试创建相同名称的临时节点,如果已经存在了,说明别人已经占有了这把锁,你就失败了。然后就对这个临时节点注册一个监听器。
释放锁:删除那个临时节点就可以了,然后一旦临时节点被删除,zk就会通知别人这个节点被干掉了,相当于锁释放掉了。
其他节点尝试创建相同名称的临时节点,如果已经存在了,说明别人已经占有了这把锁,你就失败了。然后就对这个临时节点注册一个监听器。
释放锁:删除那个临时节点就可以了,然后一旦临时节点被删除,zk就会通知别人这个节点被干掉了,相当于锁释放掉了。
使用临时节点原因:如果系统创建了锁,然后挂掉了。容易产生死锁,但是临时节点,zk感觉到连接挂掉了,那么就会删除这个临时节点
临时顺序节点实现zookeeper
如果有一把锁,被多个人竞争,此时多个人排队,第一个拿到锁的人会执行,然后释放锁。后面的每个人都会去监听排在自己前面的那个人创建的node上,一旦某个人释放了锁,排在自己后面的人就会被zookeeper给通知,一旦被通知了之后,就ok了,自己就获取到了锁,就可以自己执行代码了
redis分布式锁与zookeeper分布式锁对比
redis需要自己不断去尝试获取锁,比较耗性能
zookeeper获取不到锁只需要监听,不需要不断轮询尝试获取锁
zookeeper获取不到锁只需要监听,不需要不断轮询尝试获取锁
分布式Session
tomcat+redis
Tomcat RedisSessionManager的东西,让所有我们部署的tomcat都将session数据存储到redis中
问题:分布式会话的这个东西重耦合在tomcat中,如果将web容器换成jetty,就会很麻烦
spring session+redis
分布式事务
XA方案 两阶段提交方案
比如团建
第一阶段:tb主席问每个人能不能去,如果能去,那么ok,一起去这次tb。
如果这个阶段里,任何一个人回答说,去不了,那么tb主席取消这次活动
如果这个阶段里,任何一个人回答说,去不了,那么tb主席取消这次活动
第二阶段:那下周六大家就一起去滑雪+烧烤了
事务管理器
相当于team building主席
第一阶段:询问
事务管理器去问每个系统,能不能执行这个操作?
第二阶段:执行
挨个通知各个系统进行数据处理
常见于协调一个系统需要对多个数据库进行写入,基于spring+JTA就可以搞定
这种分布式事务方案,比较适合单块应用,跨多个库的分布式事务,因为严重依赖于数据库层面来搞定复杂的事务
但这这种一个服务多个库不符合开发规范了
TCC方案
用了补偿的概念
全程是:Try,Confirm,Cancel
1.Try阶段:对各个服务的资源做检测以及对资源进行锁定或预留
2.Confirm
这个阶段是在各个服务中执行实际的操作
3.Cancel
如果任何遗憾个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑回滚操作。
try阶段:先去冻结银行紫金
confirm阶段
调用银行B的接口,扣款。
调用银行C的接口,转账
调用银行C的接口,转账
如果执行失败,进行Cancel阶段
Cancel阶段
通过跟业务相关的代码,将银行B扣的款给加回去
比较适合的场景:
这个除非你是真的要求一致性要求太高,是你系统中核心之核心的场景,跟钱相关的资金类的场景,那你可以用TCC方案,自己编写大量的业务逻辑,自己判断各个阶段是否OK,还得确保每个系统执行的时间都比较短。
但是说实话,一般尽量别这么搞,自己手写回滚逻辑,实在太恶心。业务代码都很难维护。
本地消息表方案
流程
(1)A系统在自己本地一个事务里操作同时,插入一条数据到消息表中。
(2)接着A系统将这个消息发送到MQ中去
(3)B系统接收到消息之后,在一个事务里,先往自己本地消息表里插入一条数据,同时执行其他业务操作,如果这个消息已经被处理过了,那么此时事务会回滚,保证不会处理重复消息。
(4)B系统执行成狗之后,就会更新自己本地消息表的状态以及A系统消息表的状态(可以用zookeeper,A订阅OrderID,b成功去修改值)
(5)如果B系统处理失败了,那么久不会更新消息表的状态,那么A系统定时扫描自己的消息表,如果有没有没有处理的消息,那么会再次发送到MQ中去,让B再次处理
(6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止。
(2)接着A系统将这个消息发送到MQ中去
(3)B系统接收到消息之后,在一个事务里,先往自己本地消息表里插入一条数据,同时执行其他业务操作,如果这个消息已经被处理过了,那么此时事务会回滚,保证不会处理重复消息。
(4)B系统执行成狗之后,就会更新自己本地消息表的状态以及A系统消息表的状态(可以用zookeeper,A订阅OrderID,b成功去修改值)
(5)如果B系统处理失败了,那么久不会更新消息表的状态,那么A系统定时扫描自己的消息表,如果有没有没有处理的消息,那么会再次发送到MQ中去,让B再次处理
(6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止。
问题
最大的问题在于严重依赖于数据库的消息表来管理事务。这个如果是高并发场景咋办?咋扩展呢?所以一般确实很少用
靠谱方案:可靠消息最终一致性方案
阿里的RocketMQ支持实现消息事务
流程
1.A系统发送parpared消息到MQ,如果这个prepared消息发送失败那么直接取消操作别执行就行
2.如果消息发送成功了,那么接着执行本地事务,如果成功了就告诉MQ发送确认消息confirm,如果失败久告诉MQ回滚消息。
假如A系统发送确认消息的时候失败了,那么消息还停留在prepard状态
RocketMQ会自动轮询所有prepared消息,如果RocketMQ发现消息一直在Prepared状态,那么它会回调A系统的一个接口,问是回滚还是重新发送一次确认消息?那么A系统就选择重新发送确认消息(confirm)
3.一旦A系统发送confirm消息,那么MQ就会让B系统消费这条消息,执行本地事务
4.B系统如果失败了,可以通过MQ或者zookeeper,告诉A系统再重新发送一次。B系统要保证下幂等性,通过记录标志确定没有重复处理。
seata
术语
TC 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM 事务管理器
定义全局事务的范围:开始全局事务,提交或回滚全局事务。
RM 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
undo_log回滚日志表
undo_log必须在每个业务数据库中创建,用于保存回滚操作数据。
当全局提交时,undolog记录直接删除。
当全局回滚时,将现有数据撤销,还原至操作前的状态
beforeImage
操作前镜像
afterImage
操作后镜像
模式
AT,TCC,SAGA,XA模式
AT模式
对业务无侵入,二阶段提交
第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
第二阶段
提交异步化,非常快速的完成。
回滚通过一阶段的回滚日志进行反向补偿
具体步骤
1.TM端是有@GlobalTransaction 进行全局事务开启,提交,回滚
2.TM开始RPC调用远程服务。
3.RM端seata-client通过扩展DataSourceProxy,实现自动生成 UNDO_LOG与TC上报
4.TM告知TC提交/回滚全局事务
5.TC通知RM各自执行commit/rollback 操作,同时清除undo_log
AT模式原理解析
TC相关表
global_table:全局事务
每当有一个全局事务发起后,就会在该表中记录全局事务的ID
branch_table:分支事务
记录每一个分支事务的ID,分支事务操作的哪个数据库等信息
lock_table:全局锁
xid:全局事务的ID ip+端口+事务ID
AT模式如何做到对业务的无侵入
一阶段提交
1.TM 方法执行时,由于该方法被@GlobalTranscation修饰,该TM会向TC发起全局事务,生成XID(全局锁)
2.RM 进行写表,UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果
RM写表过程,Seata会拦截业务SQL,首先解析SQL语句,在业务数据被更新前,将其报错成before image,然后执行业务SQL,在业务数据更新之后,再将其保存为after image,最后生成行锁。以上操作全部在一个数据库事务哪完成,这样保证了一阶段操作的原子性。
二阶段步骤
因为业务SQL在一阶段已经提交至数据库,所以Seata只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
正常:TM执行成功,通知TC全局提交,TC此时通知所有的RM提交成功,删除UNDO_LOG回滚日志
异常:TM执行失败,通知TC全局回滚,TC此时通知所有RM进行回滚,根据UNDO_LOG反向操作,使用before image还原业务数据,删除UNDO_LOG,但在还原前要首先校验脏写,对比数据库当前业务数据和 after image ,如果两份数据完全一致说明没有脏写,可以还原业务数据。如果出现脏写,那么就需要转人工处理。
你们公司怎么处理分布式事务
找一个严格资金绝对不能错的场景,可以说用TCC方案
一般的分布式事务,库存数据没有那么敏感,可以用可靠消息最终一致性方案
RocketMQ 3.2.6之前的版本,后来的回调被砍掉了
分布式事务太复杂,导致系统吞吐量大幅度下跌。
正常就系统A调用系统B,系统B调用系统C。调用报错打印异常日志。
99%分布式接口调用,不要做分布式事务
只需要监控调用报错(发邮件,发短信)
记录日志(一旦出错,完整的日志)
事后快速定位,排查和出解决方案,修复数据。
记录日志(一旦出错,完整的日志)
事后快速定位,排查和出解决方案,修复数据。
比你做50个分布式事务的成本要低上百倍几十倍。
要权衡
要用分布式事务的时候,一定有成本,代码会很复杂,性能会下跌,系统更加复杂更加脆弱,更加容易出bug
好处:做好了的话TCC,可靠消息最终一致性方案,一定可以100%保证你那块数据不会出错
读写隔离
1.写隔离
一阶段本地事务提交前,需要确保先拿到 全局锁
拿不到全局锁,不能提交本地事务
拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
一阶段
tx1先开始,开启本地事务,拿到本地锁,更新操作m = 1000-100 = 900。本地事务提交前,先拿到该记录的全局锁,本地提交释放本地锁。tx2后开始,开启本地事务,拿到本地锁,更新操作m = 900 - 100 = 800。本地事务提交前,尝试拿到该记录的全局锁,tx1全局提交前,该记录的全局锁被tx1持有,tx2需要重试等待全局锁。
二阶段全局提交,释放全局锁。tx2拿到全局锁,提交本地事务。
如果二阶段全局回滚,则tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支回滚。
此时,如果tx2仍在等待该数据的全局锁,同时持有本地锁,则tx1的分支回滚会失败。分支回滚会一直重试,直到tx2的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。
因为整个过程 全局锁在tx1结束前一直是被tx1持有的,所以不会发生脏写问题。
2.读隔离
在数据库本地事务隔离级别 读已提交或以上的基础上,Seata AT模式的默认全局隔离级别是读未提交。
如果在特定场景下,必须要全局的读已提交,目前Seata方式是通过Select for update
select for update 语句执行会申请 全局锁,如果全局锁被其他事务持有,则释放本地锁(回滚SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被block住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。
数据库
分库分表
为什么要分库分表
分库分表是两个概念,可能分库不分表,或者可能分表不分库
1.部署Mqsql单机,扛不住并发。
2.Mysql单机磁盘容量快满了
3.Mysql单表数据量太大了,sql越跑越慢
2.Mysql单机磁盘容量快满了
3.Mysql单表数据量太大了,sql越跑越慢
异步写系统,消费到了40万数据按照中的某个id,比如订单id进行hash,分发到对应数据库中。
每个数据库单日增加40万数据。
每个数据库单日增加40万数据。
好处:
1.MySql从单机->3机,承受并发增加了三倍。
2.将原来的3千万数据,从一个库拆分到三个库,每个库就1/3的数据量,数据库服务器的磁盘使用率大大降低。
3.原本一个蛋表是3千万数据,一个sql要花3秒钟去跑;拆分之后,每个库的每个表就1千万数据,一个SQL花1秒钟去跑就可以了
1.MySql从单机->3机,承受并发增加了三倍。
2.将原来的3千万数据,从一个库拆分到三个库,每个库就1/3的数据量,数据库服务器的磁盘使用率大大降低。
3.原本一个蛋表是3千万数据,一个sql要花3秒钟去跑;拆分之后,每个库的每个表就1千万数据,一个SQL花1秒钟去跑就可以了
用过哪些分库分表中间件?
sharding-jdbc,mycat
代理Proxy中间件:单独部署一个中间件
客户端Client中间件:一个jar包让服务端直接用
代理Proxy中间件:单独部署一个中间件
客户端Client中间件:一个jar包让服务端直接用
sharding-jdbc
当当开源,属于client层方案。
sql语法支持比较多,没有太多限制
支持分库分表,读写分离,分布式id生成,柔性事务(最大努力送达型事务,TCC事务)
优点:是client层方案,不用部署,运维成本低,不需要代理层二次转发请求,性能很高。
但是:如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合sharding-jdbc
但是:如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合sharding-jdbc
mycat
基于cobar改造的,属于proxy层方案
支持的功能非常完善
如果在中小型公司选用sharding-jdbc,client层方案轻便,维护成本低,不需要额外增派人手。
中大型最好选用mycat这类proxy层方案,团队很大,人员充足,那么最好是专门弄个人来研究和维护mycat,然后大量项目直接透明使用即可。
中大型最好选用mycat这类proxy层方案,团队很大,人员充足,那么最好是专门弄个人来研究和维护mycat,然后大量项目直接透明使用即可。
如何进行数据库拆分
垂直拆分
一个表有很多字段,可以拆成两个表,一个表放访问频率很高的字段,另一个表放访问频率比较低的字段。
因为数据库是有缓存的,如果访问频率高的字段数据越少,在缓存里就能放更多的行,后续再访问的话效率就越好。
因为数据库是有缓存的,如果访问频率高的字段数据越少,在缓存里就能放更多的行,后续再访问的话效率就越好。
水平拆分
把单表的数据拆成三份
还可以把每个表起不同的名字,然后再拆分
按照时间分发Range分发
一个月放到一个数据库里,然后每个星期对应数据库中分出的一张表。
好处:后面扩ring的时候,就很容易,因为只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写到新的库了。
缺点:大部分请求都是请求最新的数据,用户请求可能都到了最新的库里面去。实际生产用range要看场景。
按照ID分发(Hash分发)
orderId = 11,先根据数据库取模,11%3=2 分到数据库2,然后再根据数据库2中分的表取模 11%4=3,放到test_03
好处:可以平均每个库的数据量和请求压力。
坏处:扩容起来比较麻烦,新增一个数据库,需要重新hash取模。会有一个数据迁移的这么一个过程。
好处:单库最高承载2000/s的QPS,3个库最高可承载6000/s的QPS
每个表的数据从原来的600万数据,缩小到50万数据。SQL的执行效率可能会增加好几倍。
单库原来是600万数据,假设占据了600MB的磁盘空间。现在单库才200万数据,就仅仅占用了200M的磁盘空间
每个表的数据从原来的600万数据,缩小到50万数据。SQL的执行效率可能会增加好几倍。
单库原来是600万数据,假设占据了600MB的磁盘空间。现在单库才200万数据,就仅仅占用了200M的磁盘空间
如何把系统不停机迁移到分库分表
假设你现在有一个单库单表的系统,在线上在跑,假设单表有600万数据,3个库,每个库分了4个表,每个表要放50万的数据。
假设你已经选择了分库分表的中间件,你怎么把线上系统平滑的迁移到分库分表上面去?
假设你已经选择了分库分表的中间件,你怎么把线上系统平滑的迁移到分库分表上面去?
方案1:长时间停机分库分表
在网站上挂个公告,0-6点运维。
后台开一个临时程序,部署3台机器每台机器开20个线程,一个小时完成迁移
这个临时程序把数据给到数据库中间件,数据库中间件分发到多个数据库多个表中
然后修改主系统配置,让数据写入数据库中间件
缺点:
1.一定会出现几个小时的停机。
2.如果说到了凌晨3点没搞定,就慌了,到了凌晨五点还没搞定,就得回滚,继续单库单表,第二天凌晨继续搞
1.一定会出现几个小时的停机。
2.如果说到了凌晨3点没搞定,就慌了,到了凌晨五点还没搞定,就得回滚,继续单库单表,第二天凌晨继续搞
方案2:不停机双写方案
1.修改系统中所有写库的代码,同时让他写老库和新分库分表的库。
2.开发一个后台数据临时迁移工具
- 标准的规范化的表设计里面,都会包含最后修改的时间字段。
- 判断一下分库分表中是否存在?不存在,直接写入。
- 如果存在,比较两个数据的时间戳,如果比分库分表的数据要新,就覆盖分库分表里的数据
3.迁移完一轮,600万数据迁移完这么一轮,此时就需要执行一次检查。
- 检查单库单表的数据,跟分库分表的数据是不是一致。
- 如果一摸一样的话,那么就ok,迁移成功。
- 如果不一样的话,针对不一样的数据,从单库单表中读取出来,看看是否需要再次覆盖分库分表中的数据。
- 依次循环往复,这个后台程序你可能得跑个好几天,到了凌晨的时候,几乎没什么新的数据进来了,此时一般来说老库和新库的数据会变成一样的。
4.最后一步,修改系统的代码,将写单库单表的代码给删除掉,仅仅写分库分表,再次部署一下,就ok了
如何设计动态扩容缩容的分库分表方案?
1.停机扩容(不能用)
发公告停机
开发一个工具,把所有数据抽出来,重新进行分发写到新的库和表中去。
很不靠谱,从单库单表迁移到分库分表可能数据量并不是很大,但是如果已经分库分表了,说明数据量就很大了。
2.优化后的方案
1.设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是32*32.
国内互联网公司肯定都是够用了
无论并发支撑还是数据量支撑都没问题
正常每个库的承载写入并发是1500,那么32个库就是48000的写并发,接近5万/s的写入并发,前面再加一个MQ,削峰,每秒写入MQ8万条,每秒消费5万条
假设每个表放500万条数据,在MySql里可以放50亿条数据。每秒5万并发,总共50亿数据。对于国内大部分互联网公司来说,其实一般都够了。
2.路由的规则:Id对32取模=库,Id/32再对32取模=表
3.刚开始四台服务器,每台服务器8个库。
扩容的时候,申请增加更多的数据库服务器,装好mysql,倍数扩容,四台服务器,扩到8台服务器,16台服务器
扩容的时候,申请增加更多的数据库服务器,装好mysql,倍数扩容,四台服务器,扩到8台服务器,16台服务器
注意:每台数据库服务器最大制成2000/s QPS。 扩容只需要新增四个数据库服务器,迁移四个库到新的数据库中。
直接迁移库这种方式,对于dba来说,他们都有对应的工具,方便和快捷很多
最多可以扩到32个服务器!每个数据库服务器上放一个库,这个库有32张表。
4.由dba负责将原先数据库服务器的库,迁移到新的数据库服务器上去,很多工具,库迁移比较便捷
5.我们这边只需要修改一下配置,调整迁移的库所在数据库服务器的地址
6.重新发布系统,上线,原先的路由规则变都不用变,直接可以基于2倍的数据库服务器的资源,继续进行线上系统的提供服务
好处:
- 只是改变数据库服务器的数量,不用改变表的数量。
- 而且动态缩容,可以改小数据库服务器的数量。
- 扩容和缩容只需要改库的配置就可以了
分库分表后,全局id怎么做呢
1.单独搞一个生成主键的的库,全局就一个,每次插入数据,先往这个专门生成主键的库插入一条数据,拿到id再去写入分库中
好处:简单
坏处:单库性能瓶颈
坏处:单库性能瓶颈
适合:并发很低,但是数据量很大的需求
2.uuid
好处:本地生成,不用基于数据库来做了。
坏处:uuid太长了,作为主键性能太差。
坏处:uuid太长了,作为主键性能太差。
3.获取系统当前时间
这个就是获取当前时间即可
但是并发很高的青枯杨,一秒并发几千,会有重复的情况,这个肯定不合适。基本上不用考虑了。
适合的场景:将时间和其他业务字段拼接起来,作为一个id,如果业务上可以接受也是可以的。比如时间戳+用户id+业务含义编码
4.snowflake算法
会生成一个64位的long型的id,64位的long->二进制
- 第一位:0 二进制首位0代表正数
- 后面41位:放时间戳
- 接下来5位:放机房id
- 接下来5位:机器id
- 最后:用来做序号
比如:两个机房,每个机房6台机器
机房2的第六台机器,想要在2021-01-01 10:00:00的时候,生成一个全局唯一的id
它会往snowflake分布式id生成服务器一个请求,服务器就可以感知到:机房17 机器25 2021-01-01 10:00:00
2021-01-01 10:00:00->处理后二进制换算41bit 0001100 10100010 10111110 10001001 01011100 00
机房id ,17 ->二进制 10001
机器id,25 ->二进制 11001
snowflake算法服务,会判断一下,当前请求是否是机房17 机器25 在2021-01-01 10:00:00 的第一个请求
如果是第一个请求,就全是0 :0000 00000000
如果是第一个请求,就全是0 :0000 00000000
如果是是机房17 机器25 在2021-01-01 10:00:00发送了第二条消息,那么snowflake算法在这一毫秒,之前已经生成过一个id了,此时同一个机房同一个机器在同一毫秒还想再生成一个id,此时只能加1: 0000 00000001
代码思路
1.机房和机器都不能超过32个
2.每个机房的每个机器在每个毫秒内最大不能超过4096这个范围
判断当前时间戳是否相等,如果相等sequence+1
//刚开始第一次sequence是0,同一毫秒内就+1,然后进行二进制
然后再记录一下当前生成的毫秒
判断当前时间戳是否相等,如果相等sequence+1
//刚开始第一次sequence是0,同一毫秒内就+1,然后进行二进制
然后再记录一下当前生成的毫秒
3.将时间戳左移,放到41bit那;将机房左移放到5bit那;将机器id左移放到5bit那,将序号放到最后10bit;最后拼接成一个64bit的二进制数字,转成10进制就是个long型
读写分离
为什么要读写分离
写请求:直接写入库,同时写入缓存
读请求:先读缓存,如果缓存没有再去数据库里读
什么情况下,缓存里会读不到数据?
1.缓存刚加上去,并没有把数据中的历史数据导入缓存。
2.缓存的内存塞满了,自动LRU了,删除了一些数据
2000/s 数据库开始报警,磁盘io变慢,cpu负载过高,内存使用频率过高
原理
在主库写数据后,同步到从库,读的话去从库里读
一般挂3-5台从库
如何实现
MySQL原生就支持主从复制。主库->从库
流程
1.Mysql有一个工作线程,写数据会把数据写到binlog日志里
2.从库会有一个io线程,会跟主库建立连接,主库也会有一个对应的io线程
3.主库io线程会把binlog日志读出来,然后传给从库io线程
4.从库的io线程会把收到的数据写入 relay日志
relay日志比较小,一般是存在os cache里,所以消耗的性能并不多
5.从库会有一个SQL线程,会不断从Relay日志里读数据,然后更新到自己本地的数据中
注意:从库 读取binlog日志,写relay日志,应用日志变更到自己本地数据库,都是串行化的。
从库的数据一般比主库要慢一些
说明一点:从库的IO线程,读取主库的binlog日志的时候,老版本是单线程的,5.6.x以后的新版本,也可以支持多线程读取
但是其他地方Mysql主库是多线程,但是从库还是单线程,还是会导致数据比主库慢一些
主从同步延迟问题
问题
延时:主要看主库的写并发,主库的写并发达到1000/s,从库的延时会有几ms;主库写并发达到2000/s,从库的延时可能会有几十ms;主库写并发达到4000/s,6000/s快死了,从库延时会达到几秒
宕机数据丢失:数据刚刚记录到主库中,主库如果突然宕机,从库切成主库进行写和读,但是主库中的一些数据还没同步到从库,可能丢失一些数据
解决:
1.半同步复制semi-sync复制
指的是主库写入binlog日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay logo之后,接着会返回一个ack给主库,主库至少接收到一个从库的ack之后才会认为操作完成了。
如果主库没有接收到ack就宕机了,服务端就会收到数据并没有写入成功。
2.并行复制 mysql5.7新版本功能
从库开启多个SQL线程,每个线程从relay日志里读一个库的日志,然后并行重放不同库的日志,这是库级别的并行(缓解主从同步延时问题)
3.主从同步延时问题(精华)
show status, Seconds_Behind_Master 可以看从库复制主库的数据落后了几毫秒
延迟导致生产环境的问题
插入一条数据,查出来这条数据,再更新这条数据
主从延迟导致更新失败
解决
一般来说,如果主从延迟较为严重
1.分库,一个主库拆为4个主库,每个库的并发500/s,这样,主从延迟可以忽略不计
2.打开mysql支持的并行复制,多个库并行复制,但是如果某个库的写入并发就是特别高,单库写并发达到2000/s。并行复制没意义。
其实并行复制效果不一定很好。
其实并行复制效果不一定很好。
3.重写代码,写代码的同学要慎重,插入数据之后,直接就更新,不用再查询这种
4.如果确实存在必须先插入,立马要求就查询到。可以用数据库中间件做到,直连主库查询。--不推荐这种方法,这么搞读写分离就没有意义了
总结:
1.利用5.6版本的多个io线程
2.打开并行复制
3.如果还是不行并且QPS比较高,考虑分库
4.数据丢失问题,开半自动复制semi-sync
Eureka服务中心的原理
流程
服务一启动,就进行服务注册,到服务注册表
其他服务进行服务发现,就去eureka去拉去服务注册表,可配置每隔30秒拉取一次服务注册表。
多级缓存
ReadOnly缓存
ReadWrite缓存
服务注册表
1.修改服务注册表后,会立马同步到ReadWrite缓存中。
2.在ReadOnly缓存中没有的话,就会去ReadWrite缓存中查询,然后保存到ReadOnly缓存中
3.后台会有一个定时的线程,把ReadWrite缓存定时同步到ReadOnly缓存里。(定时可配置)
2.在ReadOnly缓存中没有的话,就会去ReadWrite缓存中查询,然后保存到ReadOnly缓存中
3.后台会有一个定时的线程,把ReadWrite缓存定时同步到ReadOnly缓存里。(定时可配置)
目的:优化并发冲突
如果只有服务注册表,可能在高并发的时候有写和读的冲突然后加锁等问题
心跳与故障
所有服务启动注册完以后,会不停发送心跳。(时间可设置)
服务注册中心会维护所有注册服务的心跳。有一个线程定时会检查心跳。(时间可设置)
一旦发现某个服务超过一定时间没有心跳,就认为服务故障,就会摘掉这个服务。
直接删掉ReadWrite缓存的所有数据。
已经拉取注册表的请求可能使用的还是原来的注册表,往已经挂掉的服务发送请求,发现每次都请求失败。
每隔一段时间才能刷新
Ribbon
Feign
他是对一个接口打一个注解,他一定会针对这个注解标注的接口生成动态代理,然后针对feign的动态代理去调用他的方法的时候,此时会在底层生成http协议格式的请求,/order/create?productId=1
底层:使用http通信的框架组件,HttpClient,先得使用Ribbon去Eureka从本地的Eureka注册表的缓存里获取出来对方的机器列表,然后进行负载均衡,选择一台机器出来,接着针对那台机器发送http请求过去即可。
Zuul
配置一下不同的请求路径与服务的对应关系,你的请求到了网关,他直接查找到匹配的服务,然后就直接把请求转发给那个服务的某台机器。Ribbon从Eureka本地的缓存列表中获取一台机器,负载均衡,把请求直接通过http通信框架发送到指定机器上去。
springCloud的好处
主打微服务架构,组件齐全,功能齐全。网关直接提供了分布式配置中心,授权认证,服务调用链路追踪,Hystrix做资源隔离,熔断降级,服务请求QPS监控。
胜是胜在功能齐全,中小型公司开箱即用,直接满足系统的开发需求。
hystrix限流,降级,熔断
如何设计一个高并发系统?
1.系统拆分(2000万用户,2000/s请求)
一个系统拆成多个系统,每个系统访问一个数据库。
2.缓存(6000万个用户,6000/s请求)
大量的读请求5000/s走缓存,1000/s写请求走数据库。缓存可以轻松抗几万的并发
3.MQ(1亿用户,1万请求/s)
2000/s个请求写请求到MQ,每秒1000处理请求
4.分库分表(2亿用户,2万请求/s)
分库分表,增加写入数据库的请求/s
5.数据库读写分离(5亿用户,每秒5万个请求)
由于缓存LRU部分数据还是得去数据库里读,压力比较大。写的话写到主库,读的话读从库。写的话每秒600个请求。读每秒1000个请求
6.ElasticSearch
玩搜索,一开始数据量很小,你可以自己基于lucene包缝状搜索引擎。但是后期数据量越来越大,单机lucene已经放不下了,而且单机也支撑不了那么高的并发量。
es集群是分布式的,可以放几十亿的数据量,用20台机器,每秒可以支撑几万的请求
0 条评论
下一页